snapshot of https://github.com/dream-num/univer.git (dev, 2026-09-16; blobs > 131072B omitted)
Some checks failed
📦 Build / build (push) Has been cancelled
🧪 CI Test / test shard (1/4) (push) Has been cancelled
🧪 CI Test / test shard (2/4) (push) Has been cancelled
🧪 CI Test / test shard (3/4) (push) Has been cancelled
🧪 CI Test / test shard (4/4) (push) Has been cancelled
🧪 CI Test / test (push) Has been cancelled
🧪 CI Test / eslint (push) Has been cancelled
🧪 CI Test / typecheck (push) Has been cancelled
🔫 Sync to Pro / dispatch-sync (push) Has been cancelled
🔮 Sync Mirror / sync (push) Has been cancelled
Some checks failed
📦 Build / build (push) Has been cancelled
🧪 CI Test / test shard (1/4) (push) Has been cancelled
🧪 CI Test / test shard (2/4) (push) Has been cancelled
🧪 CI Test / test shard (3/4) (push) Has been cancelled
🧪 CI Test / test shard (4/4) (push) Has been cancelled
🧪 CI Test / test (push) Has been cancelled
🧪 CI Test / eslint (push) Has been cancelled
🧪 CI Test / typecheck (push) Has been cancelled
🔫 Sync to Pro / dispatch-sync (push) Has been cancelled
🔮 Sync Mirror / sync (push) Has been cancelled
This commit is contained in:
@@ -0,0 +1,283 @@
|
||||
/**
|
||||
* Copyright 2023-present DreamNum Co., Ltd.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import type { Injector, IWorkbookData, Workbook } from '@univerjs/core';
|
||||
import {
|
||||
ICommandService,
|
||||
Inject,
|
||||
Injector as InjectorIdentifier,
|
||||
IUniverInstanceService,
|
||||
LocaleType,
|
||||
Plugin,
|
||||
RANGE_TYPE,
|
||||
Univer,
|
||||
UniverInstanceType,
|
||||
} from '@univerjs/core';
|
||||
import { MarkDirtyFilterChangeMutation, SheetsSelectionsService } from '@univerjs/sheets';
|
||||
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
|
||||
import { SheetsFilterService } from '../../../services/sheet-filter.service';
|
||||
import {
|
||||
ReCalcSheetsFilterMutation,
|
||||
RemoveSheetsFilterMutation,
|
||||
SetSheetsFilterCriteriaMutation,
|
||||
SetSheetsFilterRangeMutation,
|
||||
} from '../../mutations/sheets-filter.mutation';
|
||||
import {
|
||||
ClearSheetsFilterCriteriaCommand,
|
||||
ReCalcSheetsFilterCommand,
|
||||
RemoveSheetFilterCommand,
|
||||
SetSheetFilterRangeCommand,
|
||||
SetSheetsFilterCriteriaCommand,
|
||||
SmartToggleSheetsFilterCommand,
|
||||
} from '../sheets-filter.command';
|
||||
|
||||
const unitId = 'filter-command-workbook';
|
||||
const subUnitId = 'sheet1';
|
||||
|
||||
function createWorkbookData(): IWorkbookData {
|
||||
return {
|
||||
id: unitId,
|
||||
appVersion: '3.0.0-alpha',
|
||||
locale: LocaleType.EN_US,
|
||||
name: 'filter command workbook',
|
||||
sheetOrder: [subUnitId],
|
||||
styles: {},
|
||||
sheets: {
|
||||
[subUnitId]: {
|
||||
id: subUnitId,
|
||||
name: 'Sheet 1',
|
||||
cellData: {
|
||||
0: {
|
||||
0: { v: 'Fruit' },
|
||||
},
|
||||
1: {
|
||||
0: { v: 'Apple' },
|
||||
},
|
||||
2: {
|
||||
0: { v: 'Banana' },
|
||||
},
|
||||
3: {
|
||||
0: { v: 'Apple' },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
class SheetsFilterCommandTestPlugin extends Plugin {
|
||||
static override type = UniverInstanceType.UNIVER_SHEET;
|
||||
static override pluginName = 'SheetsFilterCommandTestPlugin';
|
||||
|
||||
constructor(_config: unknown, @Inject(InjectorIdentifier) protected readonly _injector: Injector) {
|
||||
super();
|
||||
}
|
||||
|
||||
override onStarting(): void {
|
||||
this._injector.add([SheetsFilterService]);
|
||||
this._injector.add([SheetsSelectionsService]);
|
||||
}
|
||||
}
|
||||
|
||||
describe('sheet filter commands', () => {
|
||||
let univer: Univer;
|
||||
let workbook: Workbook;
|
||||
let commandService: ICommandService;
|
||||
let sheetsFilterService: SheetsFilterService;
|
||||
|
||||
beforeEach(() => {
|
||||
univer = new Univer();
|
||||
univer.registerPlugin(SheetsFilterCommandTestPlugin);
|
||||
workbook = univer.createUnit<IWorkbookData, Workbook>(UniverInstanceType.UNIVER_SHEET, createWorkbookData());
|
||||
|
||||
const injector = univer.__getInjector();
|
||||
injector.get(IUniverInstanceService).focusUnit(unitId);
|
||||
commandService = injector.get(ICommandService);
|
||||
sheetsFilterService = injector.get(SheetsFilterService);
|
||||
|
||||
const commands = [
|
||||
SetSheetsFilterRangeMutation,
|
||||
SetSheetsFilterCriteriaMutation,
|
||||
RemoveSheetsFilterMutation,
|
||||
ReCalcSheetsFilterMutation,
|
||||
MarkDirtyFilterChangeMutation,
|
||||
SetSheetFilterRangeCommand,
|
||||
RemoveSheetFilterCommand,
|
||||
SetSheetsFilterCriteriaCommand,
|
||||
ClearSheetsFilterCriteriaCommand,
|
||||
ReCalcSheetsFilterCommand,
|
||||
SmartToggleSheetsFilterCommand,
|
||||
];
|
||||
|
||||
for (const command of commands) {
|
||||
commandService.registerCommand(command);
|
||||
}
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
univer.dispose();
|
||||
});
|
||||
|
||||
it('sets a filter range and applies criteria to the filtered rows', async () => {
|
||||
const range = { startRow: 0, startColumn: 0, endRow: 3, endColumn: 0 };
|
||||
|
||||
const setRangeResult = await commandService.executeCommand(SetSheetFilterRangeCommand.id, {
|
||||
unitId,
|
||||
subUnitId,
|
||||
range,
|
||||
});
|
||||
const criteriaResult = await commandService.executeCommand(SetSheetsFilterCriteriaCommand.id, {
|
||||
unitId,
|
||||
subUnitId,
|
||||
col: 0,
|
||||
criteria: {
|
||||
colId: 0,
|
||||
filters: {
|
||||
filters: ['Apple'],
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const filterModel = sheetsFilterService.getFilterModel(unitId, subUnitId);
|
||||
const filterColumn = filterModel?.getFilterColumn(0);
|
||||
|
||||
expect(setRangeResult).toBe(true);
|
||||
expect(criteriaResult).toBe(true);
|
||||
expect(filterModel?.getRange()).toEqual(range);
|
||||
expect(filterColumn?.serialize()).toMatchObject({
|
||||
colId: 0,
|
||||
filters: {
|
||||
filters: ['Apple'],
|
||||
},
|
||||
});
|
||||
expect(filterModel?.isRowFiltered(1)).toBe(false);
|
||||
expect(filterModel?.isRowFiltered(2)).toBe(true);
|
||||
expect(filterModel?.isRowFiltered(3)).toBe(false);
|
||||
});
|
||||
|
||||
it('clears criteria without removing the filter range', async () => {
|
||||
const range = { startRow: 0, startColumn: 0, endRow: 3, endColumn: 0 };
|
||||
|
||||
await commandService.executeCommand(SetSheetFilterRangeCommand.id, {
|
||||
unitId,
|
||||
subUnitId,
|
||||
range,
|
||||
});
|
||||
await commandService.executeCommand(SetSheetsFilterCriteriaCommand.id, {
|
||||
unitId,
|
||||
subUnitId,
|
||||
col: 0,
|
||||
criteria: {
|
||||
colId: 0,
|
||||
filters: {
|
||||
filters: ['Apple'],
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const result = await commandService.executeCommand(ClearSheetsFilterCriteriaCommand.id, {
|
||||
unitId,
|
||||
subUnitId,
|
||||
});
|
||||
const filterModel = sheetsFilterService.getFilterModel(unitId, subUnitId);
|
||||
|
||||
expect(result).toBe(true);
|
||||
expect(filterModel?.getRange()).toEqual(range);
|
||||
expect(filterModel?.getFilterColumn(0)).toBeNull();
|
||||
expect(filterModel?.filteredOutRows.size).toBe(0);
|
||||
});
|
||||
|
||||
it('removes an existing filter model from the worksheet', async () => {
|
||||
await commandService.executeCommand(SetSheetFilterRangeCommand.id, {
|
||||
unitId,
|
||||
subUnitId,
|
||||
range: { startRow: 0, startColumn: 0, endRow: 3, endColumn: 0 },
|
||||
});
|
||||
|
||||
const result = await commandService.executeCommand(RemoveSheetFilterCommand.id, {
|
||||
unitId,
|
||||
subUnitId,
|
||||
});
|
||||
|
||||
expect(result).toBe(true);
|
||||
expect(sheetsFilterService.getFilterModel(unitId, subUnitId)).toBeNull();
|
||||
});
|
||||
|
||||
it('toggles the current selection into and out of a sheet filter', async () => {
|
||||
const selectionManager = univer.__getInjector().get(SheetsSelectionsService);
|
||||
const selectedRange = {
|
||||
startRow: 0,
|
||||
startColumn: 0,
|
||||
endRow: 3,
|
||||
endColumn: 0,
|
||||
rangeType: RANGE_TYPE.NORMAL,
|
||||
};
|
||||
selectionManager.addSelections([{
|
||||
range: selectedRange,
|
||||
primary: {
|
||||
actualRow: 0,
|
||||
actualColumn: 0,
|
||||
startRow: 0,
|
||||
startColumn: 0,
|
||||
endRow: 3,
|
||||
endColumn: 0,
|
||||
isMerged: false,
|
||||
isMergedMainCell: false,
|
||||
},
|
||||
style: null,
|
||||
}]);
|
||||
|
||||
const openResult = await commandService.executeCommand(SmartToggleSheetsFilterCommand.id);
|
||||
const filterModel = sheetsFilterService.getFilterModel(unitId, subUnitId);
|
||||
const closeResult = await commandService.executeCommand(SmartToggleSheetsFilterCommand.id);
|
||||
|
||||
expect(openResult).toBe(true);
|
||||
expect(filterModel?.getRange()).toEqual(selectedRange);
|
||||
expect(closeResult).toBe(true);
|
||||
expect(sheetsFilterService.getFilterModel(unitId, subUnitId)).toBeNull();
|
||||
});
|
||||
|
||||
it('recalculates an existing filter after sheet data changes', async () => {
|
||||
const worksheet = workbook.getSheetBySheetId(subUnitId);
|
||||
|
||||
await commandService.executeCommand(SetSheetFilterRangeCommand.id, {
|
||||
unitId,
|
||||
subUnitId,
|
||||
range: { startRow: 0, startColumn: 0, endRow: 3, endColumn: 0 },
|
||||
});
|
||||
await commandService.executeCommand(SetSheetsFilterCriteriaCommand.id, {
|
||||
unitId,
|
||||
subUnitId,
|
||||
col: 0,
|
||||
criteria: {
|
||||
colId: 0,
|
||||
filters: {
|
||||
filters: ['Apple'],
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
worksheet?.getCellMatrix().setValue(2, 0, { v: 'Apple' });
|
||||
const result = await commandService.executeCommand(ReCalcSheetsFilterCommand.id, {
|
||||
unitId,
|
||||
subUnitId,
|
||||
});
|
||||
const filterModel = sheetsFilterService.getFilterModel(unitId, subUnitId);
|
||||
|
||||
expect(result).toBe(true);
|
||||
expect(filterModel?.isRowFiltered(2)).toBe(false);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,447 @@
|
||||
/**
|
||||
* Copyright 2023-present DreamNum Co., Ltd.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import type { IAccessor, ICommand, IMutationInfo, IRange, Nullable, Workbook } from '@univerjs/core';
|
||||
import type { IMarkDirtyFilterChangeMutationParams, ISheetCommandSharedParams } from '@univerjs/sheets';
|
||||
import type { LocaleKey } from '../../locale/types';
|
||||
import type { FilterColumn } from '../../models/filter-model';
|
||||
import type { IAutoFilter, IFilterColumn } from '../../models/types';
|
||||
import type {
|
||||
ISetSheetsFilterCriteriaMutationParams,
|
||||
ISetSheetsFilterRangeMutationParams,
|
||||
} from '../mutations/sheets-filter.mutation';
|
||||
import {
|
||||
CommandType,
|
||||
ErrorService,
|
||||
ICommandService,
|
||||
IUndoRedoService,
|
||||
IUniverInstanceService,
|
||||
LocaleService,
|
||||
sequenceExecute,
|
||||
UniverInstanceType,
|
||||
} from '@univerjs/core';
|
||||
import {
|
||||
expandToContinuousRange,
|
||||
getSheetCommandTarget,
|
||||
isSingleCellSelection,
|
||||
MarkDirtyFilterChangeMutation,
|
||||
SheetsSelectionsService,
|
||||
} from '@univerjs/sheets';
|
||||
import { SheetsFilterService } from '../../services/sheet-filter.service';
|
||||
import {
|
||||
ReCalcSheetsFilterMutation,
|
||||
RemoveSheetsFilterMutation,
|
||||
SetSheetsFilterCriteriaMutation,
|
||||
SetSheetsFilterRangeMutation,
|
||||
} from '../mutations/sheets-filter.mutation';
|
||||
|
||||
/**
|
||||
* Parameters of command {@link SetSheetFilterRangeCommand}.
|
||||
* @property {IRange} range - the range to be set as filter range.
|
||||
*/
|
||||
export interface ISetSheetFilterRangeCommandParams extends ISheetCommandSharedParams {
|
||||
range: IRange;
|
||||
}
|
||||
|
||||
/**
|
||||
* A {@link CommandType.COMMAND} to set filter range in a Worksheet. Its params {@link ISetSheetFilterRangeCommandParams}
|
||||
* is required. If the {@link FilterModel} does not exist, it will be created.
|
||||
*/
|
||||
export const SetSheetFilterRangeCommand: ICommand<ISetSheetFilterRangeCommandParams> = {
|
||||
id: 'sheet.command.set-filter-range',
|
||||
type: CommandType.COMMAND,
|
||||
handler: (accessor, params: ISetSheetFilterRangeCommandParams) => {
|
||||
const sheetsFilterService = accessor.get(SheetsFilterService);
|
||||
const commandService = accessor.get(ICommandService);
|
||||
const undoRedoService = accessor.get(IUndoRedoService);
|
||||
const instanceSrv = accessor.get(IUniverInstanceService);
|
||||
|
||||
const { unitId, subUnitId, range } = params;
|
||||
|
||||
const commandTarget = getSheetCommandTarget(instanceSrv, params);
|
||||
if (!commandTarget) return false;
|
||||
|
||||
const filterModel = sheetsFilterService.getFilterModel(unitId, subUnitId);
|
||||
if (filterModel) return false;
|
||||
|
||||
if (range.endRow === range.startRow) {
|
||||
const errorService = accessor.get(ErrorService);
|
||||
const localeService = accessor.get(LocaleService);
|
||||
errorService.emit(localeService.t<LocaleKey>('sheets-filter.command.not-valid-filter-range'));
|
||||
return false;
|
||||
}
|
||||
|
||||
// Execute the command to set filter range and prepare undo redo.
|
||||
const redoMutation = { id: SetSheetsFilterRangeMutation.id, params: { unitId, subUnitId, range } };
|
||||
const result = commandService.syncExecuteCommand(redoMutation.id, redoMutation.params);
|
||||
if (result) {
|
||||
undoRedoService.pushUndoRedo({
|
||||
unitID: unitId,
|
||||
undoMutations: [{ id: RemoveSheetsFilterMutation.id, params: { unitId, subUnitId } }],
|
||||
redoMutations: [redoMutation],
|
||||
});
|
||||
}
|
||||
|
||||
return result;
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* A {@link CommandType.COMMAND} to remove filter in a Worksheet. Its params {@link ISheetCommandSharedParams} is
|
||||
* required. If the {@link FilterModel} does not exist, it will fail to execute.
|
||||
*/
|
||||
export const RemoveSheetFilterCommand: ICommand<ISheetCommandSharedParams> = {
|
||||
id: 'sheet.command.remove-sheet-filter',
|
||||
type: CommandType.COMMAND,
|
||||
handler: (accessor, params: ISheetCommandSharedParams) => {
|
||||
const target = getSheetCommandTarget(accessor.get(IUniverInstanceService), params);
|
||||
if (!target) return false;
|
||||
|
||||
const sheetsFilterService = accessor.get(SheetsFilterService);
|
||||
const { unitId, subUnitId } = target;
|
||||
const filterModel = sheetsFilterService.getFilterModel(unitId, subUnitId);
|
||||
if (!filterModel) return false;
|
||||
|
||||
const filterRange = filterModel.getRange();
|
||||
if (!filterRange) return false;
|
||||
|
||||
const commandService = accessor.get(ICommandService);
|
||||
const undoRedoService = accessor.get(IUndoRedoService);
|
||||
|
||||
const autoFilter = filterModel.serialize();
|
||||
const undoMutations = destructFilterModel(unitId, subUnitId, autoFilter);
|
||||
|
||||
const result = commandService.syncExecuteCommand(RemoveSheetsFilterMutation.id, { unitId, subUnitId });
|
||||
if (result) {
|
||||
undoRedoService.pushUndoRedo({
|
||||
unitID: unitId,
|
||||
undoMutations,
|
||||
redoMutations: [{ id: RemoveSheetsFilterMutation.id, params: { unitId, subUnitId } }],
|
||||
});
|
||||
commandService.executeCommand<IMarkDirtyFilterChangeMutationParams>(MarkDirtyFilterChangeMutation.id, {
|
||||
unitId,
|
||||
subUnitId,
|
||||
filterRange,
|
||||
});
|
||||
}
|
||||
|
||||
return result;
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* A {@link CommandType.COMMAND} to toggle filter in the current {@link Worksheet}.
|
||||
*/
|
||||
export const SmartToggleSheetsFilterCommand: ICommand = {
|
||||
id: 'sheet.command.smart-toggle-filter',
|
||||
type: CommandType.COMMAND,
|
||||
handler: async (accessor: IAccessor) => {
|
||||
const univerInstanceService = accessor.get(IUniverInstanceService);
|
||||
const sheetsFilterService = accessor.get(SheetsFilterService);
|
||||
const commandService = accessor.get(ICommandService);
|
||||
|
||||
const currentWorkbook = univerInstanceService.getCurrentUnitOfType<Workbook>(UniverInstanceType.UNIVER_SHEET);
|
||||
const currentWorksheet = currentWorkbook?.getActiveSheet();
|
||||
if (!currentWorksheet || !currentWorkbook) return false;
|
||||
|
||||
const unitId = currentWorkbook.getUnitId();
|
||||
const subUnitId = currentWorksheet.getSheetId();
|
||||
|
||||
// If there is a filter model, we should remove it and prepare undo redo.
|
||||
const filterModel = sheetsFilterService.getFilterModel(unitId, subUnitId);
|
||||
if (filterModel) {
|
||||
return commandService.executeCommand(RemoveSheetFilterCommand.id, { unitId, subUnitId });
|
||||
}
|
||||
|
||||
// Other wise we should set the filter range to the current selection.
|
||||
const selectionManager = accessor.get(SheetsSelectionsService);
|
||||
const lastSelection = selectionManager.getCurrentLastSelection();
|
||||
if (!lastSelection) return false;
|
||||
|
||||
const startRange = lastSelection.range;
|
||||
const targetFilterRange =
|
||||
// If the selection is a single cell, we should expand it to a continuous range in all directions.
|
||||
isSingleCellSelection(lastSelection)
|
||||
? expandToContinuousRange(startRange, { left: true, right: true, up: true, down: true }, currentWorksheet)
|
||||
// If the selection is only a single row, we should expand it downwards.
|
||||
: startRange.startRow === startRange.endRow
|
||||
? expandToContinuousRange(startRange, { down: true }, currentWorksheet)
|
||||
// Otherwise, we just use the selected range.
|
||||
: startRange;
|
||||
|
||||
return commandService.executeCommand(SetSheetFilterRangeCommand.id, {
|
||||
unitId,
|
||||
subUnitId,
|
||||
range: targetFilterRange,
|
||||
} as ISetSheetFilterRangeCommandParams);
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* Parameters of command {@link SetSheetsFilterCriteriaCommand}.
|
||||
* @property {number} col - the column index of the filter criteria
|
||||
* @property {Nullable<IFilterColumn>} criteria - the filter criteria to be set
|
||||
*/
|
||||
export interface ISetSheetsFilterCriteriaCommandParams extends ISheetCommandSharedParams {
|
||||
col: number;
|
||||
criteria: Nullable<IFilterColumn>;
|
||||
}
|
||||
|
||||
/**
|
||||
* A {@link CommandType.COMMAND} to set filter criteria to a column in the targeting {@link FilterModel}. Its params
|
||||
* {@link ISetSheetsFilterCriteriaCommandParams} is required.
|
||||
*/
|
||||
export const SetSheetsFilterCriteriaCommand: ICommand<ISetSheetsFilterCriteriaCommandParams> = {
|
||||
id: 'sheet.command.set-filter-criteria',
|
||||
type: CommandType.COMMAND,
|
||||
handler: (accessor: IAccessor, params: ISetSheetsFilterCriteriaCommandParams) => {
|
||||
const target = getSheetCommandTarget(accessor.get(IUniverInstanceService), params);
|
||||
if (!target) return false;
|
||||
|
||||
const sheetsFilterService = accessor.get(SheetsFilterService);
|
||||
const { unitId, subUnitId } = target;
|
||||
const filterModel = sheetsFilterService.getFilterModel(unitId, subUnitId);
|
||||
if (!filterModel) return false;
|
||||
|
||||
const { col, criteria } = params;
|
||||
const filterRange = filterModel.getRange();
|
||||
if (!filterRange || col < filterRange.startColumn || col > filterRange.endColumn) return false;
|
||||
|
||||
const commandService = accessor.get(ICommandService);
|
||||
const undoRedoService = accessor.get(IUndoRedoService);
|
||||
|
||||
const filterColumn = filterModel.getFilterColumn(col);
|
||||
const undoMutation = destructFilterColumn(unitId, subUnitId, col, filterColumn);
|
||||
const redoMutation: IMutationInfo<ISetSheetsFilterCriteriaMutationParams> = {
|
||||
id: SetSheetsFilterCriteriaMutation.id,
|
||||
params: {
|
||||
unitId,
|
||||
subUnitId,
|
||||
col,
|
||||
criteria,
|
||||
},
|
||||
};
|
||||
|
||||
const result = commandService.syncExecuteCommand(redoMutation.id, redoMutation.params);
|
||||
if (result) {
|
||||
undoRedoService.pushUndoRedo({
|
||||
unitID: unitId,
|
||||
undoMutations: [undoMutation],
|
||||
redoMutations: [redoMutation],
|
||||
});
|
||||
commandService.executeCommand<IMarkDirtyFilterChangeMutationParams>(MarkDirtyFilterChangeMutation.id, {
|
||||
unitId,
|
||||
subUnitId,
|
||||
filterRange,
|
||||
});
|
||||
}
|
||||
|
||||
return result;
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* A {@link CommandType.COMMAND} to clear all filter criteria in the targeting {@link FilterModel}. Its params
|
||||
* {@link ISheetCommandSharedParams} is required.
|
||||
*/
|
||||
export const ClearSheetsFilterCriteriaCommand: ICommand<ISheetCommandSharedParams> = {
|
||||
id: 'sheet.command.clear-filter-criteria',
|
||||
type: CommandType.COMMAND,
|
||||
handler: (accessor: IAccessor, params) => {
|
||||
const target = getSheetCommandTarget(accessor.get(IUniverInstanceService), params);
|
||||
if (!target) return false;
|
||||
|
||||
const sheetsFilterService = accessor.get(SheetsFilterService);
|
||||
const { unitId, subUnitId } = target;
|
||||
const filterModel = sheetsFilterService.getFilterModel(unitId, subUnitId);
|
||||
if (!filterModel) return false;
|
||||
|
||||
const filterRange = filterModel.getRange();
|
||||
if (!filterRange) return false;
|
||||
|
||||
const undoRedoService = accessor.get(IUndoRedoService);
|
||||
const commandService = accessor.get(ICommandService);
|
||||
|
||||
const autoFilter = filterModel.serialize();
|
||||
const undoMutations = destructFilterCriteria(unitId, subUnitId, autoFilter);
|
||||
const redoMutations = generateRemoveCriteriaMutations(unitId, subUnitId, autoFilter);
|
||||
|
||||
const result = sequenceExecute(redoMutations, commandService);
|
||||
if (result.result) {
|
||||
undoRedoService.pushUndoRedo({
|
||||
unitID: unitId,
|
||||
undoMutations,
|
||||
redoMutations,
|
||||
});
|
||||
commandService.executeCommand<IMarkDirtyFilterChangeMutationParams>(MarkDirtyFilterChangeMutation.id, {
|
||||
unitId,
|
||||
subUnitId,
|
||||
filterRange,
|
||||
});
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* A {@link CommandType.COMMAND} forcing the currently active {@link FilterModel} to re-calculate all filter criteria.
|
||||
* Its params {@link ISheetCommandSharedParams} is required.
|
||||
*/
|
||||
export const ReCalcSheetsFilterCommand: ICommand<ISheetCommandSharedParams> = {
|
||||
id: 'sheet.command.re-calc-filter',
|
||||
type: CommandType.COMMAND,
|
||||
handler: (accessor, params) => {
|
||||
const sheetsFilterService = accessor.get(SheetsFilterService);
|
||||
const commandService = accessor.get(ICommandService);
|
||||
const instanceSrv = accessor.get(IUniverInstanceService);
|
||||
|
||||
const commandTarget = getSheetCommandTarget(instanceSrv, params);
|
||||
if (!commandTarget) return false;
|
||||
|
||||
const { unitId, subUnitId } = commandTarget;
|
||||
const filterModel = sheetsFilterService.getFilterModel(commandTarget.unitId, commandTarget.subUnitId);
|
||||
if (!filterModel) return false;
|
||||
|
||||
// No need to handle undo redo for this command.
|
||||
return commandService.executeCommand(ReCalcSheetsFilterMutation.id, { unitId, subUnitId } as ISheetCommandSharedParams);
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* Destruct a `FilterModel` to a list of mutations.
|
||||
* @param {string} unitId - the unit id of the Workbook
|
||||
* @param {string} subUnitId - the sub unit id of the Worksheet
|
||||
* @param {IAutoFilter} autoFilter - the to be destructed FilterModel
|
||||
* @returns a list of mutations those can be used to reconstruct the FilterModel
|
||||
*/
|
||||
function destructFilterModel(
|
||||
unitId: string,
|
||||
subUnitId: string,
|
||||
autoFilter: IAutoFilter
|
||||
): IMutationInfo[] {
|
||||
const mutations: IMutationInfo[] = [];
|
||||
|
||||
const setFilterMutation: IMutationInfo<ISetSheetsFilterRangeMutationParams> = {
|
||||
id: SetSheetsFilterRangeMutation.id,
|
||||
params: {
|
||||
unitId,
|
||||
subUnitId,
|
||||
range: autoFilter.ref,
|
||||
},
|
||||
};
|
||||
mutations.push(setFilterMutation);
|
||||
|
||||
const criteriaMutations = destructFilterCriteria(unitId, subUnitId, autoFilter);
|
||||
criteriaMutations.forEach((m) => mutations.push(m));
|
||||
|
||||
return mutations;
|
||||
}
|
||||
|
||||
/**
|
||||
* Transform a {@link FilterModel} to a list of mutations to set the filter criteria.
|
||||
* @param unitId - the unit id of the {@link Workbook}
|
||||
* @param subUnitId - the sub unit id of the {@link Worksheet}
|
||||
* @param autoFilter - the to be destructed {@link FilterModel}
|
||||
* @returns {IMutationInfo<ISetSheetsFilterCriteriaMutationParams>} a list of mutations those can be used to
|
||||
* reconstruct the {@link FilterModel}
|
||||
*/
|
||||
export function destructFilterCriteria(
|
||||
unitId: string,
|
||||
subUnitId: string,
|
||||
autoFilter: IAutoFilter
|
||||
): IMutationInfo<ISetSheetsFilterCriteriaMutationParams>[] {
|
||||
const mutations: IMutationInfo<ISetSheetsFilterCriteriaMutationParams>[] = [];
|
||||
|
||||
autoFilter.filterColumns?.forEach((filterColumn) => {
|
||||
const setFilterCriteriaMutation: IMutationInfo<ISetSheetsFilterCriteriaMutationParams> = {
|
||||
id: SetSheetsFilterCriteriaMutation.id,
|
||||
params: {
|
||||
unitId,
|
||||
subUnitId,
|
||||
col: filterColumn.colId,
|
||||
criteria: filterColumn,
|
||||
},
|
||||
};
|
||||
mutations.push(setFilterCriteriaMutation);
|
||||
});
|
||||
|
||||
return mutations;
|
||||
}
|
||||
|
||||
/** Generate mutations to remove all criteria on a `FilterModel` */
|
||||
function generateRemoveCriteriaMutations(
|
||||
unitId: string,
|
||||
subUnitId: string,
|
||||
autoFilter: IAutoFilter
|
||||
): IMutationInfo[] {
|
||||
const mutations: IMutationInfo[] = [];
|
||||
|
||||
autoFilter.filterColumns?.forEach((filterColumn) => {
|
||||
const removeFilterCriteriaMutation: IMutationInfo<ISetSheetsFilterCriteriaMutationParams> = {
|
||||
id: SetSheetsFilterCriteriaMutation.id,
|
||||
params: {
|
||||
unitId,
|
||||
subUnitId,
|
||||
col: filterColumn.colId,
|
||||
criteria: null,
|
||||
},
|
||||
};
|
||||
mutations.push(removeFilterCriteriaMutation);
|
||||
});
|
||||
|
||||
return mutations;
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepare the undo mutation, it should rollback to the old criteria if there's already a `FilterColumn`,
|
||||
* or remove the filter criteria when there is no `FilterColumn`.
|
||||
* @param {string} unitId
|
||||
* @param {string} subUnitId
|
||||
* @param {number} colId
|
||||
* @param {Nullable<FilterColumn>} filterColumn
|
||||
* @returns {IMutationInfo<ISetSheetsFilterCriteriaMutationParams>} the undo mutation
|
||||
*/
|
||||
function destructFilterColumn(
|
||||
unitId: string,
|
||||
subUnitId: string,
|
||||
colId: number,
|
||||
filterColumn: Nullable<FilterColumn>
|
||||
): IMutationInfo<ISetSheetsFilterCriteriaMutationParams> {
|
||||
if (!filterColumn) {
|
||||
return {
|
||||
id: SetSheetsFilterCriteriaMutation.id,
|
||||
params: {
|
||||
unitId,
|
||||
subUnitId,
|
||||
col: colId,
|
||||
criteria: null,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
const serialize = filterColumn.serialize();
|
||||
return {
|
||||
id: SetSheetsFilterCriteriaMutation.id,
|
||||
params: {
|
||||
unitId,
|
||||
subUnitId,
|
||||
col: colId,
|
||||
criteria: serialize,
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,273 @@
|
||||
/**
|
||||
* Copyright 2023-present DreamNum Co., Ltd.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import type { IWorkbookData } from '@univerjs/core';
|
||||
import type { ISetSheetsFilterCriteriaMutationParams, ISetSheetsFilterRangeMutationParams } from '../sheets-filter.mutation';
|
||||
import { ICommandService, Inject, Injector, LocaleType, Plugin, Univer, UniverInstanceType } from '@univerjs/core';
|
||||
import { afterEach, describe, expect, it } from 'vitest';
|
||||
import { SHEET_FILTER_SNAPSHOT_ID, SheetsFilterService } from '../../../services/sheet-filter.service';
|
||||
import { ReCalcSheetsFilterMutation, RemoveSheetsFilterMutation, SetSheetsFilterCriteriaMutation, SetSheetsFilterRangeMutation } from '../sheets-filter.mutation';
|
||||
|
||||
function testWorkbookDataFactory(): IWorkbookData {
|
||||
return {
|
||||
id: 'test',
|
||||
appVersion: '3.0.0-alpha',
|
||||
sheets: {
|
||||
sheet1: {
|
||||
id: 'sheet1',
|
||||
cellData: {
|
||||
0: {
|
||||
0: {
|
||||
v: 'A1',
|
||||
},
|
||||
1: {
|
||||
v: 'B1',
|
||||
},
|
||||
},
|
||||
},
|
||||
name: 'Sheet-001',
|
||||
},
|
||||
},
|
||||
locale: LocaleType.ZH_CN,
|
||||
name: '',
|
||||
sheetOrder: [],
|
||||
styles: {},
|
||||
};
|
||||
};
|
||||
|
||||
function testWorkbookDataWithFilterFactory(): IWorkbookData {
|
||||
return {
|
||||
id: 'test',
|
||||
appVersion: '3.0.0-alpha',
|
||||
sheets: {
|
||||
sheet1: {
|
||||
id: 'sheet1',
|
||||
cellData: {
|
||||
0: {
|
||||
0: {
|
||||
v: 'header',
|
||||
},
|
||||
1: {
|
||||
v: 'A',
|
||||
},
|
||||
2: {
|
||||
v: 'B',
|
||||
},
|
||||
3: {
|
||||
v: 'C',
|
||||
},
|
||||
},
|
||||
},
|
||||
name: 'Sheet-001',
|
||||
},
|
||||
},
|
||||
resources: [
|
||||
{
|
||||
name: SHEET_FILTER_SNAPSHOT_ID,
|
||||
data: JSON.stringify({
|
||||
sheet1: {
|
||||
ref: { startRow: 0, startColumn: 0, endRow: 5, endColumn: 5 },
|
||||
},
|
||||
}),
|
||||
},
|
||||
],
|
||||
locale: LocaleType.ZH_CN,
|
||||
name: '',
|
||||
sheetOrder: [],
|
||||
styles: {},
|
||||
};
|
||||
}
|
||||
|
||||
function createFilterMutationTestBed(workbookData?: IWorkbookData) {
|
||||
const univer = new Univer();
|
||||
const injector = univer.__getInjector();
|
||||
const get = injector.get.bind(injector);
|
||||
|
||||
class SheetsFilterTestPlugin extends Plugin {
|
||||
static override type = UniverInstanceType.UNIVER_SHEET;
|
||||
static override pluginName = 'SheetsFilterTestPlugin';
|
||||
|
||||
constructor(_config: unknown, @Inject(Injector) protected readonly _injector: Injector) {
|
||||
super();
|
||||
}
|
||||
|
||||
override onStarting(): void {
|
||||
this._injector.add([SheetsFilterService]);
|
||||
}
|
||||
}
|
||||
|
||||
univer.registerPlugin(SheetsFilterTestPlugin);
|
||||
|
||||
const sheet = univer.createUnit(UniverInstanceType.UNIVER_SHEET, workbookData || testWorkbookDataFactory());
|
||||
|
||||
const sheetsFilterService = get(SheetsFilterService);
|
||||
const commandService = get(ICommandService);
|
||||
|
||||
// It should be registered later in avoid of time sequence problem.
|
||||
// injector.add([ISnapshotPersistenceService, { useClass: LocalSnapshotService }]);
|
||||
// injector.get(ISnapshotPersistenceService);
|
||||
|
||||
([
|
||||
SetSheetsFilterRangeMutation,
|
||||
SetSheetsFilterCriteriaMutation,
|
||||
RemoveSheetsFilterMutation,
|
||||
ReCalcSheetsFilterMutation,
|
||||
]
|
||||
).forEach((command) => commandService.registerCommand(command));
|
||||
|
||||
return {
|
||||
univer,
|
||||
get,
|
||||
sheet,
|
||||
sheetsFilterService,
|
||||
commandService,
|
||||
};
|
||||
}
|
||||
|
||||
describe('test mutations of sheets filter', () => {
|
||||
let univer: Univer;
|
||||
|
||||
afterEach(() => {
|
||||
univer.dispose();
|
||||
});
|
||||
|
||||
describe('test set sheets filter range mutations', () => {
|
||||
it('should set filter range work when there is no filter range', () => {
|
||||
const testBed = createFilterMutationTestBed(testWorkbookDataFactory());
|
||||
const { commandService, sheetsFilterService } = testBed;
|
||||
univer = testBed.univer;
|
||||
|
||||
expect(sheetsFilterService.getFilterModel('test', 'sheet1')).toBeNull();
|
||||
|
||||
expect(commandService.syncExecuteCommand(SetSheetsFilterRangeMutation.id, {
|
||||
unitId: 'test',
|
||||
subUnitId: 'sheet1',
|
||||
range: { startRow: 0, startColumn: 0, endRow: 5, endColumn: 5 },
|
||||
} as ISetSheetsFilterRangeMutationParams)).toBeTruthy();
|
||||
expect(sheetsFilterService.getFilterModel('test', 'sheet1')!.getRange())
|
||||
.toEqual({ startRow: 0, startColumn: 0, endRow: 5, endColumn: 5 });
|
||||
});
|
||||
|
||||
it('should load filter info to the service and support changing its range', () => {
|
||||
const testBed = createFilterMutationTestBed(testWorkbookDataWithFilterFactory());
|
||||
const { commandService, sheetsFilterService } = testBed;
|
||||
univer = testBed.univer;
|
||||
|
||||
const filterModel = sheetsFilterService.getFilterModel('test', 'sheet1');
|
||||
expect(filterModel).toBeTruthy();
|
||||
expect(filterModel!.getRange()).toEqual({ startRow: 0, startColumn: 0, endRow: 5, endColumn: 5 });
|
||||
|
||||
expect(commandService.syncExecuteCommand(SetSheetsFilterRangeMutation.id, {
|
||||
unitId: 'test',
|
||||
subUnitId: 'sheet1',
|
||||
range: { startRow: 5, startColumn: 5, endRow: 8, endColumn: 8 },
|
||||
} as ISetSheetsFilterRangeMutationParams)).toBeTruthy();
|
||||
expect(sheetsFilterService.getFilterModel('test', 'sheet1')!.getRange())
|
||||
.toEqual({ startRow: 5, startColumn: 5, endRow: 8, endColumn: 8 });
|
||||
});
|
||||
});
|
||||
|
||||
describe('test set sheets filter criteria mutations', () => {
|
||||
it('should re calc when set filter criteria', () => {
|
||||
const testBed = createFilterMutationTestBed(testWorkbookDataWithFilterFactory());
|
||||
const { commandService, sheetsFilterService } = testBed;
|
||||
univer = testBed.univer;
|
||||
|
||||
const filterModel = sheetsFilterService.getFilterModel('test', 'sheet1');
|
||||
expect(filterModel).toBeTruthy();
|
||||
expect(filterModel!.getRange()).toEqual({ startRow: 0, startColumn: 0, endRow: 5, endColumn: 5 });
|
||||
expect(filterModel!.filteredOutRows.size).toBe(0);
|
||||
|
||||
expect(commandService.syncExecuteCommand(SetSheetsFilterCriteriaMutation.id, {
|
||||
unitId: 'test',
|
||||
subUnitId: 'sheet1',
|
||||
col: 0,
|
||||
criteria: {
|
||||
colId: 0,
|
||||
filters: ['A'],
|
||||
},
|
||||
} as ISetSheetsFilterCriteriaMutationParams)).toBeTruthy();
|
||||
expect(filterModel!.filteredOutRows.size).toBe(5);
|
||||
});
|
||||
|
||||
it('should return false when set filter criteria on a non-existing filter model', () => {
|
||||
const testBed = createFilterMutationTestBed(testWorkbookDataWithFilterFactory());
|
||||
const { commandService, sheetsFilterService } = testBed;
|
||||
univer = testBed.univer;
|
||||
|
||||
const filterModel = sheetsFilterService.getFilterModel('test', 'sheet1');
|
||||
expect(filterModel).toBeTruthy();
|
||||
expect(filterModel!.getRange()).toEqual({ startRow: 0, startColumn: 0, endRow: 5, endColumn: 5 });
|
||||
expect(filterModel!.filteredOutRows.size).toBe(0);
|
||||
|
||||
expect(commandService.syncExecuteCommand(SetSheetsFilterCriteriaMutation.id, {
|
||||
unitId: 'test',
|
||||
subUnitId: 'sheet2',
|
||||
colId: 0,
|
||||
criteria: {
|
||||
colId: 0,
|
||||
filters: ['A'],
|
||||
},
|
||||
})).toBeFalsy();
|
||||
});
|
||||
|
||||
it('should throw error when set filter criteria outside of filter range', () => {
|
||||
const testBed = createFilterMutationTestBed(testWorkbookDataWithFilterFactory());
|
||||
const { commandService, sheetsFilterService } = testBed;
|
||||
univer = testBed.univer;
|
||||
|
||||
const filterModel = sheetsFilterService.getFilterModel('test', 'sheet1');
|
||||
expect(filterModel).toBeTruthy();
|
||||
expect(filterModel!.getRange()).toEqual({ startRow: 0, startColumn: 0, endRow: 5, endColumn: 5 });
|
||||
expect(filterModel!.filteredOutRows.size).toBe(0);
|
||||
|
||||
expect(() => commandService.syncExecuteCommand(SetSheetsFilterCriteriaMutation.id, {
|
||||
unitId: 'test',
|
||||
subUnitId: 'sheet1',
|
||||
colId: 10,
|
||||
criteria: {
|
||||
colId: 10,
|
||||
filters: ['A'],
|
||||
},
|
||||
})).toThrowError();
|
||||
});
|
||||
});
|
||||
|
||||
describe('test remove sheets filter mutations', () => {
|
||||
it('should return true when there is a filter', () => {
|
||||
const testBed = createFilterMutationTestBed(testWorkbookDataWithFilterFactory());
|
||||
const { commandService, sheetsFilterService } = testBed;
|
||||
univer = testBed.univer;
|
||||
|
||||
expect(commandService.syncExecuteCommand(RemoveSheetsFilterMutation.id, {
|
||||
unitId: 'test',
|
||||
subUnitId: 'sheet1',
|
||||
})).toBeTruthy();
|
||||
expect(sheetsFilterService.getFilterModel('test', 'sheet1')).toBeNull();
|
||||
});
|
||||
|
||||
it('should return false when there is no filter', () => {
|
||||
const testBed = createFilterMutationTestBed(testWorkbookDataFactory());
|
||||
const { commandService } = testBed;
|
||||
univer = testBed.univer;
|
||||
|
||||
expect(commandService.syncExecuteCommand(RemoveSheetsFilterMutation.id, {
|
||||
unitId: 'test',
|
||||
subUnitId: 'sheet1',
|
||||
})).toBeFalsy();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,119 @@
|
||||
/**
|
||||
* Copyright 2023-present DreamNum Co., Ltd.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import type { IMutation, IRange, Nullable } from '@univerjs/core';
|
||||
import type { ISheetCommandSharedParams } from '@univerjs/sheets';
|
||||
import type { IFilterColumn } from '../../models/types';
|
||||
import { CommandType } from '@univerjs/core';
|
||||
import {
|
||||
ReCalcSheetsFilterMutationId,
|
||||
RemoveSheetsFilterMutationId,
|
||||
SetSheetsFilterCriteriaMutationId,
|
||||
SetSheetsFilterRangeMutationId,
|
||||
} from '../../common/const';
|
||||
import { SheetsFilterService } from '../../services/sheet-filter.service';
|
||||
|
||||
/**
|
||||
* Parameters of mutation {@link SetSheetsFilterRangeMutation}.
|
||||
* @property range - the range to be set as filter range.
|
||||
*/
|
||||
export interface ISetSheetsFilterRangeMutationParams extends ISheetCommandSharedParams {
|
||||
range: IRange;
|
||||
}
|
||||
|
||||
/**
|
||||
* A {@link CommandType.MUTATION} to set filter range in a {@link Worksheet}. If no {@link FilterModel} exists,
|
||||
* a new `FilterModel` will be created.
|
||||
*
|
||||
* Since there could only be a filter on a worksheet, when you want to update the range, you
|
||||
* don't necessarily need to remove the filter first, you can just execute this mutation.
|
||||
*/
|
||||
export const SetSheetsFilterRangeMutation: IMutation<ISetSheetsFilterRangeMutationParams> = {
|
||||
id: SetSheetsFilterRangeMutationId,
|
||||
type: CommandType.MUTATION,
|
||||
handler: (accessor, params) => {
|
||||
const { subUnitId, unitId, range } = params;
|
||||
const sheetsFilterService = accessor.get(SheetsFilterService);
|
||||
|
||||
// check if the range is in bound?
|
||||
const filterModel = sheetsFilterService.ensureFilterModel(unitId, subUnitId);
|
||||
filterModel.setRange(range);
|
||||
|
||||
return true;
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* Parameters of mutation {@link SetSheetsFilterCriteriaMutation}.
|
||||
* @property {number} col - the column index to set filter criteria.
|
||||
* @property {IFilterColumn | null} criteria - the filter criteria to set. If it is `null`, the criteria will be removed.
|
||||
* @property {boolean} [reCalc=true] - if it should trigger calculation on this `FilterColumn`.
|
||||
*/
|
||||
export interface ISetSheetsFilterCriteriaMutationParams extends ISheetCommandSharedParams {
|
||||
col: number;
|
||||
criteria: Nullable<IFilterColumn>;
|
||||
reCalc?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* A {@link CommandType.MUTATION} to set filter criteria of a given column of a {@link FilterModel}.
|
||||
*/
|
||||
export const SetSheetsFilterCriteriaMutation: IMutation<ISetSheetsFilterCriteriaMutationParams> = {
|
||||
id: SetSheetsFilterCriteriaMutationId,
|
||||
type: CommandType.MUTATION,
|
||||
handler: (accessor, params) => {
|
||||
const { subUnitId, unitId, criteria, col, reCalc = true } = params;
|
||||
const sheetsFilterService = accessor.get(SheetsFilterService);
|
||||
|
||||
const filterModel = sheetsFilterService.getFilterModel(unitId, subUnitId);
|
||||
if (!filterModel) return false;
|
||||
|
||||
filterModel.setCriteria(col, criteria, reCalc);
|
||||
return true;
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* A {@link CommandType.MUTATION} to remove a {@link FilterModel} in a {@link Worksheet}.
|
||||
*/
|
||||
export const RemoveSheetsFilterMutation: IMutation<ISheetCommandSharedParams> = {
|
||||
id: RemoveSheetsFilterMutationId,
|
||||
type: CommandType.MUTATION,
|
||||
handler: (accessor, params) => {
|
||||
const { unitId, subUnitId } = params;
|
||||
const sheetsFilterService = accessor.get(SheetsFilterService);
|
||||
return sheetsFilterService.removeFilterModel(unitId, subUnitId);
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* A {@link CommandType.MUTATION} to re-calculate a {@link FilterModel}.
|
||||
*/
|
||||
export const ReCalcSheetsFilterMutation: IMutation<ISheetCommandSharedParams> = {
|
||||
id: ReCalcSheetsFilterMutationId,
|
||||
type: CommandType.MUTATION,
|
||||
handler: (accessor, params) => {
|
||||
const { unitId, subUnitId } = params;
|
||||
const sheetsFilterService = accessor.get(SheetsFilterService);
|
||||
const filterModel = sheetsFilterService.getFilterModel(unitId, subUnitId);
|
||||
if (!filterModel) {
|
||||
return false;
|
||||
}
|
||||
|
||||
filterModel.reCalc();
|
||||
return true;
|
||||
},
|
||||
};
|
||||
27
packages/sheets-filter/src/common/const.ts
Normal file
27
packages/sheets-filter/src/common/const.ts
Normal file
@@ -0,0 +1,27 @@
|
||||
/**
|
||||
* Copyright 2023-present DreamNum Co., Ltd.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
export const SetSheetsFilterRangeMutationId = 'sheet.mutation.set-filter-range';
|
||||
export const SetSheetsFilterCriteriaMutationId = 'sheet.mutation.set-filter-criteria';
|
||||
export const RemoveSheetsFilterMutationId = 'sheet.mutation.remove-filter';
|
||||
export const ReCalcSheetsFilterMutationId = 'sheet.mutation.re-calc-filter';
|
||||
|
||||
export const FILTER_MUTATIONS = new Set([
|
||||
SetSheetsFilterRangeMutationId,
|
||||
SetSheetsFilterCriteriaMutationId,
|
||||
RemoveSheetsFilterMutationId,
|
||||
ReCalcSheetsFilterMutationId,
|
||||
]);
|
||||
31
packages/sheets-filter/src/config/config.ts
Normal file
31
packages/sheets-filter/src/config/config.ts
Normal file
@@ -0,0 +1,31 @@
|
||||
/**
|
||||
* Copyright 2023-present DreamNum Co., Ltd.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
export const SHEETS_FILTER_PLUGIN_CONFIG_KEY = 'sheets-filter.config';
|
||||
|
||||
export const configSymbol = Symbol(SHEETS_FILTER_PLUGIN_CONFIG_KEY);
|
||||
|
||||
export interface IUniverSheetsFilterConfig {
|
||||
/**
|
||||
* Whether to display the filter sync switch in the filter panel.
|
||||
* This configuration can be used in a collaboration scenario to allow users to choose whether to enable filter synchronization.
|
||||
* If set to a defaultValue object, the filter sync switch will be displayed and its initial state will be determined by the defaultValue property.
|
||||
* @default false
|
||||
*/
|
||||
enableSyncSwitch?: boolean | { defaultValue: boolean };
|
||||
}
|
||||
|
||||
export const defaultPluginConfig: IUniverSheetsFilterConfig = {};
|
||||
@@ -0,0 +1,422 @@
|
||||
/**
|
||||
* Copyright 2023-present DreamNum Co., Ltd.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
/* eslint-disable perfectionist/sort-imports */
|
||||
|
||||
import type { IWorkbookData, Workbook } from '@univerjs/core';
|
||||
import { ICommandService, Inject, Injector, IUniverInstanceService, LocaleService, LocaleType, Plugin, RANGE_TYPE, UndoCommand, Univer, UniverInstanceType } from '@univerjs/core';
|
||||
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
|
||||
import type { ISetRangeValuesMutationParams } from '@univerjs/sheets';
|
||||
import { CopySheetCommand, InsertColByRangeCommand, InsertColMutation, InsertRowByRangeCommand, InsertSheetMutation, MoveColsCommand, MoveColsMutation, MoveRangeCommand, MoveRangeMutation, MoveRowsCommand, MoveRowsMutation, RefRangeService, RemoveColByRangeCommand, RemoveColCommand, RemoveColMutation, RemoveRowByRangeCommand, RemoveRowCommand, RemoveRowMutation, SetRangeValuesMutation, SetSelectionsOperation, SheetInterceptorService, SheetLazyExecuteScheduleService, SheetRangeThemeModel, SheetsSelectionsService, ZebraCrossingCacheController } from '@univerjs/sheets';
|
||||
import { SHEET_FILTER_SNAPSHOT_ID, SheetsFilterService } from '../../services/sheet-filter.service';
|
||||
import { SheetsFilterController } from '../sheets-filter.controller';
|
||||
import { SetSheetsFilterCriteriaMutation } from '../../commands/mutations/sheets-filter.mutation';
|
||||
|
||||
function testWorkbookDataWithFilterFactory(): IWorkbookData {
|
||||
return {
|
||||
id: 'test',
|
||||
appVersion: '3.0.0-alpha',
|
||||
sheets: {
|
||||
sheet1: {
|
||||
id: 'sheet1',
|
||||
cellData: {
|
||||
0: {
|
||||
0: {
|
||||
v: 'A1',
|
||||
},
|
||||
1: {
|
||||
v: 'B1',
|
||||
},
|
||||
2: {
|
||||
v: 'C1',
|
||||
},
|
||||
3: {
|
||||
v: 'D1',
|
||||
},
|
||||
},
|
||||
1: {
|
||||
0: {
|
||||
v: 'A2',
|
||||
},
|
||||
1: {
|
||||
v: 'B2',
|
||||
},
|
||||
2: {
|
||||
v: 'C2',
|
||||
},
|
||||
3: {
|
||||
v: 'D2',
|
||||
},
|
||||
},
|
||||
2: {
|
||||
0: {
|
||||
v: 'A3',
|
||||
},
|
||||
1: {
|
||||
v: 'B3',
|
||||
},
|
||||
2: {
|
||||
v: 'C3',
|
||||
},
|
||||
3: {
|
||||
v: 'D3',
|
||||
},
|
||||
},
|
||||
3: {
|
||||
0: {
|
||||
v: 'A',
|
||||
},
|
||||
1: {
|
||||
v: 'B',
|
||||
},
|
||||
2: {
|
||||
v: 'C',
|
||||
},
|
||||
3: {
|
||||
v: 'D',
|
||||
},
|
||||
},
|
||||
4: {
|
||||
0: {
|
||||
v: 'H',
|
||||
},
|
||||
},
|
||||
},
|
||||
name: 'Sheet-001',
|
||||
},
|
||||
},
|
||||
resources: [
|
||||
{
|
||||
name: SHEET_FILTER_SNAPSHOT_ID,
|
||||
data: JSON.stringify({
|
||||
sheet1: {
|
||||
ref: { startRow: 3, startColumn: 0, endRow: 5, endColumn: 5 },
|
||||
filterColumns: [{ colId: 0, filters: { blank: true } }],
|
||||
cachedFilteredOut: [4],
|
||||
},
|
||||
}),
|
||||
},
|
||||
],
|
||||
locale: LocaleType.ZH_CN,
|
||||
name: '',
|
||||
sheetOrder: [],
|
||||
styles: {},
|
||||
};
|
||||
}
|
||||
|
||||
function createFilterControllerTestBed(workbookData?: IWorkbookData) {
|
||||
const univer = new Univer();
|
||||
const injector = univer.__getInjector();
|
||||
const get = injector.get.bind(injector);
|
||||
|
||||
class SheetsFilterTestPlugin extends Plugin {
|
||||
static override type = UniverInstanceType.UNIVER_SHEET;
|
||||
static override pluginName = 'SheetsFilterTestPlugin';
|
||||
|
||||
constructor(_config: unknown, @Inject(Injector) protected readonly _injector: Injector) {
|
||||
super();
|
||||
}
|
||||
|
||||
override onStarting(): void {
|
||||
this._injector.add([RefRangeService]);
|
||||
this._injector.add([SheetsFilterService]);
|
||||
this._injector.add([SheetRangeThemeModel]);
|
||||
this._injector.add([ZebraCrossingCacheController]);
|
||||
this._injector.add([SheetsFilterController]);
|
||||
this._injector.add([SheetsSelectionsService]);
|
||||
this._injector.add([SheetInterceptorService]);
|
||||
this._injector.add([SheetLazyExecuteScheduleService]);
|
||||
}
|
||||
|
||||
override onReady(): void {
|
||||
this._injector.get(SheetsFilterController);
|
||||
}
|
||||
}
|
||||
|
||||
univer.registerPlugin(SheetsFilterTestPlugin);
|
||||
|
||||
const sheet = univer.createUnit(UniverInstanceType.UNIVER_SHEET, workbookData ?? testWorkbookDataWithFilterFactory());
|
||||
|
||||
const sheetsFilterService = get(SheetsFilterService);
|
||||
const commandService = get(ICommandService);
|
||||
|
||||
([
|
||||
MoveRowsCommand,
|
||||
MoveRowsMutation,
|
||||
MoveColsCommand,
|
||||
MoveColsMutation,
|
||||
InsertColMutation,
|
||||
InsertColByRangeCommand,
|
||||
InsertRowByRangeCommand,
|
||||
RemoveRowCommand,
|
||||
RemoveRowMutation,
|
||||
RemoveColCommand,
|
||||
RemoveColMutation,
|
||||
RemoveColByRangeCommand,
|
||||
RemoveRowByRangeCommand,
|
||||
SetSelectionsOperation,
|
||||
SetRangeValuesMutation,
|
||||
CopySheetCommand,
|
||||
InsertSheetMutation,
|
||||
MoveRangeCommand,
|
||||
MoveRangeMutation,
|
||||
]
|
||||
).forEach((command) => commandService.registerCommand(command));
|
||||
|
||||
return {
|
||||
univer,
|
||||
get,
|
||||
sheet,
|
||||
sheetsFilterService,
|
||||
commandService,
|
||||
};
|
||||
}
|
||||
|
||||
describe('test controller of sheets filter', () => {
|
||||
let univer: Univer;
|
||||
let get: Injector['get'];
|
||||
let commandService: ICommandService;
|
||||
let sheetsFilterService: SheetsFilterService;
|
||||
let instanceService: IUniverInstanceService;
|
||||
|
||||
beforeEach(() => {
|
||||
const testBed = createFilterControllerTestBed();
|
||||
univer = testBed.univer;
|
||||
get = testBed.get;
|
||||
|
||||
get(LocaleService).load({ zhCN: {} });
|
||||
|
||||
commandService = get(ICommandService);
|
||||
sheetsFilterService = get(SheetsFilterService);
|
||||
instanceService = get(IUniverInstanceService);
|
||||
instanceService.setCurrentUnitForType('test');
|
||||
instanceService.focusUnit('test');
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
univer.dispose();
|
||||
});
|
||||
|
||||
describe('test interceptor of move-rows-command', () => {
|
||||
it('should be success when move row before filter header', async () => {
|
||||
await commandService.executeCommand(SetSelectionsOperation.id, {
|
||||
unitId: 'test',
|
||||
subUnitId: 'sheet1',
|
||||
selections: [
|
||||
{
|
||||
range: {
|
||||
rangeType: RANGE_TYPE.ROW,
|
||||
startRow: 0,
|
||||
endRow: 0,
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
const result = await commandService.executeCommand(MoveRowsCommand.id, {
|
||||
unitId: 'test',
|
||||
subUnitId: 'sheet1',
|
||||
fromRange: { startRow: 0, startColumn: 0, endRow: 0, endColumn: 5 },
|
||||
toRange: { startRow: 2, startColumn: 0, endRow: 2, endColumn: 5 },
|
||||
});
|
||||
expect(result).toBeTruthy();
|
||||
expect((sheetsFilterService as SheetsFilterService).getFilterModel('test', 'sheet1')!.getRange())
|
||||
.toEqual({ startRow: 3, startColumn: 0, endRow: 5, endColumn: 5 });
|
||||
const workbook = instanceService.getCurrentUnitOfType<Workbook>(UniverInstanceType.UNIVER_SHEET)!;
|
||||
expect(workbook.getSheetBySheetId('sheet1')?.getCell(0, 0)?.v).toBe('A2');
|
||||
expect(workbook.getSheetBySheetId('sheet1')?.getCell(1, 0)?.v).toBe('A1');
|
||||
});
|
||||
});
|
||||
|
||||
describe('test interceptor of move-cols-command', () => {
|
||||
it('should be restored when undo move cols command', async () => {
|
||||
await commandService.executeCommand(SetSelectionsOperation.id, {
|
||||
unitId: 'test',
|
||||
subUnitId: 'sheet1',
|
||||
selections: [
|
||||
{
|
||||
range: {
|
||||
rangeType: RANGE_TYPE.COLUMN,
|
||||
startRow: 0,
|
||||
endRow: 10,
|
||||
startColumn: 0,
|
||||
endColumn: 5,
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
const result = await commandService.executeCommand(MoveColsCommand.id, {
|
||||
unitId: 'test',
|
||||
subUnitId: 'sheet1',
|
||||
fromRange: { startRow: 0, startColumn: 0, endRow: 10, endColumn: 5 },
|
||||
toRange: { startRow: 0, startColumn: 10, endRow: 10, endColumn: 15 },
|
||||
});
|
||||
expect(result).toBeTruthy();
|
||||
expect((sheetsFilterService as SheetsFilterService).getFilterModel('test', 'sheet1')!.getRange())
|
||||
.toEqual({ startRow: 3, startColumn: 4, endRow: 5, endColumn: 9 });
|
||||
|
||||
expect(await commandService.executeCommand(UndoCommand.id, {
|
||||
unitId: 'test',
|
||||
subUnitId: 'sheet1',
|
||||
})).toBeTruthy();
|
||||
expect((sheetsFilterService as SheetsFilterService).getFilterModel('test', 'sheet1')!.getRange())
|
||||
.toEqual({ startRow: 3, startColumn: 0, endRow: 5, endColumn: 5 });
|
||||
});
|
||||
});
|
||||
|
||||
describe('test extend range of filter', () => {
|
||||
it('should not be extended when edit null', async () => {
|
||||
expect(await commandService.executeCommand(SetRangeValuesMutation.id, {
|
||||
unitId: 'test',
|
||||
subUnitId: 'sheet1',
|
||||
cellValue: {
|
||||
6: {
|
||||
0: {
|
||||
f: null,
|
||||
},
|
||||
},
|
||||
},
|
||||
} as ISetRangeValuesMutationParams)).toBeTruthy();
|
||||
expect((sheetsFilterService as SheetsFilterService).getFilterModel('test', 'sheet1')!.getRange())
|
||||
.toEqual({ startRow: 3, startColumn: 0, endRow: 5, endColumn: 5 });
|
||||
});
|
||||
it('should be extended when edit not null', async () => {
|
||||
expect(await commandService.executeCommand(SetRangeValuesMutation.id, {
|
||||
unitId: 'test',
|
||||
subUnitId: 'sheet1',
|
||||
cellValue: {
|
||||
6: {
|
||||
0: {
|
||||
v: 'test',
|
||||
},
|
||||
},
|
||||
},
|
||||
} as ISetRangeValuesMutationParams)).toBeTruthy();
|
||||
expect((sheetsFilterService as SheetsFilterService).getFilterModel('test', 'sheet1')!.getRange())
|
||||
.toEqual({ startRow: 3, startColumn: 0, endRow: 6, endColumn: 5 });
|
||||
});
|
||||
});
|
||||
|
||||
describe('test interceptor of remove-rows-command', () => {
|
||||
it('filter range should move up if remove rows before filter header', async () => {
|
||||
expect(await commandService.executeCommand(SetSelectionsOperation.id, {
|
||||
unitId: 'test',
|
||||
subUnitId: 'sheet1',
|
||||
selections: [
|
||||
{
|
||||
range: {
|
||||
rangeType: RANGE_TYPE.ROW,
|
||||
startRow: 0,
|
||||
endRow: 0,
|
||||
},
|
||||
},
|
||||
],
|
||||
})).toBeTruthy();
|
||||
expect(await commandService.executeCommand(RemoveRowCommand.id, {
|
||||
unitId: 'test',
|
||||
subUnitId: 'sheet1',
|
||||
range: { startRow: 0, startColumn: 0, endRow: 0, endColumn: 5 },
|
||||
})).toBeTruthy();
|
||||
expect((sheetsFilterService as SheetsFilterService).getFilterModel('test', 'sheet1')!.getRange())
|
||||
.toEqual({ startRow: 2, startColumn: 0, endRow: 4, endColumn: 5 });
|
||||
});
|
||||
});
|
||||
|
||||
describe('test interceptor of remove-cols-command', () => {
|
||||
it('filter range should move left if remove cols before filter header', async () => {
|
||||
expect(await commandService.executeCommand(SetSheetsFilterCriteriaMutation.id, {
|
||||
unitId: 'test',
|
||||
subUnitId: 'sheet1',
|
||||
col: 1,
|
||||
criteria: {
|
||||
colId: 1,
|
||||
filters: {
|
||||
filters: ['test'],
|
||||
},
|
||||
},
|
||||
})).toBeTruthy();
|
||||
expect(await commandService.executeCommand(SetSelectionsOperation.id, {
|
||||
unitId: 'test',
|
||||
subUnitId: 'sheet1',
|
||||
selections: [
|
||||
{
|
||||
range: {
|
||||
rangeType: RANGE_TYPE.COLUMN,
|
||||
startRow: 0,
|
||||
endRow: 10,
|
||||
startColumn: 0,
|
||||
endColumn: 0,
|
||||
},
|
||||
},
|
||||
],
|
||||
})).toBeTruthy();
|
||||
expect(await commandService.executeCommand(RemoveColCommand.id, {
|
||||
unitId: 'test',
|
||||
subUnitId: 'sheet1',
|
||||
range: { startRow: 0, startColumn: 0, endRow: 10, endColumn: 0 },
|
||||
})).toBeTruthy();
|
||||
expect((sheetsFilterService as SheetsFilterService).getFilterModel('test', 'sheet1')!.getRange())
|
||||
.toEqual({ startRow: 3, startColumn: 0, endRow: 5, endColumn: 4 });
|
||||
expect((sheetsFilterService as SheetsFilterService).getFilterModel('test', 'sheet1')!.getAllFilterColumns()?.[0][0]).toBe(0);
|
||||
|
||||
expect(await commandService.executeCommand(UndoCommand.id, {
|
||||
unitId: 'test',
|
||||
subUnitId: 'sheet1',
|
||||
})).toBeTruthy();
|
||||
expect((sheetsFilterService as SheetsFilterService).getFilterModel('test', 'sheet1')!.getRange())
|
||||
.toEqual({ startRow: 3, startColumn: 0, endRow: 5, endColumn: 5 });
|
||||
expect((sheetsFilterService as SheetsFilterService).getFilterModel('test', 'sheet1')!.getAllFilterColumns()?.[0][0]).toBe(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('test copy sheet', () => {
|
||||
it('should copy filter', async () => {
|
||||
const res = await commandService.executeCommand(CopySheetCommand.id, {
|
||||
unitId: 'test',
|
||||
subUnitId: 'sheet1',
|
||||
});
|
||||
expect(res).toBeTruthy();
|
||||
const workbook = instanceService.getCurrentUnitOfType<Workbook>(UniverInstanceType.UNIVER_SHEET)!;
|
||||
const sheet2 = workbook.getSheets()[1];
|
||||
const filterModel = (sheetsFilterService as SheetsFilterService).getFilterModel('test', sheet2.getSheetId());
|
||||
expect(filterModel).toBeTruthy();
|
||||
expect(filterModel?.getFilterColumn(0)?.getColumnData()).toStrictEqual({ colId: 0, filters: { blank: true } });
|
||||
});
|
||||
});
|
||||
|
||||
describe('test interceptor of move range', () => {
|
||||
it('moved range intersect with filter range should expand filter', async () => {
|
||||
const res = await commandService.executeCommand(MoveRangeCommand.id, {
|
||||
fromRange: {
|
||||
startRow: 10,
|
||||
startColumn: 7,
|
||||
endRow: 16,
|
||||
endColumn: 7,
|
||||
rangeType: 0,
|
||||
},
|
||||
toRange: {
|
||||
startRow: 4,
|
||||
startColumn: 3,
|
||||
endRow: 10,
|
||||
endColumn: 3,
|
||||
rangeType: 0,
|
||||
},
|
||||
});
|
||||
expect(res).toBeTruthy();
|
||||
expect((sheetsFilterService as SheetsFilterService).getFilterModel('test', 'sheet1')!.getRange())
|
||||
.toEqual({ startRow: 3, startColumn: 0, endRow: 10, endColumn: 5 });
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,104 @@
|
||||
/**
|
||||
* Copyright 2023-present DreamNum Co., Ltd.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import type { IInsertColMutationParams, IMoveColumnsMutationParams, IRemoveColMutationParams } from '@univerjs/sheets';
|
||||
import type { IUniverSheetsFilterConfig } from '../config/config';
|
||||
import { Disposable, DisposableCollection, ICommandService, IConfigService, Inject, sequenceExecute } from '@univerjs/core';
|
||||
import { InsertColMutation, MoveColsMutation, RemoveColMutation } from '@univerjs/sheets';
|
||||
import { BehaviorSubject } from 'rxjs';
|
||||
import { ReCalcSheetsFilterMutation, SetSheetsFilterCriteriaMutation } from '../commands/mutations/sheets-filter.mutation';
|
||||
import { SHEETS_FILTER_PLUGIN_CONFIG_KEY } from '../config/config';
|
||||
import { SheetsFilterController } from './sheets-filter.controller';
|
||||
|
||||
const sheetsFilterOnlyLocalMutationIds = [
|
||||
SetSheetsFilterCriteriaMutation.id,
|
||||
ReCalcSheetsFilterMutation.id,
|
||||
];
|
||||
|
||||
const effectedByOnlyLocalMutationIds = [
|
||||
InsertColMutation.id,
|
||||
RemoveColMutation.id,
|
||||
MoveColsMutation.id,
|
||||
];
|
||||
|
||||
export class SheetsFilterSyncController extends Disposable {
|
||||
private _d: DisposableCollection = new DisposableCollection();
|
||||
|
||||
private readonly _visible$ = new BehaviorSubject<boolean>(false);
|
||||
readonly visible$ = this._visible$.asObservable();
|
||||
get visible(): boolean { return this._visible$.getValue(); }
|
||||
|
||||
private readonly _enabled$ = new BehaviorSubject<boolean>(true);
|
||||
readonly enabled$ = this._enabled$.asObservable();
|
||||
get enabled(): boolean { return this._enabled$.getValue(); }
|
||||
|
||||
constructor(
|
||||
@Inject(SheetsFilterController) private readonly _sheetsFilterController: SheetsFilterController,
|
||||
@ICommandService protected readonly _commandService: ICommandService,
|
||||
@IConfigService private readonly _configService: IConfigService
|
||||
) {
|
||||
super();
|
||||
|
||||
const config = this._configService.getConfig<IUniverSheetsFilterConfig>(SHEETS_FILTER_PLUGIN_CONFIG_KEY);
|
||||
if (config?.enableSyncSwitch) {
|
||||
this._visible$.next(true);
|
||||
|
||||
if (typeof config.enableSyncSwitch === 'object') {
|
||||
this.setEnabled(config.enableSyncSwitch.defaultValue ?? true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
setEnabled(enabled: boolean) {
|
||||
this._enabled$.next(enabled);
|
||||
if (enabled) {
|
||||
this._d.dispose();
|
||||
} else {
|
||||
this._initOnlyLocalListener();
|
||||
}
|
||||
}
|
||||
|
||||
private _initOnlyLocalListener() {
|
||||
this._d.add(
|
||||
this._commandService.beforeCommandExecuted((commandInfo, options) => {
|
||||
if (sheetsFilterOnlyLocalMutationIds.includes(commandInfo.id)) {
|
||||
if (!options) options = {};
|
||||
options.onlyLocal = true;
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
this._d.add(
|
||||
this._commandService.onCommandExecuted((commandInfo, options) => {
|
||||
if (effectedByOnlyLocalMutationIds.includes(commandInfo.id) && options?.fromCollab) {
|
||||
if (commandInfo.id === InsertColMutation.id) {
|
||||
const { range, unitId, subUnitId } = commandInfo.params as IInsertColMutationParams;
|
||||
const { redos } = this._sheetsFilterController.handleInsertColCommand(range, unitId, subUnitId);
|
||||
sequenceExecute(redos, this._commandService, options);
|
||||
} else if (commandInfo.id === RemoveColMutation.id) {
|
||||
const { range, unitId, subUnitId } = commandInfo.params as IRemoveColMutationParams;
|
||||
const { redos } = this._sheetsFilterController.handleRemoveColCommand(range, unitId, subUnitId);
|
||||
sequenceExecute(redos, this._commandService, options);
|
||||
} else if (commandInfo.id === MoveColsMutation.id) {
|
||||
const { sourceRange: fromRange, targetRange: toRange, unitId, subUnitId } = commandInfo.params as IMoveColumnsMutationParams;
|
||||
const { redos } = this._sheetsFilterController.handleMoveColsCommand({ fromRange, toRange }, unitId, subUnitId);
|
||||
sequenceExecute(redos, this._commandService, options);
|
||||
}
|
||||
}
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,995 @@
|
||||
/**
|
||||
* Copyright 2023-present DreamNum Co., Ltd.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES 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,
|
||||
ICommandInfo,
|
||||
IMutationInfo,
|
||||
IObjectArrayPrimitiveType,
|
||||
IRange,
|
||||
Nullable,
|
||||
Workbook,
|
||||
} from '@univerjs/core';
|
||||
import type {
|
||||
EffectRefRangeParams,
|
||||
IAddWorksheetMergeMutationParams,
|
||||
ICopySheetCommandParams,
|
||||
IInsertColCommandParams,
|
||||
IInsertRowCommandParams,
|
||||
IInsertRowMutationParams,
|
||||
IMoveColsCommandParams,
|
||||
IMoveRangeCommandParams,
|
||||
IMoveRowsCommandParams,
|
||||
IRemoveColMutationParams,
|
||||
IRemoveRowsMutationParams,
|
||||
IRemoveSheetCommandParams,
|
||||
ISetRangeValuesMutationParams,
|
||||
ISetWorksheetActiveOperationParams,
|
||||
ISheetCommandSharedParams,
|
||||
} from '@univerjs/sheets';
|
||||
import type {
|
||||
ISetSheetsFilterCriteriaMutationParams,
|
||||
ISetSheetsFilterRangeMutationParams,
|
||||
} from '../commands/mutations/sheets-filter.mutation';
|
||||
import type { FilterColumn } from '../models/filter-model';
|
||||
import {
|
||||
Disposable,
|
||||
DisposableCollection,
|
||||
ICommandService,
|
||||
Inject,
|
||||
IUniverInstanceService,
|
||||
moveMatrixArray,
|
||||
Optional,
|
||||
Rectangle,
|
||||
UniverInstanceType,
|
||||
} from '@univerjs/core';
|
||||
import { DataSyncPrimaryController } from '@univerjs/rpc';
|
||||
import {
|
||||
CopySheetCommand,
|
||||
EffectRefRangId,
|
||||
expandToContinuousRange,
|
||||
getSheetCommandTarget,
|
||||
InsertColCommand,
|
||||
InsertRowCommand,
|
||||
InsertRowMutation,
|
||||
INTERCEPTOR_POINT,
|
||||
MoveRangeCommand,
|
||||
MoveRowsCommand,
|
||||
RefRangeService,
|
||||
RemoveColCommand,
|
||||
RemoveRowCommand,
|
||||
RemoveRowMutation,
|
||||
RemoveSheetCommand,
|
||||
SetRangeValuesMutation,
|
||||
SetWorksheetActiveOperation,
|
||||
SheetInterceptorService,
|
||||
ZebraCrossingCacheController,
|
||||
} from '@univerjs/sheets';
|
||||
import {
|
||||
ClearSheetsFilterCriteriaCommand,
|
||||
ReCalcSheetsFilterCommand,
|
||||
RemoveSheetFilterCommand,
|
||||
SetSheetFilterRangeCommand,
|
||||
SetSheetsFilterCriteriaCommand,
|
||||
} from '../commands/commands/sheets-filter.command';
|
||||
import {
|
||||
ReCalcSheetsFilterMutation,
|
||||
RemoveSheetsFilterMutation,
|
||||
SetSheetsFilterCriteriaMutation,
|
||||
SetSheetsFilterRangeMutation,
|
||||
} from '../commands/mutations/sheets-filter.mutation';
|
||||
import { SheetsFilterService } from '../services/sheet-filter.service';
|
||||
import { mergeSetFilterCriteria } from '../utils';
|
||||
|
||||
export class SheetsFilterController extends Disposable {
|
||||
private _disposableCollection: DisposableCollection = new DisposableCollection();
|
||||
constructor(
|
||||
@ICommandService private readonly _commandService: ICommandService,
|
||||
@Inject(SheetInterceptorService) private readonly _sheetInterceptorService: SheetInterceptorService,
|
||||
@Inject(SheetsFilterService) private readonly _sheetsFilterService: SheetsFilterService,
|
||||
@IUniverInstanceService private readonly _univerInstanceService: IUniverInstanceService,
|
||||
@Inject(RefRangeService) private readonly _refRangeService: RefRangeService,
|
||||
@Optional(DataSyncPrimaryController) private readonly _dataSyncPrimaryController: DataSyncPrimaryController,
|
||||
@Inject(ZebraCrossingCacheController) private readonly _zebraCrossingCacheController: ZebraCrossingCacheController
|
||||
) {
|
||||
super();
|
||||
|
||||
this._initCommands();
|
||||
this._initRowFilteredInterceptor();
|
||||
this._initInterceptors();
|
||||
this._commandExecutedListener();
|
||||
this._initErrorHandling();
|
||||
this._initZebraCrossingCacheListener();
|
||||
}
|
||||
|
||||
private _initZebraCrossingCacheListener() {
|
||||
this.disposeWithMe(
|
||||
this._sheetsFilterService.activeFilterModel$.subscribe((filterModel) => {
|
||||
if (!filterModel) return;
|
||||
|
||||
this.disposeWithMe(
|
||||
filterModel.filteredOutRows$.subscribe(() => {
|
||||
this._zebraCrossingCacheController.updateZebraCrossingCache(filterModel.unitId, filterModel.subUnitId);
|
||||
})
|
||||
);
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
private _initCommands(): void {
|
||||
[
|
||||
SetSheetFilterRangeCommand,
|
||||
RemoveSheetFilterCommand,
|
||||
SetSheetsFilterCriteriaCommand,
|
||||
ClearSheetsFilterCriteriaCommand,
|
||||
ReCalcSheetsFilterCommand,
|
||||
].forEach((command) => {
|
||||
this.disposeWithMe(this._commandService.registerCommand(command));
|
||||
});
|
||||
|
||||
[
|
||||
SetSheetsFilterCriteriaMutation,
|
||||
SetSheetsFilterRangeMutation,
|
||||
ReCalcSheetsFilterMutation,
|
||||
RemoveSheetsFilterMutation,
|
||||
].forEach((command) => {
|
||||
this.disposeWithMe(this._commandService.registerCommand(command));
|
||||
this._dataSyncPrimaryController?.registerSyncingMutations(command);
|
||||
});
|
||||
}
|
||||
|
||||
private _initInterceptors(): void {
|
||||
this.disposeWithMe(this._sheetInterceptorService.interceptCommand({
|
||||
getMutations: (command) => this._getUpdateFilter(command),
|
||||
}));
|
||||
|
||||
this.disposeWithMe(this._commandService.onCommandExecuted((commandInfo) => {
|
||||
if (commandInfo.id === SetWorksheetActiveOperation.id) {
|
||||
const params = commandInfo.params as ISetWorksheetActiveOperationParams;
|
||||
const sheetId = params.subUnitId;
|
||||
const unitId = params.unitId;
|
||||
if (!sheetId || !unitId) {
|
||||
return;
|
||||
}
|
||||
this._registerRefRange(unitId, sheetId);
|
||||
}
|
||||
if (commandInfo.id === SetSheetsFilterRangeMutation.id) {
|
||||
const params = commandInfo.params as IAddWorksheetMergeMutationParams;
|
||||
const sheetId = params.subUnitId;
|
||||
const unitId = params.unitId;
|
||||
if (!sheetId || !unitId) {
|
||||
return;
|
||||
}
|
||||
this._registerRefRange(params.unitId, params.subUnitId);
|
||||
}
|
||||
}));
|
||||
|
||||
this.disposeWithMe(this._sheetsFilterService.loadedUnitId$.subscribe((unitId) => {
|
||||
if (unitId) {
|
||||
const workbook = this._univerInstanceService.getUnit<Workbook>(unitId, UniverInstanceType.UNIVER_SHEET);
|
||||
const sheet = workbook?.getActiveSheet();
|
||||
if (sheet) {
|
||||
this._registerRefRange(unitId, sheet.getSheetId());
|
||||
}
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
private _registerRefRange(unitId: string, subUnitId: string): void {
|
||||
this._disposableCollection.dispose();
|
||||
const workbook = this._univerInstanceService.getUnit<Workbook>(unitId, UniverInstanceType.UNIVER_SHEET);
|
||||
const workSheet = workbook?.getSheetBySheetId(subUnitId);
|
||||
if (!workbook || !workSheet) return;
|
||||
const range = this._sheetsFilterService.getFilterModel(unitId, subUnitId)?.getRange();
|
||||
const handler = (config: EffectRefRangeParams) => {
|
||||
switch (config.id) {
|
||||
case InsertRowCommand.id: {
|
||||
const params = config.params as IInsertRowCommandParams;
|
||||
const _unitId = params.unitId || unitId;
|
||||
const _subUnitId = params.subUnitId || subUnitId;
|
||||
return this._handleInsertRowCommand(params, _unitId, _subUnitId);
|
||||
}
|
||||
case InsertColCommand.id: {
|
||||
const params = config.params as IInsertColCommandParams;
|
||||
const _unitId = params.unitId || unitId;
|
||||
const _subUnitId = params.subUnitId || subUnitId;
|
||||
return this.handleInsertColCommand(params.range, _unitId, _subUnitId);
|
||||
}
|
||||
case RemoveColCommand.id: {
|
||||
const params = config.params as IRemoveColMutationParams;
|
||||
return this.handleRemoveColCommand(params.range, unitId, subUnitId);
|
||||
}
|
||||
case RemoveRowCommand.id: {
|
||||
const params = config.params as IRemoveRowsMutationParams;
|
||||
return this._handleRemoveRowCommand(params, unitId, subUnitId);
|
||||
}
|
||||
case EffectRefRangId.MoveColsCommandId: {
|
||||
const params = config.params as IMoveColsCommandParams;
|
||||
return this.handleMoveColsCommand({
|
||||
fromRange: params.fromRange,
|
||||
toRange: params.toRange,
|
||||
}, unitId, subUnitId);
|
||||
}
|
||||
case EffectRefRangId.MoveRowsCommandId: {
|
||||
const params = config.params as IMoveRowsCommandParams;
|
||||
return this._handleMoveRowsCommand(params, unitId, subUnitId);
|
||||
}
|
||||
case MoveRangeCommand.id: {
|
||||
const params = config.params as IMoveRangeCommandParams;
|
||||
return this._handleMoveRangeCommand(params, unitId, subUnitId);
|
||||
}
|
||||
}
|
||||
return { redos: [], undos: [] };
|
||||
};
|
||||
|
||||
if (range) {
|
||||
this._disposableCollection.add(this._refRangeService.registerRefRange(range, handler, unitId, subUnitId));
|
||||
}
|
||||
}
|
||||
|
||||
private _getUpdateFilter(command: ICommandInfo) {
|
||||
const { id } = command;
|
||||
switch (id) {
|
||||
case RemoveSheetCommand.id: {
|
||||
const params = command.params as ISheetCommandSharedParams;
|
||||
return this._handleRemoveSheetCommand(params, params.unitId, params.subUnitId);
|
||||
}
|
||||
case CopySheetCommand.id: {
|
||||
const params = command.params as ICopySheetCommandParams & { targetSubUnitId: string };
|
||||
const { targetSubUnitId, unitId, subUnitId } = params;
|
||||
if (!unitId || !subUnitId || !targetSubUnitId) {
|
||||
return this._handleNull();
|
||||
}
|
||||
|
||||
return this._handleCopySheetCommand(unitId, subUnitId, targetSubUnitId);
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
redos: [],
|
||||
undos: [],
|
||||
};
|
||||
}
|
||||
|
||||
handleInsertColCommand(range: IRange, unitId: string, subUnitId: string) {
|
||||
const filterModel = this._sheetsFilterService.getFilterModel(unitId, subUnitId);
|
||||
const filterRange = filterModel?.getRange() ?? null;
|
||||
if (!filterModel || !filterRange) {
|
||||
return this._handleNull();
|
||||
}
|
||||
const { startColumn, endColumn } = filterRange;
|
||||
const { startColumn: insertStartColumn, endColumn: insertEndColumn } = range;
|
||||
const count = insertEndColumn - insertStartColumn + 1;
|
||||
|
||||
if (insertEndColumn > endColumn) {
|
||||
return this._handleNull();
|
||||
}
|
||||
|
||||
const redos: IMutationInfo[] = [];
|
||||
const undos: IMutationInfo[] = [];
|
||||
|
||||
const anchor = insertStartColumn;
|
||||
const setFilterRangeMutationParams: ISetSheetsFilterRangeMutationParams = {
|
||||
unitId,
|
||||
subUnitId,
|
||||
range: {
|
||||
...filterRange,
|
||||
startColumn: insertStartColumn <= startColumn ? startColumn + count : startColumn,
|
||||
endColumn: endColumn + count,
|
||||
},
|
||||
};
|
||||
|
||||
const undoSetFilterRangeMutationParams: ISetSheetsFilterRangeMutationParams = {
|
||||
unitId,
|
||||
subUnitId,
|
||||
range: filterRange,
|
||||
};
|
||||
|
||||
redos.push({ id: SetSheetsFilterRangeMutation.id, params: setFilterRangeMutationParams });
|
||||
undos.push({ id: SetSheetsFilterRangeMutation.id, params: undoSetFilterRangeMutationParams });
|
||||
|
||||
const filterColumn = filterModel.getAllFilterColumns();
|
||||
const effected = filterColumn.filter((column) => column[0] >= anchor);
|
||||
if (effected.length !== 0) {
|
||||
const { newRange, oldRange } = this._moveCriteria(unitId, subUnitId, effected, count);
|
||||
redos.push(...oldRange.redos, ...newRange.redos);
|
||||
undos.push(...newRange.undos, ...oldRange.undos);
|
||||
}
|
||||
|
||||
return { redos: mergeSetFilterCriteria(redos), undos: mergeSetFilterCriteria(undos) };
|
||||
}
|
||||
|
||||
private _handleInsertRowCommand(config: IInsertRowCommandParams, unitId: string, subUnitId: string) {
|
||||
const filterModel = this._sheetsFilterService.getFilterModel(unitId, subUnitId);
|
||||
const filterRange = filterModel?.getRange() ?? null;
|
||||
if (!filterModel || !filterRange) {
|
||||
return this._handleNull();
|
||||
}
|
||||
const { startRow, endRow } = filterRange;
|
||||
const { startRow: insertStartRow, endRow: insertEndRow } = config.range;
|
||||
const rowCount = insertEndRow - insertStartRow + 1;
|
||||
if (insertEndRow > endRow) {
|
||||
return this._handleNull();
|
||||
}
|
||||
const redos: IMutationInfo[] = [];
|
||||
const undos: IMutationInfo[] = [];
|
||||
const setFilterRangeParams: ISetSheetsFilterRangeMutationParams = {
|
||||
unitId,
|
||||
subUnitId,
|
||||
range: {
|
||||
...filterRange,
|
||||
startRow: insertStartRow <= startRow ? startRow + rowCount : startRow,
|
||||
endRow: endRow + rowCount,
|
||||
},
|
||||
};
|
||||
const undoSetFilterRangeMutationParams: ISetSheetsFilterRangeMutationParams = {
|
||||
unitId,
|
||||
subUnitId,
|
||||
range: filterRange,
|
||||
};
|
||||
|
||||
redos.push({ id: SetSheetsFilterRangeMutation.id, params: setFilterRangeParams });
|
||||
undos.push({ id: SetSheetsFilterRangeMutation.id, params: undoSetFilterRangeMutationParams });
|
||||
return {
|
||||
redos: mergeSetFilterCriteria(redos),
|
||||
undos: mergeSetFilterCriteria(undos),
|
||||
};
|
||||
}
|
||||
|
||||
handleRemoveColCommand(range: IRange, unitId: string, subUnitId: string) {
|
||||
const filterModel = this._sheetsFilterService.getFilterModel(unitId, subUnitId);
|
||||
const filterRange = filterModel?.getRange() ?? null;
|
||||
if (!filterModel || !filterRange) {
|
||||
return this._handleNull();
|
||||
}
|
||||
const { startColumn, endColumn } = filterRange;
|
||||
const { startColumn: removeStartColumn, endColumn: removeEndColumn } = range;
|
||||
|
||||
if (removeStartColumn > endColumn) {
|
||||
return this._handleNull();
|
||||
}
|
||||
const redos: IMutationInfo[] = [];
|
||||
const undos: IMutationInfo[] = [];
|
||||
|
||||
const rangeRemoveCount =
|
||||
removeEndColumn < startColumn
|
||||
? 0 :
|
||||
Math.min(removeEndColumn, endColumn) - Math.max(removeStartColumn, startColumn) + 1;
|
||||
|
||||
const removeCount = removeEndColumn - removeStartColumn + 1;
|
||||
|
||||
const filterColumn = filterModel.getAllFilterColumns();
|
||||
filterColumn.forEach((column) => {
|
||||
const [col, filter] = column;
|
||||
if (col <= removeEndColumn && col >= removeStartColumn) {
|
||||
redos.push({ id: SetSheetsFilterCriteriaMutation.id, params: { unitId, subUnitId, col, criteria: null } });
|
||||
undos.push({ id: SetSheetsFilterCriteriaMutation.id, params: { unitId, subUnitId, col, criteria: { ...filter.serialize(), colId: col } } });
|
||||
}
|
||||
});
|
||||
|
||||
const shifted = filterColumn.filter((column) => {
|
||||
const [col, _] = column;
|
||||
return col > removeEndColumn;
|
||||
});
|
||||
|
||||
let newRangeCriteria: { undos: IMutationInfo[]; redos: IMutationInfo[] } = { undos: [], redos: [] };
|
||||
if (shifted.length > 0) {
|
||||
const { oldRange, newRange } = this._moveCriteria(unitId, subUnitId, shifted, -removeCount);
|
||||
newRangeCriteria = newRange;
|
||||
redos.push(...oldRange.redos);
|
||||
undos.unshift(...oldRange.undos);
|
||||
}
|
||||
|
||||
if (rangeRemoveCount === endColumn - startColumn + 1) {
|
||||
const removeFilterRangeMutationParams: ISheetCommandSharedParams = {
|
||||
unitId,
|
||||
subUnitId,
|
||||
};
|
||||
redos.push({ id: RemoveSheetsFilterMutation.id, params: removeFilterRangeMutationParams });
|
||||
undos.unshift({ id: SetSheetsFilterRangeMutation.id, params: { range: filterRange, unitId, subUnitId } });
|
||||
} else {
|
||||
const newStartColumn = startColumn <= removeStartColumn
|
||||
? startColumn :
|
||||
(rangeRemoveCount === 0 ? startColumn - removeCount : removeStartColumn);
|
||||
const newEndColumn = startColumn <= removeStartColumn ? endColumn - rangeRemoveCount : endColumn - removeCount;
|
||||
const setFilterRangeMutationParams: ISetSheetsFilterRangeMutationParams = {
|
||||
unitId,
|
||||
subUnitId,
|
||||
range: { ...filterRange, startColumn: newStartColumn, endColumn: newEndColumn },
|
||||
};
|
||||
redos.push({ id: SetSheetsFilterRangeMutation.id, params: setFilterRangeMutationParams });
|
||||
undos.unshift({ id: SetSheetsFilterRangeMutation.id, params: { range: filterRange, unitId, subUnitId } });
|
||||
|
||||
redos.push(...newRangeCriteria.redos);
|
||||
undos.unshift(...newRangeCriteria.undos);
|
||||
}
|
||||
|
||||
return {
|
||||
undos,
|
||||
redos,
|
||||
};
|
||||
}
|
||||
|
||||
private _handleRemoveRowCommand(config: IRemoveRowsMutationParams, unitId: string, subUnitId: string) {
|
||||
const filterModel = this._sheetsFilterService.getFilterModel(unitId, subUnitId);
|
||||
if (!filterModel) {
|
||||
return this._handleNull();
|
||||
}
|
||||
|
||||
const filterRange = filterModel.getRange();
|
||||
const { startRow, endRow } = filterRange;
|
||||
const { startRow: removeStartRow, endRow: removeEndRow } = config.range;
|
||||
if (removeStartRow > endRow) {
|
||||
return this._handleNull();
|
||||
}
|
||||
if (removeEndRow < startRow) {
|
||||
return {
|
||||
undos: [{ id: SetSheetsFilterRangeMutation.id, params: { range: filterRange, unitId, subUnitId } }],
|
||||
redos: [{
|
||||
id: SetSheetsFilterRangeMutation.id,
|
||||
params: {
|
||||
range: {
|
||||
...filterRange,
|
||||
startRow: startRow - (removeEndRow - removeStartRow + 1),
|
||||
endRow: endRow - (removeEndRow - removeStartRow + 1),
|
||||
},
|
||||
unitId,
|
||||
subUnitId,
|
||||
},
|
||||
}],
|
||||
};
|
||||
}
|
||||
const redos: IMutationInfo[] = [];
|
||||
const undos: IMutationInfo[] = [];
|
||||
const filterColumn = filterModel.getAllFilterColumns();
|
||||
|
||||
const filterHeaderIsRemoved = startRow <= removeEndRow && startRow >= removeStartRow;
|
||||
|
||||
undos.push({ id: SetSheetsFilterRangeMutation.id, params: { range: filterRange, unitId, subUnitId } });
|
||||
|
||||
const count = Math.min(removeEndRow, endRow) - Math.max(removeStartRow, startRow) + 1;
|
||||
if (count === endRow - startRow + 1 || filterHeaderIsRemoved) {
|
||||
const removeFilterRangeMutationParams: ISheetCommandSharedParams = {
|
||||
unitId,
|
||||
subUnitId,
|
||||
};
|
||||
redos.push({ id: RemoveSheetsFilterMutation.id, params: removeFilterRangeMutationParams });
|
||||
|
||||
filterColumn.forEach((column) => {
|
||||
const [offset, filter] = column;
|
||||
const setCriteriaMutationParams: ISetSheetsFilterCriteriaMutationParams = {
|
||||
unitId,
|
||||
subUnitId,
|
||||
col: offset,
|
||||
criteria: { ...filter.serialize(), colId: offset },
|
||||
};
|
||||
undos.push({ id: SetSheetsFilterCriteriaMutation.id, params: setCriteriaMutationParams });
|
||||
});
|
||||
} else {
|
||||
const worksheet = this._univerInstanceService.getUnit<Workbook>(unitId, UniverInstanceType.UNIVER_SHEET)?.getSheetBySheetId(subUnitId);
|
||||
if (!worksheet) {
|
||||
return this._handleNull();
|
||||
}
|
||||
const hiddenRowCount = this._getFilteredRowCount(worksheet, removeStartRow, removeEndRow);
|
||||
const afterStartRow = Math.min(startRow, removeStartRow);
|
||||
const afterEndRow = afterStartRow + (endRow - startRow) - count + hiddenRowCount;
|
||||
const setFilterRangeMutationParams: ISetSheetsFilterRangeMutationParams = {
|
||||
unitId,
|
||||
subUnitId,
|
||||
range: {
|
||||
...filterRange,
|
||||
startRow: afterStartRow,
|
||||
endRow: afterEndRow,
|
||||
},
|
||||
};
|
||||
redos.push({ id: SetSheetsFilterRangeMutation.id, params: setFilterRangeMutationParams });
|
||||
}
|
||||
|
||||
return {
|
||||
undos: mergeSetFilterCriteria(undos),
|
||||
redos: mergeSetFilterCriteria(redos),
|
||||
};
|
||||
}
|
||||
|
||||
private _getFilteredRowCount(worksheet: { getRowFiltered(row: number): boolean }, startRow: number, endRow: number): number {
|
||||
let count = 0;
|
||||
for (let row = startRow; row <= endRow; row++) {
|
||||
if (worksheet.getRowFiltered(row)) {
|
||||
count++;
|
||||
}
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
// eslint-disable-next-line max-lines-per-function
|
||||
handleMoveColsCommand({ fromRange, toRange }: { fromRange: IRange; toRange: IRange }, unitId: string, subUnitId: string) {
|
||||
const filterModel = this._sheetsFilterService.getFilterModel(unitId, subUnitId);
|
||||
const filterRange = filterModel?.getRange() ?? null;
|
||||
if (!filterModel || !filterRange) {
|
||||
return this._handleNull();
|
||||
}
|
||||
const { startColumn, endColumn } = filterRange;
|
||||
if ((fromRange.endColumn < startColumn && toRange.startColumn <= startColumn) || (
|
||||
fromRange.startColumn > endColumn && toRange.endColumn > endColumn
|
||||
)) {
|
||||
return this._handleNull();
|
||||
}
|
||||
const redos: IMutationInfo[] = [];
|
||||
const undos: IMutationInfo[] = [];
|
||||
const filterCol: IObjectArrayPrimitiveType<{ colIndex: number; filter: Nullable<FilterColumn> }> = {};
|
||||
for (let col = startColumn; col <= endColumn; col++) {
|
||||
filterCol[col] = {
|
||||
colIndex: col,
|
||||
filter: filterModel.getFilterColumn(col),
|
||||
};
|
||||
}
|
||||
moveMatrixArray(fromRange.startColumn, fromRange.endColumn - fromRange.startColumn + 1, toRange.startColumn, filterCol);
|
||||
|
||||
let startBorder = filterRange.startColumn;
|
||||
let endBorder = filterRange.endColumn;
|
||||
|
||||
// border will change if first col or last col moves.
|
||||
if (startColumn >= fromRange.startColumn && startColumn <= fromRange.endColumn
|
||||
&& toRange.startColumn > fromRange.startColumn
|
||||
&& fromRange.endColumn < endColumn) {
|
||||
startBorder = fromRange.endColumn + 1;
|
||||
}
|
||||
if (endColumn >= fromRange.startColumn && endColumn <= fromRange.endColumn
|
||||
&& toRange.startColumn < fromRange.startColumn
|
||||
&& fromRange.startColumn > startColumn) {
|
||||
endBorder = fromRange.startColumn - 1;
|
||||
}
|
||||
|
||||
const numberCols = Object.keys(filterCol).map((col) => Number(col)) as number[];
|
||||
|
||||
// find the start col & end col of new filter range by border.
|
||||
const newEnd = numberCols.find((col) => filterCol[col].colIndex === endBorder) as number;
|
||||
const newStart = numberCols.find((col) => filterCol[col].colIndex === startBorder) as number;
|
||||
|
||||
numberCols.forEach((col) => {
|
||||
const { colIndex: oldColIndex, filter } = filterCol[col];
|
||||
const newColIndex = col;
|
||||
|
||||
if (filter) {
|
||||
if (newColIndex >= newStart && newColIndex <= newEnd) {
|
||||
const setCriteriaMutationParams: ISetSheetsFilterCriteriaMutationParams = {
|
||||
unitId,
|
||||
subUnitId,
|
||||
col: newColIndex,
|
||||
criteria: { ...filter.serialize(), colId: newColIndex },
|
||||
};
|
||||
|
||||
const undoSetCriteriaMutationParams: ISetSheetsFilterCriteriaMutationParams = {
|
||||
unitId,
|
||||
subUnitId,
|
||||
col: newColIndex,
|
||||
criteria: filterModel.getFilterColumn(newColIndex) ?
|
||||
{ ...filterModel.getFilterColumn(newColIndex)?.serialize(), colId: newColIndex }
|
||||
: null,
|
||||
};
|
||||
redos.push({ id: SetSheetsFilterCriteriaMutation.id, params: setCriteriaMutationParams });
|
||||
undos.push({ id: SetSheetsFilterCriteriaMutation.id, params: undoSetCriteriaMutationParams });
|
||||
}
|
||||
if (!filterCol[oldColIndex]?.filter) {
|
||||
const setCriteriaMutationParams: ISetSheetsFilterCriteriaMutationParams = {
|
||||
unitId,
|
||||
subUnitId,
|
||||
col: oldColIndex,
|
||||
criteria: null,
|
||||
};
|
||||
redos.push({ id: SetSheetsFilterCriteriaMutation.id, params: setCriteriaMutationParams });
|
||||
undos.push({ id: SetSheetsFilterCriteriaMutation.id, params: { unitId, subUnitId, col: oldColIndex, criteria: { ...filter.serialize(), colId: oldColIndex } } });
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if (startColumn !== newStart || endColumn !== newEnd) {
|
||||
const setFilterRangeMutationParams: ISetSheetsFilterRangeMutationParams = {
|
||||
unitId,
|
||||
subUnitId,
|
||||
range: {
|
||||
...filterRange,
|
||||
startColumn: newStart,
|
||||
endColumn: newEnd,
|
||||
},
|
||||
};
|
||||
redos.unshift({ id: SetSheetsFilterRangeMutation.id, params: setFilterRangeMutationParams });
|
||||
undos.unshift({ id: SetSheetsFilterRangeMutation.id, params: { range: filterRange, unitId, subUnitId } });
|
||||
}
|
||||
|
||||
return {
|
||||
undos,
|
||||
redos,
|
||||
};
|
||||
}
|
||||
|
||||
private _handleMoveRowsCommand(config: IMoveRowsCommandParams, unitId: string, subUnitId: string) {
|
||||
const filterModel = this._sheetsFilterService.getFilterModel(unitId, subUnitId);
|
||||
const filterRange = filterModel?.getRange() ?? null;
|
||||
if (!filterModel || !filterRange) {
|
||||
return this._handleNull();
|
||||
}
|
||||
const { startRow, endRow } = filterRange;
|
||||
const { fromRange, toRange } = config;
|
||||
if ((fromRange.endRow < startRow && toRange.startRow <= startRow) || (
|
||||
fromRange.startRow > endRow && toRange.endRow > endRow
|
||||
)) {
|
||||
return this._handleNull();
|
||||
}
|
||||
const redos: IMutationInfo[] = [];
|
||||
const undos: IMutationInfo[] = [];
|
||||
const filterRow: IObjectArrayPrimitiveType<{ oldIndex: number }> = {};
|
||||
for (let row = startRow; row <= endRow; row++) {
|
||||
filterRow[row] = {
|
||||
oldIndex: row,
|
||||
};
|
||||
}
|
||||
const startBorder = startRow;
|
||||
let endBorder = endRow;
|
||||
|
||||
// only need to deal with endBorder, startRow will not be moved.
|
||||
if (endRow >= fromRange.startRow && endRow <= fromRange.endRow
|
||||
&& toRange.startRow < fromRange.startRow
|
||||
&& fromRange.startRow > startRow) {
|
||||
endBorder = fromRange.startRow - 1;
|
||||
}
|
||||
|
||||
moveMatrixArray(fromRange.startRow, fromRange.endRow - fromRange.startRow + 1, toRange.startRow, filterRow);
|
||||
const numberRows = Object.keys(filterRow).map((row) => Number(row));
|
||||
|
||||
const newEnd = numberRows.find((row) => filterRow[row].oldIndex === endBorder) as number;
|
||||
const newStart = numberRows.find((row) => filterRow[row].oldIndex === startBorder) as number;
|
||||
|
||||
if (startRow !== newStart || endRow !== newEnd) {
|
||||
const setFilterRangeMutationParams: ISetSheetsFilterRangeMutationParams = {
|
||||
unitId,
|
||||
subUnitId,
|
||||
range: {
|
||||
...filterRange,
|
||||
startRow: newStart,
|
||||
endRow: newEnd,
|
||||
},
|
||||
};
|
||||
redos.push({ id: SetSheetsFilterRangeMutation.id, params: setFilterRangeMutationParams }, { id: ReCalcSheetsFilterMutation.id, params: { unitId, subUnitId } });
|
||||
undos.push({ id: SetSheetsFilterRangeMutation.id, params: { range: filterRange, unitId, subUnitId } }, { id: ReCalcSheetsFilterMutation.id, params: { unitId, subUnitId } });
|
||||
}
|
||||
return {
|
||||
redos,
|
||||
undos,
|
||||
};
|
||||
}
|
||||
|
||||
private _handleMoveRangeCommand(config: IMoveRangeCommandParams, unitId: string, subUnitId: string) {
|
||||
const { fromRange, toRange } = config;
|
||||
const filterModel = this._sheetsFilterService.getFilterModel(unitId, subUnitId);
|
||||
if (!filterModel) {
|
||||
return this._handleNull();
|
||||
}
|
||||
const filterRange = filterModel.getRange();
|
||||
if (!filterRange) {
|
||||
return this._handleNull();
|
||||
}
|
||||
|
||||
const redos: IMutationInfo[] = [];
|
||||
const undos: IMutationInfo[] = [];
|
||||
|
||||
if (Rectangle.contains(fromRange, filterRange)) {
|
||||
const rowOffset = filterRange.startRow - fromRange.startRow;
|
||||
const colOffset = filterRange.startColumn - fromRange.startColumn;
|
||||
const newFilterRange = {
|
||||
startRow: toRange.startRow + rowOffset,
|
||||
startColumn: toRange.startColumn + colOffset,
|
||||
endRow: toRange.startRow + rowOffset + (filterRange.endRow - filterRange.startRow),
|
||||
endColumn: toRange.startColumn + colOffset + (filterRange.endColumn - filterRange.startColumn),
|
||||
};
|
||||
const removeFilter = {
|
||||
id: RemoveSheetsFilterMutation.id,
|
||||
params: {
|
||||
unitId,
|
||||
subUnitId,
|
||||
},
|
||||
};
|
||||
const setNewFilterRange = { id: SetSheetsFilterRangeMutation.id, params: { unitId, subUnitId, range: newFilterRange } as ISetSheetsFilterRangeMutationParams };
|
||||
const setOldFilterRange = { id: SetSheetsFilterRangeMutation.id, params: { unitId, subUnitId, range: filterRange } as ISetSheetsFilterRangeMutationParams };
|
||||
|
||||
redos.push(removeFilter, setNewFilterRange);
|
||||
|
||||
undos.push(removeFilter, setOldFilterRange);
|
||||
|
||||
const filterColumn = filterModel.getAllFilterColumns();
|
||||
const moveColDelta = toRange.startColumn - fromRange.startColumn;
|
||||
filterColumn.forEach((column) => {
|
||||
const [col, criteria] = column;
|
||||
if (criteria) {
|
||||
redos.push({ id: SetSheetsFilterCriteriaMutation.id, params: { unitId, subUnitId, col: col + moveColDelta, criteria: { ...criteria.serialize(), colId: col + moveColDelta } } });
|
||||
undos.push({ id: SetSheetsFilterCriteriaMutation.id, params: { unitId, subUnitId, col, criteria: { ...criteria.serialize(), colId: col } } });
|
||||
}
|
||||
});
|
||||
|
||||
// redos.push({ id: ReCalcSheetsFilterMutation.id, params: { unitId, subUnitId } });
|
||||
// undos.push({ id: ReCalcSheetsFilterMutation.id, params: { unitId, subUnitId } });
|
||||
} else if (Rectangle.intersects(toRange, filterRange)) {
|
||||
const newFilterRange: IRange = {
|
||||
...filterRange,
|
||||
endRow: Math.max(filterRange.endRow, toRange.endRow),
|
||||
};
|
||||
redos.push({ id: SetSheetsFilterRangeMutation.id, params: { unitId, subUnitId, range: newFilterRange } });
|
||||
undos.push({ id: SetSheetsFilterRangeMutation.id, params: { unitId, subUnitId, range: filterRange } });
|
||||
}
|
||||
return {
|
||||
redos,
|
||||
undos,
|
||||
};
|
||||
}
|
||||
|
||||
private _handleRemoveSheetCommand(config: IRemoveSheetCommandParams, unitId: string, subUnitId: string) {
|
||||
const filterModel = this._sheetsFilterService.getFilterModel(unitId, subUnitId);
|
||||
if (!filterModel) {
|
||||
return this._handleNull();
|
||||
}
|
||||
const filterRange = filterModel.getRange();
|
||||
if (!filterRange) {
|
||||
return this._handleNull();
|
||||
}
|
||||
const redos: IMutationInfo[] = [];
|
||||
const undos: IMutationInfo[] = [];
|
||||
const filterCols = filterModel.getAllFilterColumns();
|
||||
filterCols.forEach(([col, filter]) => {
|
||||
undos.push({ id: SetSheetsFilterCriteriaMutation.id, params: { unitId, subUnitId, col, criteria: { ...filter.serialize(), colId: col } } });
|
||||
});
|
||||
redos.push({ id: RemoveSheetsFilterMutation.id, params: { unitId, subUnitId, range: filterRange } });
|
||||
undos.unshift({ id: SetSheetsFilterRangeMutation.id, params: { range: filterRange, unitId, subUnitId } });
|
||||
return {
|
||||
undos,
|
||||
redos,
|
||||
};
|
||||
}
|
||||
|
||||
private _handleCopySheetCommand(unitId: string, subUnitId: string, targetSubUnitId: string) {
|
||||
const filterModel = this._sheetsFilterService.getFilterModel(unitId, subUnitId);
|
||||
if (!filterModel) {
|
||||
return this._handleNull();
|
||||
}
|
||||
const filterRange = filterModel.getRange();
|
||||
if (!filterRange) {
|
||||
return this._handleNull();
|
||||
}
|
||||
const redos: IMutationInfo[] = [];
|
||||
const undos: IMutationInfo[] = [];
|
||||
const preUndos: IMutationInfo[] = [];
|
||||
const preRedos: IMutationInfo[] = [];
|
||||
const filterCols = filterModel.getAllFilterColumns();
|
||||
filterCols.forEach(([col, filter]) => {
|
||||
redos.push({ id: SetSheetsFilterCriteriaMutation.id, params: { unitId, subUnitId: targetSubUnitId, col, criteria: { ...filter.serialize(), colId: col } } });
|
||||
preUndos.push({ id: SetSheetsFilterCriteriaMutation.id, params: { unitId, subUnitId: targetSubUnitId, col, criteria: null } });
|
||||
});
|
||||
preUndos.push({ id: RemoveSheetsFilterMutation.id, params: { unitId, subUnitId: targetSubUnitId, range: filterRange } });
|
||||
redos.unshift({ id: SetSheetsFilterRangeMutation.id, params: { range: filterRange, unitId, subUnitId: targetSubUnitId } });
|
||||
return {
|
||||
undos,
|
||||
redos,
|
||||
preUndos,
|
||||
preRedos,
|
||||
};
|
||||
}
|
||||
|
||||
private _handleNull() {
|
||||
return { redos: [], undos: [] };
|
||||
}
|
||||
|
||||
private _initRowFilteredInterceptor(): void {
|
||||
this.disposeWithMe(this._sheetInterceptorService.intercept(INTERCEPTOR_POINT.ROW_FILTERED, {
|
||||
|
||||
// sheet-interceptor.service.ts
|
||||
handler: (filtered, rowLocation) => {
|
||||
if (filtered) return true;
|
||||
return this._sheetsFilterService.getFilterModel(
|
||||
rowLocation.unitId,
|
||||
rowLocation.subUnitId
|
||||
)?.isRowFiltered(rowLocation.row) ?? false;
|
||||
},
|
||||
}));
|
||||
}
|
||||
|
||||
private _moveCriteria(unitId: string, subUnitId: string, target: [number, FilterColumn][], step: number) {
|
||||
const defaultSetCriteriaMutationParams: ISetSheetsFilterCriteriaMutationParams = {
|
||||
unitId,
|
||||
subUnitId,
|
||||
criteria: null,
|
||||
col: -1,
|
||||
};
|
||||
const oldUndos: IMutationInfo[] = [];
|
||||
const oldRedos: IMutationInfo[] = [];
|
||||
const newUndos: IMutationInfo[] = [];
|
||||
const newRedos: IMutationInfo[] = [];
|
||||
|
||||
target.forEach((column) => {
|
||||
const [offset, filter] = column;
|
||||
oldRedos.push({
|
||||
id: SetSheetsFilterCriteriaMutation.id,
|
||||
params: {
|
||||
...defaultSetCriteriaMutationParams,
|
||||
col: offset,
|
||||
},
|
||||
});
|
||||
oldUndos.push({
|
||||
id: SetSheetsFilterCriteriaMutation.id,
|
||||
params: {
|
||||
...defaultSetCriteriaMutationParams,
|
||||
col: offset,
|
||||
criteria: { ...filter.serialize(), colId: offset },
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
target.forEach((column) => {
|
||||
const [offset, filter] = column;
|
||||
newRedos.push({
|
||||
id: SetSheetsFilterCriteriaMutation.id,
|
||||
params: {
|
||||
...defaultSetCriteriaMutationParams,
|
||||
col: offset + step,
|
||||
criteria: { ...filter.serialize(), colId: offset + step },
|
||||
},
|
||||
});
|
||||
newUndos.push({
|
||||
id: SetSheetsFilterCriteriaMutation.id,
|
||||
params: {
|
||||
...defaultSetCriteriaMutationParams,
|
||||
col: offset + step,
|
||||
criteria: null,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
return {
|
||||
newRange: {
|
||||
redos: newRedos,
|
||||
undos: newUndos,
|
||||
},
|
||||
oldRange: {
|
||||
redos: oldRedos,
|
||||
undos: oldUndos,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
private _commandExecutedListener(): void {
|
||||
this.disposeWithMe(this._commandService.onCommandExecuted((command: ICommandInfo, options) => {
|
||||
const { unitId, subUnitId } = command.params as unknown as ISheetCommandSharedParams || {};
|
||||
|
||||
const filterModel = this._sheetsFilterService.getFilterModel(unitId, subUnitId);
|
||||
if (!filterModel) return;
|
||||
const filteredOutRows = Array.from(filterModel.filteredOutRows).sort((a, b) => a - b);
|
||||
const newFilteredOutRows: number[] = [];
|
||||
let changed = false;
|
||||
|
||||
if (command.id === RemoveRowMutation.id) {
|
||||
const { startRow, endRow } = (command.params as IRemoveRowsMutationParams).range;
|
||||
const filterOutInRemove = filteredOutRows.filter((row) => row >= startRow && row <= endRow);
|
||||
filteredOutRows.forEach((row) => {
|
||||
if (row < startRow) {
|
||||
newFilteredOutRows.push(row);
|
||||
} else {
|
||||
changed = true;
|
||||
if (row <= endRow) {
|
||||
const newIndex = Math.max(startRow, newFilteredOutRows.length ? newFilteredOutRows[newFilteredOutRows.length - 1] + 1 : startRow);
|
||||
newFilteredOutRows.push(newIndex);
|
||||
} else {
|
||||
newFilteredOutRows.push(row - (endRow - startRow + 1 - filterOutInRemove.length));
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (command.id === InsertRowMutation.id) {
|
||||
const { startRow, endRow } = (command.params as IInsertRowMutationParams).range;
|
||||
filteredOutRows.forEach((row) => {
|
||||
if (row >= startRow) {
|
||||
changed = true;
|
||||
newFilteredOutRows.push(row + (endRow - startRow + 1));
|
||||
} else {
|
||||
newFilteredOutRows.push(row);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (changed) {
|
||||
filterModel.filteredOutRows = new Set(newFilteredOutRows);
|
||||
}
|
||||
|
||||
// extend filter range when set range values
|
||||
if (command.id === SetRangeValuesMutation.id && !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 && this._cellHasValue(cell)) {
|
||||
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,
|
||||
});
|
||||
this._registerRefRange(unitId, subUnitId);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
private _getExtendRegion(unitId: string, subUnitId: string): Nullable<IRange> {
|
||||
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;
|
||||
const target = getSheetCommandTarget(this._univerInstanceService, params);
|
||||
if (!target) return;
|
||||
|
||||
const { subUnitId, unitId } = target;
|
||||
const filterModel = this._sheetsFilterService.getFilterModel(unitId, subUnitId);
|
||||
if (!filterModel) return;
|
||||
const filterRange = filterModel.getRange();
|
||||
if (command.id === MoveRowsCommand.id && params.fromRange.startRow <= filterRange.startRow && params.fromRange.endRow < filterRange.endRow && params.fromRange.endRow >= filterRange.startRow) {
|
||||
this._sheetsFilterService.setFilterErrorMsg('sheets-filter.msg.filter-header-forbidden');
|
||||
throw new Error('[SheetsFilterController]: Cannot move header row of filter');
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
private _cellHasValue(cell: ICellData): boolean {
|
||||
const values = Object.values(cell);
|
||||
if (values.length === 0 || values.every((v) => v == null)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
126
packages/sheets-filter/src/facade/__tests__/create-test-bed.ts
Normal file
126
packages/sheets-filter/src/facade/__tests__/create-test-bed.ts
Normal file
@@ -0,0 +1,126 @@
|
||||
/**
|
||||
* Copyright 2023-present DreamNum Co., Ltd.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import type { Injector, IWorkbookData, UnitModel } from '@univerjs/core';
|
||||
import {
|
||||
ILogService,
|
||||
IUniverInstanceService,
|
||||
LocaleService,
|
||||
LocaleType,
|
||||
LogLevel,
|
||||
set,
|
||||
ThemeService,
|
||||
Univer,
|
||||
UniverInstanceType,
|
||||
} from '@univerjs/core';
|
||||
import { FUniver } from '@univerjs/core/facade';
|
||||
import { UniverSheetsFilterPlugin } from '@univerjs/sheets-filter';
|
||||
import enUS from '@univerjs/sheets/locale/en-US';
|
||||
import zhCN from '@univerjs/sheets/locale/zh-CN';
|
||||
import '@univerjs/sheets/facade';
|
||||
import '@univerjs/sheets-filter/facade';
|
||||
|
||||
function getTestWorkbookDataDemo(): IWorkbookData {
|
||||
return {
|
||||
id: 'test',
|
||||
appVersion: '3.0.0-alpha',
|
||||
sheets: {
|
||||
sheet1: {
|
||||
id: 'sheet1',
|
||||
name: 'sheet1',
|
||||
cellData: {
|
||||
0: {
|
||||
3: {
|
||||
f: '=SUM(A1)',
|
||||
si: '3e4r5t',
|
||||
},
|
||||
4: {
|
||||
v: 123,
|
||||
t: 2,
|
||||
},
|
||||
},
|
||||
1: {
|
||||
3: {
|
||||
f: '=SUM(A2)',
|
||||
si: 'OSPtzm',
|
||||
},
|
||||
},
|
||||
2: {
|
||||
3: {
|
||||
si: 'OSPtzm',
|
||||
},
|
||||
},
|
||||
3: {
|
||||
3: {
|
||||
si: 'OSPtzm',
|
||||
},
|
||||
},
|
||||
},
|
||||
rowCount: 100,
|
||||
columnCount: 100,
|
||||
},
|
||||
},
|
||||
locale: LocaleType.ZH_CN,
|
||||
name: '',
|
||||
sheetOrder: [],
|
||||
styles: {},
|
||||
};
|
||||
}
|
||||
|
||||
export interface ITestBed {
|
||||
univer: Univer;
|
||||
get: Injector['get'];
|
||||
sheet: UnitModel<IWorkbookData>;
|
||||
univerAPI: FUniver;
|
||||
injector: Injector;
|
||||
}
|
||||
|
||||
export function createFacadeTestBed(workbookData?: IWorkbookData): ITestBed {
|
||||
const univer = new Univer();
|
||||
const injector = univer.__getInjector();
|
||||
|
||||
// load i18n
|
||||
injector.get(LocaleService).load({ zhCN, enUS });
|
||||
|
||||
// load theme service
|
||||
const themeService = injector.get(ThemeService);
|
||||
const theme = themeService.getCurrentTheme();
|
||||
const newTheme = set(theme, 'gray.1000', '#35322b');
|
||||
themeService.setTheme(newTheme);
|
||||
|
||||
// register builtin plugins
|
||||
// note that UI plugins are not registered here, because the unit test environment does not have a UI
|
||||
univer.registerPlugin(UniverSheetsFilterPlugin);
|
||||
|
||||
const sheet = univer.createUnit<IWorkbookData, UnitModel<IWorkbookData>>(UniverInstanceType.UNIVER_SHEET, workbookData || getTestWorkbookDataDemo());
|
||||
const univerInstanceService = injector.get(IUniverInstanceService);
|
||||
univerInstanceService.focusUnit('test');
|
||||
|
||||
// set log level
|
||||
const logService = injector.get(ILogService);
|
||||
logService.setLogLevel(LogLevel.SILENT); // NOTE: change this to `LogLevel.VERBOSE` to debug tests via logs
|
||||
|
||||
// init data validation
|
||||
const univerAPI = FUniver.newAPI(injector);
|
||||
|
||||
return {
|
||||
univer,
|
||||
get: injector.get.bind(injector),
|
||||
sheet,
|
||||
univerAPI,
|
||||
injector,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,119 @@
|
||||
/**
|
||||
* Copyright 2023-present DreamNum Co., Ltd.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import type { Injector, IWorkbookData, UnitModel } from '@univerjs/core';
|
||||
import {
|
||||
ILogService,
|
||||
IUniverInstanceService,
|
||||
LocaleService,
|
||||
LocaleType,
|
||||
LogLevel,
|
||||
set,
|
||||
ThemeService,
|
||||
Univer,
|
||||
UniverInstanceType,
|
||||
} from '@univerjs/core';
|
||||
import { FUniver } from '@univerjs/core/facade';
|
||||
import { UniverSheetsFilterPlugin } from '@univerjs/sheets-filter';
|
||||
import enUS from '@univerjs/sheets/locale/en-US';
|
||||
import zhCN from '@univerjs/sheets/locale/zh-CN';
|
||||
import '@univerjs/sheets/facade';
|
||||
import '@univerjs/sheets-filter/facade';
|
||||
|
||||
function getTestWorkbookDataDemo(): IWorkbookData {
|
||||
return {
|
||||
id: 'test',
|
||||
appVersion: '3.0.0-alpha',
|
||||
sheets: {
|
||||
sheet1: {
|
||||
id: 'sheet1',
|
||||
name: 'sheet1',
|
||||
cellData: {
|
||||
0: {
|
||||
0: {
|
||||
v: 1,
|
||||
},
|
||||
},
|
||||
1: {
|
||||
0: {
|
||||
v: 2,
|
||||
},
|
||||
},
|
||||
2: {
|
||||
0: {
|
||||
v: 3,
|
||||
},
|
||||
},
|
||||
3: {
|
||||
0: {
|
||||
v: 4,
|
||||
},
|
||||
},
|
||||
},
|
||||
rowCount: 100,
|
||||
columnCount: 100,
|
||||
},
|
||||
},
|
||||
locale: LocaleType.ZH_CN,
|
||||
name: '',
|
||||
sheetOrder: [],
|
||||
styles: {},
|
||||
};
|
||||
}
|
||||
|
||||
export interface ITestBed {
|
||||
univer: Univer;
|
||||
get: Injector['get'];
|
||||
sheet: UnitModel<IWorkbookData>;
|
||||
univerAPI: FUniver;
|
||||
injector: Injector;
|
||||
}
|
||||
|
||||
export function createWorksheetTestBed(workbookData?: IWorkbookData): ITestBed {
|
||||
const univer = new Univer();
|
||||
const injector = univer.__getInjector();
|
||||
|
||||
// load i18n
|
||||
injector.get(LocaleService).load({ zhCN, enUS });
|
||||
|
||||
// load theme service
|
||||
const themeService = injector.get(ThemeService);
|
||||
const theme = themeService.getCurrentTheme();
|
||||
const newTheme = set(theme, 'gray.1000', '#35322b');
|
||||
themeService.setTheme(newTheme);
|
||||
|
||||
// register builtin plugins
|
||||
// note that UI plugins are not registered here, because the unit test environment does not have a UI
|
||||
univer.registerPlugin(UniverSheetsFilterPlugin);
|
||||
|
||||
const sheet = univer.createUnit<IWorkbookData, UnitModel<IWorkbookData>>(UniverInstanceType.UNIVER_SHEET, workbookData || getTestWorkbookDataDemo());
|
||||
const univerInstanceService = injector.get(IUniverInstanceService);
|
||||
univerInstanceService.focusUnit('test');
|
||||
|
||||
// set log level
|
||||
const logService = injector.get(ILogService);
|
||||
logService.setLogLevel(LogLevel.SILENT); // NOTE: change this to `LogLevel.VERBOSE` to debug tests via logs
|
||||
|
||||
const univerAPI = FUniver.newAPI(injector);
|
||||
|
||||
return {
|
||||
univer,
|
||||
get: injector.get.bind(injector),
|
||||
sheet,
|
||||
univerAPI,
|
||||
injector,
|
||||
};
|
||||
}
|
||||
98
packages/sheets-filter/src/facade/__tests__/f-range.spec.ts
Normal file
98
packages/sheets-filter/src/facade/__tests__/f-range.spec.ts
Normal file
@@ -0,0 +1,98 @@
|
||||
/**
|
||||
* Copyright 2023-present DreamNum Co., Ltd.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import type { IRange } from '@univerjs/core';
|
||||
import type { FUniver } from '@univerjs/core/facade';
|
||||
import { beforeEach, describe, expect, it } from 'vitest';
|
||||
import { createFacadeTestBed } from './create-test-bed';
|
||||
|
||||
describe('Test FRange', () => {
|
||||
let univerAPI: FUniver;
|
||||
|
||||
beforeEach(() => {
|
||||
const testBed = createFacadeTestBed();
|
||||
|
||||
univerAPI = testBed.univerAPI;
|
||||
});
|
||||
|
||||
describe('FFilter', () => {
|
||||
it('create, modify and clear filters with Facade API without UI plugin', async () => {
|
||||
const activeSheet = univerAPI.getActiveWorkbook()!.getActiveSheet();
|
||||
const filterEvents: string[] = [];
|
||||
const beforeFilterDisposable = univerAPI.addEvent(univerAPI.Event.SheetBeforeRangeFilter, (params) => {
|
||||
filterEvents.push(`before:${params.col}`);
|
||||
});
|
||||
const filteredDisposable = univerAPI.addEvent(univerAPI.Event.SheetRangeFiltered, (params) => {
|
||||
filterEvents.push(`after:${params.col}`);
|
||||
});
|
||||
const beforeClearDisposable = univerAPI.addEvent(univerAPI.Event.SheetBeforeRangeFilterClear, (params) => {
|
||||
filterEvents.push(`before-clear:${params.worksheet.getSheetId()}`);
|
||||
});
|
||||
const clearedDisposable = univerAPI.addEvent(univerAPI.Event.SheetRangeFilterCleared, (params) => {
|
||||
filterEvents.push(`after-clear:${params.worksheet.getSheetId()}`);
|
||||
});
|
||||
|
||||
expect(activeSheet.getFilter()).toBeNull();
|
||||
expect(activeSheet.getRange(0, 0, 1, 1).getFilter()).toBeNull();
|
||||
|
||||
const range = activeSheet.getRange(0, 0, 10, 10);
|
||||
const filter = (await range.createFilter())!;
|
||||
|
||||
expect(filter).not.toBeNull();
|
||||
expect(activeSheet.getFilter()).not.toBeNull();
|
||||
expect(activeSheet.getRange(0, 0, 1, 1).getFilter()).not.toBeNull();
|
||||
expect(filter.getRange().getRange()).toStrictEqual({
|
||||
unitId: univerAPI.getActiveWorkbook()?.getId(),
|
||||
sheetId: activeSheet.getSheetId(),
|
||||
startColumn: 0,
|
||||
startRow: 0,
|
||||
endColumn: 9,
|
||||
endRow: 9,
|
||||
} as IRange);
|
||||
|
||||
expect(await filter.setColumnFilterCriteria(1, { colId: 1, filters: { blank: true } })).toBeTruthy();
|
||||
expect(filter.getColumnFilterCriteria(1)).toEqual({ colId: 1, filters: { blank: true } });
|
||||
|
||||
expect(await filter.setColumnFilterCriteria(2, { colId: 2, filters: { filters: ['a'] } })).toBeTruthy();
|
||||
expect(filter.getColumnFilterCriteria(2)).toEqual({ colId: 2, filters: { filters: ['a'] } });
|
||||
|
||||
expect(await filter.removeColumnFilterCriteria(1)).toBeTruthy();
|
||||
expect(filter.getColumnFilterCriteria(1)).toBeFalsy();
|
||||
|
||||
expect(await filter.removeFilterCriteria()).toBeTruthy();
|
||||
expect(filter.getColumnFilterCriteria(2)).toBeFalsy();
|
||||
|
||||
expect(filterEvents).toEqual([
|
||||
'before:1',
|
||||
'after:1',
|
||||
'before:2',
|
||||
'after:2',
|
||||
'before:1',
|
||||
'after:1',
|
||||
'before-clear:sheet1',
|
||||
'after-clear:sheet1',
|
||||
]);
|
||||
|
||||
expect(await filter.remove()).toBeTruthy();
|
||||
expect(activeSheet.getFilter()).toBeNull();
|
||||
|
||||
beforeFilterDisposable.dispose();
|
||||
filteredDisposable.dispose();
|
||||
beforeClearDisposable.dispose();
|
||||
clearedDisposable.dispose();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,34 @@
|
||||
/**
|
||||
* Copyright 2023-present DreamNum Co., Ltd.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import type { FUniver } from '@univerjs/core/facade';
|
||||
import { beforeEach, describe, expect, it } from 'vitest';
|
||||
import { createWorksheetTestBed } from './create-worksheet-test-bed';
|
||||
|
||||
describe('Test FWorksheet', () => {
|
||||
let univerAPI: FUniver;
|
||||
|
||||
beforeEach(() => {
|
||||
const testBed = createWorksheetTestBed();
|
||||
|
||||
univerAPI = testBed.univerAPI;
|
||||
});
|
||||
|
||||
it('Worksheet getFilter', async () => {
|
||||
const activeSheet = univerAPI.getActiveWorkbook()?.getSheetByName('sheet1');
|
||||
expect(activeSheet?.getFilter()).toBeDefined();
|
||||
});
|
||||
});
|
||||
39
packages/sheets-filter/src/facade/f-enum.ts
Normal file
39
packages/sheets-filter/src/facade/f-enum.ts
Normal file
@@ -0,0 +1,39 @@
|
||||
/**
|
||||
* Copyright 2023-present DreamNum Co., Ltd.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { FEnum } from '@univerjs/core/facade';
|
||||
import { CustomFilterOperator } from '@univerjs/sheets-filter';
|
||||
|
||||
/**
|
||||
* @ignore
|
||||
*/
|
||||
export interface IFSheetsFilterEnumMixin {
|
||||
/** Please refer to {@link CustomFilterOperator}. */
|
||||
CustomFilterOperator: typeof CustomFilterOperator;
|
||||
}
|
||||
|
||||
export class FSheetsFilterEnumMixin extends FEnum implements IFSheetsFilterEnumMixin {
|
||||
override get CustomFilterOperator(): typeof CustomFilterOperator {
|
||||
return CustomFilterOperator;
|
||||
};
|
||||
}
|
||||
|
||||
FEnum.extend(FSheetsFilterEnumMixin);
|
||||
declare module '@univerjs/core/facade' {
|
||||
// eslint-disable-next-line ts/naming-convention
|
||||
interface FEnum extends IFSheetsFilterEnumMixin {
|
||||
}
|
||||
}
|
||||
145
packages/sheets-filter/src/facade/f-event.ts
Normal file
145
packages/sheets-filter/src/facade/f-event.ts
Normal file
@@ -0,0 +1,145 @@
|
||||
/**
|
||||
* Copyright 2023-present DreamNum Co., Ltd.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import type { IEventBase } from '@univerjs/core/facade';
|
||||
import type { ISetSheetsFilterCriteriaCommandParams } from '@univerjs/sheets-filter';
|
||||
import type { FWorkbook, FWorksheet } from '@univerjs/sheets/facade';
|
||||
import { FEventName } from '@univerjs/core/facade';
|
||||
|
||||
/**
|
||||
* The params of SheetRangeFiltered and SheetBeforeRangeFilter events.
|
||||
* @param workbook - The corresponding workbook wrapped in {@link FWorkbook}.
|
||||
* @param worksheet - The corresponding worksheet wrapped in {@link FWorksheet}.
|
||||
* @param col - The column on which the filter criteria is changed.
|
||||
* @param criteria - Raw filter criteria.
|
||||
*/
|
||||
export interface ISheetsRangeFilteredEventParams extends IEventBase, Pick<ISetSheetsFilterCriteriaCommandParams, 'criteria' | 'col'> {
|
||||
workbook: FWorkbook;
|
||||
worksheet: FWorksheet;
|
||||
}
|
||||
|
||||
/**
|
||||
* The params of SheetRangeFilterCleared and SheetBeforeRangeFilterClear events.
|
||||
* @param workbook - The corresponding workbook wrapped in {@link FWorkbook}.
|
||||
* @param worksheet - The corresponding worksheet wrapped in {@link FWorksheet}.
|
||||
*/
|
||||
export interface ISheetsRangeFilterClearedEventParams extends IEventBase {
|
||||
workbook: FWorkbook;
|
||||
worksheet: FWorksheet;
|
||||
}
|
||||
|
||||
/**
|
||||
* @ignore
|
||||
*/
|
||||
interface ISheetsFilterEventParamConfig {
|
||||
SheetBeforeRangeFilter: ISheetsRangeFilteredEventParams;
|
||||
SheetRangeFiltered: ISheetsRangeFilteredEventParams;
|
||||
SheetBeforeRangeFilterClear: ISheetsRangeFilterClearedEventParams;
|
||||
SheetRangeFilterCleared: ISheetsRangeFilterClearedEventParams;
|
||||
}
|
||||
|
||||
/**
|
||||
* @ignore
|
||||
*/
|
||||
export interface IFSheetsFilterEventNameMixin {
|
||||
/**
|
||||
* This event will be emitted when the filter criteria on a column is changed.
|
||||
* @see {@link ISheetRangeFilteredParams}
|
||||
* @example
|
||||
* ```typescript
|
||||
* const callbackDisposable = univerAPI.addEvent(univerAPI.Event.SheetRangeFiltered, (params) => {
|
||||
* console.log(params);
|
||||
* const { workbook, worksheet, col, criteria } = params;
|
||||
*
|
||||
* // your custom logic
|
||||
* });
|
||||
*
|
||||
* // Remove the event listener, use `callbackDisposable.dispose()`
|
||||
* ```
|
||||
*/
|
||||
readonly SheetRangeFiltered: 'SheetRangeFiltered';
|
||||
|
||||
/**
|
||||
* This event will be emitted before the filter criteria on a column is changed.
|
||||
* @see {@link ISheetRangeFilteredParams}
|
||||
* @example
|
||||
* ```typescript
|
||||
* const callbackDisposable = univerAPI.addEvent(univerAPI.Event.SheetBeforeRangeFilter, (params) => {
|
||||
* console.log(params);
|
||||
* const { workbook, worksheet, col, criteria } = params;
|
||||
*
|
||||
* // your custom logic
|
||||
*
|
||||
* // Cancel the filter criteria change operation
|
||||
* params.cancel = true;
|
||||
* });
|
||||
*
|
||||
* // Remove the event listener, use `callbackDisposable.dispose()`
|
||||
* ```
|
||||
*/
|
||||
readonly SheetBeforeRangeFilter: 'SheetBeforeRangeFilter';
|
||||
|
||||
/**
|
||||
* This event will be emitted when the filter on a worksheet is cleared.
|
||||
* @see {@link ISheetRangeFilterClearedEventParams}
|
||||
* @example
|
||||
* ```typescript
|
||||
* const callbackDisposable = univerAPI.addEvent(univerAPI.Event.SheetRangeFilterCleared, (params) => {
|
||||
* console.log(params);
|
||||
* const { workbook, worksheet } = params;
|
||||
*
|
||||
* // your custom logic
|
||||
* });
|
||||
*
|
||||
* // Remove the event listener, use `callbackDisposable.dispose()`
|
||||
* ```
|
||||
*/
|
||||
readonly SheetRangeFilterCleared: 'SheetRangeFilterCleared';
|
||||
|
||||
/**
|
||||
* This event will be emitted after the filter on a worksheet is cleared.
|
||||
* @see {@link ISheetRangeFilterClearedEventParams}
|
||||
* @example
|
||||
* ```typescript
|
||||
* const callbackDisposable = univerAPI.addEvent(univerAPI.Event.SheetBeforeRangeFilterClear, (params) => {
|
||||
* console.log(params);
|
||||
* const { workbook, worksheet } = params;
|
||||
*
|
||||
* // your custom logic
|
||||
*
|
||||
* // Cancel the filter clear operation
|
||||
* params.cancel = true;
|
||||
* });
|
||||
*
|
||||
* // Remove the event listener, use `callbackDisposable.dispose()`
|
||||
* ```
|
||||
*/
|
||||
readonly SheetBeforeRangeFilterClear: 'SheetBeforeRangeFilterClear';
|
||||
}
|
||||
|
||||
export class FSheetsFilterEventNameMixin extends FEventName implements IFSheetsFilterEventNameMixin {
|
||||
override get SheetBeforeRangeFilter(): 'SheetBeforeRangeFilter' { return 'SheetBeforeRangeFilter'; }
|
||||
override get SheetRangeFiltered(): 'SheetRangeFiltered' { return 'SheetRangeFiltered'; }
|
||||
override get SheetRangeFilterCleared(): 'SheetRangeFilterCleared' { return 'SheetRangeFilterCleared'; };
|
||||
override get SheetBeforeRangeFilterClear(): 'SheetBeforeRangeFilterClear' { return 'SheetBeforeRangeFilterClear'; }
|
||||
}
|
||||
|
||||
FEventName.extend(FSheetsFilterEventNameMixin);
|
||||
declare module '@univerjs/core/facade' {
|
||||
// eslint-disable-next-line ts/naming-convention
|
||||
interface FEventName extends IFSheetsFilterEventNameMixin { }
|
||||
interface IEventParamConfig extends ISheetsFilterEventParamConfig { }
|
||||
}
|
||||
354
packages/sheets-filter/src/facade/f-filter.ts
Normal file
354
packages/sheets-filter/src/facade/f-filter.ts
Normal file
@@ -0,0 +1,354 @@
|
||||
/**
|
||||
* Copyright 2023-present DreamNum Co., Ltd.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES 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 { Nullable, Workbook, Worksheet } from '@univerjs/core';
|
||||
import type { ISheetCommandSharedParams } from '@univerjs/sheets';
|
||||
import type { FilterModel, IFilterColumn, ISetSheetsFilterCriteriaCommandParams } from '@univerjs/sheets-filter';
|
||||
import { ICommandService, Inject, Injector } from '@univerjs/core';
|
||||
import {
|
||||
ClearSheetsFilterCriteriaCommand,
|
||||
RemoveSheetFilterCommand,
|
||||
SetSheetsFilterCriteriaCommand,
|
||||
} from '@univerjs/sheets-filter';
|
||||
import { FRange } from '@univerjs/sheets/facade';
|
||||
|
||||
/**
|
||||
* This interface class provides methods to modify the filter settings of a worksheet.
|
||||
* @hideconstructor
|
||||
*/
|
||||
export class FFilter {
|
||||
constructor(
|
||||
private readonly _workbook: Workbook,
|
||||
private readonly _worksheet: Worksheet,
|
||||
private readonly _filterModel: FilterModel,
|
||||
@Inject(Injector) private readonly _injector: Injector,
|
||||
@ICommandService private readonly _commandSrv: ICommandService
|
||||
) {
|
||||
// empty
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the filtered out rows by this filter.
|
||||
* @returns {number[]} Filtered out rows by this filter.
|
||||
* @example
|
||||
* ```typescript
|
||||
* const fWorkbook = univerAPI.getActiveWorkbook();
|
||||
* const fWorksheet = fWorkbook.getSheetByName('Sheet1');
|
||||
* if (!fWorksheet) return;
|
||||
*
|
||||
* // Set some values of the range C1:F10
|
||||
* const fRange = fWorksheet.getRange('C1:F10');
|
||||
* fRange.setValues([
|
||||
* [1, 2, 3, 4],
|
||||
* [2, 3, 4, 5],
|
||||
* [3, 4, 5, 6],
|
||||
* [4, 5, 6, 7],
|
||||
* [5, 6, 7, 8],
|
||||
* [6, 7, 8, 9],
|
||||
* [7, 8, 9, 10],
|
||||
* [8, 9, 10, 11],
|
||||
* [9, 10, 11, 12],
|
||||
* [10, 11, 12, 13],
|
||||
* ]);
|
||||
*
|
||||
* // Create a filter on the range C1:F10
|
||||
* let fFilter = fRange.createFilter();
|
||||
*
|
||||
* // If the filter already exists, remove it and create a new one
|
||||
* if (!fFilter) {
|
||||
* fRange.getFilter().remove();
|
||||
* fFilter = fRange.createFilter();
|
||||
* }
|
||||
*
|
||||
* // Set the filter criteria of the column C, filter out the rows that are not 1, 5, 9
|
||||
* const column = fWorksheet.getRange('C:C').getColumn();
|
||||
* fFilter.setColumnFilterCriteria(column, {
|
||||
* colId: 0,
|
||||
* filters: {
|
||||
* filters: ['1', '5', '9'],
|
||||
* },
|
||||
* });
|
||||
*
|
||||
* // Get the filtered out rows
|
||||
* console.log(fFilter.getFilteredOutRows()); // [1, 2, 3, 5, 6, 7, 9]
|
||||
* ```
|
||||
*/
|
||||
getFilteredOutRows(): number[] {
|
||||
return Array.from(this._filterModel.filteredOutRows).sort();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the filter criteria of a column.
|
||||
* @param {number} column - The absolute, zero-based worksheet column index, not an index relative to the filter range.
|
||||
* @returns {Nullable<IFilterColumn>} The filter criteria of the column.
|
||||
* @example
|
||||
* ```typescript
|
||||
* const fWorkbook = univerAPI.getActiveWorkbook();
|
||||
* const fWorksheet = fWorkbook.getSheetByName('Sheet1');
|
||||
* if (!fWorksheet) return;
|
||||
*
|
||||
* // Set some values of the range C1:F10
|
||||
* const fRange = fWorksheet.getRange('C1:F10');
|
||||
* fRange.setValues([
|
||||
* [1, 2, 3, 4],
|
||||
* [2, 3, 4, 5],
|
||||
* [3, 4, 5, 6],
|
||||
* [4, 5, 6, 7],
|
||||
* [5, 6, 7, 8],
|
||||
* [6, 7, 8, 9],
|
||||
* [7, 8, 9, 10],
|
||||
* [8, 9, 10, 11],
|
||||
* [9, 10, 11, 12],
|
||||
* [10, 11, 12, 13],
|
||||
* ]);
|
||||
*
|
||||
* // Create a filter on the range C1:F10
|
||||
* let fFilter = fRange.createFilter();
|
||||
*
|
||||
* // If the filter already exists, remove it and create a new one
|
||||
* if (!fFilter) {
|
||||
* fRange.getFilter().remove();
|
||||
* fFilter = fRange.createFilter();
|
||||
* }
|
||||
*
|
||||
* // Set the filter criteria of the column C, filter out the rows that are not 1, 5, 9
|
||||
* const column = fWorksheet.getRange('C:C').getColumn();
|
||||
* fFilter.setColumnFilterCriteria(column, {
|
||||
* colId: 0,
|
||||
* filters: {
|
||||
* filters: ['1', '5', '9'],
|
||||
* },
|
||||
* });
|
||||
*
|
||||
* // Print the filter criteria of the column C and D
|
||||
* console.log(fFilter.getColumnFilterCriteria(column)); // { colId: 0, filters: { filters: ['1', '5', '9'] } }
|
||||
* console.log(fFilter.getColumnFilterCriteria(column + 1)); // undefined
|
||||
* ```
|
||||
*/
|
||||
getColumnFilterCriteria(column: number): Nullable<IFilterColumn> {
|
||||
return this._filterModel.getFilterColumn(column)?.getColumnData();
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear the filter criteria of a column.
|
||||
* @param {number} column - The absolute, zero-based worksheet column index, not an index relative to the filter range.
|
||||
* @returns {FFilter} The FFilter instance for chaining.
|
||||
* @example
|
||||
* ```typescript
|
||||
* const fWorkbook = univerAPI.getActiveWorkbook();
|
||||
* const fWorksheet = fWorkbook.getSheetByName('Sheet1');
|
||||
* if (!fWorksheet) return;
|
||||
*
|
||||
* // Set some values of the range C1:F10
|
||||
* const fRange = fWorksheet.getRange('C1:F10');
|
||||
* fRange.setValues([
|
||||
* [1, 2, 3, 4],
|
||||
* [2, 3, 4, 5],
|
||||
* [3, 4, 5, 6],
|
||||
* [4, 5, 6, 7],
|
||||
* [5, 6, 7, 8],
|
||||
* [6, 7, 8, 9],
|
||||
* [7, 8, 9, 10],
|
||||
* [8, 9, 10, 11],
|
||||
* [9, 10, 11, 12],
|
||||
* [10, 11, 12, 13],
|
||||
* ]);
|
||||
*
|
||||
* // Create a filter on the range C1:F10
|
||||
* let fFilter = fRange.createFilter();
|
||||
*
|
||||
* // If the filter already exists, remove it and create a new one
|
||||
* if (!fFilter) {
|
||||
* fRange.getFilter().remove();
|
||||
* fFilter = fRange.createFilter();
|
||||
* }
|
||||
*
|
||||
* // Set the filter criteria of the column C, filter out the rows that are not 1, 5, 9
|
||||
* const column = fWorksheet.getRange('C:C').getColumn();
|
||||
* fFilter.setColumnFilterCriteria(column, {
|
||||
* colId: 0,
|
||||
* filters: {
|
||||
* filters: ['1', '5', '9'],
|
||||
* },
|
||||
* });
|
||||
*
|
||||
* // Clear the filter criteria of the column C after 3 seconds
|
||||
* setTimeout(() => {
|
||||
* fFilter.removeColumnFilterCriteria(column);
|
||||
* }, 3000);
|
||||
* ```
|
||||
*/
|
||||
removeColumnFilterCriteria(column: number): FFilter {
|
||||
this._commandSrv.syncExecuteCommand(SetSheetsFilterCriteriaCommand.id, {
|
||||
unitId: this._workbook.getUnitId(),
|
||||
subUnitId: this._worksheet.getSheetId(),
|
||||
col: column,
|
||||
criteria: null,
|
||||
} as ISetSheetsFilterCriteriaCommandParams);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the filter criteria of a column.
|
||||
* @param {number} column - The absolute, zero-based worksheet column index, not an index relative to the filter range.
|
||||
* @param {ISetSheetsFilterCriteriaCommandParams['criteria']} criteria - The new filter criteria.
|
||||
* @returns {FFilter} The FFilter instance for chaining.
|
||||
* @example
|
||||
* ```typescript
|
||||
* const fWorkbook = univerAPI.getActiveWorkbook();
|
||||
* const fWorksheet = fWorkbook.getSheetByName('Sheet1');
|
||||
* if (!fWorksheet) return;
|
||||
*
|
||||
* // Set some values of the range C1:F10
|
||||
* const fRange = fWorksheet.getRange('C1:F10');
|
||||
* fRange.setValues([
|
||||
* [1, 2, 3, 4],
|
||||
* [2, 3, 4, 5],
|
||||
* [3, 4, 5, 6],
|
||||
* [4, 5, 6, 7],
|
||||
* [5, 6, 7, 8],
|
||||
* [6, 7, 8, 9],
|
||||
* [7, 8, 9, 10],
|
||||
* [8, 9, 10, 11],
|
||||
* [9, 10, 11, 12],
|
||||
* [10, 11, 12, 13],
|
||||
* ]);
|
||||
*
|
||||
* // Create a filter on the range C1:F10
|
||||
* let fFilter = fRange.createFilter();
|
||||
*
|
||||
* // If the filter already exists, remove it and create a new one
|
||||
* if (!fFilter) {
|
||||
* fRange.getFilter().remove();
|
||||
* fFilter = fRange.createFilter();
|
||||
* }
|
||||
*
|
||||
* // Set the filter criteria of the column C, filter out the rows that are not 1, 5, 9
|
||||
* const column = fWorksheet.getRange('C:C').getColumn();
|
||||
* fFilter.setColumnFilterCriteria(column, {
|
||||
* colId: 0,
|
||||
* filters: {
|
||||
* filters: ['1', '5', '9'],
|
||||
* },
|
||||
* });
|
||||
* ```
|
||||
*/
|
||||
setColumnFilterCriteria(column: number, criteria: ISetSheetsFilterCriteriaCommandParams['criteria']): FFilter {
|
||||
this._commandSrv.syncExecuteCommand(SetSheetsFilterCriteriaCommand.id, {
|
||||
unitId: this._workbook.getUnitId(),
|
||||
subUnitId: this._worksheet.getSheetId(),
|
||||
col: column,
|
||||
criteria,
|
||||
} as ISetSheetsFilterCriteriaCommandParams);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the range of the filter.
|
||||
* @returns {FRange} The range of the filter.
|
||||
* @example
|
||||
* ```typescript
|
||||
* const fWorkbook = univerAPI.getActiveWorkbook();
|
||||
* const fWorksheet = fWorkbook.getSheetByName('Sheet1');
|
||||
* if (!fWorksheet) return;
|
||||
* const fFilter = fWorksheet.getFilter();
|
||||
* console.log(fFilter?.getRange().getA1Notation());
|
||||
* ```
|
||||
*/
|
||||
getRange(): FRange {
|
||||
const range = this._filterModel.getRange();
|
||||
return this._injector.createInstance(FRange, this._workbook, this._worksheet, range);
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove the filter criteria of all columns.
|
||||
* @returns {FFilter} The FFilter instance for chaining.
|
||||
* @example
|
||||
* ```typescript
|
||||
* const fWorkbook = univerAPI.getActiveWorkbook();
|
||||
* const fWorksheet = fWorkbook.getSheetByName('Sheet1');
|
||||
* if (!fWorksheet) return;
|
||||
*
|
||||
* // Set some values of the range C1:F10
|
||||
* const fRange = fWorksheet.getRange('C1:F10');
|
||||
* fRange.setValues([
|
||||
* [1, 2, 3, 4],
|
||||
* [2, 3, 4, 5],
|
||||
* [3, 4, 5, 6],
|
||||
* [4, 5, 6, 7],
|
||||
* [5, 6, 7, 8],
|
||||
* [6, 7, 8, 9],
|
||||
* [7, 8, 9, 10],
|
||||
* [8, 9, 10, 11],
|
||||
* [9, 10, 11, 12],
|
||||
* [10, 11, 12, 13],
|
||||
* ]);
|
||||
*
|
||||
* // Create a filter on the range C1:F10
|
||||
* let fFilter = fRange.createFilter();
|
||||
*
|
||||
* // If the filter already exists, remove it and create a new one
|
||||
* if (!fFilter) {
|
||||
* fRange.getFilter().remove();
|
||||
* fFilter = fRange.createFilter();
|
||||
* }
|
||||
*
|
||||
* // Set the filter criteria of the column C, filter out the rows that are not 1, 5, 9
|
||||
* const column = fWorksheet.getRange('C:C').getColumn();
|
||||
* fFilter.setColumnFilterCriteria(column, {
|
||||
* colId: 0,
|
||||
* filters: {
|
||||
* filters: ['1', '5', '9'],
|
||||
* },
|
||||
* });
|
||||
*
|
||||
* // Clear the filter criteria of all columns after 3 seconds
|
||||
* setTimeout(() => {
|
||||
* fFilter.removeFilterCriteria();
|
||||
* }, 3000);
|
||||
* ```
|
||||
*/
|
||||
removeFilterCriteria(): FFilter {
|
||||
this._commandSrv.syncExecuteCommand(ClearSheetsFilterCriteriaCommand.id);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove the filter from the worksheet.
|
||||
* @returns {boolean} True if the filter is removed successfully; otherwise, false.
|
||||
* @example
|
||||
* ```typescript
|
||||
* const fWorkbook = univerAPI.getActiveWorkbook();
|
||||
* const fWorksheet = fWorkbook.getSheetByName('Sheet1');
|
||||
* if (!fWorksheet) return;
|
||||
* const fRange = fWorksheet.getRange('A1:D14');
|
||||
* let fFilter = fRange.createFilter();
|
||||
*
|
||||
* // If the worksheet already has a filter, remove it and create a new filter.
|
||||
* if (!fFilter) {
|
||||
* fWorksheet.getFilter().remove();
|
||||
* fFilter = fRange.createFilter();
|
||||
* }
|
||||
* console.log(fFilter);
|
||||
* ```
|
||||
*/
|
||||
remove(): boolean {
|
||||
return this._commandSrv.syncExecuteCommand(RemoveSheetFilterCommand.id, {
|
||||
unitId: this._workbook.getUnitId(),
|
||||
subUnitId: this._worksheet.getSheetId(),
|
||||
} as ISheetCommandSharedParams);
|
||||
}
|
||||
}
|
||||
111
packages/sheets-filter/src/facade/f-range.ts
Normal file
111
packages/sheets-filter/src/facade/f-range.ts
Normal file
@@ -0,0 +1,111 @@
|
||||
/**
|
||||
* Copyright 2023-present DreamNum Co., Ltd.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import type { Nullable } from '@univerjs/core';
|
||||
import type { FilterModel, ISetSheetFilterRangeCommandParams } from '@univerjs/sheets-filter';
|
||||
import { SetSheetFilterRangeCommand, SheetsFilterService } from '@univerjs/sheets-filter';
|
||||
import { FRange } from '@univerjs/sheets/facade';
|
||||
import { FFilter } from './f-filter';
|
||||
|
||||
// TODO: add jsdoc comments for the following API
|
||||
|
||||
/**
|
||||
* @ignore
|
||||
*/
|
||||
export interface IFRangeSheetsFilterMixin {
|
||||
/**
|
||||
* Create a filter for the current range. If the worksheet already has a filter, this method would return `null`.
|
||||
* @returns {FFilter | null} The FFilter instance to handle the filter.
|
||||
* @example
|
||||
* ```typescript
|
||||
* const fWorkbook = univerAPI.getActiveWorkbook();
|
||||
* const fWorksheet = fWorkbook.getSheetByName('Sheet1');
|
||||
* if (!fWorksheet) return;
|
||||
* const fRange = fWorksheet.getRange('A1:D14');
|
||||
* let fFilter = fRange.createFilter();
|
||||
*
|
||||
* // If the worksheet already has a filter, remove it and create a new filter.
|
||||
* if (!fFilter) {
|
||||
* fWorksheet.getFilter().remove();
|
||||
* fFilter = fRange.createFilter();
|
||||
* }
|
||||
* console.log(fFilter, fFilter.getRange().getA1Notation());
|
||||
* ```
|
||||
*/
|
||||
createFilter(this: FRange): FFilter | null;
|
||||
|
||||
/**
|
||||
* Get the filter in the worksheet to which the range belongs. If the worksheet does not have a filter, this method would return `null`.
|
||||
* Normally, you can directly call `getFilter` on {@link FWorksheet}.
|
||||
* @returns {FFilter | null} The FFilter instance to handle the filter.
|
||||
* @example
|
||||
* ```typescript
|
||||
* const fWorkbook = univerAPI.getActiveWorkbook();
|
||||
* const fWorksheet = fWorkbook.getSheetByName('Sheet1');
|
||||
* if (!fWorksheet) return;
|
||||
* const fRange = fWorksheet.getRange('A1:D14');
|
||||
* let fFilter = fRange.getFilter();
|
||||
*
|
||||
* // If the worksheet does not have a filter, create a new filter.
|
||||
* if (!fFilter) {
|
||||
* fFilter = fRange.createFilter();
|
||||
* }
|
||||
* console.log(fFilter, fFilter.getRange().getA1Notation());
|
||||
* ```
|
||||
*/
|
||||
getFilter(): FFilter | null;
|
||||
}
|
||||
|
||||
export class FRangeSheetsFilterMixin extends FRange implements IFRangeSheetsFilterMixin {
|
||||
override createFilter(): FFilter | null {
|
||||
if (this._getFilterModel()) return null;
|
||||
|
||||
const success = this._commandService.syncExecuteCommand(SetSheetFilterRangeCommand.id, <ISetSheetFilterRangeCommandParams>{
|
||||
unitId: this._workbook.getUnitId(),
|
||||
subUnitId: this._worksheet.getSheetId(),
|
||||
range: this._range,
|
||||
});
|
||||
|
||||
if (!success) return null;
|
||||
|
||||
return this.getFilter();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the filter for the current range's worksheet.
|
||||
* @returns {FFilter | null} The interface class to handle the filter. If the worksheet does not have a filter,
|
||||
* this method would return `null`.
|
||||
*/
|
||||
override getFilter(): FFilter | null {
|
||||
const filterModel = this._getFilterModel();
|
||||
if (!filterModel) return null;
|
||||
|
||||
return this._injector.createInstance(FFilter, this._workbook, this._worksheet, filterModel);
|
||||
}
|
||||
|
||||
private _getFilterModel(): Nullable<FilterModel> {
|
||||
return this._injector.get(SheetsFilterService).getFilterModel(
|
||||
this._workbook.getUnitId(),
|
||||
this._worksheet.getSheetId()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
FRange.extend(FRangeSheetsFilterMixin);
|
||||
declare module '@univerjs/sheets/facade' {
|
||||
// eslint-disable-next-line ts/naming-convention
|
||||
interface FRange extends IFRangeSheetsFilterMixin { }
|
||||
}
|
||||
135
packages/sheets-filter/src/facade/f-univer.ts
Normal file
135
packages/sheets-filter/src/facade/f-univer.ts
Normal file
@@ -0,0 +1,135 @@
|
||||
/**
|
||||
* Copyright 2023-present DreamNum Co., Ltd.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import type { ICommandInfo, Injector } from '@univerjs/core';
|
||||
import type { ISetSheetsFilterCriteriaCommandParams } from '@univerjs/sheets-filter';
|
||||
import type { ISheetsRangeFilterClearedEventParams, ISheetsRangeFilteredEventParams } from './f-event';
|
||||
import { ICommandService } from '@univerjs/core';
|
||||
import { FUniver } from '@univerjs/core/facade';
|
||||
import { ClearSheetsFilterCriteriaCommand, SetSheetsFilterCriteriaCommand } from '@univerjs/sheets-filter';
|
||||
|
||||
export class FUniverSheetsFilterMixin extends FUniver {
|
||||
/**
|
||||
* @ignore
|
||||
*/
|
||||
override _initialize(injector: Injector): void {
|
||||
const commandService = injector.get(ICommandService);
|
||||
|
||||
// Register filter criteria set event handlers
|
||||
this.disposeWithMe(
|
||||
this.registerEventHandler(
|
||||
this.Event.SheetBeforeRangeFilter,
|
||||
() => commandService.beforeCommandExecuted((commandInfo) => {
|
||||
if (commandInfo.id === SetSheetsFilterCriteriaCommand.id) {
|
||||
this._beforeRangeFilter(commandInfo as Readonly<ICommandInfo<ISetSheetsFilterCriteriaCommandParams>>);
|
||||
}
|
||||
})
|
||||
)
|
||||
);
|
||||
|
||||
this.disposeWithMe(
|
||||
this.registerEventHandler(
|
||||
this.Event.SheetBeforeRangeFilterClear,
|
||||
() => commandService.beforeCommandExecuted((commandInfo) => {
|
||||
if (commandInfo.id === ClearSheetsFilterCriteriaCommand.id) {
|
||||
this._beforeRangeFilterClear();
|
||||
}
|
||||
})
|
||||
)
|
||||
);
|
||||
|
||||
// Register filter criteria execution event handlers
|
||||
this.disposeWithMe(
|
||||
this.registerEventHandler(
|
||||
this.Event.SheetRangeFiltered,
|
||||
() => commandService.onCommandExecuted((commandInfo) => {
|
||||
if (commandInfo.id === SetSheetsFilterCriteriaCommand.id) {
|
||||
this._onRangeFiltered(commandInfo as Readonly<ICommandInfo<ISetSheetsFilterCriteriaCommandParams>>);
|
||||
}
|
||||
})
|
||||
)
|
||||
);
|
||||
|
||||
this.disposeWithMe(
|
||||
this.registerEventHandler(
|
||||
this.Event.SheetRangeFilterCleared,
|
||||
() => commandService.onCommandExecuted((commandInfo) => {
|
||||
if (commandInfo.id === ClearSheetsFilterCriteriaCommand.id) {
|
||||
this._onRangeFilterCleared();
|
||||
}
|
||||
})
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
private _beforeRangeFilter(commandInfo: Readonly<ICommandInfo<ISetSheetsFilterCriteriaCommandParams>>): void {
|
||||
const params = commandInfo.params!;
|
||||
const fWorkbook = this.getWorkbook(params.unitId)!;
|
||||
const eventParams: ISheetsRangeFilteredEventParams = {
|
||||
workbook: fWorkbook,
|
||||
worksheet: fWorkbook.getSheetBySheetId(params.subUnitId)!,
|
||||
col: params.col,
|
||||
criteria: params.criteria,
|
||||
};
|
||||
|
||||
this.fireEvent(this.Event.SheetBeforeRangeFilter, eventParams);
|
||||
if (eventParams.cancel) {
|
||||
throw new Error('SetSheetsFilterCriteriaCommand canceled.');
|
||||
}
|
||||
}
|
||||
|
||||
private _onRangeFiltered(commandInfo: Readonly<ICommandInfo<ISetSheetsFilterCriteriaCommandParams>>): void {
|
||||
const params = commandInfo.params!;
|
||||
const fWorkbook = this.getWorkbook(params.unitId)!;
|
||||
const eventParams: ISheetsRangeFilteredEventParams = {
|
||||
workbook: fWorkbook,
|
||||
worksheet: fWorkbook.getSheetBySheetId(params.subUnitId)!,
|
||||
col: params.col,
|
||||
criteria: params.criteria,
|
||||
};
|
||||
|
||||
this.fireEvent(this.Event.SheetRangeFiltered, eventParams);
|
||||
}
|
||||
|
||||
private _beforeRangeFilterClear(): void {
|
||||
const fWorkbook = this.getActiveWorkbook();
|
||||
if (!fWorkbook) return;
|
||||
|
||||
const eventParams: ISheetsRangeFilterClearedEventParams = {
|
||||
workbook: fWorkbook,
|
||||
worksheet: fWorkbook.getActiveSheet(),
|
||||
};
|
||||
|
||||
this.fireEvent(this.Event.SheetBeforeRangeFilterClear, eventParams);
|
||||
if (eventParams.cancel) {
|
||||
throw new Error('SetSheetsFilterCriteriaCommand canceled.');
|
||||
}
|
||||
}
|
||||
|
||||
private _onRangeFilterCleared(): void {
|
||||
const fWorkbook = this.getActiveWorkbook();
|
||||
if (!fWorkbook) return;
|
||||
|
||||
const eventParams: ISheetsRangeFilterClearedEventParams = {
|
||||
workbook: fWorkbook,
|
||||
worksheet: fWorkbook.getActiveSheet(),
|
||||
};
|
||||
|
||||
this.fireEvent(this.Event.SheetRangeFilterCleared, eventParams);
|
||||
}
|
||||
}
|
||||
|
||||
FUniver.extend(FUniverSheetsFilterMixin);
|
||||
63
packages/sheets-filter/src/facade/f-worksheet.ts
Normal file
63
packages/sheets-filter/src/facade/f-worksheet.ts
Normal file
@@ -0,0 +1,63 @@
|
||||
/**
|
||||
* Copyright 2023-present DreamNum Co., Ltd.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES 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 { Nullable } from '@univerjs/core';
|
||||
import type { FilterModel } from '@univerjs/sheets-filter';
|
||||
import { SheetsFilterService } from '@univerjs/sheets-filter';
|
||||
import { FWorksheet } from '@univerjs/sheets/facade';
|
||||
import { FFilter } from './f-filter';
|
||||
|
||||
/**
|
||||
* @ignore
|
||||
*/
|
||||
export interface IFWorksheetFilterMixin {
|
||||
/**
|
||||
* Get the filter for the current worksheet.
|
||||
* @returns {FFilter | null} The interface class to handle the filter. If the worksheet does not have a filter,
|
||||
* this method would return `null`.
|
||||
* @example
|
||||
* ```typescript
|
||||
* const workbook = univerAPI.getActiveWorkbook();
|
||||
* const worksheet = workbook.getSheetByName('Sheet1');
|
||||
* if (!worksheet) return;
|
||||
* const filter = worksheet.getFilter();
|
||||
* console.log(filter, filter?.getRange().getA1Notation());
|
||||
* ```
|
||||
*/
|
||||
getFilter(): FFilter | null;
|
||||
}
|
||||
|
||||
export class FWorksheetFilterMixin extends FWorksheet implements IFWorksheetFilterMixin {
|
||||
override getFilter(): FFilter | null {
|
||||
const filterModel = this._getFilterModel();
|
||||
if (!filterModel) return null;
|
||||
|
||||
return this._injector.createInstance(FFilter, this._workbook, this._worksheet, filterModel);
|
||||
}
|
||||
|
||||
private _getFilterModel(): Nullable<FilterModel> {
|
||||
return this._injector.get(SheetsFilterService).getFilterModel(
|
||||
this._workbook.getUnitId(),
|
||||
this._worksheet.getSheetId()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
FWorksheet.extend(FWorksheetFilterMixin);
|
||||
declare module '@univerjs/sheets/facade' {
|
||||
// eslint-disable-next-line ts/naming-convention
|
||||
interface FWorksheet extends IFWorksheetFilterMixin { }
|
||||
}
|
||||
30
packages/sheets-filter/src/facade/index.ts
Normal file
30
packages/sheets-filter/src/facade/index.ts
Normal file
@@ -0,0 +1,30 @@
|
||||
/**
|
||||
* Copyright 2023-present DreamNum Co., Ltd.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import './f-univer';
|
||||
import './f-filter';
|
||||
import './f-range';
|
||||
import './f-worksheet';
|
||||
import './f-enum';
|
||||
import './f-event';
|
||||
|
||||
export type * from './f-enum';
|
||||
export type * from './f-event';
|
||||
export { FFilter } from './f-filter';
|
||||
export type * from './f-filter';
|
||||
export type * from './f-range';
|
||||
export type * from './f-univer';
|
||||
export type * from './f-worksheet';
|
||||
62
packages/sheets-filter/src/index.ts
Normal file
62
packages/sheets-filter/src/index.ts
Normal file
@@ -0,0 +1,62 @@
|
||||
/**
|
||||
* Copyright 2023-present DreamNum Co., Ltd.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
export {
|
||||
ClearSheetsFilterCriteriaCommand,
|
||||
ReCalcSheetsFilterCommand,
|
||||
RemoveSheetFilterCommand,
|
||||
SetSheetFilterRangeCommand,
|
||||
SetSheetsFilterCriteriaCommand,
|
||||
SmartToggleSheetsFilterCommand,
|
||||
} from './commands/commands/sheets-filter.command';
|
||||
export type {
|
||||
ISetSheetFilterRangeCommandParams,
|
||||
ISetSheetsFilterCriteriaCommandParams,
|
||||
} from './commands/commands/sheets-filter.command';
|
||||
export {
|
||||
ReCalcSheetsFilterMutation,
|
||||
RemoveSheetsFilterMutation,
|
||||
SetSheetsFilterCriteriaMutation,
|
||||
SetSheetsFilterRangeMutation,
|
||||
} from './commands/mutations/sheets-filter.mutation';
|
||||
export type {
|
||||
ISetSheetsFilterCriteriaMutationParams,
|
||||
ISetSheetsFilterRangeMutationParams,
|
||||
} from './commands/mutations/sheets-filter.mutation';
|
||||
export { FILTER_MUTATIONS } from './common/const';
|
||||
export type { IUniverSheetsFilterConfig } from './config/config';
|
||||
export { SheetsFilterSyncController } from './controllers/sheets-filter-sync.controller';
|
||||
export {
|
||||
equals,
|
||||
getCustomFilterFn,
|
||||
greaterThan,
|
||||
greaterThanOrEqualTo,
|
||||
lessThan,
|
||||
lessThanOrEqualTo,
|
||||
notEquals,
|
||||
} from './models/custom-filters';
|
||||
export { FilterColumn, FilterModel } from './models/filter-model';
|
||||
export type {
|
||||
IAutoFilter,
|
||||
IColorFilters,
|
||||
ICustomFilter,
|
||||
ICustomFilters,
|
||||
IFilterColumn,
|
||||
IFilters,
|
||||
} from './models/types';
|
||||
export { CustomFilterOperator, FilterBy } from './models/types';
|
||||
export { UniverSheetsFilterPlugin } from './plugin';
|
||||
export { SHEET_FILTER_SNAPSHOT_ID, SheetsFilterService } from './services/sheet-filter.service';
|
||||
30
packages/sheets-filter/src/locale/ar-SA.ts
Normal file
30
packages/sheets-filter/src/locale/ar-SA.ts
Normal file
@@ -0,0 +1,30 @@
|
||||
/**
|
||||
* Copyright 2023-present DreamNum Co., Ltd.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import type enUS from './en-US';
|
||||
|
||||
const locale: typeof enUS = {
|
||||
'sheets-filter': {
|
||||
command: {
|
||||
'not-valid-filter-range': 'يحتوي النطاق المحدد على صف واحد فقط وهو غير صالح للتصفية.',
|
||||
},
|
||||
msg: {
|
||||
'filter-header-forbidden': 'لا يمكنك نقل صف الرأس الخاص بالتصفية.',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default locale;
|
||||
30
packages/sheets-filter/src/locale/ca-ES.ts
Normal file
30
packages/sheets-filter/src/locale/ca-ES.ts
Normal file
@@ -0,0 +1,30 @@
|
||||
/**
|
||||
* Copyright 2023-present DreamNum Co., Ltd.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import type enUS from './en-US';
|
||||
|
||||
const locale: typeof enUS = {
|
||||
'sheets-filter': {
|
||||
command: {
|
||||
'not-valid-filter-range': 'L\'interval seleccionat només té una fila i no és vàlid per filtrar.',
|
||||
},
|
||||
msg: {
|
||||
'filter-header-forbidden': 'No pots moure la fila de capçalera d\'un filtre.',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default locale;
|
||||
30
packages/sheets-filter/src/locale/de-DE.ts
Normal file
30
packages/sheets-filter/src/locale/de-DE.ts
Normal file
@@ -0,0 +1,30 @@
|
||||
/**
|
||||
* Copyright 2023-present DreamNum Co., Ltd.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import type enUS from './en-US';
|
||||
|
||||
const locale: typeof enUS = {
|
||||
'sheets-filter': {
|
||||
command: {
|
||||
'not-valid-filter-range': 'Der ausgewählte Bereich hat nur eine Zeile und ist für den Filter ungültig.',
|
||||
},
|
||||
msg: {
|
||||
'filter-header-forbidden': 'Die Kopfzeile eines Filters kann nicht verschoben werden.',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default locale;
|
||||
28
packages/sheets-filter/src/locale/en-US.ts
Normal file
28
packages/sheets-filter/src/locale/en-US.ts
Normal file
@@ -0,0 +1,28 @@
|
||||
/**
|
||||
* Copyright 2023-present DreamNum Co., Ltd.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
const locale = {
|
||||
'sheets-filter': {
|
||||
command: {
|
||||
'not-valid-filter-range': 'The selected range only has one row and not valid for filter.',
|
||||
},
|
||||
msg: {
|
||||
'filter-header-forbidden': 'You can\'t move the header row of a filter.',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default locale;
|
||||
30
packages/sheets-filter/src/locale/es-ES.ts
Normal file
30
packages/sheets-filter/src/locale/es-ES.ts
Normal file
@@ -0,0 +1,30 @@
|
||||
/**
|
||||
* Copyright 2023-present DreamNum Co., Ltd.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import type enUS from './en-US';
|
||||
|
||||
const locale: typeof enUS = {
|
||||
'sheets-filter': {
|
||||
command: {
|
||||
'not-valid-filter-range': 'El rango seleccionado solo tiene una fila y no es válido para filtrar.',
|
||||
},
|
||||
msg: {
|
||||
'filter-header-forbidden': 'No puedes mover la fila de encabezado de un filtro.',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default locale;
|
||||
30
packages/sheets-filter/src/locale/fa-IR.ts
Normal file
30
packages/sheets-filter/src/locale/fa-IR.ts
Normal file
@@ -0,0 +1,30 @@
|
||||
/**
|
||||
* Copyright 2023-present DreamNum Co., Ltd.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import type enUS from './en-US';
|
||||
|
||||
const locale: typeof enUS = {
|
||||
'sheets-filter': {
|
||||
command: {
|
||||
'not-valid-filter-range': 'محدوده انتخاب شده فقط یک ردیف دارد و برای فیلتر معتبر نیست.',
|
||||
},
|
||||
msg: {
|
||||
'filter-header-forbidden': 'شما نمیتوانید ردیف هدر یک فیلتر را جابهجا کنید.',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default locale;
|
||||
30
packages/sheets-filter/src/locale/fr-FR.ts
Normal file
30
packages/sheets-filter/src/locale/fr-FR.ts
Normal file
@@ -0,0 +1,30 @@
|
||||
/**
|
||||
* Copyright 2023-present DreamNum Co., Ltd.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import type enUS from './en-US';
|
||||
|
||||
const locale: typeof enUS = {
|
||||
'sheets-filter': {
|
||||
command: {
|
||||
'not-valid-filter-range': 'La plage sélectionnée n\'a qu\'une seule ligne et n\'est pas valide pour le filtre.',
|
||||
},
|
||||
msg: {
|
||||
'filter-header-forbidden': 'Vous ne pouvez pas déplacer la ligne d\'en-tête d\'un filtre.',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default locale;
|
||||
30
packages/sheets-filter/src/locale/id-ID.ts
Normal file
30
packages/sheets-filter/src/locale/id-ID.ts
Normal file
@@ -0,0 +1,30 @@
|
||||
/**
|
||||
* Copyright 2023-present DreamNum Co., Ltd.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import type enUS from './en-US';
|
||||
|
||||
const locale: typeof enUS = {
|
||||
'sheets-filter': {
|
||||
command: {
|
||||
'not-valid-filter-range': 'Rentang yang dipilih hanya memiliki satu baris dan tidak valid untuk filter.',
|
||||
},
|
||||
msg: {
|
||||
'filter-header-forbidden': 'Anda tidak dapat memindahkan baris header filter.',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default locale;
|
||||
30
packages/sheets-filter/src/locale/it-IT.ts
Normal file
30
packages/sheets-filter/src/locale/it-IT.ts
Normal file
@@ -0,0 +1,30 @@
|
||||
/**
|
||||
* Copyright 2023-present DreamNum Co., Ltd.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import type enUS from './en-US';
|
||||
|
||||
const locale: typeof enUS = {
|
||||
'sheets-filter': {
|
||||
command: {
|
||||
'not-valid-filter-range': "L'intervallo selezionato ha una sola riga e non è valido per il filtro.",
|
||||
},
|
||||
msg: {
|
||||
'filter-header-forbidden': 'Non è possibile spostare la riga di intestazione di un filtro.',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default locale;
|
||||
30
packages/sheets-filter/src/locale/ja-JP.ts
Normal file
30
packages/sheets-filter/src/locale/ja-JP.ts
Normal file
@@ -0,0 +1,30 @@
|
||||
/**
|
||||
* Copyright 2023-present DreamNum Co., Ltd.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import type enUS from './en-US';
|
||||
|
||||
const locale: typeof enUS = {
|
||||
'sheets-filter': {
|
||||
command: {
|
||||
'not-valid-filter-range': '選択範囲に1行しか含まれていないため、フィルタを適用できません。',
|
||||
},
|
||||
msg: {
|
||||
'filter-header-forbidden': 'フィルタヘッダー行は移動できません。',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default locale;
|
||||
30
packages/sheets-filter/src/locale/ko-KR.ts
Normal file
30
packages/sheets-filter/src/locale/ko-KR.ts
Normal file
@@ -0,0 +1,30 @@
|
||||
/**
|
||||
* Copyright 2023-present DreamNum Co., Ltd.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import type enUS from './en-US';
|
||||
|
||||
const locale: typeof enUS = {
|
||||
'sheets-filter': {
|
||||
command: {
|
||||
'not-valid-filter-range': '선택한 범위에 한 행만 있어 필터를 적용할 수 없습니다.',
|
||||
},
|
||||
msg: {
|
||||
'filter-header-forbidden': '필터 헤더 행은 이동할 수 없습니다.',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default locale;
|
||||
30
packages/sheets-filter/src/locale/pl-PL.ts
Normal file
30
packages/sheets-filter/src/locale/pl-PL.ts
Normal file
@@ -0,0 +1,30 @@
|
||||
/**
|
||||
* Copyright 2023-present DreamNum Co., Ltd.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import type enUS from './en-US';
|
||||
|
||||
const locale: typeof enUS = {
|
||||
'sheets-filter': {
|
||||
command: {
|
||||
'not-valid-filter-range': 'Wybrany zakres ma tylko jeden wiersz i nie jest prawidłowy do filtrowania.',
|
||||
},
|
||||
msg: {
|
||||
'filter-header-forbidden': 'Nie można przenieść wiersza nagłówka filtra.',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default locale;
|
||||
30
packages/sheets-filter/src/locale/pt-BR.ts
Normal file
30
packages/sheets-filter/src/locale/pt-BR.ts
Normal file
@@ -0,0 +1,30 @@
|
||||
/**
|
||||
* Copyright 2023-present DreamNum Co., Ltd.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import type enUS from './en-US';
|
||||
|
||||
const locale: typeof enUS = {
|
||||
'sheets-filter': {
|
||||
command: {
|
||||
'not-valid-filter-range': 'O intervalo selecionado possui apenas uma linha e não é válido para filtro.',
|
||||
},
|
||||
msg: {
|
||||
'filter-header-forbidden': 'Você não pode mover a linha de cabeçalho de um filtro.',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default locale;
|
||||
30
packages/sheets-filter/src/locale/ru-RU.ts
Normal file
30
packages/sheets-filter/src/locale/ru-RU.ts
Normal file
@@ -0,0 +1,30 @@
|
||||
/**
|
||||
* Copyright 2023-present DreamNum Co., Ltd.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import type enUS from './en-US';
|
||||
|
||||
const locale: typeof enUS = {
|
||||
'sheets-filter': {
|
||||
command: {
|
||||
'not-valid-filter-range': 'Выбранный диапазон содержит только одну строку и не подходит для фильтра.',
|
||||
},
|
||||
msg: {
|
||||
'filter-header-forbidden': 'Вы не можете переместить строку заголовка фильтра.',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default locale;
|
||||
30
packages/sheets-filter/src/locale/sk-SK.ts
Normal file
30
packages/sheets-filter/src/locale/sk-SK.ts
Normal file
@@ -0,0 +1,30 @@
|
||||
/**
|
||||
* Copyright 2023-present DreamNum Co., Ltd.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import type enUS from './en-US';
|
||||
|
||||
const locale: typeof enUS = {
|
||||
'sheets-filter': {
|
||||
command: {
|
||||
'not-valid-filter-range': 'Vybraný rozsah má iba jeden riadok a nie je vhodný na filtrovanie.',
|
||||
},
|
||||
msg: {
|
||||
'filter-header-forbidden': 'Riadok hlavičky filtra sa nedá presunúť.',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default locale;
|
||||
20
packages/sheets-filter/src/locale/types.ts
Normal file
20
packages/sheets-filter/src/locale/types.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
/**
|
||||
* Copyright 2023-present DreamNum Co., Ltd.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import type { LocaleLeafKeys } from '@univerjs/core';
|
||||
import type enUS from './en-US';
|
||||
|
||||
export type LocaleKey = LocaleLeafKeys<typeof enUS>;
|
||||
30
packages/sheets-filter/src/locale/vi-VN.ts
Normal file
30
packages/sheets-filter/src/locale/vi-VN.ts
Normal file
@@ -0,0 +1,30 @@
|
||||
/**
|
||||
* Copyright 2023-present DreamNum Co., Ltd.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import type enUS from './en-US';
|
||||
|
||||
const locale: typeof enUS = {
|
||||
'sheets-filter': {
|
||||
command: {
|
||||
'not-valid-filter-range': 'Khu vực được chọn chỉ có một hàng, không thể lọc',
|
||||
},
|
||||
msg: {
|
||||
'filter-header-forbidden': 'Không thể di chuyển hàng đầu lọc',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default locale;
|
||||
30
packages/sheets-filter/src/locale/zh-CN.ts
Normal file
30
packages/sheets-filter/src/locale/zh-CN.ts
Normal file
@@ -0,0 +1,30 @@
|
||||
/**
|
||||
* Copyright 2023-present DreamNum Co., Ltd.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import type enUS from './en-US';
|
||||
|
||||
const locale: typeof enUS = {
|
||||
'sheets-filter': {
|
||||
command: {
|
||||
'not-valid-filter-range': '选中的区域只有一行,无法进行筛选',
|
||||
},
|
||||
msg: {
|
||||
'filter-header-forbidden': '无法移动筛选行头',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default locale;
|
||||
30
packages/sheets-filter/src/locale/zh-HK.ts
Normal file
30
packages/sheets-filter/src/locale/zh-HK.ts
Normal file
@@ -0,0 +1,30 @@
|
||||
/**
|
||||
* Copyright 2023-present DreamNum Co., Ltd.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import type enUS from './en-US';
|
||||
|
||||
const locale: typeof enUS = {
|
||||
'sheets-filter': {
|
||||
command: {
|
||||
'not-valid-filter-range': '選取的區域只有一列,無法進行篩選',
|
||||
},
|
||||
msg: {
|
||||
'filter-header-forbidden': '無法移動篩選列頭',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default locale;
|
||||
30
packages/sheets-filter/src/locale/zh-TW.ts
Normal file
30
packages/sheets-filter/src/locale/zh-TW.ts
Normal file
@@ -0,0 +1,30 @@
|
||||
/**
|
||||
* Copyright 2023-present DreamNum Co., Ltd.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import type enUS from './en-US';
|
||||
|
||||
const locale: typeof enUS = {
|
||||
'sheets-filter': {
|
||||
command: {
|
||||
'not-valid-filter-range': '選取的區域只有一列,無法進行篩選',
|
||||
},
|
||||
msg: {
|
||||
'filter-header-forbidden': '無法移動篩選列頭',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default locale;
|
||||
@@ -0,0 +1,130 @@
|
||||
/**
|
||||
* Copyright 2023-present DreamNum Co., Ltd.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { equals, greaterThan, greaterThanOrEqualTo, lessThan, lessThanOrEqualTo, notEquals, textMatch } from '../custom-filters';
|
||||
|
||||
describe('Test custom filter functions', () => {
|
||||
describe('Test "greaterThan" operator', () => {
|
||||
it('filter out values if there are not of type number', () => {
|
||||
expect(greaterThan.fn('1', 0)).toBeFalsy();
|
||||
});
|
||||
|
||||
it('should execute "greaterThan" correctly', () => {
|
||||
expect(greaterThan.fn(1, 0)).toBeTruthy();
|
||||
expect(greaterThan.fn(0, 0)).toBeFalsy();
|
||||
expect(greaterThan.fn(-1, 0)).toBeFalsy();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Test "greaterThanOrEqual" operator', () => {
|
||||
it('filter out values if there are not of type number', () => {
|
||||
expect(greaterThanOrEqualTo.fn('1', 0)).toBeFalsy();
|
||||
});
|
||||
|
||||
it('should execute "greaterThanOrEqual" correctly', () => {
|
||||
expect(greaterThanOrEqualTo.fn(1, 0)).toBeTruthy();
|
||||
expect(greaterThanOrEqualTo.fn(0, 0)).toBeTruthy();
|
||||
expect(greaterThanOrEqualTo.fn(-1, 0)).toBeFalsy();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Test "lessThan" operator', () => {
|
||||
it('filter out values if there are not of type number', () => {
|
||||
expect(lessThan.fn('1', 0)).toBeFalsy();
|
||||
});
|
||||
|
||||
it('should execute "lessThan" correctly', () => {
|
||||
expect(lessThan.fn(1, 0)).toBeFalsy();
|
||||
expect(lessThan.fn(0, 0)).toBeFalsy();
|
||||
expect(lessThan.fn(-1, 0)).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Test "lessThanOrEqual" operator', () => {
|
||||
it('filter out values if there are not of type number', () => {
|
||||
expect(lessThanOrEqualTo.fn('1', 0)).toBeFalsy();
|
||||
});
|
||||
|
||||
it('should execute "lessThanOrEqual" correctly', () => {
|
||||
expect(lessThanOrEqualTo.fn(1, 0)).toBeFalsy();
|
||||
expect(lessThanOrEqualTo.fn(0, 0)).toBeTruthy();
|
||||
expect(lessThanOrEqualTo.fn(-1, 0)).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Test "equals" operator', () => {
|
||||
it('filter out values if there are not of type number', () => {
|
||||
expect(equals.fn('0', 0)).toBeFalsy();
|
||||
});
|
||||
|
||||
it('should execute "equals" correctly', () => {
|
||||
expect(equals.fn(1, 0)).toBeFalsy();
|
||||
expect(equals.fn(0, 0)).toBeTruthy();
|
||||
expect(equals.fn(-1, 0)).toBeFalsy();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Test "notEquals" operator', () => {
|
||||
it('should not filter out values if there are not of type number', () => {
|
||||
expect(notEquals.fn('0', 0)).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should execute "notEquals" correctly', () => {
|
||||
expect(notEquals.fn(1, 0)).toBeTruthy();
|
||||
expect(notEquals.fn(0, 0)).toBeFalsy();
|
||||
expect(notEquals.fn(-1, 0)).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should "notEquals" implement support matching strings if `compare` is a string', () => {
|
||||
// equivalent to "notContains"
|
||||
expect(notEquals.fn(123, '*123*')).toBeFalsy();
|
||||
expect(notEquals.fn('12', '*123*')).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Test text match operator', () => {
|
||||
it('should execute "textMatch" correctly', () => {
|
||||
// equivalent to "startsWith"
|
||||
expect(textMatch.fn('hello world', 'hello*')).toBeTruthy();
|
||||
expect(textMatch.fn('hello', 'hello*')).toBeTruthy();
|
||||
expect(textMatch.fn('hell', 'hello*')).toBeFalsy();
|
||||
|
||||
// equivalent to "endsWith"
|
||||
expect(textMatch.fn('hello world', '*world')).toBeTruthy();
|
||||
expect(textMatch.fn('world', '*world')).toBeTruthy();
|
||||
expect(textMatch.fn('wrld', '*world')).toBeFalsy();
|
||||
|
||||
// equivalent to "contains"
|
||||
expect(textMatch.fn('hello world', '*llo w*')).toBeTruthy();
|
||||
expect(textMatch.fn('hell world', '*llo w*')).toBeFalsy();
|
||||
});
|
||||
|
||||
it('should support "?" wild card', () => {
|
||||
expect(textMatch.fn('hello', 'hell?')).toBeTruthy();
|
||||
expect(textMatch.fn('helloooo', 'hell?')).toBeFalsy();
|
||||
expect(textMatch.fn('helloooo', 'hell?ooo')).toBeTruthy();
|
||||
expect(textMatch.fn('hello world', 'hell?')).toBeFalsy();
|
||||
expect(textMatch.fn('hello', '?hell?')).toBeFalsy();
|
||||
expect(textMatch.fn('yhell', '?hell?')).toBeFalsy();
|
||||
|
||||
expect(textMatch.fn('hello111', '*hello?*')).toBeTruthy();
|
||||
expect(textMatch.fn('hello111\r\n', '*hello?*')).toBeFalsy();
|
||||
expect(textMatch.fn('hello111\r\n'.trimEnd(), '*hello?*')).toBeTruthy();
|
||||
expect(textMatch.fn('hello', '*hello?*')).toBeFalsy();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,499 @@
|
||||
/**
|
||||
* Copyright 2023-present DreamNum Co., Ltd.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import type { Dependency, IWorkbookData, LocaleType } from '@univerjs/core';
|
||||
import type {
|
||||
IInsertColCommandParams,
|
||||
IInsertRowCommandParams,
|
||||
IMoveColsCommandParams,
|
||||
IMoveRowsCommandParams,
|
||||
IRemoveRowColCommandParams,
|
||||
IRemoveSheetCommandParams,
|
||||
ISetSelectionsOperationParams,
|
||||
} from '@univerjs/sheets';
|
||||
import { Direction, ICommandService, Inject, Injector, Plugin, RANGE_TYPE, Univer, UniverInstanceType } from '@univerjs/core';
|
||||
import {
|
||||
InsertColByRangeCommand,
|
||||
InsertColCommand,
|
||||
InsertColMutation,
|
||||
InsertRowByRangeCommand,
|
||||
InsertRowCommand,
|
||||
InsertRowMutation,
|
||||
MoveColsCommand,
|
||||
MoveColsMutation,
|
||||
MoveRowsCommand,
|
||||
MoveRowsMutation,
|
||||
RefRangeService,
|
||||
RemoveColByRangeCommand,
|
||||
RemoveColCommand,
|
||||
RemoveColMutation,
|
||||
RemoveRowByRangeCommand,
|
||||
RemoveRowCommand,
|
||||
RemoveRowMutation,
|
||||
RemoveSheetCommand,
|
||||
RemoveSheetMutation,
|
||||
SetSelectionsOperation,
|
||||
SheetInterceptorService,
|
||||
SheetRangeThemeModel,
|
||||
SheetsSelectionsService,
|
||||
ZebraCrossingCacheController,
|
||||
} from '@univerjs/sheets';
|
||||
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
|
||||
import { SheetsFilterController } from '../../controllers/sheets-filter.controller';
|
||||
import { SHEET_FILTER_SNAPSHOT_ID, SheetsFilterService } from '../../services/sheet-filter.service';
|
||||
|
||||
describe('Test "Filter Interceptor"', () => {
|
||||
let univer: Univer;
|
||||
let sheetsFilterService: SheetsFilterService;
|
||||
let commandService: ICommandService;
|
||||
|
||||
beforeEach(() => {
|
||||
const testBed = createFilterTestUniver();
|
||||
univer = testBed.univer;
|
||||
sheetsFilterService = testBed.sheetsFilterService;
|
||||
commandService = testBed.commandService;
|
||||
univer.__getInjector();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
univer.dispose();
|
||||
});
|
||||
|
||||
describe('Test "insert Command"', () => {
|
||||
it('insert col command, in filter range', async () => {
|
||||
const insertColCommandParams = {
|
||||
unitId: 'workbookId',
|
||||
subUnitId: 'worksheetId',
|
||||
range: {
|
||||
startColumn: 2,
|
||||
endColumn: 2,
|
||||
startRow: 0,
|
||||
endRow: 4,
|
||||
type: RANGE_TYPE.COLUMN,
|
||||
},
|
||||
direction: Direction.RIGHT,
|
||||
} as IInsertColCommandParams;
|
||||
await commandService.executeCommand(InsertColCommand.id, insertColCommandParams);
|
||||
expect(sheetsFilterService.getFilterModel('workbookId', 'worksheetId')!.getRange()).toStrictEqual({ startColumn: 1, endColumn: 3, startRow: 1, endRow: 2 });
|
||||
expect(sheetsFilterService.getFilterModel('workbookId', 'worksheetId')!.getAllFilterColumns().map((x) => x[0])).toStrictEqual([3]);
|
||||
});
|
||||
|
||||
it('insert col command, before filter range', async () => {
|
||||
const insertColCommandParams = {
|
||||
unitId: 'workbookId',
|
||||
subUnitId: 'worksheetId',
|
||||
range: {
|
||||
startColumn: 0,
|
||||
endColumn: 0,
|
||||
startRow: 0,
|
||||
endRow: 4,
|
||||
type: RANGE_TYPE.COLUMN,
|
||||
},
|
||||
direction: Direction.LEFT,
|
||||
} as IInsertColCommandParams;
|
||||
await commandService.executeCommand(InsertColCommand.id, insertColCommandParams);
|
||||
expect(sheetsFilterService.getFilterModel('workbookId', 'worksheetId')!.getRange()).toStrictEqual({ startColumn: 2, endColumn: 3, startRow: 1, endRow: 2 });
|
||||
expect(sheetsFilterService.getFilterModel('workbookId', 'worksheetId')!.getAllFilterColumns().map((x) => x[0])).toStrictEqual([3]);
|
||||
});
|
||||
|
||||
it('insert row command, in filter range', async () => {
|
||||
const insertRowCommandParams = {
|
||||
unitId: 'workbookId',
|
||||
subUnitId: 'worksheetId',
|
||||
range: {
|
||||
startColumn: 0,
|
||||
endColumn: 3,
|
||||
startRow: 2,
|
||||
endRow: 2,
|
||||
type: RANGE_TYPE.ROW,
|
||||
},
|
||||
direction: Direction.DOWN,
|
||||
} as IInsertRowCommandParams;
|
||||
await commandService.executeCommand(InsertRowCommand.id, insertRowCommandParams);
|
||||
expect(sheetsFilterService.getFilterModel('workbookId', 'worksheetId')!.getRange()).toStrictEqual({ startColumn: 1, endColumn: 2, startRow: 1, endRow: 3 });
|
||||
expect(sheetsFilterService.getFilterModel('workbookId', 'worksheetId')!.getAllFilterColumns().map((x) => x[0])).toStrictEqual([2]);
|
||||
});
|
||||
|
||||
it('insert row command, before filter range', async () => {
|
||||
const insertRowCommandParams = {
|
||||
unitId: 'workbookId',
|
||||
subUnitId: 'worksheetId',
|
||||
range: {
|
||||
startColumn: 0,
|
||||
endColumn: 3,
|
||||
startRow: 0,
|
||||
endRow: 0,
|
||||
type: RANGE_TYPE.ROW,
|
||||
},
|
||||
direction: Direction.UP,
|
||||
} as IInsertRowCommandParams;
|
||||
await commandService.executeCommand(InsertRowCommand.id, insertRowCommandParams);
|
||||
expect(sheetsFilterService.getFilterModel('workbookId', 'worksheetId')!.getRange()).toStrictEqual({ startColumn: 1, endColumn: 2, startRow: 2, endRow: 3 });
|
||||
expect(sheetsFilterService.getFilterModel('workbookId', 'worksheetId')!.getAllFilterColumns().map((x) => x[0])).toStrictEqual([2]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Test "remove Command"', () => {
|
||||
it('remove col command, in filter range', async () => {
|
||||
const removeColCommandParams = {
|
||||
unitId: 'workbookId',
|
||||
subUnitId: 'worksheetId',
|
||||
range: {
|
||||
startColumn: 1,
|
||||
endColumn: 1,
|
||||
startRow: 0,
|
||||
endRow: 4,
|
||||
type: RANGE_TYPE.COLUMN,
|
||||
},
|
||||
direction: Direction.RIGHT,
|
||||
} as IRemoveRowColCommandParams;
|
||||
await commandService.executeCommand(RemoveColCommand.id, removeColCommandParams);
|
||||
expect(sheetsFilterService.getFilterModel('workbookId', 'worksheetId')!.getRange()).toStrictEqual({ startColumn: 1, endColumn: 1, startRow: 1, endRow: 2 });
|
||||
expect(sheetsFilterService.getFilterModel('workbookId', 'worksheetId')!.getAllFilterColumns().map((x) => x[0])).toStrictEqual([1]);
|
||||
});
|
||||
it('remove col command, before filter range', async () => {
|
||||
const removeColCommandParams = {
|
||||
unitId: 'workbookId',
|
||||
subUnitId: 'worksheetId',
|
||||
range: {
|
||||
startColumn: 0,
|
||||
endColumn: 0,
|
||||
startRow: 0,
|
||||
endRow: 4,
|
||||
type: RANGE_TYPE.COLUMN,
|
||||
},
|
||||
direction: Direction.RIGHT,
|
||||
} as IRemoveRowColCommandParams;
|
||||
await commandService.executeCommand(RemoveColCommand.id, removeColCommandParams);
|
||||
expect(sheetsFilterService.getFilterModel('workbookId', 'worksheetId')!.getRange()).toStrictEqual({ startColumn: 0, endColumn: 1, startRow: 1, endRow: 2 });
|
||||
expect(sheetsFilterService.getFilterModel('workbookId', 'worksheetId')!.getAllFilterColumns().map((x) => x[0])).toStrictEqual([1]);
|
||||
});
|
||||
|
||||
it('remove row command, in filter range', async () => {
|
||||
const removeRowCommandParams = {
|
||||
unitId: 'workbookId',
|
||||
subUnitId: 'worksheetId',
|
||||
range: {
|
||||
startColumn: 0,
|
||||
endColumn: 3,
|
||||
startRow: 1,
|
||||
endRow: 1,
|
||||
type: RANGE_TYPE.ROW,
|
||||
},
|
||||
direction: Direction.DOWN,
|
||||
} as IRemoveRowColCommandParams;
|
||||
await commandService.executeCommand(RemoveRowCommand.id, removeRowCommandParams);
|
||||
expect(sheetsFilterService.getFilterModel('workbookId', 'worksheetId')).toBe(null);
|
||||
});
|
||||
|
||||
it('remove row command, before filter range', async () => {
|
||||
const removeRowCommandParams = {
|
||||
unitId: 'workbookId',
|
||||
subUnitId: 'worksheetId',
|
||||
range: {
|
||||
startColumn: 0,
|
||||
endColumn: 3,
|
||||
startRow: 0,
|
||||
endRow: 0,
|
||||
type: RANGE_TYPE.ROW,
|
||||
},
|
||||
direction: Direction.DOWN,
|
||||
} as IRemoveRowColCommandParams;
|
||||
await commandService.executeCommand(RemoveRowCommand.id, removeRowCommandParams);
|
||||
expect(sheetsFilterService.getFilterModel('workbookId', 'worksheetId')!.getRange()).toStrictEqual({ startColumn: 1, endColumn: 2, startRow: 0, endRow: 1 });
|
||||
expect(sheetsFilterService.getFilterModel('workbookId', 'worksheetId')!.getAllFilterColumns().map((x) => x[0])).toStrictEqual([2]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Test "move Command"', () => {
|
||||
it('move col command, filter column move to left', async () => {
|
||||
const moveColCommandParams = {
|
||||
unitId: 'workbookId',
|
||||
subUnitId: 'worksheetId',
|
||||
fromRange: {
|
||||
startColumn: 1,
|
||||
endColumn: 1,
|
||||
startRow: 0,
|
||||
endRow: 4,
|
||||
type: RANGE_TYPE.COLUMN,
|
||||
},
|
||||
toRange: { startColumn: 3, endColumn: 3, startRow: 0, endRow: 4, type: RANGE_TYPE.COLUMN },
|
||||
direction: Direction.RIGHT,
|
||||
} as IMoveColsCommandParams;
|
||||
await commandService.executeCommand(SetSelectionsOperation.id, { unitId: 'workbookId', subUnitId: 'worksheetId', selections: [{ style: null, range: { startColumn: 1, endColumn: 1, startRow: 0, endRow: 4, rangeType: RANGE_TYPE.COLUMN }, primary: {} }] } as ISetSelectionsOperationParams);
|
||||
await commandService.executeCommand(MoveColsCommand.id, moveColCommandParams);
|
||||
expect(sheetsFilterService.getFilterModel('workbookId', 'worksheetId')!.getRange()).toStrictEqual({ startColumn: 1, endColumn: 1, startRow: 1, endRow: 2 });
|
||||
expect(sheetsFilterService.getFilterModel('workbookId', 'worksheetId')!.getAllFilterColumns().map((x) => x[0])).toStrictEqual([1]);
|
||||
});
|
||||
it('move col command, filter column move to right', async () => {
|
||||
const moveColCommandParams = {
|
||||
unitId: 'workbookId',
|
||||
subUnitId: 'worksheetId',
|
||||
fromRange: {
|
||||
startColumn: 2,
|
||||
endColumn: 2,
|
||||
startRow: 0,
|
||||
endRow: 4,
|
||||
type: RANGE_TYPE.COLUMN,
|
||||
},
|
||||
toRange: { startColumn: 4, endColumn: 4, startRow: 0, endRow: 4, type: RANGE_TYPE.COLUMN },
|
||||
direction: Direction.RIGHT,
|
||||
} as IMoveColsCommandParams;
|
||||
await commandService.executeCommand(SetSelectionsOperation.id, { unitId: 'workbookId', subUnitId: 'worksheetId', selections: [{ style: null, range: { startColumn: 2, endColumn: 2, startRow: 0, endRow: 4, rangeType: RANGE_TYPE.COLUMN }, primary: {} }] } as ISetSelectionsOperationParams);
|
||||
await commandService.executeCommand(MoveColsCommand.id, moveColCommandParams);
|
||||
expect(sheetsFilterService.getFilterModel('workbookId', 'worksheetId')!.getRange()).toStrictEqual({ startColumn: 1, endColumn: 3, startRow: 1, endRow: 2 });
|
||||
expect(sheetsFilterService.getFilterModel('workbookId', 'worksheetId')!.getAllFilterColumns().map((x) => x[0])).toStrictEqual([3]);
|
||||
});
|
||||
|
||||
it('move row command', async () => {
|
||||
const moveRowCommandParams = {
|
||||
unitId: 'workbookId',
|
||||
subUnitId: 'worksheetId',
|
||||
fromRange: {
|
||||
startColumn: 0,
|
||||
endColumn: 3,
|
||||
startRow: 2,
|
||||
endRow: 2,
|
||||
type: RANGE_TYPE.ROW,
|
||||
},
|
||||
toRange: { startColumn: 0, endColumn: 3, startRow: 4, endRow: 4, type: RANGE_TYPE.ROW },
|
||||
direction: Direction.RIGHT,
|
||||
} as IMoveRowsCommandParams;
|
||||
await commandService.executeCommand(SetSelectionsOperation.id, { unitId: 'workbookId', subUnitId: 'worksheetId', selections: [{ style: null, range: { startColumn: 0, endColumn: 3, startRow: 2, endRow: 2, rangeType: RANGE_TYPE.ROW }, primary: {} }] } as ISetSelectionsOperationParams);
|
||||
await commandService.executeCommand(MoveRowsCommand.id, moveRowCommandParams);
|
||||
expect(sheetsFilterService.getFilterModel('workbookId', 'worksheetId')!.getRange()).toStrictEqual({ startColumn: 1, endColumn: 2, startRow: 1, endRow: 3 });
|
||||
expect(sheetsFilterService.getFilterModel('workbookId', 'worksheetId')!.getAllFilterColumns().map((x) => x[0])).toStrictEqual([2]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Test "remove sheet Command"', () => {
|
||||
it('remove sheet command', async () => {
|
||||
const removeSheetCommandParams = { unitId: 'workbookId', subUnitId: 'worksheetId' } as IRemoveSheetCommandParams;
|
||||
expect(await commandService.executeCommand(RemoveSheetCommand.id, removeSheetCommandParams)).toBeTruthy();
|
||||
expect(sheetsFilterService.getFilterModel('workbookId', 'worksheetId') === null).toBeTruthy();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
function createFilterTestUniver(dependencies?: Dependency[], workbookData?: IWorkbookData) {
|
||||
const univer = new Univer();
|
||||
const injector = univer.__getInjector();
|
||||
const get = injector.get.bind(injector);
|
||||
|
||||
/**
|
||||
* This plugin hooks into Sheet's DI system to expose API to test scripts
|
||||
*/
|
||||
class TestPlugin extends Plugin {
|
||||
static override type = UniverInstanceType.UNIVER_SHEET;
|
||||
static override pluginName = 'test-plugin';
|
||||
|
||||
constructor(
|
||||
_config: undefined,
|
||||
@Inject(Injector) override readonly _injector: Injector
|
||||
) {
|
||||
super();
|
||||
}
|
||||
|
||||
override onStarting(): void {
|
||||
const injector = this._injector;
|
||||
|
||||
dependencies?.forEach((d) => injector.add(d));
|
||||
|
||||
injector.add([SheetInterceptorService]);
|
||||
injector.add([SheetsFilterService]);
|
||||
injector.add([SheetsSelectionsService]);
|
||||
injector.add([RefRangeService]);
|
||||
injector.add([SheetRangeThemeModel]);
|
||||
injector.add([ZebraCrossingCacheController]);
|
||||
injector.add([SheetsFilterController]);
|
||||
}
|
||||
|
||||
override onReady(): void {
|
||||
this._injector.get(SheetsFilterController);
|
||||
}
|
||||
}
|
||||
|
||||
univer.registerPlugin(TestPlugin);
|
||||
|
||||
univer.createUnit(UniverInstanceType.UNIVER_SHEET, workbookData || testWorkbookDataFactory());
|
||||
|
||||
const sheetsFilterService = injector.get(SheetsFilterService);
|
||||
|
||||
// It should be registered later in avoid of time sequence problem.
|
||||
// injector.add([ISnapshotPersistenceService, { useClass: LocalSnapshotService }]);
|
||||
// injector.get(ISnapshotPersistenceService);
|
||||
|
||||
const commandService = get(ICommandService);
|
||||
[
|
||||
InsertColMutation,
|
||||
RemoveColMutation,
|
||||
InsertColCommand,
|
||||
InsertRowMutation,
|
||||
RemoveRowMutation,
|
||||
InsertRowCommand,
|
||||
RemoveColCommand,
|
||||
RemoveRowCommand,
|
||||
MoveColsMutation,
|
||||
MoveColsCommand,
|
||||
MoveRowsCommand,
|
||||
MoveRowsMutation,
|
||||
SetSelectionsOperation,
|
||||
RemoveSheetCommand,
|
||||
RemoveSheetMutation,
|
||||
RemoveColByRangeCommand,
|
||||
RemoveRowByRangeCommand,
|
||||
InsertRowByRangeCommand,
|
||||
InsertColByRangeCommand,
|
||||
].forEach((command) => {
|
||||
commandService.registerCommand(command);
|
||||
});
|
||||
|
||||
return {
|
||||
univer,
|
||||
get,
|
||||
sheetsFilterService,
|
||||
commandService,
|
||||
};
|
||||
}
|
||||
|
||||
function testWorkbookDataFactory(): IWorkbookData {
|
||||
return {
|
||||
id: 'workbookId',
|
||||
sheetOrder: ['worksheetId'],
|
||||
name: 'UniverSheet Demo',
|
||||
appVersion: '3.0.0-alpha',
|
||||
locale: 'zhCN' as LocaleType,
|
||||
styles: {},
|
||||
sheets: {
|
||||
worksheetId: {
|
||||
name: '工作表1',
|
||||
id: 'worksheetId',
|
||||
tabColor: '',
|
||||
hidden: 0,
|
||||
rowCount: 1000,
|
||||
columnCount: 20,
|
||||
zoomRatio: 1,
|
||||
scrollTop: 0,
|
||||
scrollLeft: 0,
|
||||
defaultColumnWidth: 73,
|
||||
defaultRowHeight: 19,
|
||||
mergeData: [],
|
||||
cellData: {
|
||||
0: {
|
||||
0: {
|
||||
v: 'A1',
|
||||
},
|
||||
1: {
|
||||
v: 'B1',
|
||||
},
|
||||
2: {
|
||||
v: 'C1',
|
||||
},
|
||||
3: {
|
||||
v: 'D1',
|
||||
},
|
||||
},
|
||||
1: {
|
||||
0: {
|
||||
v: 'A2',
|
||||
},
|
||||
1: {
|
||||
v: 'B2',
|
||||
},
|
||||
2: {
|
||||
v: 'C2',
|
||||
},
|
||||
3: {
|
||||
v: 'D2',
|
||||
},
|
||||
},
|
||||
2: {
|
||||
0: {
|
||||
v: 'A3',
|
||||
},
|
||||
1: {
|
||||
v: 'B3',
|
||||
},
|
||||
2: {
|
||||
v: 'C3',
|
||||
},
|
||||
3: {
|
||||
v: 'D3',
|
||||
},
|
||||
},
|
||||
3: {
|
||||
0: {
|
||||
v: 'A4',
|
||||
},
|
||||
1: {
|
||||
v: 'B4',
|
||||
},
|
||||
2: {
|
||||
v: 'C4',
|
||||
},
|
||||
3: {
|
||||
v: 'D4',
|
||||
},
|
||||
},
|
||||
4: {
|
||||
0: {
|
||||
v: 'A5',
|
||||
},
|
||||
1: {
|
||||
v: 'B5',
|
||||
},
|
||||
2: {
|
||||
v: 'C5',
|
||||
},
|
||||
3: {
|
||||
v: 'D5',
|
||||
},
|
||||
},
|
||||
},
|
||||
showGridlines: 1,
|
||||
rightToLeft: 0,
|
||||
},
|
||||
sheet1: {
|
||||
name: 'sheet1',
|
||||
id: 'sheet1',
|
||||
cellData: {},
|
||||
},
|
||||
},
|
||||
resources: [
|
||||
{
|
||||
name: SHEET_FILTER_SNAPSHOT_ID,
|
||||
data: JSON.stringify({
|
||||
worksheetId: {
|
||||
ref: {
|
||||
startColumn: 1,
|
||||
endColumn: 2,
|
||||
startRow: 1,
|
||||
endRow: 2,
|
||||
},
|
||||
|
||||
filterColumns: [{
|
||||
colId: 2,
|
||||
filters: {
|
||||
filters: ['1'],
|
||||
},
|
||||
}],
|
||||
cachedFilteredOut: [],
|
||||
},
|
||||
}),
|
||||
},
|
||||
],
|
||||
};
|
||||
};
|
||||
239
packages/sheets-filter/src/models/__tests__/filter-model.spec.ts
Normal file
239
packages/sheets-filter/src/models/__tests__/filter-model.spec.ts
Normal file
@@ -0,0 +1,239 @@
|
||||
/**
|
||||
* Copyright 2023-present DreamNum Co., Ltd.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import type { Injector, IWorkbookData, Workbook } from '@univerjs/core';
|
||||
import { ILogService, IUniverInstanceService, LocaleType, LogLevel, Univer, UniverInstanceType } from '@univerjs/core';
|
||||
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
|
||||
import { FilterColumn, generateFilterFn } from '../filter-model';
|
||||
import { CustomFilterOperator } from '../types';
|
||||
|
||||
describe('Test filter model and related utils', () => {
|
||||
describe('Test "FilterFn"s', () => {
|
||||
it('should AND work', () => {
|
||||
// equivalent to "between"
|
||||
const fn = generateFilterFn({
|
||||
colId: 0,
|
||||
customFilters: {
|
||||
and: 1,
|
||||
customFilters: [
|
||||
{ operator: CustomFilterOperator.LESS_THAN_OR_EQUAL, val: 456 },
|
||||
{ operator: CustomFilterOperator.GREATER_THAN_OR_EQUAL, val: 123 },
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
expect(fn(123)).toBeTruthy();
|
||||
expect(fn(456)).toBeTruthy();
|
||||
expect(fn(400)).toBeTruthy();
|
||||
expect(fn(100)).toBeFalsy();
|
||||
expect(fn(500)).toBeFalsy();
|
||||
});
|
||||
|
||||
it('should OR work', () => {
|
||||
// equivalent to "notBetween"
|
||||
const fn = generateFilterFn({
|
||||
colId: 0,
|
||||
customFilters: {
|
||||
customFilters: [
|
||||
{ operator: CustomFilterOperator.LESS_THAN, val: 123 },
|
||||
{ operator: CustomFilterOperator.GREATER_THAN, val: 456 },
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
expect(fn(123)).toBeFalsy();
|
||||
expect(fn(456)).toBeFalsy();
|
||||
expect(fn(400)).toBeFalsy();
|
||||
expect(fn(100)).toBeTruthy();
|
||||
expect(fn(500)).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should throw error on not supported yet filter types', () => {
|
||||
// eslint-disable-next-line ts/no-explicit-any
|
||||
expect(() => generateFilterFn({ colId: 0, dynamicFilters: {} } as any)).toThrowError();
|
||||
});
|
||||
|
||||
it('should "filter by values" work', () => {
|
||||
const fn = generateFilterFn({ colId: 0, filters: { filters: ['hello', 'univer'] } });
|
||||
|
||||
expect(fn('hello')).toBeTruthy();
|
||||
expect(fn('univer')).toBeTruthy();
|
||||
expect(fn('wzhudev')).toBeFalsy();
|
||||
expect(fn('')).toBeFalsy();
|
||||
expect(fn(undefined)).toBeFalsy();
|
||||
});
|
||||
|
||||
it('should "filter by values" work with blank values', () => {
|
||||
const fn = generateFilterFn({ colId: 0, filters: { filters: ['hello', 'univer'], blank: true } });
|
||||
expect(fn('hello')).toBeTruthy();
|
||||
expect(fn('univer')).toBeTruthy();
|
||||
expect(fn('wzhudev')).toBeFalsy();
|
||||
expect(fn('')).toBeTruthy();
|
||||
expect(fn(undefined)).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Test "FilterColumn"', () => {
|
||||
let univer: Univer;
|
||||
let filterColumn: FilterColumn;
|
||||
let get: Injector['get'];
|
||||
|
||||
beforeEach(() => {
|
||||
const testBed = createFilterModelTestBed({
|
||||
id: 'test',
|
||||
appVersion: '3.0.0-alpha',
|
||||
sheets: {
|
||||
sheet1: {
|
||||
id: 'sheet1',
|
||||
name: 'sheet1',
|
||||
cellData: {
|
||||
0: {
|
||||
1: { v: 'header' },
|
||||
},
|
||||
1: {
|
||||
1: { v: 123 },
|
||||
},
|
||||
2: {
|
||||
1: { v: 345 },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
locale: LocaleType.ZH_CN,
|
||||
name: '',
|
||||
sheetOrder: [],
|
||||
styles: {},
|
||||
});
|
||||
|
||||
univer = testBed.univer;
|
||||
get = testBed.get;
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
univer.dispose();
|
||||
filterColumn.dispose();
|
||||
});
|
||||
|
||||
it('should not calc when filter fn or range is not set', () => {
|
||||
filterColumn = new FilterColumn(
|
||||
'test',
|
||||
'sheet1',
|
||||
get(IUniverInstanceService).getCurrentUnitOfType<Workbook>(UniverInstanceType.UNIVER_SHEET)!.getActiveSheet()!,
|
||||
{ colId: 0, customFilters: { customFilters: [{ operator: CustomFilterOperator.LESS_THAN, val: 123 }] } },
|
||||
{ getAlreadyFilteredOutRows() { return new Set(); } }
|
||||
);
|
||||
|
||||
expect(filterColumn.hasCache()).toBeFalsy();
|
||||
|
||||
filterColumn.setRangeAndColumn({ startRow: 0, endRow: 100, startColumn: 0, endColumn: 0 }, 0);
|
||||
expect(filterColumn.hasCache()).toBeFalsy();
|
||||
|
||||
expect(() => filterColumn.reCalc()).toThrowError();
|
||||
expect(filterColumn.hasCache()).toBeFalsy();
|
||||
|
||||
filterColumn.dispose();
|
||||
|
||||
// -------------------------------------------------------
|
||||
|
||||
filterColumn = new FilterColumn(
|
||||
'test',
|
||||
'sheet1',
|
||||
get(IUniverInstanceService).getCurrentUnitOfType<Workbook>(UniverInstanceType.UNIVER_SHEET)!.getActiveSheet()!,
|
||||
{ colId: 0, customFilters: { customFilters: [{ operator: CustomFilterOperator.LESS_THAN, val: 123 }] } },
|
||||
{ getAlreadyFilteredOutRows() { return new Set(); } }
|
||||
);
|
||||
|
||||
expect(filterColumn.hasCache()).toBeFalsy();
|
||||
|
||||
filterColumn.setCriteria({ colId: 0, customFilters: { customFilters: [{ operator: CustomFilterOperator.LESS_THAN, val: 123 }] } });
|
||||
expect(filterColumn.hasCache()).toBeFalsy();
|
||||
|
||||
expect(() => filterColumn.reCalc()).toThrowError();
|
||||
expect(filterColumn.hasCache()).toBeFalsy();
|
||||
});
|
||||
|
||||
it('should not auto calc when set criteria', () => {
|
||||
filterColumn = new FilterColumn(
|
||||
'test',
|
||||
'sheet1',
|
||||
get(IUniverInstanceService).getCurrentUnitOfType<Workbook>(UniverInstanceType.UNIVER_SHEET)!.getActiveSheet()!,
|
||||
{ colId: 0, customFilters: { customFilters: [{ operator: CustomFilterOperator.LESS_THAN, val: 123 }] } },
|
||||
{ getAlreadyFilteredOutRows() { return new Set(); } }
|
||||
);
|
||||
|
||||
expect(filterColumn.hasCache()).toBeFalsy();
|
||||
|
||||
filterColumn.setRangeAndColumn({ startRow: 0, endRow: 100, startColumn: 0, endColumn: 0 }, 0);
|
||||
filterColumn.setCriteria({ colId: 0, customFilters: { customFilters: [{ operator: CustomFilterOperator.LESS_THAN, val: 123 }] } });
|
||||
expect(filterColumn.hasCache()).toBeFalsy();
|
||||
expect(filterColumn.filteredOutRows).toBeNull();
|
||||
|
||||
filterColumn.reCalc();
|
||||
expect(filterColumn.hasCache()).toBeTruthy();
|
||||
expect(filterColumn.filteredOutRows!.size).toBe(100);
|
||||
});
|
||||
|
||||
it('should filtered out rows correctly', () => {
|
||||
filterColumn = new FilterColumn(
|
||||
'test',
|
||||
'sheet1',
|
||||
get(IUniverInstanceService).getCurrentUnitOfType<Workbook>(UniverInstanceType.UNIVER_SHEET)!.getActiveSheet()!,
|
||||
{ colId: 0, customFilters: { customFilters: [{ operator: CustomFilterOperator.LESS_THAN, val: 123 }] } },
|
||||
{ getAlreadyFilteredOutRows() { return new Set(); } }
|
||||
);
|
||||
|
||||
filterColumn.setRangeAndColumn({ startRow: 1, endRow: 3, startColumn: 1, endColumn: 1 }, 1);
|
||||
filterColumn.setCriteria({ colId: 1, customFilters: { customFilters: [{ operator: CustomFilterOperator.GREATER_THAN, val: 200 }] } });
|
||||
filterColumn.reCalc();
|
||||
expect(filterColumn.filteredOutRows!.size).toBe(1);
|
||||
});
|
||||
|
||||
it('should skip rows that already filtered out by other rows', () => {
|
||||
filterColumn = new FilterColumn(
|
||||
'test',
|
||||
'sheet1',
|
||||
get(IUniverInstanceService).getCurrentUnitOfType<Workbook>(UniverInstanceType.UNIVER_SHEET)!.getActiveSheet()!,
|
||||
{ colId: 0, customFilters: { customFilters: [{ operator: CustomFilterOperator.LESS_THAN, val: 123 }] } },
|
||||
{ getAlreadyFilteredOutRows() { return new Set([1]); } }
|
||||
);
|
||||
|
||||
filterColumn.setRangeAndColumn({ startRow: 1, endRow: 3, startColumn: 1, endColumn: 1 }, 1);
|
||||
filterColumn.setCriteria({ colId: 1, customFilters: { customFilters: [{ operator: CustomFilterOperator.GREATER_THAN, val: 200 }] } });
|
||||
filterColumn.reCalc();
|
||||
expect(filterColumn.filteredOutRows!.size).toBe(1);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
function createFilterModelTestBed(workbookData: IWorkbookData) {
|
||||
const univer = new Univer();
|
||||
const injector = univer.__getInjector();
|
||||
const get = injector.get.bind(injector);
|
||||
|
||||
const sheet = univer.createUnit(UniverInstanceType.UNIVER_SHEET, workbookData);
|
||||
|
||||
const univerInstanceService = get(IUniverInstanceService);
|
||||
univerInstanceService.focusUnit('test');
|
||||
|
||||
const logService = get(ILogService);
|
||||
logService.setLogLevel(LogLevel.SILENT);
|
||||
|
||||
return {
|
||||
univer,
|
||||
get,
|
||||
sheet,
|
||||
};
|
||||
}
|
||||
192
packages/sheets-filter/src/models/custom-filters.ts
Normal file
192
packages/sheets-filter/src/models/custom-filters.ts
Normal file
@@ -0,0 +1,192 @@
|
||||
/**
|
||||
* Copyright 2023-present DreamNum Co., Ltd.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import type { CellValue, Nullable } from '@univerjs/core';
|
||||
import { isNumeric, regexp } from '@univerjs/core';
|
||||
import { CustomFilterOperator } from './types';
|
||||
|
||||
export interface IFilterFn<P extends unknown[]> {
|
||||
label?: string;
|
||||
|
||||
/**
|
||||
* Description of this operator. Should be an i18n key.
|
||||
*/
|
||||
description?: string;
|
||||
|
||||
fn: (...params: P) => boolean;
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Custom filter functions normally used in "Filter by Conditions".
|
||||
*/
|
||||
export interface ICustomFilterFn<P extends unknown[]> extends IFilterFn<P> {
|
||||
/**
|
||||
* Operator of the custom filter function.
|
||||
*/
|
||||
operator?: CustomFilterOperator;
|
||||
|
||||
/**
|
||||
* Group of the custom filter belongs to. It would be rendered in the panel by groups.
|
||||
*/
|
||||
group?: string;
|
||||
|
||||
fn: (...params: P) => boolean;
|
||||
}
|
||||
|
||||
type TwoParameters<C = string> = [value: Nullable<CellValue>, compare: C];
|
||||
|
||||
export const greaterThan: ICustomFilterFn<TwoParameters<number>> = {
|
||||
operator: CustomFilterOperator.GREATER_THAN,
|
||||
fn: (value, compare): boolean => {
|
||||
if (!ensureNumber(value)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return value > compare;
|
||||
},
|
||||
};
|
||||
|
||||
export const greaterThanOrEqualTo: ICustomFilterFn<TwoParameters<number>> = {
|
||||
operator: CustomFilterOperator.GREATER_THAN_OR_EQUAL,
|
||||
fn: (value, compare): boolean => {
|
||||
if (!ensureNumber(value)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return value >= compare;
|
||||
},
|
||||
};
|
||||
|
||||
export const lessThan: ICustomFilterFn<TwoParameters<number>> = {
|
||||
operator: CustomFilterOperator.LESS_THAN,
|
||||
fn: (value, compare): boolean => {
|
||||
if (!ensureNumber(value)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return value < compare;
|
||||
},
|
||||
};
|
||||
|
||||
export const lessThanOrEqualTo: ICustomFilterFn<TwoParameters<number>> = {
|
||||
operator: CustomFilterOperator.LESS_THAN_OR_EQUAL,
|
||||
fn: (value, compare): boolean => {
|
||||
if (!ensureNumber(value)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return value <= compare;
|
||||
},
|
||||
};
|
||||
|
||||
export const equals: ICustomFilterFn<TwoParameters<number>> = {
|
||||
operator: CustomFilterOperator.EQUAL,
|
||||
fn: (value, compare): boolean => {
|
||||
if (!ensureNumber(value)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return value === compare;
|
||||
},
|
||||
};
|
||||
|
||||
export const notEquals: ICustomFilterFn<TwoParameters<number | string>> = {
|
||||
operator: CustomFilterOperator.NOT_EQUALS,
|
||||
fn: (value, compare): boolean => {
|
||||
// As text match.
|
||||
if (typeof compare === 'string') {
|
||||
if (compare === ' ') {
|
||||
if (value !== undefined && value !== null) return true;
|
||||
return false;
|
||||
};
|
||||
|
||||
const ensuredString = ensureString(value);
|
||||
if (ensuredString && isWildCardString(compare)) return !regexp.createFromWildcard(compare as string).test(ensuredString);
|
||||
return ensuredString !== compare;
|
||||
}
|
||||
|
||||
// As numeric match.
|
||||
if (!ensureNumber(value)) return true;
|
||||
return value !== compare;
|
||||
},
|
||||
};
|
||||
|
||||
// Register the custom filter functions to the registry, making it easier to get them.
|
||||
export const CustomFilterFnRegistry = new Map<CustomFilterOperator, ICustomFilterFn<TwoParameters<number>>>([]);
|
||||
|
||||
const ALL_CUSTOM_FILTER_FUNCTIONS = [greaterThan, greaterThanOrEqualTo, lessThan, lessThanOrEqualTo, equals, notEquals];
|
||||
ALL_CUSTOM_FILTER_FUNCTIONS.forEach((fn) => {
|
||||
CustomFilterFnRegistry.set(fn.operator!, fn);
|
||||
});
|
||||
|
||||
export function isNumericFilterFn(operator?: CustomFilterOperator): boolean {
|
||||
return !!operator;
|
||||
}
|
||||
|
||||
/** This operators matches texts. */
|
||||
export const textMatch: ICustomFilterFn<TwoParameters<string>> = {
|
||||
fn: (value, compare): boolean => {
|
||||
const ensured = ensureString(value);
|
||||
if (ensured === null) {
|
||||
if (compare === '') return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
return regexp.createFromWildcard(compare).test(ensured);
|
||||
},
|
||||
};
|
||||
|
||||
// eslint-disable-next-line ts/no-explicit-any
|
||||
export function getCustomFilterFn(operator?: CustomFilterOperator): ICustomFilterFn<TwoParameters<any>> {
|
||||
if (!operator) {
|
||||
return textMatch;
|
||||
}
|
||||
|
||||
return CustomFilterFnRegistry.get(operator)!;
|
||||
}
|
||||
|
||||
function ensureNumber(value: Nullable<CellValue>): value is number {
|
||||
return typeof value === 'number';
|
||||
}
|
||||
|
||||
export function ensureNumeric(value: Nullable<CellValue>): boolean {
|
||||
if (typeof value === 'number') {
|
||||
return true;
|
||||
};
|
||||
|
||||
if (typeof value === 'string' && isNumeric(value)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
function ensureString(value: Nullable<CellValue>): string | null {
|
||||
if (typeof value === 'boolean' || value == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return typeof value === 'string' ? value : value.toString();
|
||||
}
|
||||
|
||||
function isWildCardString(str: string | number): boolean {
|
||||
if (typeof str === 'number') {
|
||||
return false;
|
||||
}
|
||||
|
||||
return str.indexOf('*') !== -1 || str.indexOf('?') !== -1;
|
||||
}
|
||||
621
packages/sheets-filter/src/models/filter-model.ts
Normal file
621
packages/sheets-filter/src/models/filter-model.ts
Normal file
@@ -0,0 +1,621 @@
|
||||
/**
|
||||
* Copyright 2023-present DreamNum Co., Ltd.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import type { CellValue, ICellData, IRange, IStyleData, Nullable, Worksheet } from '@univerjs/core';
|
||||
import type { Observable } from 'rxjs';
|
||||
import type { IAutoFilter, IColorFilters, ICustomFilter, ICustomFilters, IFilterColumn, IFilters } from './types';
|
||||
import { CellValueType, ColorKit, Disposable, extractPureTextFromCell, mergeSets, Rectangle, Tools } from '@univerjs/core';
|
||||
import { COLOR_BLACK_RGB } from '@univerjs/engine-render';
|
||||
import { BehaviorSubject } from 'rxjs';
|
||||
import { ensureNumeric, getCustomFilterFn, isNumericFilterFn, notEquals } from './custom-filters';
|
||||
import { CustomFilterOperator, FilterBy } from './types';
|
||||
|
||||
const EMPTY = () => new Set<number>();
|
||||
|
||||
/**
|
||||
* This is the in-memory model of filter.
|
||||
*/
|
||||
export class FilterModel extends Disposable {
|
||||
private readonly _filteredOutRows$ = new BehaviorSubject<Readonly<Set<number>>>(EMPTY());
|
||||
/** An observable value. A set of filtered out rows. */
|
||||
readonly filteredOutRows$: Observable<Readonly<Set<number>>> = this._filteredOutRows$.asObservable();
|
||||
get filteredOutRows() { return this._filteredOutRows$.getValue(); }
|
||||
set filteredOutRows(rows: Set<number>) {
|
||||
this._alreadyFilteredOutRows = rows;
|
||||
this._filteredOutRows$.next(rows);
|
||||
}
|
||||
// TODO: we may need to update which cols have criteria rather than simple boolean
|
||||
|
||||
private readonly _hasCriteria$ = new BehaviorSubject<boolean>(false);
|
||||
readonly hasCriteria$: Observable<boolean> = this._hasCriteria$.asObservable();
|
||||
|
||||
private _filterColumnByIndex = new Map<number, FilterColumn>();
|
||||
|
||||
private _alreadyFilteredOutRows = EMPTY();
|
||||
|
||||
private _range: Nullable<IRange>;
|
||||
|
||||
constructor(
|
||||
public readonly unitId: string,
|
||||
public readonly subUnitId: string,
|
||||
private readonly _worksheet: Worksheet
|
||||
) {
|
||||
super();
|
||||
}
|
||||
|
||||
override dispose(): void {
|
||||
super.dispose();
|
||||
|
||||
this._filteredOutRows$.complete();
|
||||
this._hasCriteria$.complete();
|
||||
//@ts-ignore
|
||||
this._worksheet = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Serialize this filter model to the JSON format representation.
|
||||
*/
|
||||
serialize(): IAutoFilter {
|
||||
const result: IAutoFilter = {
|
||||
ref: Rectangle.clone(this._range!),
|
||||
filterColumns: this._getAllFilterColumns(true)
|
||||
.sort(([offset1], [offset2]) => offset1 - offset2)
|
||||
.map(([_, filterColumn]) => filterColumn.serialize()),
|
||||
};
|
||||
|
||||
if (this._alreadyFilteredOutRows) {
|
||||
result.cachedFilteredOut = Array.from(this._alreadyFilteredOutRows).sort();
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Deserialize auto filter info to construct a `FilterModel` object.
|
||||
* @param unitId workbook id
|
||||
* @param subUnitId worksheet id
|
||||
* @param worksheet the Worksheet object
|
||||
* @param autoFilter auto filter data
|
||||
*/
|
||||
static deserialize(
|
||||
unitId: string,
|
||||
subUnitId: string,
|
||||
worksheet: Worksheet,
|
||||
autoFilter: IAutoFilter
|
||||
): FilterModel {
|
||||
const filterModel = new FilterModel(unitId, subUnitId, worksheet);
|
||||
filterModel._dump(autoFilter);
|
||||
|
||||
return filterModel;
|
||||
}
|
||||
|
||||
private _dump(autoFilter: IAutoFilter) {
|
||||
this.setRange(autoFilter.ref);
|
||||
autoFilter.filterColumns?.filter((filterColumn) => {
|
||||
// we only support 3 types of filters now.Other types of filters are not supported yet.
|
||||
if (!filterColumn.filters && !filterColumn.colorFilters && !filterColumn.customFilters) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}).forEach((filterColumn) => this._setCriteriaWithoutReCalc(filterColumn.colId, filterColumn));
|
||||
|
||||
if (autoFilter.cachedFilteredOut) {
|
||||
this._alreadyFilteredOutRows = new Set(autoFilter.cachedFilteredOut);
|
||||
this._emit();
|
||||
} else if (autoFilter.filterColumns && autoFilter.filterColumns.length > 0) {
|
||||
// If there is no cached filtered out rows, we need to re-calc all columns.
|
||||
this._reCalcAllColumns();
|
||||
this._emit();
|
||||
}
|
||||
|
||||
this._emitHasCriteria();
|
||||
}
|
||||
|
||||
isRowFiltered(row: number): boolean {
|
||||
return this._alreadyFilteredOutRows.has(row);
|
||||
}
|
||||
|
||||
getRange(): IRange {
|
||||
if (!this._range) {
|
||||
throw new Error('[FilterModel] could not get range before a range is set!');
|
||||
}
|
||||
|
||||
return this._range;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get filtered out rows except the specific column. This method is considered as "pure". In
|
||||
* another word it would not change `filteredOutRows` on `FilterModel` nor `FilterColumn`.
|
||||
* @param col
|
||||
*/
|
||||
getFilteredOutRowsExceptCol(col: number): Set<number> {
|
||||
return this._getAllFilterColumns(true)
|
||||
.filter(([colOffset]) => colOffset !== col)
|
||||
.reduce((acc, [, filterColumn]) => {
|
||||
const newResult = filterColumn.calc({ getAlreadyFilteredOutRows: () => acc });
|
||||
if (newResult) return mergeSets(acc, newResult);
|
||||
return acc;
|
||||
}, new Set<number>());
|
||||
}
|
||||
|
||||
/**
|
||||
* Set range of the filter model, this would remove some `IFilterColumn`
|
||||
* if the new range not overlaps the old range.
|
||||
*/
|
||||
setRange(range: IRange): void {
|
||||
this._range = range;
|
||||
|
||||
// TODO@wzhudev: maybe we should remove the FilterColumn that is not in the new range!
|
||||
// TODO@wzhudev: when a column in the range is deleted, we may need to change some FilterColumns' offset
|
||||
|
||||
// set range for each FilterColumn
|
||||
this._getAllFilterColumns(true)
|
||||
.forEach(([col, filterColumn]) => {
|
||||
filterColumn.setRangeAndColumn({
|
||||
startRow: range.startRow,
|
||||
endRow: range.endRow,
|
||||
startColumn: col,
|
||||
endColumn: col,
|
||||
}, col);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Set or remove filter criteria on a specific row.
|
||||
*/
|
||||
setCriteria(col: number, criteria: Nullable<IFilterColumn>, reCalc = false): void {
|
||||
if (!this._range) {
|
||||
throw new Error('[FilterModel] could not set criteria before a range is set!');
|
||||
}
|
||||
|
||||
if (!criteria) {
|
||||
this._removeCriteria(col);
|
||||
this._rebuildAlreadyFilteredOutRowsWithCache();
|
||||
if (reCalc) {
|
||||
this._reCalcAllColumns();
|
||||
}
|
||||
this._emit();
|
||||
this._emitHasCriteria();
|
||||
return;
|
||||
}
|
||||
|
||||
this._setCriteriaWithoutReCalc(col, criteria);
|
||||
if (reCalc) {
|
||||
// save some performance if we can reuse the cached filtered out rows.
|
||||
this._rebuildAlreadyFilteredOutRowsWithCache();
|
||||
// but we still need to recalc the all columns.
|
||||
this._getAllFilterColumns().forEach((filterColumn) => filterColumn.__clearCache());
|
||||
this._reCalcWithNoCacheColumns();
|
||||
this._emit();
|
||||
this._emitHasCriteria();
|
||||
}
|
||||
}
|
||||
|
||||
getAllFilterColumns(): [number, FilterColumn][] {
|
||||
return this._getAllFilterColumns(true);
|
||||
}
|
||||
|
||||
getFilterColumn(index: number): Nullable<FilterColumn> {
|
||||
return this._filterColumnByIndex.get(index) ?? null;
|
||||
}
|
||||
|
||||
reCalc(): void {
|
||||
this._reCalcAllColumns();
|
||||
this._emit();
|
||||
}
|
||||
|
||||
private _getAllFilterColumns(): FilterColumn[];
|
||||
private _getAllFilterColumns(withCol: true): [number, FilterColumn][];
|
||||
private _getAllFilterColumns(withCol = false): [number, FilterColumn][] | FilterColumn[] {
|
||||
const columns = Array.from(this._filterColumnByIndex.entries());
|
||||
if (withCol) {
|
||||
return columns;
|
||||
}
|
||||
|
||||
return columns.map(([_, filterColumn]) => filterColumn);
|
||||
}
|
||||
|
||||
private _reCalcAllColumns(): void {
|
||||
this._alreadyFilteredOutRows = EMPTY();
|
||||
this._getAllFilterColumns().forEach((filterColumn) => filterColumn.__clearCache());
|
||||
this._reCalcWithNoCacheColumns();
|
||||
}
|
||||
|
||||
private _setCriteriaWithoutReCalc(col: number, criteria: IFilterColumn): void {
|
||||
const range = this._range;
|
||||
if (!range) {
|
||||
throw new Error('[FilterModel] could not set criteria before a range is set!');
|
||||
}
|
||||
|
||||
const { startColumn, endColumn } = range;
|
||||
if (col > endColumn || col < startColumn) {
|
||||
throw new Error(`[FilterModel] could not set criteria on column ${col} which is out of range!`);
|
||||
}
|
||||
|
||||
let filterColumn: FilterColumn;
|
||||
if (this._filterColumnByIndex.has(col)) {
|
||||
filterColumn = this._filterColumnByIndex.get(col)!;
|
||||
} else {
|
||||
filterColumn = new FilterColumn(
|
||||
this.unitId,
|
||||
this.subUnitId,
|
||||
this._worksheet,
|
||||
criteria,
|
||||
{ getAlreadyFilteredOutRows: () => this._alreadyFilteredOutRows }
|
||||
);
|
||||
filterColumn.setRangeAndColumn(range, col);
|
||||
|
||||
this._filterColumnByIndex.set(col, filterColumn);
|
||||
}
|
||||
|
||||
filterColumn.setCriteria(criteria);
|
||||
}
|
||||
|
||||
private _removeCriteria(col: number): void {
|
||||
const filterColumn = this._filterColumnByIndex.get(col);
|
||||
if (filterColumn) {
|
||||
filterColumn.dispose();
|
||||
this._filterColumnByIndex.delete(col);
|
||||
}
|
||||
}
|
||||
|
||||
private _emit(): void {
|
||||
this._filteredOutRows$.next(this._alreadyFilteredOutRows);
|
||||
}
|
||||
|
||||
private _emitHasCriteria(): void {
|
||||
this._hasCriteria$.next(this._filterColumnByIndex.size > 0);
|
||||
}
|
||||
|
||||
private _rebuildAlreadyFilteredOutRowsWithCache(): void {
|
||||
const newFilteredOutRows = this._getAllFilterColumns()
|
||||
.filter((filterColumn) => filterColumn.hasCache())
|
||||
.reduce((acc, filterColumn) => {
|
||||
return mergeSets(acc, filterColumn.filteredOutRows!);
|
||||
}, new Set<number>());
|
||||
|
||||
this._alreadyFilteredOutRows = newFilteredOutRows;
|
||||
}
|
||||
|
||||
private _reCalcWithNoCacheColumns(): void {
|
||||
const noCacheFilteredOutRows = this._getAllFilterColumns().filter((filterColumn) => !filterColumn.hasCache());
|
||||
for (const filterColumn of noCacheFilteredOutRows) {
|
||||
const filteredRows = filterColumn.reCalc();
|
||||
if (filteredRows) {
|
||||
this._alreadyFilteredOutRows = mergeSets(this._alreadyFilteredOutRows, filteredRows);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
interface IFilterColumnContext {
|
||||
getAlreadyFilteredOutRows(): Set<number>;
|
||||
}
|
||||
|
||||
/**
|
||||
* This is the filter criteria on a specific column.
|
||||
*/
|
||||
export class FilterColumn extends Disposable {
|
||||
private _filteredOutRows: Nullable<Set<number>> = null;
|
||||
get filteredOutRows(): Readonly<Nullable<Set<number>>> { return this._filteredOutRows; }
|
||||
|
||||
/** Cache the filter function. */
|
||||
private _filterFn: Nullable<FilterFn | ColorFilterFn> = null;
|
||||
|
||||
private _range: Nullable<IRange> = null;
|
||||
private _column: number = 0;
|
||||
|
||||
private _filterBy: FilterBy = FilterBy.VALUES;
|
||||
get filterBy(): Readonly<FilterBy> { return this._filterBy; }
|
||||
|
||||
constructor(
|
||||
public readonly unitId: string,
|
||||
public readonly subUnitId: string,
|
||||
private readonly _worksheet: Worksheet,
|
||||
|
||||
/**
|
||||
* A `FilterColumn` instance should not be created without a filter criteria.
|
||||
*/
|
||||
private _criteria: IFilterColumn,
|
||||
private readonly _filterColumnContext: IFilterColumnContext
|
||||
) {
|
||||
super();
|
||||
}
|
||||
|
||||
override dispose(): void {
|
||||
super.dispose();
|
||||
|
||||
this._filteredOutRows = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
__clearCache(): void {
|
||||
this._filteredOutRows = null;
|
||||
}
|
||||
|
||||
serialize(): IFilterColumn {
|
||||
if (!this._criteria) {
|
||||
throw new Error('[FilterColumn]: could not serialize without a filter column!');
|
||||
}
|
||||
|
||||
return Tools.deepClone({
|
||||
...this._criteria,
|
||||
colId: this._column,
|
||||
});
|
||||
}
|
||||
|
||||
hasCache(): boolean {
|
||||
return this._filteredOutRows !== null;
|
||||
}
|
||||
|
||||
// The first row should be omitted!
|
||||
setRangeAndColumn(range: IRange, column: number): void {
|
||||
this._range = range;
|
||||
this._column = column;
|
||||
}
|
||||
|
||||
setCriteria(criteria: IFilterColumn): void {
|
||||
this._criteria = criteria;
|
||||
this._generateFilterFn();
|
||||
|
||||
// clear cache
|
||||
this._filteredOutRows = null;
|
||||
}
|
||||
|
||||
getColumnData(): Readonly<IFilterColumn> {
|
||||
return Tools.deepClone(this._criteria);
|
||||
}
|
||||
|
||||
/**
|
||||
* Trigger new calculation on this `FilterModel` instance.
|
||||
*
|
||||
* @external DO NOT EVER call this method from `FilterColumn` itself. The whole process heavily relies on
|
||||
* `filteredOutByOthers`, and it is more comprehensible if we let `FilterModel` take full control over the process.
|
||||
*/
|
||||
reCalc(): Readonly<Nullable<Set<number>>> {
|
||||
this._filteredOutRows = this.calc(this._filterColumnContext);
|
||||
return this._filteredOutRows;
|
||||
}
|
||||
|
||||
calc(context: IFilterColumnContext): Readonly<Nullable<Set<number>>> {
|
||||
if (!this._filterFn) {
|
||||
throw new Error('[FilterColumn] cannot calculate without a filter fn!');
|
||||
}
|
||||
|
||||
if (!this._range) {
|
||||
throw new Error('[FilterColumn] cannot calculate without a range!');
|
||||
}
|
||||
|
||||
if (typeof this._column !== 'number') {
|
||||
throw new TypeError('[FilterColumn] cannot calculate without a column offset!');
|
||||
}
|
||||
|
||||
const column = this._column;
|
||||
const iterateRange: IRange = { startColumn: column, endColumn: column, startRow: this._range.startRow + 1, endRow: this._range.endRow };
|
||||
const filteredOutRows = new Set<number>();
|
||||
const filteredOutByOthers = context.getAlreadyFilteredOutRows();
|
||||
|
||||
// Merged cells are take into consideration here.
|
||||
for (const range of this._worksheet.iterateByColumn(iterateRange, false, false)) {
|
||||
const { row, rowSpan, col } = range;
|
||||
|
||||
// If this row is already filtered out by others, we don't need to check it again.
|
||||
// But it only works for non-vertically-merged cells.
|
||||
if (filteredOutByOthers.has(row) && (!rowSpan || rowSpan === 1)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const filterResult = this._filterBy === FilterBy.VALUES
|
||||
? (this._filterFn as FilterFn)(extractPureTextFromCell(this._worksheet.getCell(row, col)))
|
||||
: this._filterBy === FilterBy.COLORS
|
||||
? (this._filterFn as ColorFilterFn)(this._worksheet.getComposedCellStyle(row, col))
|
||||
: (this._filterFn as FilterFn)(getFilterValueForConditionalFiltering(this._worksheet, row, col));
|
||||
|
||||
if (!filterResult) {
|
||||
filteredOutRows.add(row);
|
||||
|
||||
// Add all rows into filtered out rows if the cell is a merged cell.
|
||||
if (rowSpan) {
|
||||
for (let i = 1; i < rowSpan; i++) {
|
||||
filteredOutRows.add(row + i);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return filteredOutRows;
|
||||
}
|
||||
|
||||
private _generateFilterFn(): void {
|
||||
if (!this._criteria) {
|
||||
return;
|
||||
}
|
||||
|
||||
this._filterFn = generateFilterFn(this._criteria);
|
||||
this._filterBy = this._criteria.filters
|
||||
? FilterBy.VALUES
|
||||
: this._criteria.colorFilters
|
||||
? FilterBy.COLORS
|
||||
: FilterBy.CONDITIONS;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Filter function is a close function which received a cell's content and determine this value is considered as
|
||||
* "matched" and the corresponding row would not be filtered out.
|
||||
*/
|
||||
export type FilterFn = (value: Nullable<CellValue>) => boolean;
|
||||
export type ColorFilterFn = (value: Nullable<IStyleData>) => boolean;
|
||||
|
||||
/**
|
||||
* This functions take a `IFilterColumn` as input and return a function that can be used to filter rows.
|
||||
* @param column
|
||||
* @returns the filter function that takes the cell's value and return a boolean.
|
||||
*/
|
||||
export function generateFilterFn(column: IFilterColumn): FilterFn | ColorFilterFn {
|
||||
if (column.filters) {
|
||||
return filterByValuesFnFactory(column.filters);
|
||||
}
|
||||
|
||||
if (column.colorFilters) {
|
||||
return filterByColorsFnFactory(column.colorFilters);
|
||||
}
|
||||
|
||||
if (column.customFilters) {
|
||||
return customFilterFnFactory(column.customFilters);
|
||||
}
|
||||
|
||||
throw new Error('[FilterModel]: other types of filters are not supported yet.');
|
||||
}
|
||||
|
||||
function filterByValuesFnFactory(values: IFilters): FilterFn {
|
||||
const includeBlank = !!values.blank;
|
||||
const valuesSet = new Set(values.filters);
|
||||
|
||||
return (value) => {
|
||||
if (value === undefined || value === '') return includeBlank;
|
||||
return valuesSet.has(typeof value === 'string' ? value : `${value}`);
|
||||
};
|
||||
}
|
||||
|
||||
function filterByColorsFnFactory(colorFilters: IColorFilters): ColorFilterFn {
|
||||
if (colorFilters.cellFillColors) {
|
||||
const fillColorsSet = new Set(colorFilters.cellFillColors);
|
||||
return (cellStyle) => {
|
||||
// default cell fill color is save as null in the color filters.
|
||||
if (!cellStyle || !cellStyle.bg?.rgb) {
|
||||
if (fillColorsSet.has(null)) return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
const bg = new ColorKit(cellStyle.bg.rgb).toRgbString();
|
||||
return fillColorsSet.has(bg);
|
||||
};
|
||||
}
|
||||
|
||||
if (colorFilters.cellTextColors) {
|
||||
const textColorsSet = new Set(colorFilters.cellTextColors);
|
||||
return (cellStyle) => {
|
||||
if (!cellStyle || !cellStyle.cl?.rgb) {
|
||||
if (textColorsSet.has(COLOR_BLACK_RGB)) return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
const cl = new ColorKit(cellStyle.cl.rgb).toRgbString();
|
||||
return textColorsSet.has(cl);
|
||||
};
|
||||
}
|
||||
|
||||
throw new Error('[FilterModel]: color filters are not supported yet.');
|
||||
}
|
||||
|
||||
function customFilterFnFactory(customFilters: ICustomFilters): FilterFn {
|
||||
const customFilterFns: FilterFn[] = customFilters.customFilters.map((filter) => generateCustomFilterFn(filter));
|
||||
if (isCompoundCustomFilter(customFilterFns)) {
|
||||
if (customFilters.and) {
|
||||
return AND(customFilterFns);
|
||||
}
|
||||
|
||||
return OR(customFilterFns);
|
||||
}
|
||||
|
||||
return customFilterFns[0];
|
||||
}
|
||||
|
||||
function AND(filterFns: [FilterFn, FilterFn]): FilterFn {
|
||||
const [fn1, fn2] = filterFns;
|
||||
return (value) => fn1(value) && fn2(value);
|
||||
}
|
||||
|
||||
function OR(filterFns: [FilterFn, FilterFn]): FilterFn {
|
||||
const [fn1, fn2] = filterFns;
|
||||
return (value) => fn1(value) || fn2(value);
|
||||
}
|
||||
|
||||
function isCompoundCustomFilter(filter: FilterFn[]): filter is [FilterFn, FilterFn] {
|
||||
return filter.length === 2;
|
||||
}
|
||||
|
||||
// TODO@wzhudev: this is not ideal
|
||||
function generateCustomFilterFn(filter: ICustomFilter): FilterFn {
|
||||
const compare = filter.val;
|
||||
|
||||
// Not NOT_EQUALS, if the compare cannot be ensured as number, we should treat it like test not matching.
|
||||
// Otherwise it goes to numeric match.
|
||||
if (filter.operator === CustomFilterOperator.NOT_EQUALS) {
|
||||
const ensured = ensureNumeric(compare);
|
||||
if (!ensured) return (value) => notEquals.fn(value, compare);
|
||||
}
|
||||
|
||||
// numeric match
|
||||
if (isNumericFilterFn(filter.operator)) {
|
||||
const ensured = ensureNumeric(compare);
|
||||
if (!ensured) return () => false;
|
||||
|
||||
const customFilterFn = getCustomFilterFn(filter.operator);
|
||||
const ensuredNumber = Number(compare);
|
||||
return (value) => customFilterFn.fn(value, ensuredNumber);
|
||||
}
|
||||
|
||||
// text match
|
||||
const customFilterFn = getCustomFilterFn(filter.operator);
|
||||
return (value) => customFilterFn.fn(value, compare);
|
||||
}
|
||||
|
||||
function getFilterValueForConditionalFiltering(worksheet: Worksheet, row: number, col: number): Nullable<string | number | boolean> {
|
||||
const interceptedCell = worksheet.getCell(row, col);
|
||||
if (!interceptedCell) return null;
|
||||
|
||||
const rawCell = worksheet.getCellRaw(row, col);
|
||||
|
||||
if (interceptedCell && !rawCell) return extractFilterValueFromCell(interceptedCell);
|
||||
|
||||
if (!rawCell) return null;
|
||||
|
||||
if (interceptedCell.t === CellValueType.NUMBER && typeof interceptedCell.v === 'string') {
|
||||
return rawCell.v as number;
|
||||
}
|
||||
|
||||
// if the type is number, no matter what the raw value is, we should always return a number.
|
||||
if (interceptedCell.t === CellValueType.NUMBER) {
|
||||
return Number(rawCell.v);
|
||||
}
|
||||
|
||||
return extractFilterValueFromCell(rawCell);
|
||||
}
|
||||
|
||||
function extractFilterValueFromCell(cell: ICellData): string | number {
|
||||
const richTextValue = cell.p?.body?.dataStream;
|
||||
if (richTextValue) return richTextValue.trimEnd();
|
||||
|
||||
const rawValue = cell.v;
|
||||
|
||||
if (typeof rawValue === 'string') {
|
||||
if (cell.t === CellValueType.BOOLEAN) return rawValue.toUpperCase();
|
||||
return rawValue;
|
||||
};
|
||||
|
||||
if (typeof rawValue === 'number') {
|
||||
if (cell.t === CellValueType.BOOLEAN) return rawValue ? 'TRUE' : 'FALSE';
|
||||
return rawValue;
|
||||
};
|
||||
|
||||
if (typeof rawValue === 'boolean') return rawValue ? 'TRUE' : 'FALSE';
|
||||
|
||||
return '';
|
||||
}
|
||||
136
packages/sheets-filter/src/models/types.ts
Normal file
136
packages/sheets-filter/src/models/types.ts
Normal file
@@ -0,0 +1,136 @@
|
||||
/**
|
||||
* Copyright 2023-present DreamNum Co., Ltd.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import type { BooleanNumber, IRange } from '@univerjs/core';
|
||||
|
||||
/**
|
||||
* The filter types.
|
||||
*/
|
||||
export enum FilterBy {
|
||||
VALUES,
|
||||
COLORS,
|
||||
CONDITIONS,
|
||||
}
|
||||
|
||||
/**
|
||||
* Data of a filter in a {@link Worksheet}.
|
||||
*
|
||||
* Please refer to 18.3.2 AutoFilter Settings. Properties of this interface would be added in the future.
|
||||
* Please make sure that it is backward compatible.
|
||||
*
|
||||
* @property {IRange} ref The range of the filter.
|
||||
* @property {IFilterColumn[]} [filterColumns] The filter criteria of each column.
|
||||
* @property {number[]} [cachedFilteredOut] The cached filtered out row numbers.
|
||||
*/
|
||||
export interface IAutoFilter {
|
||||
ref: IRange;
|
||||
|
||||
filterColumns?: IFilterColumn[];
|
||||
cachedFilteredOut?: number[];
|
||||
}
|
||||
|
||||
/**
|
||||
* The filter criteria of a column.
|
||||
*
|
||||
* @property {number} colId The column number.
|
||||
* @property {IFilters} [filters] The basic filters (filter by value).
|
||||
* @property {ICustomFilters} [customFilters] The custom filters.
|
||||
*/
|
||||
export interface IFilterColumn {
|
||||
colId: number;
|
||||
filters?: IFilters;
|
||||
colorFilters?: IColorFilters;
|
||||
customFilters?: ICustomFilters;
|
||||
};
|
||||
|
||||
/**
|
||||
* Basic filters (filter by value).
|
||||
*
|
||||
* @property {true} [blank] Filter by blank. If this field is present, blank cells would be visible.
|
||||
* @property {Array<string>} [filters] Filter by values. Cells with values in this array would be visible.
|
||||
*/
|
||||
export interface IFilters {
|
||||
blank?: true;
|
||||
filters?: Array<string>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Color filters.
|
||||
*
|
||||
* @property {Array<string | null>} [cellFillColor] The cell fill colors to filter by. If this field is present, cells with these fill colors would be visible.
|
||||
* @property {Array<string>} [cellTextColor] The cell text colors to filter by. If this field is present, cells with these text colors would be visible.
|
||||
*/
|
||||
export interface IColorFilters {
|
||||
cellFillColors?: Array<string | null>;
|
||||
cellTextColors?: Array<string>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Custom filters.
|
||||
*
|
||||
* @property {BooleanNumber.TRUE} and The logical operator of the custom filters. If this field is present,
|
||||
* the custom filters would be connected by 'and'. Otherwise, they would be connected by 'or'.
|
||||
* @property {ICustomFilter[]} customFilters The custom filters.
|
||||
*/
|
||||
export interface ICustomFilters {
|
||||
and?: BooleanNumber.TRUE;
|
||||
customFilters: [ICustomFilter] | [ICustomFilter, ICustomFilter];
|
||||
}
|
||||
|
||||
/**
|
||||
* A custom filter.
|
||||
*
|
||||
* @property {string | number} val The value to be compared.
|
||||
* @property {CustomFilterOperator} [operator] The operator of the comparison. If this field is empty, the operator would be 'equal'.
|
||||
*/
|
||||
export interface ICustomFilter {
|
||||
val: string | number;
|
||||
operator?: CustomFilterOperator;
|
||||
}
|
||||
|
||||
/**
|
||||
* Basic custom filter operators.
|
||||
*
|
||||
* @internal
|
||||
* doesNotContain, isBlank, isNotBlank are not defined in OOXML. They are represented by regex-like values.
|
||||
*/
|
||||
export enum CustomFilterOperator {
|
||||
/** "EQUAL" operator. */
|
||||
EQUAL = 'equal',
|
||||
/** "GREATER_THAN" operator. */
|
||||
GREATER_THAN = 'greaterThan',
|
||||
/** "GREATER_THAN_OR_EQUAL" operator. */
|
||||
GREATER_THAN_OR_EQUAL = 'greaterThanOrEqual',
|
||||
/** "LESS_THAN" operator. */
|
||||
LESS_THAN = 'lessThan',
|
||||
/** "LESS_THAN_OR_EQUAL" operator. */
|
||||
LESS_THAN_OR_EQUAL = 'lessThanOrEqual',
|
||||
/** "NOT_EQUALS" operator. */
|
||||
NOT_EQUALS = 'notEqual',
|
||||
}
|
||||
|
||||
export interface IDynamicFilter {
|
||||
val: string | number;
|
||||
|
||||
type: DynamicFilterOperator;
|
||||
}
|
||||
|
||||
/**
|
||||
* Not used now. Would be used in the future.
|
||||
*/
|
||||
export enum DynamicFilterOperator {
|
||||
ABOVE_AVERAGE = 'aboveAverage',
|
||||
}
|
||||
68
packages/sheets-filter/src/plugin.ts
Normal file
68
packages/sheets-filter/src/plugin.ts
Normal file
@@ -0,0 +1,68 @@
|
||||
/**
|
||||
* Copyright 2023-present DreamNum Co., Ltd.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import type { Dependency } from '@univerjs/core';
|
||||
import type { IUniverSheetsFilterConfig } from './config/config';
|
||||
import { DependentOn, IConfigService, Inject, Injector, merge, Plugin, touchDependencies, UniverInstanceType } from '@univerjs/core';
|
||||
import { UniverFormulaEnginePlugin } from '@univerjs/engine-formula';
|
||||
import { UniverSheetsPlugin } from '@univerjs/sheets';
|
||||
import pkg from '../package.json';
|
||||
import { defaultPluginConfig, SHEETS_FILTER_PLUGIN_CONFIG_KEY } from './config/config';
|
||||
import { SheetsFilterSyncController } from './controllers/sheets-filter-sync.controller';
|
||||
import { SheetsFilterController } from './controllers/sheets-filter.controller';
|
||||
import { SheetsFilterFormulaService } from './services/sheet-filter-formula.service';
|
||||
import { SHEET_FILTER_SNAPSHOT_ID, SheetsFilterService } from './services/sheet-filter.service';
|
||||
|
||||
@DependentOn(UniverFormulaEnginePlugin, UniverSheetsPlugin)
|
||||
export class UniverSheetsFilterPlugin extends Plugin {
|
||||
static override type = UniverInstanceType.UNIVER_SHEET;
|
||||
static override pluginName = SHEET_FILTER_SNAPSHOT_ID;
|
||||
static override packageName = pkg.name;
|
||||
static override version = pkg.version;
|
||||
|
||||
constructor(
|
||||
private readonly _config: Partial<IUniverSheetsFilterConfig> = defaultPluginConfig,
|
||||
@Inject(Injector) protected readonly _injector: Injector,
|
||||
@IConfigService private readonly _configService: IConfigService
|
||||
) {
|
||||
super();
|
||||
|
||||
// Manage the plugin configuration.
|
||||
const { ...rest } = merge(
|
||||
{},
|
||||
defaultPluginConfig,
|
||||
this._config
|
||||
);
|
||||
this._configService.setConfig(SHEETS_FILTER_PLUGIN_CONFIG_KEY, rest);
|
||||
}
|
||||
|
||||
override onStarting(): void {
|
||||
([
|
||||
[SheetsFilterFormulaService],
|
||||
[SheetsFilterService],
|
||||
[SheetsFilterController],
|
||||
[SheetsFilterSyncController],
|
||||
] as Dependency[]).forEach((d) => this._injector.add(d));
|
||||
}
|
||||
|
||||
override onReady(): void {
|
||||
touchDependencies(this._injector, [
|
||||
[SheetsFilterFormulaService],
|
||||
[SheetsFilterController],
|
||||
[SheetsFilterSyncController],
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
/**
|
||||
* Copyright 2023-present DreamNum Co., Ltd.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { Injector, IUniverInstanceService } from '@univerjs/core';
|
||||
import { IActiveDirtyManagerService, ISheetRowFilteredService } from '@univerjs/engine-formula';
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
import { SheetsFilterFormulaService } from '../sheet-filter-formula.service';
|
||||
import { SheetsFilterService } from '../sheet-filter.service';
|
||||
|
||||
describe('SheetsFilterFormulaService', () => {
|
||||
let dirtyConverters: Map<string, { getDirtyData: (commandInfo: unknown) => unknown }>;
|
||||
let rowFilteredCallback: ((unitId: string, subUnitId: string, row: number) => boolean) | undefined;
|
||||
|
||||
beforeEach(() => {
|
||||
dirtyConverters = new Map();
|
||||
class TestActiveDirtyManagerService {
|
||||
register = (commandId: string, converter: never) => dirtyConverters.set(commandId, converter);
|
||||
}
|
||||
|
||||
class TestSheetRowFilteredService {
|
||||
register = (callback: never) => (rowFilteredCallback = callback);
|
||||
}
|
||||
|
||||
class TestSheetsFilterService {
|
||||
getFilterModel = vi.fn(() => ({ getRange: () => ({ startRow: 2, endRow: 5 }), isRowFiltered: (row: number) => row === 3 }));
|
||||
}
|
||||
|
||||
class TestUniverInstanceService {
|
||||
getUnit = () => ({ getSheetBySheetId: () => ({ getColumnCount: () => 10 }) });
|
||||
}
|
||||
|
||||
const injector = new Injector();
|
||||
injector.add([IActiveDirtyManagerService, { useClass: TestActiveDirtyManagerService as never }]);
|
||||
injector.add([ISheetRowFilteredService, { useClass: TestSheetRowFilteredService as never }]);
|
||||
injector.add([SheetsFilterService, { useClass: TestSheetsFilterService as never }]);
|
||||
injector.add([IUniverInstanceService, { useClass: TestUniverInstanceService as never }]);
|
||||
injector.add([SheetsFilterFormulaService]);
|
||||
injector.get(SheetsFilterFormulaService);
|
||||
});
|
||||
|
||||
it('marks the filtered row span dirty for formula recalculation', () => {
|
||||
const converter = [...dirtyConverters.values()][0];
|
||||
|
||||
expect(converter.getDirtyData({ params: { unitId: 'book-1', subUnitId: 'sheet-1' } })).toEqual({
|
||||
dirtyRanges: [{
|
||||
unitId: 'book-1',
|
||||
sheetId: 'sheet-1',
|
||||
range: { startRow: 2, startColumn: 0, endRow: 5, endColumn: 9 },
|
||||
}],
|
||||
clearDependencyTreeCache: { 'book-1': { 'sheet-1': '1' } },
|
||||
});
|
||||
});
|
||||
|
||||
it('registers formula row-filter lookups against the active sheet filter model', () => {
|
||||
expect(rowFilteredCallback?.('book-1', 'sheet-1', 3)).toBe(true);
|
||||
expect(rowFilteredCallback?.('book-1', 'sheet-1', 4)).toBe(false);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,170 @@
|
||||
/**
|
||||
* Copyright 2023-present DreamNum Co., Ltd.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import {
|
||||
CommandService,
|
||||
ConfigService,
|
||||
ContextService,
|
||||
DesktopLogService,
|
||||
ICommandService,
|
||||
IConfigService,
|
||||
IContextService,
|
||||
ILogService,
|
||||
Injector,
|
||||
IResourceManagerService,
|
||||
IUniverInstanceService,
|
||||
ResourceManagerService,
|
||||
UniverInstanceService,
|
||||
Workbook,
|
||||
} from '@univerjs/core';
|
||||
import { afterEach, describe, expect, it } from 'vitest';
|
||||
import { SheetsFilterService } from '../sheet-filter.service';
|
||||
|
||||
const injectors: Injector[] = [];
|
||||
|
||||
function createService() {
|
||||
const injector = new Injector();
|
||||
injectors.push(injector);
|
||||
injector.add([ILogService, { useClass: DesktopLogService }]);
|
||||
injector.add([IConfigService, { useClass: ConfigService }]);
|
||||
injector.add([IContextService, { useClass: ContextService }]);
|
||||
injector.add([ICommandService, { useClass: CommandService }]);
|
||||
injector.add([IResourceManagerService, { useClass: ResourceManagerService }]);
|
||||
injector.add([IUniverInstanceService, { useClass: UniverInstanceService }]);
|
||||
injector.add([SheetsFilterService]);
|
||||
const univerInstanceService = injector.get(IUniverInstanceService) as UniverInstanceService;
|
||||
const workbook = injector.createInstance(Workbook, {
|
||||
id: 'unit-1',
|
||||
sheets: { 'sheet-1': { id: 'sheet-1' } },
|
||||
sheetOrder: ['sheet-1'],
|
||||
});
|
||||
univerInstanceService.__addUnit(workbook);
|
||||
univerInstanceService.focusUnit('unit-1');
|
||||
|
||||
return injector.get(SheetsFilterService);
|
||||
}
|
||||
|
||||
function createServiceWithCapturedResource() {
|
||||
let resourceConfig: any;
|
||||
|
||||
class TestResourceManagerService {
|
||||
registerPluginResource(config: unknown) {
|
||||
resourceConfig = config;
|
||||
return { dispose: () => undefined };
|
||||
}
|
||||
}
|
||||
|
||||
class TestCommandService {
|
||||
onCommandExecuted = () => ({ dispose: () => undefined });
|
||||
}
|
||||
|
||||
const injector = new Injector();
|
||||
injectors.push(injector);
|
||||
injector.add([ILogService, { useClass: DesktopLogService }]);
|
||||
injector.add([IContextService, { useClass: ContextService }]);
|
||||
injector.add([IResourceManagerService, { useClass: TestResourceManagerService as never }]);
|
||||
injector.add([IUniverInstanceService, { useClass: UniverInstanceService }]);
|
||||
injector.add([ICommandService, { useClass: TestCommandService as never }]);
|
||||
injector.add([SheetsFilterService]);
|
||||
|
||||
const univerInstanceService = injector.get(IUniverInstanceService) as UniverInstanceService;
|
||||
const workbook = injector.createInstance(Workbook, {
|
||||
id: 'unit-1',
|
||||
sheets: { 'sheet-1': { id: 'sheet-1' } },
|
||||
sheetOrder: ['sheet-1'],
|
||||
});
|
||||
univerInstanceService.__addUnit(workbook);
|
||||
univerInstanceService.focusUnit('unit-1');
|
||||
|
||||
return { service: injector.get(SheetsFilterService), getResourceConfig: () => resourceConfig };
|
||||
}
|
||||
|
||||
afterEach(() => injectors.splice(0).forEach((injector) => injector.dispose()));
|
||||
|
||||
describe('SheetsFilterService', () => {
|
||||
it('creates and removes filter models for workbook sheets', () => {
|
||||
const service = createService();
|
||||
|
||||
const model = service.ensureFilterModel('unit-1', 'sheet-1');
|
||||
|
||||
expect(service.getFilterModel('unit-1', 'sheet-1')).toBe(model);
|
||||
expect(service.removeFilterModel('unit-1', 'sheet-1')).toBe(true);
|
||||
expect(service.getFilterModel('unit-1', 'sheet-1')).toBeNull();
|
||||
expect(service.removeFilterModel('unit-1', 'sheet-1')).toBe(false);
|
||||
});
|
||||
|
||||
it('throws when creating filter models for missing workbook or worksheet', () => {
|
||||
const service = createService();
|
||||
|
||||
expect(() => service.ensureFilterModel('missing', 'sheet-1')).toThrow('non-existing workbook missing');
|
||||
expect(() => service.ensureFilterModel('unit-1', 'missing')).toThrow('non-existing worksheet missing');
|
||||
});
|
||||
|
||||
it('publishes filter error messages for UI consumers', () => {
|
||||
const service = createService();
|
||||
const messages: unknown[] = [];
|
||||
const sub = service.errorMsg$.subscribe((message) => messages.push(message));
|
||||
|
||||
service.setFilterErrorMsg('Invalid filter range');
|
||||
|
||||
expect(messages).toEqual([null, 'Invalid filter range']);
|
||||
sub.unsubscribe();
|
||||
});
|
||||
|
||||
it('disposes cached filter models and completes state streams', () => {
|
||||
const service = createService();
|
||||
const model = service.ensureFilterModel('unit-1', 'sheet-1');
|
||||
const completions: string[] = [];
|
||||
|
||||
service.loadedUnitId$.subscribe({ complete: () => completions.push('loaded') });
|
||||
service.errorMsg$.subscribe({ complete: () => completions.push('error') });
|
||||
service.activeFilterModel$.subscribe({ complete: () => completions.push('active') });
|
||||
|
||||
service.dispose();
|
||||
|
||||
expect(service.getFilterModel('unit-1', 'sheet-1')).toBeNull();
|
||||
expect((model as never as { _disposed: boolean })._disposed).toBe(true);
|
||||
expect(completions).toEqual(['loaded', 'error', 'active']);
|
||||
});
|
||||
|
||||
it('loads and unloads filter models from snapshot resources and updates active model', () => {
|
||||
const { service, getResourceConfig } = createServiceWithCapturedResource();
|
||||
const loaded: unknown[] = [];
|
||||
const active: unknown[] = [];
|
||||
service.loadedUnitId$.subscribe((unitId) => loaded.push(unitId));
|
||||
service.activeFilterModel$.subscribe((model) => active.push(model));
|
||||
|
||||
expect(getResourceConfig().toJson('unit-1')).toBe('{}');
|
||||
|
||||
getResourceConfig().onLoad('unit-1', {
|
||||
'sheet-1': {
|
||||
ref: { startRow: 0, endRow: 3, startColumn: 0, endColumn: 2 },
|
||||
filterColumns: [],
|
||||
},
|
||||
});
|
||||
|
||||
expect(loaded).toEqual([null, 'unit-1']);
|
||||
expect(service.getFilterModel('unit-1', 'sheet-1')).toBe(service.activeFilterModel);
|
||||
expect(active.at(-1)).toBe(service.activeFilterModel);
|
||||
expect(JSON.parse(getResourceConfig().toJson('unit-1'))['sheet-1']).toMatchObject({
|
||||
ref: { startRow: 0, endRow: 3, startColumn: 0, endColumn: 2 },
|
||||
});
|
||||
|
||||
getResourceConfig().onUnLoad('unit-1');
|
||||
expect(service.getFilterModel('unit-1', 'sheet-1')).toBeNull();
|
||||
expect(JSON.stringify(getResourceConfig().parseJson('{"sheet-1":{"filterColumns":[]}}'))).toBe('{"sheet-1":{"filterColumns":[]}}');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,94 @@
|
||||
/**
|
||||
* Copyright 2023-present DreamNum Co., Ltd.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import type { Workbook } from '@univerjs/core';
|
||||
import type { ISheetCommandSharedParams } from '@univerjs/sheets';
|
||||
import { Disposable, Inject, IUniverInstanceService } from '@univerjs/core';
|
||||
import { IActiveDirtyManagerService, ISheetRowFilteredService } from '@univerjs/engine-formula';
|
||||
import { FILTER_MUTATIONS } from '../common/const';
|
||||
import { SheetsFilterService } from './sheet-filter.service';
|
||||
|
||||
/**
|
||||
* Hidden rows after filtering affect formula calculations, such as SUBTOTAL
|
||||
*/
|
||||
export class SheetsFilterFormulaService extends Disposable {
|
||||
constructor(
|
||||
@Inject(IActiveDirtyManagerService) private _activeDirtyManagerService: IActiveDirtyManagerService,
|
||||
@Inject(ISheetRowFilteredService) private _sheetRowFilteredService: ISheetRowFilteredService,
|
||||
@Inject(SheetsFilterService) private _sheetsFilterService: SheetsFilterService,
|
||||
@IUniverInstanceService private readonly _univerInstanceService: IUniverInstanceService
|
||||
) {
|
||||
super();
|
||||
|
||||
this._initFormulaDirtyRange();
|
||||
this._registerSheetRowFiltered();
|
||||
}
|
||||
|
||||
private _initFormulaDirtyRange() {
|
||||
FILTER_MUTATIONS.forEach((commandId) => {
|
||||
this._activeDirtyManagerService.register(
|
||||
commandId,
|
||||
{
|
||||
commandId,
|
||||
getDirtyData: (commandInfo) => {
|
||||
const params = commandInfo.params as ISheetCommandSharedParams;
|
||||
const { unitId, subUnitId } = params;
|
||||
return {
|
||||
dirtyRanges: this._getHideRowMutation(unitId, subUnitId),
|
||||
clearDependencyTreeCache: {
|
||||
[unitId]: {
|
||||
[subUnitId]: '1',
|
||||
},
|
||||
},
|
||||
};
|
||||
},
|
||||
}
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
private _getHideRowMutation(unitId: string, subUnitId: string) {
|
||||
const range = this._sheetsFilterService.getFilterModel(unitId, subUnitId)?.getRange();
|
||||
|
||||
const sheet = this._univerInstanceService.getUnit<Workbook>(unitId)?.getSheetBySheetId(subUnitId);
|
||||
|
||||
if (range == null || sheet == null) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const { startRow, endRow } = range;
|
||||
|
||||
// covert hidden rows to dirtyRanges
|
||||
const dirtyRanges = [{
|
||||
unitId,
|
||||
sheetId: subUnitId,
|
||||
range: {
|
||||
startRow,
|
||||
startColumn: 0,
|
||||
endRow,
|
||||
endColumn: sheet.getColumnCount() - 1,
|
||||
},
|
||||
}];
|
||||
|
||||
return dirtyRanges;
|
||||
}
|
||||
|
||||
private _registerSheetRowFiltered() {
|
||||
this._sheetRowFilteredService.register((unitId, subUnitId, row) => {
|
||||
return this._sheetsFilterService.getFilterModel(unitId, subUnitId)?.isRowFiltered(row) ?? false;
|
||||
});
|
||||
}
|
||||
}
|
||||
216
packages/sheets-filter/src/services/sheet-filter.service.ts
Normal file
216
packages/sheets-filter/src/services/sheet-filter.service.ts
Normal file
@@ -0,0 +1,216 @@
|
||||
/**
|
||||
* Copyright 2023-present DreamNum Co., Ltd.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES 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 { Nullable, Workbook } from '@univerjs/core';
|
||||
import type { IAutoFilter } from '../models/types';
|
||||
import {
|
||||
CommandType,
|
||||
Disposable,
|
||||
fromCallback,
|
||||
ICommandService,
|
||||
IResourceManagerService,
|
||||
IUniverInstanceService,
|
||||
UniverInstanceType,
|
||||
} from '@univerjs/core';
|
||||
import { BehaviorSubject, filter, merge, of, switchMap } from 'rxjs';
|
||||
import { FILTER_MUTATIONS } from '../common/const';
|
||||
import { FilterModel } from '../models/filter-model';
|
||||
|
||||
type WorksheetID = string;
|
||||
export interface ISheetsFilterResource {
|
||||
[key: WorksheetID]: IAutoFilter;
|
||||
}
|
||||
|
||||
export const SHEET_FILTER_SNAPSHOT_ID = 'SHEET_FILTER_PLUGIN';
|
||||
|
||||
/**
|
||||
* This service is responsible for managing filter models, especially their lifecycle.
|
||||
*/
|
||||
export class SheetsFilterService extends Disposable {
|
||||
private readonly _filterModels = new Map<string, Map<string, FilterModel>>();
|
||||
|
||||
private readonly _loadedUnitId$ = new BehaviorSubject<Nullable<string>>(null);
|
||||
readonly loadedUnitId$ = this._loadedUnitId$.asObservable();
|
||||
|
||||
private readonly _errorMsg$ = new BehaviorSubject<Nullable<string>>(null);
|
||||
readonly errorMsg$ = this._errorMsg$.asObservable();
|
||||
|
||||
private readonly _activeFilterModel$ = new BehaviorSubject<Nullable<FilterModel>>(null);
|
||||
/** An observable value emitting the current Workbook's active Worksheet's filter model (if there is one). */
|
||||
readonly activeFilterModel$ = this._activeFilterModel$.asObservable();
|
||||
/** The current Workbook's active Worksheet's filter model (if there is one). */
|
||||
get activeFilterModel(): Nullable<FilterModel> { return this._activeFilterModel$.getValue(); }
|
||||
|
||||
constructor(
|
||||
@IResourceManagerService private readonly _resourcesManagerService: IResourceManagerService,
|
||||
@IUniverInstanceService private readonly _univerInstanceService: IUniverInstanceService,
|
||||
@ICommandService private readonly _commandService: ICommandService
|
||||
) {
|
||||
super();
|
||||
|
||||
this._initModel();
|
||||
this._initActiveFilterModel();
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param unitId
|
||||
* @param subUnitId
|
||||
*/
|
||||
ensureFilterModel(unitId: string, subUnitId: string): FilterModel {
|
||||
const already = this.getFilterModel(unitId, subUnitId);
|
||||
if (already) {
|
||||
return already;
|
||||
}
|
||||
|
||||
const workbook = this._univerInstanceService.getUnit<Workbook>(unitId, UniverInstanceType.UNIVER_SHEET);
|
||||
if (!workbook) {
|
||||
throw new Error(`[SheetsFilterService]: could not create "FilterModel" on a non-existing workbook ${unitId}!`);
|
||||
}
|
||||
|
||||
const worksheet = workbook.getSheetBySheetId(subUnitId);
|
||||
if (!worksheet) {
|
||||
throw new Error(`[SheetsFilterService]: could not create "FilterModel" on a non-existing worksheet ${subUnitId}!`);
|
||||
}
|
||||
|
||||
const filterModel = new FilterModel(unitId, subUnitId, worksheet);
|
||||
this._cacheFilterModel(unitId, subUnitId, filterModel);
|
||||
return filterModel;
|
||||
}
|
||||
|
||||
getFilterModel(unitId: string, subUnitId: string): Nullable<FilterModel> {
|
||||
return this._filterModels.get(unitId)?.get(subUnitId) ?? null;
|
||||
}
|
||||
|
||||
removeFilterModel(unitId: string, subUnitId: string): boolean {
|
||||
const already = this.getFilterModel(unitId, subUnitId);
|
||||
if (already) {
|
||||
already.dispose();
|
||||
this._filterModels.get(unitId)!.delete(subUnitId);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
setFilterErrorMsg(content: string): void {
|
||||
this._errorMsg$.next(content);
|
||||
}
|
||||
|
||||
private _updateActiveFilterModel(): void {
|
||||
let workbook: Nullable<Workbook>;
|
||||
try {
|
||||
workbook = this._univerInstanceService.getCurrentUnitOfType(UniverInstanceType.UNIVER_SHEET);
|
||||
if (!workbook) {
|
||||
this._activeFilterModel$.next(null);
|
||||
return;
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('[SheetsFilterService]: could not get active workbook!', err);
|
||||
return;
|
||||
}
|
||||
|
||||
// Use getActiveSheet to avoid automatically activating the next sheet when deleting the sheet, causing the sheet switching in ActiveWorksheetController to be invalid.
|
||||
const activeSheet = workbook.getActiveSheet(true);
|
||||
if (!activeSheet) {
|
||||
this._activeFilterModel$.next(null);
|
||||
return;
|
||||
}
|
||||
|
||||
const unitId = activeSheet.getUnitId();
|
||||
const subUnitId = activeSheet.getSheetId();
|
||||
const filterModel = this.getFilterModel(unitId, subUnitId);
|
||||
this._activeFilterModel$.next(filterModel);
|
||||
}
|
||||
|
||||
private _initActiveFilterModel(): void {
|
||||
this.disposeWithMe(
|
||||
merge(
|
||||
// source1: executing filter related mutations
|
||||
fromCallback(this._commandService.onCommandExecuted.bind(this._commandService))
|
||||
.pipe(filter(([command]) => command.type === CommandType.MUTATION && FILTER_MUTATIONS.has(command.id))),
|
||||
|
||||
// source2: activate sheet changes
|
||||
this._univerInstanceService.getCurrentTypeOfUnit$<Workbook>(UniverInstanceType.UNIVER_SHEET)
|
||||
.pipe(switchMap((workbook) => workbook?.activeSheet$ ?? of(null)))
|
||||
).subscribe(() => this._updateActiveFilterModel())
|
||||
);
|
||||
}
|
||||
|
||||
private _serializeAutoFiltersForUnit(unitId: string): string {
|
||||
const allFilterModels = this._filterModels.get(unitId);
|
||||
if (!allFilterModels) {
|
||||
return '{}';
|
||||
}
|
||||
|
||||
const json: ISheetsFilterResource = {};
|
||||
allFilterModels.forEach((model, worksheetId) => {
|
||||
json[worksheetId] = model.serialize();
|
||||
});
|
||||
|
||||
return JSON.stringify(json);
|
||||
}
|
||||
|
||||
private _deserializeAutoFiltersForUnit(unitId: string, json: ISheetsFilterResource): void {
|
||||
const workbook = this._univerInstanceService.getUnit<Workbook>(unitId, UniverInstanceType.UNIVER_SHEET)!;
|
||||
Object.keys(json).forEach((worksheetId: WorksheetID) => {
|
||||
const autoFilter = json[worksheetId]!;
|
||||
const filterModel = FilterModel.deserialize(unitId, worksheetId, workbook.getSheetBySheetId(worksheetId)!, autoFilter);
|
||||
this._cacheFilterModel(unitId, worksheetId, filterModel);
|
||||
});
|
||||
}
|
||||
|
||||
override dispose(): void {
|
||||
super.dispose();
|
||||
|
||||
this._loadedUnitId$.complete();
|
||||
this._errorMsg$.complete();
|
||||
this._activeFilterModel$.complete();
|
||||
this._filterModels.forEach((allFilterModels) => {
|
||||
allFilterModels.forEach((model) => model.dispose());
|
||||
allFilterModels.clear();
|
||||
});
|
||||
this._filterModels.clear();
|
||||
}
|
||||
|
||||
private _initModel(): void {
|
||||
this._resourcesManagerService.registerPluginResource<ISheetsFilterResource>({
|
||||
pluginName: SHEET_FILTER_SNAPSHOT_ID,
|
||||
businesses: [UniverInstanceType.UNIVER_SHEET],
|
||||
toJson: (id) => this._serializeAutoFiltersForUnit(id),
|
||||
parseJson: (json) => JSON.parse(json),
|
||||
onLoad: (unitId, value) => {
|
||||
this._deserializeAutoFiltersForUnit(unitId, value);
|
||||
this._loadedUnitId$.next(unitId);
|
||||
this._updateActiveFilterModel();
|
||||
},
|
||||
onUnLoad: (unitId: string) => {
|
||||
const allFilterModels = this._filterModels.get(unitId);
|
||||
if (allFilterModels) {
|
||||
allFilterModels.forEach((model) => model.dispose());
|
||||
this._filterModels.delete(unitId);
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
private _cacheFilterModel(unitId: string, subUnitId: string, filterModel: FilterModel): void {
|
||||
if (!this._filterModels.has(unitId)) {
|
||||
this._filterModels.set(unitId, new Map());
|
||||
}
|
||||
this._filterModels.get(unitId)!.set(subUnitId, filterModel);
|
||||
}
|
||||
}
|
||||
54
packages/sheets-filter/src/utils.ts
Normal file
54
packages/sheets-filter/src/utils.ts
Normal file
@@ -0,0 +1,54 @@
|
||||
/**
|
||||
* Copyright 2023-present DreamNum Co., Ltd.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import type { IMutationInfo, Nullable } from '@univerjs/core';
|
||||
import type { ISetSheetsFilterCriteriaMutationParams } from './commands/mutations/sheets-filter.mutation';
|
||||
import { SetSheetsFilterCriteriaMutation } from './commands/mutations/sheets-filter.mutation';
|
||||
|
||||
interface ILine {
|
||||
start: number;
|
||||
end: number;
|
||||
}
|
||||
export function lineIntersect(line1: ILine, line2: ILine): boolean {
|
||||
return line1.start <= line2.end && line1.end >= line2.start;
|
||||
}
|
||||
|
||||
export function lineContains(line1: ILine, line2: ILine): boolean {
|
||||
return line1.start <= line2.start && line1.end >= line2.end;
|
||||
}
|
||||
|
||||
export function objectsShaker<T>(target: Nullable<T>[], isEqual: (o1: T, o2: T) => boolean) {
|
||||
for (let i = 0; i < target.length; i++) {
|
||||
let cur = i;
|
||||
if (target[i]) {
|
||||
for (let j = i + 1; j < target.length; j++) {
|
||||
if (target[cur] && target[j] && isEqual(target[cur]!, target[j]!)) {
|
||||
target[cur] = null;
|
||||
cur = j;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return target.filter((o) => o !== null) as T[];
|
||||
};
|
||||
|
||||
export function mergeSetFilterCriteria(mutations: IMutationInfo[]) {
|
||||
return objectsShaker(mutations, (o1, o2) =>
|
||||
o1.id === SetSheetsFilterCriteriaMutation.id && o2.id === SetSheetsFilterCriteriaMutation.id
|
||||
&& (o1.params as ISetSheetsFilterCriteriaMutationParams).unitId === (o2.params as ISetSheetsFilterCriteriaMutationParams).unitId
|
||||
&& (o1.params as ISetSheetsFilterCriteriaMutationParams).subUnitId === (o2.params as ISetSheetsFilterCriteriaMutationParams).subUnitId
|
||||
&& (o1.params as ISetSheetsFilterCriteriaMutationParams).col === (o2.params as ISetSheetsFilterCriteriaMutationParams).col);
|
||||
}
|
||||
Reference in New Issue
Block a user