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:
629
packages/sheets-filter-ui/src/__tests__/data.ts
Normal file
629
packages/sheets-filter-ui/src/__tests__/data.ts
Normal file
@@ -0,0 +1,629 @@
|
||||
/**
|
||||
* 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 { LocaleType } from '@univerjs/core';
|
||||
import { CustomFilterOperator, SHEET_FILTER_SNAPSHOT_ID } from '@univerjs/sheets-filter';
|
||||
|
||||
/// This file contains mock up snapshots for testing purposes.
|
||||
|
||||
export function WithCustomFilterModelFactory(): IWorkbookData {
|
||||
return {
|
||||
id: 'test',
|
||||
appVersion: '3.0.0-alpha',
|
||||
sheets: {
|
||||
sheet1: {
|
||||
id: 'sheet1',
|
||||
cellData: {
|
||||
0: { 0: { v: 'Header' } },
|
||||
1: { 0: { v: 1 } },
|
||||
2: { 0: { v: 2 } },
|
||||
3: { 0: { v: 3 } },
|
||||
4: { 0: { v: 4 } },
|
||||
5: { 0: { v: 5 } },
|
||||
6: { 0: { v: 6 } },
|
||||
7: { 0: { v: 7 } },
|
||||
8: { 0: { v: 8 } },
|
||||
9: { 0: { v: 9 } },
|
||||
},
|
||||
},
|
||||
},
|
||||
resources: [
|
||||
{
|
||||
name: SHEET_FILTER_SNAPSHOT_ID,
|
||||
data: JSON.stringify({
|
||||
sheet1: {
|
||||
ref: { startColumn: 0, startRow: 0, endColumn: 5, endRow: 5 },
|
||||
filterColumns: [
|
||||
{
|
||||
colId: 0,
|
||||
customFilters: {
|
||||
customFilters: [{ val: 123, operator: CustomFilterOperator.GREATER_THAN }],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
}),
|
||||
},
|
||||
],
|
||||
locale: LocaleType.ZH_CN,
|
||||
name: '',
|
||||
sheetOrder: [
|
||||
'sheet1',
|
||||
],
|
||||
styles: {},
|
||||
};
|
||||
}
|
||||
|
||||
/* eslint-disable max-lines-per-function */
|
||||
export function WithValuesFilterModelFactory(): IWorkbookData {
|
||||
return {
|
||||
id: 'test',
|
||||
sheetOrder: [
|
||||
'sheet1',
|
||||
],
|
||||
name: '',
|
||||
appVersion: '3.0.0-alpha',
|
||||
locale: LocaleType.ZH_CN,
|
||||
styles: {
|
||||
GOAhZb: {
|
||||
n: {
|
||||
pattern: 'yyyy-mm-dd',
|
||||
},
|
||||
},
|
||||
'A-iqEg': {
|
||||
n: {
|
||||
pattern: 'yyyy/mm/dd',
|
||||
},
|
||||
},
|
||||
_yIXzW: {
|
||||
n: {
|
||||
pattern: 'yyyy-m-d am/pm h:mm',
|
||||
},
|
||||
},
|
||||
qLSh7Z: {
|
||||
n: {
|
||||
pattern: 'yyyy"年"MM"月"dd"日"',
|
||||
},
|
||||
},
|
||||
},
|
||||
sheets: {
|
||||
sheet1: {
|
||||
id: 'sheet1',
|
||||
cellData: {
|
||||
0: {
|
||||
0: {
|
||||
v: 'Column Header',
|
||||
},
|
||||
},
|
||||
1: {
|
||||
0: {
|
||||
v: '1',
|
||||
},
|
||||
},
|
||||
2: {
|
||||
0: {
|
||||
v: '2',
|
||||
},
|
||||
},
|
||||
3: {
|
||||
0: {
|
||||
v: 'Michael Jackson King of Pop',
|
||||
},
|
||||
},
|
||||
4: {
|
||||
0: {
|
||||
v: 'Michael',
|
||||
},
|
||||
},
|
||||
5: {
|
||||
0: {
|
||||
v: 'Jackson',
|
||||
},
|
||||
},
|
||||
6: {
|
||||
0: {
|
||||
v: 44876,
|
||||
t: 2,
|
||||
s: 'GOAhZb',
|
||||
},
|
||||
},
|
||||
7: {
|
||||
0: {
|
||||
v: 44877,
|
||||
t: 2,
|
||||
s: 'GOAhZb',
|
||||
},
|
||||
},
|
||||
8: {
|
||||
0: {
|
||||
v: 44878,
|
||||
t: 2,
|
||||
s: 'A-iqEg',
|
||||
},
|
||||
},
|
||||
9: {
|
||||
0: {
|
||||
v: 44879,
|
||||
t: 2,
|
||||
s: '_yIXzW',
|
||||
},
|
||||
},
|
||||
10: {
|
||||
0: {
|
||||
v: 44880,
|
||||
t: 2,
|
||||
s: 'qLSh7Z',
|
||||
},
|
||||
},
|
||||
},
|
||||
name: 'Sheet1',
|
||||
rowCount: 1000,
|
||||
columnCount: 20,
|
||||
mergeData: [],
|
||||
},
|
||||
},
|
||||
resources: [
|
||||
{
|
||||
name: 'SHEET_FILTER_PLUGIN',
|
||||
data: '{"sheet1":{"ref":{"startRow":0,"startColumn":0,"endRow":10,"endColumn":10},"filterColumns":[],"cachedFilteredOut":[]}}',
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
export function WithValuesAndEmptyFilterModelFactory(): IWorkbookData {
|
||||
return {
|
||||
id: 'test',
|
||||
appVersion: '3.0.0-alpha',
|
||||
sheets: {
|
||||
sheet1: {
|
||||
id: 'sheet1',
|
||||
cellData: {
|
||||
0: { 0: { v: 'Column Header' } }, // this row should not appear in filter panel
|
||||
1: { 0: { v: '1' } },
|
||||
2: { 0: { v: '2' } },
|
||||
3: { 0: { v: 'Michael Jackson King of Pop' } },
|
||||
4: { 0: { v: 'Michael' } },
|
||||
5: { 0: { v: 'Jackson' } },
|
||||
6: { 0: { v: 'Evan Wallace' } },
|
||||
7: { 0: { v: 'Evan' } },
|
||||
8: { 0: { v: 'Wallace' } },
|
||||
9: { 0: { v: 'Steve' } },
|
||||
10: { 0: { v: 'Minecraft' } },
|
||||
11: { 0: { v: '' } },
|
||||
},
|
||||
},
|
||||
},
|
||||
resources: [
|
||||
{
|
||||
name: SHEET_FILTER_SNAPSHOT_ID,
|
||||
data: JSON.stringify({
|
||||
sheet1: {
|
||||
ref: { startColumn: 0, startRow: 0, endColumn: 11, endRow: 11 },
|
||||
filterColumns: [
|
||||
{
|
||||
colId: 0,
|
||||
filters: { blank: true, filters: ['1'] },
|
||||
},
|
||||
],
|
||||
|
||||
},
|
||||
}),
|
||||
},
|
||||
],
|
||||
locale: LocaleType.ZH_CN,
|
||||
name: '',
|
||||
sheetOrder: [
|
||||
'sheet1',
|
||||
],
|
||||
styles: {},
|
||||
};
|
||||
}
|
||||
|
||||
export function WithMultiEmptyCellsModelFactory(): IWorkbookData {
|
||||
return {
|
||||
id: 'test',
|
||||
appVersion: '3.0.0-alpha',
|
||||
sheets: {
|
||||
sheet1: {
|
||||
id: 'sheet1',
|
||||
cellData: {
|
||||
0: { 0: { v: 'Column Header' } }, // this row should not appear in filter panel
|
||||
1: { 0: { v: '1' } },
|
||||
2: { 0: { v: '2' } },
|
||||
3: { 0: { v: 'Michael Jackson King of Pop' } },
|
||||
4: { 0: { v: 'Michael' } },
|
||||
5: { 0: { v: 'Jackson' } },
|
||||
6: { 0: { v: '' } },
|
||||
7: { 0: { v: '' } },
|
||||
8: { 0: { v: '' } },
|
||||
9: { 0: { v: 'Steve' } },
|
||||
10: { 0: { v: 'Minecraft' } },
|
||||
11: { 0: { v: '' } },
|
||||
},
|
||||
},
|
||||
},
|
||||
resources: [
|
||||
{
|
||||
name: SHEET_FILTER_SNAPSHOT_ID,
|
||||
data: JSON.stringify({
|
||||
sheet1: {
|
||||
ref: { startColumn: 0, startRow: 0, endColumn: 11, endRow: 11 },
|
||||
filterColumns: [
|
||||
{
|
||||
colId: 0,
|
||||
filters: { blank: true, filters: ['1'] },
|
||||
},
|
||||
],
|
||||
|
||||
},
|
||||
}),
|
||||
},
|
||||
],
|
||||
locale: LocaleType.ZH_CN,
|
||||
name: '',
|
||||
sheetOrder: [
|
||||
'sheet1',
|
||||
],
|
||||
styles: {},
|
||||
};
|
||||
}
|
||||
|
||||
export function WithTwoFilterColumnsFactory(): IWorkbookData {
|
||||
return {
|
||||
id: 'test',
|
||||
appVersion: '3.0.0-alpha',
|
||||
sheets: {
|
||||
sheet1: {
|
||||
id: 'sheet1',
|
||||
cellData: {
|
||||
0: { 0: { v: 'Column Header' }, 1: { v: 'Header 2' } }, // this row should not appear in filter panel
|
||||
1: { 0: { v: '1' }, 1: { v: 'a' } },
|
||||
2: { 0: { v: '2' }, 1: { v: 'b' } },
|
||||
3: { 0: { v: '3' }, 1: { v: 'c' } },
|
||||
4: { 0: { v: '4' }, 1: { v: 'd' } },
|
||||
5: { 0: { v: '5' }, 1: { v: 'e' } },
|
||||
6: { 0: { v: '6' }, 1: { v: 'f' } },
|
||||
7: { 0: { v: '7' }, 1: { v: 'g' } },
|
||||
8: { 0: { v: '8' }, 1: { v: 'h' } },
|
||||
},
|
||||
},
|
||||
},
|
||||
resources: [
|
||||
{
|
||||
name: SHEET_FILTER_SNAPSHOT_ID,
|
||||
data: JSON.stringify({
|
||||
sheet1: {
|
||||
ref: { startColumn: 0, startRow: 0, endColumn: 11, endRow: 11 },
|
||||
filterColumns: [
|
||||
{
|
||||
colId: 0,
|
||||
filters: { blank: true, filters: ['1', '2', '3'] },
|
||||
},
|
||||
],
|
||||
},
|
||||
}),
|
||||
},
|
||||
],
|
||||
locale: LocaleType.ZH_CN,
|
||||
name: '',
|
||||
sheetOrder: [
|
||||
'sheet1',
|
||||
],
|
||||
styles: {},
|
||||
};
|
||||
}
|
||||
|
||||
export function WithMergedCellFilterFactory(): IWorkbookData {
|
||||
return {
|
||||
id: 'test',
|
||||
appVersion: '3.0.0-alpha',
|
||||
sheets: {
|
||||
sheet1: {
|
||||
id: 'sheet1',
|
||||
cellData: {
|
||||
0: { 0: { v: 'Column Header' }, 1: { v: 'Header 2' } }, // this row should not appear in filter panel
|
||||
1: { 0: { v: '1' }, 1: { v: 'a' } },
|
||||
2: { 0: { v: '2' }, 1: { v: 'b' } },
|
||||
3: { 0: { v: '3' }, 1: { v: 'c' } },
|
||||
4: { 0: { v: '' }, 1: { v: '' } },
|
||||
5: { 0: { v: '5' }, 1: { v: 'e' } },
|
||||
6: { 0: { v: '6' }, 1: { v: 'f' } },
|
||||
7: { 0: { v: '7' }, 1: { v: 'g' } },
|
||||
8: { 0: { v: '8' }, 1: { v: 'h' } },
|
||||
},
|
||||
mergeData: [
|
||||
{ startRow: 3, endRow: 4, startColumn: 0, endColumn: 1 },
|
||||
],
|
||||
},
|
||||
},
|
||||
resources: [
|
||||
{
|
||||
name: SHEET_FILTER_SNAPSHOT_ID,
|
||||
data: JSON.stringify({
|
||||
sheet1: {
|
||||
ref: { startColumn: 0, startRow: 0, endColumn: 11, endRow: 11 },
|
||||
filterColumns: [
|
||||
{
|
||||
colId: 0,
|
||||
filters: { blank: true, filters: ['3', '4', '5'] },
|
||||
},
|
||||
],
|
||||
|
||||
},
|
||||
}),
|
||||
},
|
||||
],
|
||||
locale: LocaleType.ZH_CN,
|
||||
name: '',
|
||||
sheetOrder: [
|
||||
'sheet1',
|
||||
],
|
||||
styles: {},
|
||||
};
|
||||
}
|
||||
|
||||
export const ITEMS = [
|
||||
{
|
||||
title: '2022',
|
||||
key: '2022',
|
||||
children: [
|
||||
{
|
||||
title: 'sheets-filter-ui.date.11',
|
||||
key: '2022-11',
|
||||
children: [
|
||||
{
|
||||
title: '11',
|
||||
key: '2022-11-11',
|
||||
count: 1,
|
||||
originValues: new Set(['44876']),
|
||||
leaf: true,
|
||||
checked: true,
|
||||
},
|
||||
{
|
||||
title: '12',
|
||||
key: '2022-11-12',
|
||||
count: 1,
|
||||
originValues: new Set(['44877']),
|
||||
leaf: true,
|
||||
checked: true,
|
||||
},
|
||||
{
|
||||
title: '13',
|
||||
key: '2022-11-13',
|
||||
count: 1,
|
||||
originValues: new Set(['44878']),
|
||||
leaf: true,
|
||||
checked: true,
|
||||
},
|
||||
{
|
||||
title: '14',
|
||||
key: '2022-11-14',
|
||||
count: 1,
|
||||
originValues: new Set(['44879']),
|
||||
leaf: true,
|
||||
checked: true,
|
||||
},
|
||||
{
|
||||
title: '15',
|
||||
key: '2022-11-15',
|
||||
count: 1,
|
||||
originValues: new Set(['44880']),
|
||||
leaf: true,
|
||||
checked: true,
|
||||
},
|
||||
],
|
||||
count: 5,
|
||||
leaf: false,
|
||||
checked: false,
|
||||
},
|
||||
],
|
||||
count: 5,
|
||||
leaf: false,
|
||||
checked: false,
|
||||
},
|
||||
{
|
||||
title: '1',
|
||||
leaf: true,
|
||||
checked: true,
|
||||
key: '1',
|
||||
count: 1,
|
||||
},
|
||||
{
|
||||
title: '2',
|
||||
leaf: true,
|
||||
checked: true,
|
||||
key: '2',
|
||||
count: 1,
|
||||
},
|
||||
{
|
||||
title: 'Jackson',
|
||||
leaf: true,
|
||||
checked: true,
|
||||
key: 'Jackson',
|
||||
count: 1,
|
||||
},
|
||||
{
|
||||
title: 'Michael',
|
||||
leaf: true,
|
||||
checked: true,
|
||||
key: 'Michael',
|
||||
count: 1,
|
||||
},
|
||||
{
|
||||
title: 'Michael Jackson King of Pop',
|
||||
leaf: true,
|
||||
checked: true,
|
||||
key: 'Michael Jackson King of Pop',
|
||||
count: 1,
|
||||
},
|
||||
];
|
||||
|
||||
export const ITEMS_WITH_EMPTY = [
|
||||
{
|
||||
title: '1',
|
||||
leaf: true,
|
||||
checked: true,
|
||||
key: '1',
|
||||
count: 1,
|
||||
},
|
||||
{
|
||||
title: '2',
|
||||
leaf: true,
|
||||
checked: false,
|
||||
key: '2',
|
||||
count: 1,
|
||||
},
|
||||
{
|
||||
title: 'Evan',
|
||||
leaf: true,
|
||||
checked: false,
|
||||
key: 'Evan',
|
||||
count: 1,
|
||||
},
|
||||
{
|
||||
title: 'Evan Wallace',
|
||||
leaf: true,
|
||||
checked: false,
|
||||
key: 'Evan Wallace',
|
||||
count: 1,
|
||||
},
|
||||
{
|
||||
title: 'Jackson',
|
||||
leaf: true,
|
||||
checked: false,
|
||||
key: 'Jackson',
|
||||
count: 1,
|
||||
},
|
||||
{
|
||||
title: 'Michael',
|
||||
leaf: true,
|
||||
checked: false,
|
||||
key: 'Michael',
|
||||
count: 1,
|
||||
},
|
||||
{
|
||||
title: 'Michael Jackson King of Pop',
|
||||
leaf: true,
|
||||
checked: false,
|
||||
key: 'Michael Jackson King of Pop',
|
||||
count: 1,
|
||||
},
|
||||
{
|
||||
title: 'Minecraft',
|
||||
leaf: true,
|
||||
checked: false,
|
||||
key: 'Minecraft',
|
||||
count: 1,
|
||||
},
|
||||
{
|
||||
title: 'sheets-filter-ui.panel.empty',
|
||||
count: 1,
|
||||
leaf: true,
|
||||
checked: true,
|
||||
key: 'empty',
|
||||
},
|
||||
{
|
||||
title: 'Steve',
|
||||
leaf: true,
|
||||
checked: false,
|
||||
key: 'Steve',
|
||||
count: 1,
|
||||
},
|
||||
{
|
||||
title: 'Wallace',
|
||||
leaf: true,
|
||||
checked: false,
|
||||
key: 'Wallace',
|
||||
count: 1,
|
||||
},
|
||||
];
|
||||
|
||||
export const E_ITEMS = [
|
||||
{
|
||||
title: '2022',
|
||||
key: '2022',
|
||||
children: [
|
||||
{
|
||||
title: 'sheets-filter-ui.date.11',
|
||||
key: '2022-11',
|
||||
children: [
|
||||
{
|
||||
title: '11',
|
||||
key: '2022-11-11',
|
||||
count: 1,
|
||||
originValues: new Set(['44876']),
|
||||
leaf: true,
|
||||
checked: true,
|
||||
},
|
||||
{
|
||||
title: '12',
|
||||
key: '2022-11-12',
|
||||
count: 1,
|
||||
originValues: new Set(['44877']),
|
||||
leaf: true,
|
||||
checked: true,
|
||||
},
|
||||
{
|
||||
title: '13',
|
||||
key: '2022-11-13',
|
||||
count: 1,
|
||||
originValues: new Set(['44878']),
|
||||
leaf: true,
|
||||
checked: true,
|
||||
},
|
||||
{
|
||||
title: '14',
|
||||
key: '2022-11-14',
|
||||
count: 1,
|
||||
originValues: new Set(['44879']),
|
||||
leaf: true,
|
||||
checked: true,
|
||||
},
|
||||
{
|
||||
title: '15',
|
||||
key: '2022-11-15',
|
||||
count: 1,
|
||||
originValues: new Set(['44880']),
|
||||
leaf: true,
|
||||
checked: true,
|
||||
},
|
||||
],
|
||||
count: 5,
|
||||
leaf: false,
|
||||
checked: false,
|
||||
},
|
||||
],
|
||||
count: 5,
|
||||
leaf: false,
|
||||
checked: false,
|
||||
},
|
||||
{
|
||||
title: 'Michael',
|
||||
leaf: true,
|
||||
checked: true,
|
||||
key: 'Michael',
|
||||
count: 1,
|
||||
},
|
||||
{
|
||||
title: 'Michael Jackson King of Pop',
|
||||
leaf: true,
|
||||
checked: true,
|
||||
key: 'Michael Jackson King of Pop',
|
||||
count: 1,
|
||||
},
|
||||
];
|
||||
@@ -0,0 +1,352 @@
|
||||
/**
|
||||
* 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 { IDisposable, IRange, IWorkbookData, Workbook } from '@univerjs/core';
|
||||
import type { ISetRangeValuesCommandParams } from '@univerjs/sheets';
|
||||
import {
|
||||
ICommandService,
|
||||
Inject,
|
||||
Injector,
|
||||
IUniverInstanceService,
|
||||
LocaleService,
|
||||
LocaleType,
|
||||
Plugin,
|
||||
RANGE_TYPE,
|
||||
RedoCommand,
|
||||
toDisposable,
|
||||
UndoCommand,
|
||||
Univer,
|
||||
UniverInstanceType,
|
||||
} from '@univerjs/core';
|
||||
import { SetRangeValuesCommand, SheetsSelectionsService } from '@univerjs/sheets';
|
||||
import {
|
||||
ClearSheetsFilterCriteriaCommand,
|
||||
ReCalcSheetsFilterCommand,
|
||||
SetSheetsFilterCriteriaCommand,
|
||||
SetSheetsFilterRangeMutation,
|
||||
SheetsFilterService,
|
||||
SmartToggleSheetsFilterCommand,
|
||||
UniverSheetsFilterPlugin,
|
||||
} from '@univerjs/sheets-filter';
|
||||
import { IMessageService } from '@univerjs/ui';
|
||||
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
|
||||
|
||||
class MockMessageService {
|
||||
show(): IDisposable {
|
||||
return toDisposable(() => undefined);
|
||||
}
|
||||
|
||||
remove(): void {}
|
||||
|
||||
removeAll(): void {}
|
||||
|
||||
setContainer(): void {}
|
||||
|
||||
getContainer(): undefined {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
function testWorkbookDataFactory(): IWorkbookData {
|
||||
return {
|
||||
id: 'test',
|
||||
appVersion: '3.0.0-alpha',
|
||||
sheets: {
|
||||
sheet1: {
|
||||
id: 'sheet1',
|
||||
cellData: {
|
||||
0: {
|
||||
0: {
|
||||
v: 'A1',
|
||||
},
|
||||
},
|
||||
1: {
|
||||
0: {
|
||||
v: '1',
|
||||
},
|
||||
1: {
|
||||
v: 'a',
|
||||
},
|
||||
},
|
||||
2: {
|
||||
0: {
|
||||
v: '2',
|
||||
},
|
||||
1: {
|
||||
v: 'b',
|
||||
},
|
||||
},
|
||||
3: {
|
||||
0: {
|
||||
v: '3',
|
||||
},
|
||||
1: {
|
||||
v: 'c',
|
||||
},
|
||||
},
|
||||
},
|
||||
name: 'Sheet-001',
|
||||
},
|
||||
},
|
||||
locale: LocaleType.ZH_CN,
|
||||
name: '',
|
||||
sheetOrder: [],
|
||||
styles: {},
|
||||
};
|
||||
};
|
||||
|
||||
function createFilterCommandTestBed() {
|
||||
const univer = new Univer();
|
||||
const injector = univer.__getInjector();
|
||||
const get = injector.get.bind(injector);
|
||||
|
||||
class SheetsFilterCommandTestPlugin extends Plugin {
|
||||
static override pluginName = 'sheets-filter-command-test';
|
||||
static override type = UniverInstanceType.UNIVER_SHEET;
|
||||
|
||||
constructor(_config: unknown, @Inject(Injector) protected readonly _injector: Injector) {
|
||||
super();
|
||||
}
|
||||
|
||||
override onStarting(): void {
|
||||
this._injector.add([IMessageService, { useClass: MockMessageService }]);
|
||||
}
|
||||
}
|
||||
|
||||
univer.registerPlugin(UniverSheetsFilterPlugin);
|
||||
univer.registerPlugin(SheetsFilterCommandTestPlugin);
|
||||
|
||||
get(LocaleService).load({});
|
||||
univer.createUnit<IWorkbookData, Workbook>(UniverInstanceType.UNIVER_SHEET, testWorkbookDataFactory());
|
||||
|
||||
get(ICommandService).registerCommand(SmartToggleSheetsFilterCommand);
|
||||
|
||||
const univerInstanceService = get(IUniverInstanceService);
|
||||
univerInstanceService.focusUnit('test');
|
||||
|
||||
return { univer, get };
|
||||
}
|
||||
|
||||
describe('test sheets filter commands', () => {
|
||||
let get: Injector['get'];
|
||||
let commandService: ICommandService;
|
||||
let sheetsFilterService: SheetsFilterService;
|
||||
let selectionManagerService: SheetsSelectionsService;
|
||||
|
||||
beforeEach(() => {
|
||||
const testBed = createFilterCommandTestBed();
|
||||
get = testBed.get;
|
||||
|
||||
commandService = get(ICommandService);
|
||||
sheetsFilterService = get(SheetsFilterService);
|
||||
selectionManagerService = get(SheetsSelectionsService);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
});
|
||||
|
||||
function select(range: IRange) {
|
||||
const { startColumn, startRow, endColumn, endRow } = range;
|
||||
selectionManagerService.addSelections([
|
||||
{
|
||||
range: { startRow, startColumn, endColumn, endRow, rangeType: RANGE_TYPE.NORMAL },
|
||||
primary: {
|
||||
startRow,
|
||||
startColumn,
|
||||
endColumn,
|
||||
endRow,
|
||||
actualRow: startRow,
|
||||
actualColumn: startColumn,
|
||||
isMerged: false,
|
||||
isMergedMainCell: false,
|
||||
},
|
||||
style: null,
|
||||
},
|
||||
]);
|
||||
}
|
||||
|
||||
function getFilterModel() {
|
||||
return sheetsFilterService.getFilterModel('test', 'sheet1')!;
|
||||
}
|
||||
|
||||
describe('test "SmartToggleSheetsFilterCommand"', () => {
|
||||
it('should remove filter when there is one', async () => {
|
||||
expect(commandService.syncExecuteCommand(SetSheetsFilterRangeMutation.id, {
|
||||
unitId: 'test',
|
||||
subUnitId: 'sheet1',
|
||||
range: { startRow: 0, startColumn: 0, endRow: 5, endColumn: 5 },
|
||||
}));
|
||||
expect(getFilterModel()).toBeTruthy();
|
||||
|
||||
expect(await commandService.executeCommand(SmartToggleSheetsFilterCommand.id)).toBeTruthy();
|
||||
expect(getFilterModel()).toBeNull();
|
||||
|
||||
expect(await commandService.executeCommand(UndoCommand.id)).toBeTruthy();
|
||||
const filterModel = getFilterModel();
|
||||
expect(filterModel).toBeTruthy();
|
||||
expect(filterModel!.getRange()).toEqual({ startRow: 0, startColumn: 0, endRow: 5, endColumn: 5 });
|
||||
|
||||
expect(await commandService.executeCommand(RedoCommand.id)).toBeTruthy();
|
||||
expect(getFilterModel()).toBeNull();
|
||||
});
|
||||
|
||||
it('should add filter when there is no filter', async () => {
|
||||
expect(getFilterModel()).toBeNull();
|
||||
|
||||
// since there is no selection, it should return false
|
||||
expect(await commandService.executeCommand(SmartToggleSheetsFilterCommand.id)).toBeFalsy();
|
||||
|
||||
select({ startRow: 0, startColumn: 0, endRow: 5, endColumn: 5 });
|
||||
expect(await commandService.executeCommand(SmartToggleSheetsFilterCommand.id)).toBeTruthy();
|
||||
expect(getFilterModel()!.getRange()).toEqual({
|
||||
startRow: 0,
|
||||
startColumn: 0,
|
||||
endRow: 5,
|
||||
endColumn: 5,
|
||||
rangeType: RANGE_TYPE.NORMAL,
|
||||
});
|
||||
|
||||
expect(await commandService.executeCommand(UndoCommand.id)).toBeTruthy();
|
||||
expect(getFilterModel()).toBeNull();
|
||||
|
||||
expect(await commandService.executeCommand(RedoCommand.id)).toBeTruthy();
|
||||
expect(getFilterModel()).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
||||
describe('test "SetSheetsFilterCriteriaCommand"', () => {
|
||||
it('should return false when there is not filter', async () => {
|
||||
expect(await commandService.executeCommand(SetSheetsFilterCriteriaCommand.id, {
|
||||
unitId: 'test',
|
||||
subUnitId: 'sheet1',
|
||||
col: 0,
|
||||
criteria: null,
|
||||
})).toBeFalsy();
|
||||
});
|
||||
|
||||
it('should set filter criteria works', async () => {
|
||||
select({ startRow: 0, startColumn: 0, endRow: 5, endColumn: 5 });
|
||||
expect(await commandService.executeCommand(SmartToggleSheetsFilterCommand.id)).toBeTruthy();
|
||||
|
||||
// set filter criteria
|
||||
expect(await commandService.executeCommand(SetSheetsFilterCriteriaCommand.id, {
|
||||
unitId: 'test',
|
||||
subUnitId: 'sheet1',
|
||||
col: 0,
|
||||
criteria: {
|
||||
filters: { filters: ['1'] },
|
||||
},
|
||||
})).toBeTruthy();
|
||||
expect(getFilterModel()!.filteredOutRows).toEqual(new Set([2, 3, 4, 5]));
|
||||
|
||||
expect(await commandService.executeCommand(UndoCommand.id)).toBeTruthy();
|
||||
expect(getFilterModel()!.filteredOutRows).toEqual(new Set());
|
||||
|
||||
expect(await commandService.executeCommand(RedoCommand.id)).toBeTruthy();
|
||||
expect(getFilterModel()!.filteredOutRows).toEqual(new Set([2, 3, 4, 5]));
|
||||
|
||||
// manually set criteria to null
|
||||
expect(await commandService.executeCommand(SetSheetsFilterCriteriaCommand.id, {
|
||||
unitId: 'test',
|
||||
subUnitId: 'sheet1',
|
||||
col: 0,
|
||||
criteria: null,
|
||||
})).toBeTruthy();
|
||||
expect(getFilterModel()!.filteredOutRows).toEqual(new Set());
|
||||
|
||||
expect(await commandService.executeCommand(UndoCommand.id)).toBeTruthy();
|
||||
expect(getFilterModel()!.filteredOutRows).toEqual(new Set([2, 3, 4, 5]));
|
||||
|
||||
expect(await commandService.executeCommand(RedoCommand.id)).toBeTruthy();
|
||||
expect(getFilterModel()!.filteredOutRows).toEqual(new Set());
|
||||
});
|
||||
});
|
||||
|
||||
describe('test "ClearSheetsFilterConditionsCommand"', () => {
|
||||
it('should clear all filter criteria', async () => {
|
||||
select({ startRow: 0, startColumn: 0, endRow: 5, endColumn: 5 });
|
||||
expect(await commandService.executeCommand(SmartToggleSheetsFilterCommand.id)).toBeTruthy();
|
||||
|
||||
expect(await commandService.executeCommand(SetSheetsFilterCriteriaCommand.id, {
|
||||
unitId: 'test',
|
||||
subUnitId: 'sheet1',
|
||||
col: 0,
|
||||
criteria: {
|
||||
filters: { filters: ['1'] },
|
||||
},
|
||||
})).toBeTruthy();
|
||||
expect(await commandService.executeCommand(SetSheetsFilterCriteriaCommand.id, {
|
||||
unitId: 'test',
|
||||
subUnitId: 'sheet1',
|
||||
col: 1,
|
||||
criteria: {
|
||||
filters: { filters: ['b'] },
|
||||
},
|
||||
})).toBeTruthy();
|
||||
expect(getFilterModel()!.filteredOutRows).toEqual(new Set([1, 2, 3, 4, 5]));
|
||||
|
||||
expect(await commandService.executeCommand(ClearSheetsFilterCriteriaCommand.id, {
|
||||
unitId: 'test',
|
||||
subUnitId: 'sheet1',
|
||||
})).toBeTruthy();
|
||||
expect(getFilterModel()!.filteredOutRows).toEqual(new Set());
|
||||
|
||||
expect(await commandService.executeCommand(UndoCommand.id)).toBeTruthy();
|
||||
expect(getFilterModel()!.filteredOutRows).toEqual(new Set([1, 2, 3, 4, 5]));
|
||||
|
||||
expect(await commandService.executeCommand(RedoCommand.id)).toBeTruthy();
|
||||
expect(getFilterModel()!.filteredOutRows).toEqual(new Set());
|
||||
});
|
||||
});
|
||||
|
||||
describe('test "ReCalcSheetsFilterConditionsCommand"', () => {
|
||||
it('should reCalc after set range values and get correct result', async () => {
|
||||
select({ startRow: 0, startColumn: 0, endRow: 5, endColumn: 5 });
|
||||
expect(await commandService.executeCommand(SmartToggleSheetsFilterCommand.id)).toBeTruthy();
|
||||
|
||||
expect(await commandService.executeCommand(SetSheetsFilterCriteriaCommand.id, {
|
||||
unitId: 'test',
|
||||
subUnitId: 'sheet1',
|
||||
col: 0,
|
||||
criteria: {
|
||||
filters: { filters: ['1'] },
|
||||
},
|
||||
})).toBeTruthy();
|
||||
expect(await commandService.executeCommand(SetSheetsFilterCriteriaCommand.id, {
|
||||
unitId: 'test',
|
||||
subUnitId: 'sheet1',
|
||||
col: 1,
|
||||
criteria: {
|
||||
filters: { filters: ['b'] },
|
||||
},
|
||||
})).toBeTruthy();
|
||||
expect(getFilterModel()!.filteredOutRows).toEqual(new Set([1, 2, 3, 4, 5]));
|
||||
|
||||
expect(await commandService.executeCommand(SetRangeValuesCommand.id, {
|
||||
unitId: 'test',
|
||||
subUnitId: 'sheet1',
|
||||
value: { 1: { 1: { v: 'b' } } },
|
||||
} as ISetRangeValuesCommandParams)).toBeTruthy();
|
||||
expect(getFilterModel()!.filteredOutRows).toEqual(new Set([1, 2, 3, 4, 5]));
|
||||
|
||||
expect(await commandService.executeCommand(ReCalcSheetsFilterCommand.id, {
|
||||
unitId: 'test',
|
||||
subUnitId: 'sheet1',
|
||||
})).toBeTruthy();
|
||||
expect(getFilterModel()!.filteredOutRows).toEqual(new Set([2, 3, 4, 5]));
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,205 @@
|
||||
/**
|
||||
* 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 { IOperation, IWorkbookData, Workbook } from '@univerjs/core';
|
||||
import type { ISetSheetsFilterRangeMutationParams } from '@univerjs/sheets-filter';
|
||||
import type { IEditorBridgeServiceVisibleParam } from '@univerjs/sheets-ui';
|
||||
import {
|
||||
CommandType,
|
||||
ICommandService,
|
||||
IContextService,
|
||||
Inject,
|
||||
Injector,
|
||||
LocaleService,
|
||||
LocaleType,
|
||||
Plugin,
|
||||
Univer,
|
||||
UniverInstanceType,
|
||||
} from '@univerjs/core';
|
||||
import { FilterBy, SetSheetsFilterRangeMutation, UniverSheetsFilterPlugin } from '@univerjs/sheets-filter';
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
import { ISheetsFilterPanelService, SheetsFilterPanelService } from '../../../services/sheets-filter-panel.service';
|
||||
import {
|
||||
ChangeFilterByOperation,
|
||||
CloseFilterPanelOperation,
|
||||
FILTER_PANEL_OPENED_KEY,
|
||||
OpenFilterPanelOperation,
|
||||
} from '../sheets-filter.operation';
|
||||
|
||||
const SetCellEditVisibleOperation: IOperation<IEditorBridgeServiceVisibleParam> = {
|
||||
id: 'sheet.operation.set-cell-edit-visible',
|
||||
type: CommandType.OPERATION,
|
||||
handler: () => {
|
||||
return true;
|
||||
},
|
||||
};
|
||||
|
||||
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 createFilterOperationTestBed() {
|
||||
const univer = new Univer();
|
||||
const injector = univer.__getInjector();
|
||||
const get = injector.get.bind(injector);
|
||||
|
||||
class SheetsFilterOperationTestPlugin extends Plugin {
|
||||
static override type = UniverInstanceType.UNIVER_SHEET;
|
||||
static override pluginName = 'sheets-filter-operation-test';
|
||||
|
||||
constructor(_config: unknown, @Inject(Injector) protected readonly _injector: Injector) {
|
||||
super();
|
||||
}
|
||||
|
||||
override onStarting(): void {
|
||||
this._injector.add([ISheetsFilterPanelService, { useClass: SheetsFilterPanelService }]);
|
||||
}
|
||||
}
|
||||
|
||||
univer.registerPlugin(UniverSheetsFilterPlugin);
|
||||
univer.registerPlugin(SheetsFilterOperationTestPlugin);
|
||||
|
||||
univer.createUnit<IWorkbookData, Workbook>(UniverInstanceType.UNIVER_SHEET, testWorkbookDataFactory());
|
||||
|
||||
get(LocaleService).load({});
|
||||
|
||||
const commandService = get(ICommandService);
|
||||
|
||||
[
|
||||
OpenFilterPanelOperation,
|
||||
CloseFilterPanelOperation,
|
||||
ChangeFilterByOperation,
|
||||
SetCellEditVisibleOperation,
|
||||
].forEach((command) => commandService.registerCommand(command));
|
||||
|
||||
return { univer, get };
|
||||
}
|
||||
|
||||
describe('test sheets filter ui operations', () => {
|
||||
let univer: Univer;
|
||||
let get: Injector['get'];
|
||||
let contextService: IContextService;
|
||||
let commandService: ICommandService;
|
||||
let sheetsFilterPanelService: ISheetsFilterPanelService;
|
||||
|
||||
beforeEach(() => {
|
||||
const testBed = createFilterOperationTestBed();
|
||||
univer = testBed.univer;
|
||||
get = testBed.get;
|
||||
|
||||
contextService = get(IContextService);
|
||||
commandService = get(ICommandService);
|
||||
sheetsFilterPanelService = get(ISheetsFilterPanelService);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
univer.dispose();
|
||||
});
|
||||
|
||||
describe('test "OpenFilerPanelOperation"', () => {
|
||||
it('should return false when the filter model is not found', () => {
|
||||
const spy = vi.spyOn(sheetsFilterPanelService, 'setupCol');
|
||||
expect(commandService.syncExecuteCommand(OpenFilterPanelOperation.id, {
|
||||
unitId: 'test',
|
||||
subUnitId: 'sheet1',
|
||||
col: 0,
|
||||
})).toBeFalsy();
|
||||
expect(spy).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should open the filter panel and setup the filter model on the specific column', () => {
|
||||
const spy = vi.spyOn(sheetsFilterPanelService, 'setupCol');
|
||||
expect(commandService.syncExecuteCommand(SetSheetsFilterRangeMutation.id, {
|
||||
unitId: 'test',
|
||||
subUnitId: 'sheet1',
|
||||
range: { startRow: 0, startColumn: 0, endRow: 5, endColumn: 5 },
|
||||
} as ISetSheetsFilterRangeMutationParams)).toBeTruthy();
|
||||
expect(commandService.syncExecuteCommand(OpenFilterPanelOperation.id, {
|
||||
unitId: 'test',
|
||||
subUnitId: 'sheet1',
|
||||
col: 0,
|
||||
})).toBeTruthy();
|
||||
expect(spy).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('test "CloseFilterPanelOperation"', () => {
|
||||
it('should return false when the filter panel is not activated', () => {
|
||||
const spy = vi.spyOn(sheetsFilterPanelService, 'terminate');
|
||||
contextService.setContextValue(FILTER_PANEL_OPENED_KEY, false);
|
||||
expect(commandService.syncExecuteCommand(CloseFilterPanelOperation.id)).toBeFalsy();
|
||||
expect(spy).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should call sheets filter panel service to close the panel is activated', () => {
|
||||
const spy = vi.spyOn(sheetsFilterPanelService, 'terminate');
|
||||
contextService.setContextValue(FILTER_PANEL_OPENED_KEY, true);
|
||||
expect(commandService.syncExecuteCommand(CloseFilterPanelOperation.id)).toBeTruthy();
|
||||
expect(contextService.getContextValue(FILTER_PANEL_OPENED_KEY)).toBeFalsy();
|
||||
expect(spy).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('test "ChangeFilterByOperation"', () => {
|
||||
it('should switch the active filter panel to condition filtering', () => {
|
||||
expect(commandService.syncExecuteCommand(SetSheetsFilterRangeMutation.id, {
|
||||
unitId: 'test',
|
||||
subUnitId: 'sheet1',
|
||||
range: { startRow: 0, startColumn: 0, endRow: 5, endColumn: 5 },
|
||||
} as ISetSheetsFilterRangeMutationParams)).toBeTruthy();
|
||||
expect(commandService.syncExecuteCommand(OpenFilterPanelOperation.id, {
|
||||
unitId: 'test',
|
||||
subUnitId: 'sheet1',
|
||||
col: 0,
|
||||
})).toBeTruthy();
|
||||
|
||||
expect(commandService.syncExecuteCommand(ChangeFilterByOperation.id, {
|
||||
filterBy: FilterBy.CONDITIONS,
|
||||
})).toBe(true);
|
||||
expect(sheetsFilterPanelService.filterBy).toBe(FilterBy.CONDITIONS);
|
||||
});
|
||||
|
||||
it('should not switch filter type before a filter panel column is prepared', () => {
|
||||
expect(commandService.syncExecuteCommand(ChangeFilterByOperation.id, {
|
||||
filterBy: FilterBy.CONDITIONS,
|
||||
})).toBe(false);
|
||||
expect(sheetsFilterPanelService.filterBy).toBe(FilterBy.VALUES);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,96 @@
|
||||
/**
|
||||
* 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 { IOperation } from '@univerjs/core';
|
||||
import type { FilterBy } from '@univerjs/sheets-filter';
|
||||
import { CommandType, ICommandService, IContextService, Quantity } from '@univerjs/core';
|
||||
import { SheetsFilterService } from '@univerjs/sheets-filter';
|
||||
import { IEditorBridgeService, SetCellEditVisibleOperation } from '@univerjs/sheets-ui';
|
||||
import { ILayoutService } from '@univerjs/ui';
|
||||
import { ISheetsFilterPanelService } from '../../services/sheets-filter-panel.service';
|
||||
|
||||
export const FILTER_PANEL_OPENED_KEY = 'FILTER_PANEL_OPENED';
|
||||
|
||||
export interface IOpenFilterPanelOperationParams {
|
||||
unitId: string;
|
||||
subUnitId: string;
|
||||
|
||||
col: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* The operation to open the filter panel and prepare for changing the filter conditions on a given column.
|
||||
*/
|
||||
export const OpenFilterPanelOperation: IOperation<IOpenFilterPanelOperationParams> = {
|
||||
id: 'sheet.operation.open-filter-panel',
|
||||
type: CommandType.OPERATION,
|
||||
handler: (accessor, params: IOpenFilterPanelOperationParams) => {
|
||||
const contextService = accessor.get(IContextService);
|
||||
const sheetsFilterService = accessor.get(SheetsFilterService);
|
||||
const sheetsFilterPanelService = accessor.get(ISheetsFilterPanelService);
|
||||
const commandService = accessor.get(ICommandService);
|
||||
|
||||
// Close the cell edit if it is opened.
|
||||
const editorBridgeService = accessor.has(IEditorBridgeService) ? accessor.get(IEditorBridgeService) : null;
|
||||
if (editorBridgeService?.isVisible().visible) {
|
||||
commandService.syncExecuteCommand(SetCellEditVisibleOperation.id, { visible: false });
|
||||
}
|
||||
|
||||
const { unitId, subUnitId, col } = params;
|
||||
const filterModel = sheetsFilterService.getFilterModel(unitId, subUnitId);
|
||||
if (!filterModel) return false;
|
||||
|
||||
sheetsFilterPanelService.setupCol(filterModel, col);
|
||||
if (!contextService.getContextValue(FILTER_PANEL_OPENED_KEY)) {
|
||||
contextService.setContextValue(FILTER_PANEL_OPENED_KEY, true);
|
||||
}
|
||||
|
||||
// Set the filter condition to the filter ui service on the specific column.
|
||||
return true;
|
||||
},
|
||||
};
|
||||
|
||||
export const CloseFilterPanelOperation: IOperation = {
|
||||
id: 'sheet.operation.close-filter-panel',
|
||||
type: CommandType.OPERATION,
|
||||
handler: (accessor) => {
|
||||
const contextService = accessor.get(IContextService);
|
||||
const sheetsFilterPanelService = accessor.get(ISheetsFilterPanelService);
|
||||
const layoutService = accessor.get(ILayoutService, Quantity.OPTIONAL);
|
||||
|
||||
if (contextService.getContextValue(FILTER_PANEL_OPENED_KEY)) {
|
||||
contextService.setContextValue(FILTER_PANEL_OPENED_KEY, false);
|
||||
layoutService?.focus();
|
||||
|
||||
return sheetsFilterPanelService.terminate();
|
||||
}
|
||||
|
||||
return false;
|
||||
},
|
||||
};
|
||||
|
||||
export interface IChangeFilterByOperationParams {
|
||||
filterBy: FilterBy;
|
||||
}
|
||||
export const ChangeFilterByOperation: IOperation<IChangeFilterByOperationParams> = {
|
||||
id: 'sheet.operation.apply-filter',
|
||||
type: CommandType.OPERATION,
|
||||
handler: (accessor, params) => {
|
||||
const { filterBy } = params!;
|
||||
const sheetsFilterPanelService = accessor.get(ISheetsFilterPanelService);
|
||||
return sheetsFilterPanelService.changeFilterBy(filterBy);
|
||||
},
|
||||
};
|
||||
28
packages/sheets-filter-ui/src/config/config.ts
Normal file
28
packages/sheets-filter-ui/src/config/config.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.
|
||||
*/
|
||||
|
||||
import type { MenuConfig } from '@univerjs/ui';
|
||||
|
||||
export const SHEETS_FILTER_UI_PLUGIN_CONFIG_KEY = 'sheets-filter-ui.config';
|
||||
|
||||
export const configSymbol = Symbol(SHEETS_FILTER_UI_PLUGIN_CONFIG_KEY);
|
||||
|
||||
export interface IUniverSheetsFilterUIConfig {
|
||||
menu?: MenuConfig;
|
||||
useRemoteFilterValuesGenerator?: boolean;
|
||||
}
|
||||
|
||||
export const defaultPluginConfig: IUniverSheetsFilterUIConfig = {};
|
||||
@@ -0,0 +1,48 @@
|
||||
/**
|
||||
* Copyright 2023-present DreamNum Co., Ltd.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { Disposable, Inject } from '@univerjs/core';
|
||||
import { FilterIcon } from '@univerjs/icons';
|
||||
import { ComponentManager, IconManager } from '@univerjs/ui';
|
||||
import { FILTER_PANEL_POPUP_KEY, FilterPanel } from '../views/components/SheetsFilterPanel';
|
||||
|
||||
export class ComponentsController extends Disposable {
|
||||
constructor(
|
||||
@Inject(ComponentManager) private readonly _componentManager: ComponentManager,
|
||||
@Inject(IconManager) private readonly _iconManager: IconManager
|
||||
) {
|
||||
super();
|
||||
|
||||
this._registerIcons();
|
||||
this._registerComponents();
|
||||
}
|
||||
|
||||
private _registerIcons(): void {
|
||||
this.disposeWithMe(this._iconManager.register({
|
||||
FilterIcon,
|
||||
}));
|
||||
}
|
||||
|
||||
private _registerComponents(): void {
|
||||
([
|
||||
[FILTER_PANEL_POPUP_KEY, FilterPanel],
|
||||
] as const).forEach(([key, comp]) => {
|
||||
this.disposeWithMe(
|
||||
this._componentManager.register(key, comp)
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
/**
|
||||
* Copyright 2023-present DreamNum Co., Ltd.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { Disposable, ICommandService } from '@univerjs/core';
|
||||
import { SmartToggleSheetsFilterCommand } from '@univerjs/sheets-filter';
|
||||
import { IMenuManagerService } from '@univerjs/ui';
|
||||
import { menuSchema } from '../menu/schema';
|
||||
|
||||
export class SheetsFilterMobileMenuController extends Disposable {
|
||||
constructor(
|
||||
@ICommandService commandService: ICommandService,
|
||||
@IMenuManagerService menuManagerService: IMenuManagerService
|
||||
) {
|
||||
super();
|
||||
|
||||
this.disposeWithMe(commandService.registerCommand(SmartToggleSheetsFilterCommand));
|
||||
menuManagerService.mergeMenu(menuSchema);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
/**
|
||||
* Copyright 2023-present DreamNum Co., Ltd.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { Disposable, Inject } from '@univerjs/core';
|
||||
import { FilterIcon } from '@univerjs/icons';
|
||||
import { IconManager } from '@univerjs/ui';
|
||||
|
||||
export class MobileComponentsController extends Disposable {
|
||||
constructor(@Inject(IconManager) iconManager: IconManager) {
|
||||
super();
|
||||
|
||||
this.disposeWithMe(iconManager.register({ FilterIcon }));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
/**
|
||||
* 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 { ICommandService, Inject, Injector, LocaleService, RxDisposable, UniverInstanceType } from '@univerjs/core';
|
||||
import { MessageType } from '@univerjs/design';
|
||||
import { IRenderManagerService } from '@univerjs/engine-render';
|
||||
import {
|
||||
ReCalcSheetsFilterMutation,
|
||||
RemoveSheetsFilterMutation,
|
||||
SetSheetsFilterCriteriaMutation,
|
||||
SetSheetsFilterRangeMutation,
|
||||
SheetsFilterService,
|
||||
} from '@univerjs/sheets-filter';
|
||||
import { SheetsRenderService } from '@univerjs/sheets-ui';
|
||||
import { BuiltInUIPart, connectInjector, IMessageService, IUIPartsService } from '@univerjs/ui';
|
||||
import {
|
||||
ChangeFilterByOperation,
|
||||
CloseFilterPanelOperation,
|
||||
OpenFilterPanelOperation,
|
||||
} from '../../commands/operations/sheets-filter.operation';
|
||||
import { MobileSheetsFilterPanel } from '../../views/mobile/MobileSheetsFilterPanel';
|
||||
import { SheetsFilterRenderController } from '../../views/render-modules/sheets-filter.render-controller';
|
||||
|
||||
export class SheetsFilterUIMobileController extends RxDisposable {
|
||||
constructor(
|
||||
@Inject(Injector) injector: Injector,
|
||||
@ICommandService commandService: ICommandService,
|
||||
@Inject(LocaleService) localeService: LocaleService,
|
||||
@Inject(SheetsFilterService) sheetsFilterService: SheetsFilterService,
|
||||
@IMessageService messageService: IMessageService,
|
||||
@IUIPartsService uiPartsService: IUIPartsService,
|
||||
@IRenderManagerService renderManagerService: IRenderManagerService,
|
||||
@Inject(SheetsRenderService) sheetsRenderService: SheetsRenderService
|
||||
) {
|
||||
super();
|
||||
|
||||
[
|
||||
ChangeFilterByOperation,
|
||||
OpenFilterPanelOperation,
|
||||
CloseFilterPanelOperation,
|
||||
].forEach((command) => {
|
||||
this.disposeWithMe(commandService.registerCommand(command));
|
||||
});
|
||||
|
||||
[
|
||||
SetSheetsFilterRangeMutation,
|
||||
SetSheetsFilterCriteriaMutation,
|
||||
RemoveSheetsFilterMutation,
|
||||
ReCalcSheetsFilterMutation,
|
||||
].forEach((mutation) => {
|
||||
this.disposeWithMe(sheetsRenderService.registerSkeletonChangingMutations(mutation.id));
|
||||
});
|
||||
|
||||
this.disposeWithMe(renderManagerService.registerRenderModule(
|
||||
UniverInstanceType.UNIVER_SHEET,
|
||||
[SheetsFilterRenderController] as Dependency
|
||||
));
|
||||
this.disposeWithMe(uiPartsService.registerComponent(
|
||||
BuiltInUIPart.GLOBAL,
|
||||
() => connectInjector(MobileSheetsFilterPanel, injector)
|
||||
));
|
||||
this.disposeWithMe(sheetsFilterService.errorMsg$.subscribe((content) => {
|
||||
if (content) {
|
||||
messageService.show({
|
||||
type: MessageType.Error,
|
||||
content: localeService.t(content),
|
||||
});
|
||||
}
|
||||
}));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,107 @@
|
||||
/**
|
||||
* 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 } from '@univerjs/core';
|
||||
import type { MenuConfig } from '@univerjs/ui';
|
||||
import type { IOpenFilterPanelOperationParams } from '../commands/operations/sheets-filter.operation';
|
||||
import type { LocaleKey } from '../locale/types';
|
||||
import { Disposable, ICommandService, Inject, Injector, IUniverInstanceService, LocaleService, Tools } from '@univerjs/core';
|
||||
import { expandToContinuousRange, getSheetCommandTarget, RangeProtectionPermissionViewPoint, SheetPermissionCheckController, SheetsSelectionsService, WorksheetFilterPermission, WorksheetViewPermission } from '@univerjs/sheets';
|
||||
import { SheetsFilterService, SmartToggleSheetsFilterCommand } from '@univerjs/sheets-filter';
|
||||
import { OpenFilterPanelOperation } from '../commands/operations/sheets-filter.operation';
|
||||
|
||||
export interface IUniverSheetsFilterUIConfig {
|
||||
menu: MenuConfig;
|
||||
}
|
||||
|
||||
export const DefaultSheetFilterUiConfig = {};
|
||||
|
||||
/**
|
||||
* This controller controls the UI of "filter" features. Menus, commands and filter panel etc. Except for the rendering.
|
||||
*/
|
||||
export class SheetsFilterPermissionController extends Disposable {
|
||||
constructor(
|
||||
@Inject(SheetsFilterService) private _sheetsFilterService: SheetsFilterService,
|
||||
@Inject(LocaleService) private _localeService: LocaleService,
|
||||
@ICommandService private readonly _commandService: ICommandService,
|
||||
@Inject(SheetPermissionCheckController)
|
||||
private readonly _sheetPermissionCheckPermission: SheetPermissionCheckController,
|
||||
@Inject(Injector) private _injector: Injector,
|
||||
@Inject(SheetsSelectionsService) private _sheetsSelectionService: SheetsSelectionsService
|
||||
) {
|
||||
super();
|
||||
|
||||
this._commandExecutedListener();
|
||||
}
|
||||
|
||||
private _commandExecutedListener() {
|
||||
this.disposeWithMe(
|
||||
this._commandService.beforeCommandExecuted((command: ICommandInfo) => {
|
||||
if (command.id === SmartToggleSheetsFilterCommand.id) {
|
||||
const univerInstanceService = this._injector.get(IUniverInstanceService);
|
||||
const target = getSheetCommandTarget(univerInstanceService);
|
||||
if (!target) return;
|
||||
const { unitId, subUnitId, worksheet } = target;
|
||||
const filterRange = this._sheetsFilterService.getFilterModel(unitId, subUnitId)?.getRange();
|
||||
let permission;
|
||||
if (filterRange) {
|
||||
permission = this._sheetPermissionCheckPermission.permissionCheckWithRanges({
|
||||
rangeTypes: [RangeProtectionPermissionViewPoint],
|
||||
worksheetTypes: [WorksheetFilterPermission, WorksheetViewPermission],
|
||||
}, [filterRange], unitId, subUnitId);
|
||||
} else {
|
||||
const range = this._sheetsSelectionService.getCurrentLastSelection()?.range;
|
||||
if (range) {
|
||||
let newRange = { ...range };
|
||||
const isCellRange = range.startColumn === range.endColumn && range.startRow === range.endRow;
|
||||
newRange = isCellRange ? expandToContinuousRange(newRange, { left: true, right: true, up: true, down: true }, worksheet) : newRange;
|
||||
permission = this._sheetPermissionCheckPermission.permissionCheckWithRanges({
|
||||
rangeTypes: [RangeProtectionPermissionViewPoint],
|
||||
worksheetTypes: [WorksheetViewPermission, WorksheetFilterPermission],
|
||||
}, [newRange], unitId, subUnitId);
|
||||
} else {
|
||||
permission = this._sheetPermissionCheckPermission.permissionCheckWithoutRange({
|
||||
rangeTypes: [RangeProtectionPermissionViewPoint],
|
||||
worksheetTypes: [WorksheetViewPermission, WorksheetFilterPermission],
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (!permission) {
|
||||
this._sheetPermissionCheckPermission.blockExecuteWithoutPermission(this._localeService.t<LocaleKey>('sheets-filter-ui.permission.filterErr'));
|
||||
}
|
||||
}
|
||||
if (command.id === OpenFilterPanelOperation.id) {
|
||||
const params = command.params as IOpenFilterPanelOperationParams;
|
||||
const { unitId, subUnitId } = params;
|
||||
const filterRange = this._sheetsFilterService.getFilterModel(unitId, subUnitId)?.getRange();
|
||||
const colRange = Tools.deepClone(filterRange);
|
||||
if (colRange) {
|
||||
colRange.startColumn = params.col;
|
||||
colRange.endColumn = params.col;
|
||||
const permission = this._sheetPermissionCheckPermission.permissionCheckWithRanges({
|
||||
rangeTypes: [RangeProtectionPermissionViewPoint],
|
||||
worksheetTypes: [WorksheetFilterPermission, WorksheetViewPermission],
|
||||
}, [colRange], unitId, subUnitId);
|
||||
if (!permission) {
|
||||
this._sheetPermissionCheckPermission.blockExecuteWithoutPermission(this._localeService.t<LocaleKey>('sheets-filter-ui.permission.filterErr'));
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
/**
|
||||
* 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 { IShortcutItem } from '@univerjs/ui';
|
||||
import { SmartToggleSheetsFilterCommand } from '@univerjs/sheets-filter';
|
||||
import { whenSheetEditorFocused } from '@univerjs/sheets-ui';
|
||||
import { KeyCode, MetaKeys } from '@univerjs/ui';
|
||||
|
||||
export const SmartToggleFilterShortcut: IShortcutItem = {
|
||||
id: SmartToggleSheetsFilterCommand.id,
|
||||
binding: KeyCode.L | MetaKeys.CTRL_COMMAND | MetaKeys.SHIFT,
|
||||
description: 'sheets-filter-ui.shortcut.smart-toggle-filter',
|
||||
preconditions: whenSheetEditorFocused,
|
||||
group: '4_sheet-edit',
|
||||
groupTitle: 'sheets-ui.shortcut.sheet-edit',
|
||||
};
|
||||
155
packages/sheets-filter-ui/src/controllers/ui.controller.ts
Normal file
155
packages/sheets-filter-ui/src/controllers/ui.controller.ts
Normal file
@@ -0,0 +1,155 @@
|
||||
/**
|
||||
* 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, IDisposable, Nullable } from '@univerjs/core';
|
||||
import { ICommandService, IContextService, Inject, Injector, LocaleService, RxDisposable, UniverInstanceType } from '@univerjs/core';
|
||||
import { MessageType } from '@univerjs/design';
|
||||
import { IRenderManagerService } from '@univerjs/engine-render';
|
||||
import {
|
||||
ReCalcSheetsFilterMutation,
|
||||
RemoveSheetsFilterMutation,
|
||||
SetSheetsFilterCriteriaMutation,
|
||||
SetSheetsFilterRangeMutation,
|
||||
SheetsFilterService,
|
||||
SmartToggleSheetsFilterCommand,
|
||||
} from '@univerjs/sheets-filter';
|
||||
import { SheetCanvasPopManagerService, SheetsRenderService } from '@univerjs/sheets-ui';
|
||||
import { IMenuManagerService, IMessageService, IShortcutService } from '@univerjs/ui';
|
||||
import { distinctUntilChanged } from 'rxjs';
|
||||
import {
|
||||
ChangeFilterByOperation,
|
||||
CloseFilterPanelOperation,
|
||||
FILTER_PANEL_OPENED_KEY,
|
||||
OpenFilterPanelOperation,
|
||||
} from '../commands/operations/sheets-filter.operation';
|
||||
import { menuSchema } from '../menu/schema';
|
||||
import { ISheetsFilterPanelService } from '../services/sheets-filter-panel.service';
|
||||
import { FILTER_PANEL_POPUP_KEY } from '../views/components/SheetsFilterPanel';
|
||||
import { SheetsFilterRenderController } from '../views/render-modules/sheets-filter.render-controller';
|
||||
import { SmartToggleFilterShortcut } from './sheets-filter.shortcut';
|
||||
|
||||
/**
|
||||
* This controller controls the UI of "filter" features. Menus, commands and filter panel etc. Except for the rendering.
|
||||
*/
|
||||
export class SheetsFilterUIDesktopController extends RxDisposable {
|
||||
constructor(
|
||||
@Inject(Injector) private readonly _injector: Injector,
|
||||
@ISheetsFilterPanelService private readonly _sheetsFilterPanelService: ISheetsFilterPanelService,
|
||||
@Inject(SheetCanvasPopManagerService) private _sheetCanvasPopupService: SheetCanvasPopManagerService,
|
||||
@Inject(SheetsFilterService) private _sheetsFilterService: SheetsFilterService,
|
||||
@Inject(LocaleService) private _localeService: LocaleService,
|
||||
@IShortcutService private readonly _shortcutService: IShortcutService,
|
||||
@ICommandService private readonly _commandService: ICommandService,
|
||||
@IMenuManagerService private readonly _menuManagerService: IMenuManagerService,
|
||||
@IContextService private readonly _contextService: IContextService,
|
||||
@IMessageService private readonly _messageService: IMessageService,
|
||||
@Inject(SheetsRenderService) sheetsRenderService: SheetsRenderService,
|
||||
@IRenderManagerService renderManagerService: IRenderManagerService
|
||||
) {
|
||||
super();
|
||||
|
||||
[
|
||||
SetSheetsFilterRangeMutation,
|
||||
SetSheetsFilterCriteriaMutation,
|
||||
RemoveSheetsFilterMutation,
|
||||
ReCalcSheetsFilterMutation,
|
||||
].forEach((mutation) => {
|
||||
this.disposeWithMe(sheetsRenderService.registerSkeletonChangingMutations(mutation.id));
|
||||
});
|
||||
this.disposeWithMe(renderManagerService.registerRenderModule(
|
||||
UniverInstanceType.UNIVER_SHEET,
|
||||
[SheetsFilterRenderController] as Dependency
|
||||
));
|
||||
|
||||
this._initCommands();
|
||||
this._initShortcuts();
|
||||
this._initMenuItems();
|
||||
this._initUI();
|
||||
}
|
||||
|
||||
override dispose(): void {
|
||||
super.dispose();
|
||||
|
||||
this._closeFilterPopup();
|
||||
}
|
||||
|
||||
private _initShortcuts(): void {
|
||||
[
|
||||
SmartToggleFilterShortcut,
|
||||
].forEach((shortcut) => {
|
||||
this.disposeWithMe(this._shortcutService.registerShortcut(shortcut));
|
||||
});
|
||||
}
|
||||
|
||||
private _initCommands(): void {
|
||||
[
|
||||
SmartToggleSheetsFilterCommand,
|
||||
ChangeFilterByOperation,
|
||||
OpenFilterPanelOperation,
|
||||
CloseFilterPanelOperation,
|
||||
].forEach((c) => {
|
||||
this.disposeWithMe(this._commandService.registerCommand(c));
|
||||
});
|
||||
}
|
||||
|
||||
private _initMenuItems(): void {
|
||||
this._menuManagerService.mergeMenu(menuSchema);
|
||||
}
|
||||
|
||||
private _initUI(): void {
|
||||
this.disposeWithMe(this._contextService.subscribeContextValue$(FILTER_PANEL_OPENED_KEY)
|
||||
.pipe(distinctUntilChanged())
|
||||
.subscribe((open) => {
|
||||
if (open) {
|
||||
this._openFilterPopup();
|
||||
} else {
|
||||
this._closeFilterPopup();
|
||||
}
|
||||
}));
|
||||
this.disposeWithMe(this._sheetsFilterService.errorMsg$.subscribe((content) => {
|
||||
if (content) {
|
||||
this._messageService.show({
|
||||
type: MessageType.Error,
|
||||
content: this._localeService.t(content),
|
||||
});
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
private _popupDisposable?: Nullable<IDisposable>;
|
||||
|
||||
private _openFilterPopup(): void {
|
||||
const currentFilterModel = this._sheetsFilterPanelService.filterModel;
|
||||
if (!currentFilterModel) {
|
||||
throw new Error('[SheetsFilterUIController]: no filter model when opening filter popup!');
|
||||
}
|
||||
|
||||
const range = currentFilterModel.getRange();
|
||||
const col = this._sheetsFilterPanelService.col;
|
||||
const { startRow } = range;
|
||||
this._popupDisposable = this._sheetCanvasPopupService.attachPopupToCell(startRow, col, {
|
||||
componentKey: FILTER_PANEL_POPUP_KEY,
|
||||
direction: 'horizontal',
|
||||
onClickOutside: () => this._commandService.syncExecuteCommand(CloseFilterPanelOperation.id),
|
||||
offset: [5, 0],
|
||||
});
|
||||
}
|
||||
|
||||
private _closeFilterPopup(): void {
|
||||
this._popupDisposable?.dispose();
|
||||
this._popupDisposable = null;
|
||||
}
|
||||
}
|
||||
2
packages/sheets-filter-ui/src/global.css
Normal file
2
packages/sheets-filter-ui/src/global.css
Normal file
@@ -0,0 +1,2 @@
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
25
packages/sheets-filter-ui/src/index.ts
Normal file
25
packages/sheets-filter-ui/src/index.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
/**
|
||||
* Copyright 2023-present DreamNum Co., Ltd.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import './global.css';
|
||||
|
||||
export { ChangeFilterByOperation, CloseFilterPanelOperation, OpenFilterPanelOperation } from './commands/operations/sheets-filter.operation';
|
||||
export type { IOpenFilterPanelOperationParams } from './commands/operations/sheets-filter.operation';
|
||||
export type { IUniverSheetsFilterUIConfig } from './config/config';
|
||||
export { menuSchema as SheetsFilterUIMenuSchema } from './menu/schema';
|
||||
export { UniverSheetsFilterMobileUIPlugin } from './mobile-plugin';
|
||||
export { UniverSheetsFilterUIPlugin } from './plugin';
|
||||
export { UniverSheetsFilterUIWorkerPlugin } from './worker/plugin';
|
||||
106
packages/sheets-filter-ui/src/locale/ar-SA.ts
Normal file
106
packages/sheets-filter-ui/src/locale/ar-SA.ts
Normal file
@@ -0,0 +1,106 @@
|
||||
/**
|
||||
* Copyright 2023-present DreamNum Co., Ltd.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import type enUS from './en-US';
|
||||
|
||||
const locale: typeof enUS = {
|
||||
'sheets-filter-ui': {
|
||||
toolbar: {
|
||||
'smart-toggle-filter-tooltip': 'تبديل التصفية',
|
||||
'clear-filter-criteria': 'مسح شروط التصفية',
|
||||
're-calc-filter-conditions': 'إعادة حساب شروط التصفية',
|
||||
},
|
||||
shortcut: {
|
||||
'smart-toggle-filter': 'تبديل التصفية',
|
||||
},
|
||||
permission: {
|
||||
filterErr: 'ليس لديك إذن لاستخدام التصفية.',
|
||||
},
|
||||
panel: {
|
||||
'clear-filter': 'مسح التصفية',
|
||||
cancel: 'إلغاء',
|
||||
confirm: 'تأكيد',
|
||||
mobile: {
|
||||
resize: 'تغيير حجم لوحة التصفية',
|
||||
back: 'رجوع',
|
||||
column: 'العمود {0}',
|
||||
'previous-column': 'العمود السابق',
|
||||
'next-column': 'العمود التالي',
|
||||
},
|
||||
'by-values': 'حسب القيم',
|
||||
'by-colors': 'حسب الألوان',
|
||||
'filter-by-cell-fill-color': 'تصفية حسب لون تعبئة الخلية',
|
||||
'filter-by-cell-text-color': 'تصفية حسب لون نص الخلية',
|
||||
'filter-by-color-none': 'يحتوي العمود على لون واحد فقط',
|
||||
'by-conditions': 'حسب الشروط',
|
||||
'filter-only': 'تصفية فقط',
|
||||
'search-placeholder': 'استخدم المسافة لفصل الكلمات المفتاحية',
|
||||
'select-all': 'تحديد الكل',
|
||||
'input-values-placeholder': 'إدخال القيم',
|
||||
and: 'و',
|
||||
or: 'أو',
|
||||
empty: '(فارغ)',
|
||||
'?': 'استخدم "؟" لتمثيل حرف واحد.',
|
||||
'*': 'استخدم "*" لتمثيل عدة أحرف.',
|
||||
},
|
||||
conditions: {
|
||||
none: 'لا شيء',
|
||||
empty: 'فارغ',
|
||||
'not-empty': 'غير فارغ',
|
||||
'text-contains': 'النص يحتوي على',
|
||||
'does-not-contain': 'النص لا يحتوي على',
|
||||
'starts-with': 'النص يبدأ بـ',
|
||||
'ends-with': 'النص ينتهي بـ',
|
||||
equals: 'النص يساوي',
|
||||
'greater-than': 'أكبر من',
|
||||
'greater-than-or-equal': 'أكبر من أو يساوي',
|
||||
'less-than': 'أقل من',
|
||||
'less-than-or-equal': 'أقل من أو يساوي',
|
||||
equal: 'يساوي',
|
||||
'not-equal': 'لا يساوي',
|
||||
between: 'بين',
|
||||
'not-between': 'ليس بين',
|
||||
custom: 'مخصص',
|
||||
},
|
||||
date: {
|
||||
1: 'يناير',
|
||||
2: 'فبراير',
|
||||
3: 'مارس',
|
||||
4: 'أبريل',
|
||||
5: 'مايو',
|
||||
6: 'يونيو',
|
||||
7: 'يوليو',
|
||||
8: 'أغسطس',
|
||||
9: 'سبتمبر',
|
||||
10: 'أكتوبر',
|
||||
11: 'نوفمبر',
|
||||
12: 'ديسمبر',
|
||||
},
|
||||
sync: {
|
||||
title: 'التصفية مرئية للجميع',
|
||||
statusTips: {
|
||||
on: 'عند التفعيل، سيرى جميع المتعاونين نتائج التصفية',
|
||||
off: 'عند الإيقاف، سترى نتائج التصفية أنت فقط',
|
||||
},
|
||||
switchTips: {
|
||||
on: '"التصفية مرئية للجميع" مفعّلة، سيرى جميع المتعاونين نتائج التصفية',
|
||||
off: '"التصفية مرئية للجميع" غير مفعّلة، سترى نتائج التصفية أنت فقط',
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default locale;
|
||||
106
packages/sheets-filter-ui/src/locale/ca-ES.ts
Normal file
106
packages/sheets-filter-ui/src/locale/ca-ES.ts
Normal file
@@ -0,0 +1,106 @@
|
||||
/**
|
||||
* Copyright 2023-present DreamNum Co., Ltd.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import type enUS from './en-US';
|
||||
|
||||
const locale: typeof enUS = {
|
||||
'sheets-filter-ui': {
|
||||
toolbar: {
|
||||
'smart-toggle-filter-tooltip': 'Alterna el filtre',
|
||||
'clear-filter-criteria': 'Neteja les condicions del filtre',
|
||||
're-calc-filter-conditions': 'Recalcula les condicions del filtre',
|
||||
},
|
||||
shortcut: {
|
||||
'smart-toggle-filter': 'Alterna el filtre',
|
||||
},
|
||||
permission: {
|
||||
filterErr: 'No teniu permís per utilitzar el filtre.',
|
||||
},
|
||||
panel: {
|
||||
'clear-filter': 'Neteja el filtre',
|
||||
cancel: 'Cancel·la',
|
||||
confirm: 'Confirma',
|
||||
mobile: {
|
||||
resize: 'Redimensiona el tauler de filtre',
|
||||
back: 'Enrere',
|
||||
column: 'Columna {0}',
|
||||
'previous-column': 'Columna anterior',
|
||||
'next-column': 'Columna següent',
|
||||
},
|
||||
'by-values': 'Per valors',
|
||||
'by-colors': 'Per colors',
|
||||
'filter-by-cell-fill-color': 'Filtra per color de fons de la cel·la',
|
||||
'filter-by-cell-text-color': 'Filtra per color de text de la cel·la',
|
||||
'filter-by-color-none': 'La columna només conté un color',
|
||||
'by-conditions': 'Per condicions',
|
||||
'filter-only': 'Només filtra',
|
||||
'search-placeholder': 'Utilitza espai per separar paraules clau',
|
||||
'select-all': 'Selecciona-ho tot',
|
||||
'input-values-placeholder': 'Introdueix valors',
|
||||
and: 'I',
|
||||
or: 'O',
|
||||
empty: '(buit)',
|
||||
'?': 'Utilitza “?” per representar un sol caràcter.',
|
||||
'*': 'Utilitza “*” per representar diversos caràcters.',
|
||||
},
|
||||
conditions: {
|
||||
none: 'Cap',
|
||||
empty: 'Està buit',
|
||||
'not-empty': 'No està buit',
|
||||
'text-contains': 'El text conté',
|
||||
'does-not-contain': 'El text no conté',
|
||||
'starts-with': 'El text comença amb',
|
||||
'ends-with': 'El text acaba amb',
|
||||
equals: 'El text és igual a',
|
||||
'greater-than': 'Més gran que',
|
||||
'greater-than-or-equal': 'Més gran o igual que',
|
||||
'less-than': 'Menys que',
|
||||
'less-than-or-equal': 'Menys o igual que',
|
||||
equal: 'Igual',
|
||||
'not-equal': 'No igual',
|
||||
between: 'Entre',
|
||||
'not-between': 'No entre',
|
||||
custom: 'Personalitzat',
|
||||
},
|
||||
date: {
|
||||
1: 'Gener',
|
||||
2: 'Febrer',
|
||||
3: 'Març',
|
||||
4: 'Abril',
|
||||
5: 'Maig',
|
||||
6: 'Juny',
|
||||
7: 'Juliol',
|
||||
8: 'Agost',
|
||||
9: 'Setembre',
|
||||
10: 'Octubre',
|
||||
11: 'Novembre',
|
||||
12: 'Desembre',
|
||||
},
|
||||
sync: {
|
||||
title: 'El filtre és visible per a tothom',
|
||||
statusTips: {
|
||||
on: 'Un cop activat, tots els col·laboradors veuran els resultats del filtre',
|
||||
off: 'Un cop desactivat, només tu veuràs els resultats del filtre',
|
||||
},
|
||||
switchTips: {
|
||||
on: 'S’ha activat "El filtre és visible per a tothom", tots els col·laboradors veuran els resultats del filtre',
|
||||
off: 'S’ha desactivat "El filtre és visible per a tothom", només tu veuràs els resultats del filtre',
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default locale;
|
||||
106
packages/sheets-filter-ui/src/locale/de-DE.ts
Normal file
106
packages/sheets-filter-ui/src/locale/de-DE.ts
Normal file
@@ -0,0 +1,106 @@
|
||||
/**
|
||||
* Copyright 2023-present DreamNum Co., Ltd.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import type enUS from './en-US';
|
||||
|
||||
const locale: typeof enUS = {
|
||||
'sheets-filter-ui': {
|
||||
toolbar: {
|
||||
'smart-toggle-filter-tooltip': 'Filter ein-/ausschalten',
|
||||
'clear-filter-criteria': 'Filterbedingungen löschen',
|
||||
're-calc-filter-conditions': 'Filterbedingungen neu berechnen',
|
||||
},
|
||||
shortcut: {
|
||||
'smart-toggle-filter': 'Filter ein-/ausschalten',
|
||||
},
|
||||
permission: {
|
||||
filterErr: 'Sie haben keine Berechtigung, den Filter zu verwenden.',
|
||||
},
|
||||
panel: {
|
||||
'clear-filter': 'Filter löschen',
|
||||
cancel: 'Abbrechen',
|
||||
confirm: 'Bestätigen',
|
||||
mobile: {
|
||||
resize: 'Größe des Filterbereichs ändern',
|
||||
back: 'Zurück',
|
||||
column: 'Spalte {0}',
|
||||
'previous-column': 'Vorherige Spalte',
|
||||
'next-column': 'Nächste Spalte',
|
||||
},
|
||||
'by-values': 'Nach Werten',
|
||||
'by-colors': 'Nach Farben',
|
||||
'filter-by-cell-fill-color': 'Nach Zellenfüllfarbe filtern',
|
||||
'filter-by-cell-text-color': 'Nach Zellentextfarbe filtern',
|
||||
'filter-by-color-none': 'Die Spalte enthält nur eine Farbe',
|
||||
'by-conditions': 'Nach Bedingungen',
|
||||
'filter-only': 'Nur filtern',
|
||||
'search-placeholder': 'Verwenden Sie Leerzeichen, um Schlüsselwörter zu trennen',
|
||||
'select-all': 'Alle auswählen',
|
||||
'input-values-placeholder': 'Werte eingeben',
|
||||
and: 'UND',
|
||||
or: 'ODER',
|
||||
empty: '(leer)',
|
||||
'?': 'Verwenden Sie "?" für ein einzelnes Zeichen.',
|
||||
'*': 'Verwenden Sie "*" für mehrere Zeichen.',
|
||||
},
|
||||
conditions: {
|
||||
none: 'Keine',
|
||||
empty: 'Ist leer',
|
||||
'not-empty': 'Ist nicht leer',
|
||||
'text-contains': 'Text enthält',
|
||||
'does-not-contain': 'Text enthält nicht',
|
||||
'starts-with': 'Text beginnt mit',
|
||||
'ends-with': 'Text endet mit',
|
||||
equals: 'Text ist gleich',
|
||||
'greater-than': 'Größer als',
|
||||
'greater-than-or-equal': 'Größer als oder gleich',
|
||||
'less-than': 'Kleiner als',
|
||||
'less-than-or-equal': 'Kleiner als oder gleich',
|
||||
equal: 'Gleich',
|
||||
'not-equal': 'Ungleich',
|
||||
between: 'Zwischen',
|
||||
'not-between': 'Nicht zwischen',
|
||||
custom: 'Benutzerdefiniert',
|
||||
},
|
||||
date: {
|
||||
1: 'Januar',
|
||||
2: 'Februar',
|
||||
3: 'März',
|
||||
4: 'April',
|
||||
5: 'Mai',
|
||||
6: 'Juni',
|
||||
7: 'Juli',
|
||||
8: 'August',
|
||||
9: 'September',
|
||||
10: 'Oktober',
|
||||
11: 'November',
|
||||
12: 'Dezember',
|
||||
},
|
||||
sync: {
|
||||
title: 'Filter ist für alle sichtbar',
|
||||
statusTips: {
|
||||
on: 'Wenn eingeschaltet, sehen alle Bearbeiter die Filterergebnisse',
|
||||
off: 'Wenn ausgeschaltet, sehen nur Sie die Filterergebnisse',
|
||||
},
|
||||
switchTips: {
|
||||
on: '"Filter ist für alle sichtbar" ist eingeschaltet, alle Bearbeiter sehen die Filterergebnisse',
|
||||
off: '"Filter ist für alle sichtbar" ist ausgeschaltet, nur Sie sehen die Filterergebnisse',
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default locale;
|
||||
104
packages/sheets-filter-ui/src/locale/en-US.ts
Normal file
104
packages/sheets-filter-ui/src/locale/en-US.ts
Normal file
@@ -0,0 +1,104 @@
|
||||
/**
|
||||
* Copyright 2023-present DreamNum Co., Ltd.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
const locale = {
|
||||
'sheets-filter-ui': {
|
||||
toolbar: {
|
||||
'smart-toggle-filter-tooltip': 'Toggle Filter',
|
||||
'clear-filter-criteria': 'Clear Filter Conditions',
|
||||
're-calc-filter-conditions': 'Re-calc Filter Conditions',
|
||||
},
|
||||
shortcut: {
|
||||
'smart-toggle-filter': 'Toggle Filter',
|
||||
},
|
||||
permission: {
|
||||
filterErr: 'You do not have permission to use the filter.',
|
||||
},
|
||||
panel: {
|
||||
'clear-filter': 'Clear Filter',
|
||||
cancel: 'Cancel',
|
||||
confirm: 'Confirm',
|
||||
mobile: {
|
||||
resize: 'Resize filter panel',
|
||||
back: 'Back',
|
||||
column: 'Column {0}',
|
||||
'previous-column': 'Previous column',
|
||||
'next-column': 'Next column',
|
||||
},
|
||||
'by-values': 'By Values',
|
||||
'by-colors': 'By Colors',
|
||||
'filter-by-cell-fill-color': 'Filter by cell fill color',
|
||||
'filter-by-cell-text-color': 'Filter by cell text color',
|
||||
'filter-by-color-none': 'The column contains only one color',
|
||||
'by-conditions': 'By Conditions',
|
||||
'filter-only': 'Filter Only',
|
||||
'search-placeholder': 'Use space to separate keywords',
|
||||
'select-all': 'Select All',
|
||||
'input-values-placeholder': 'Input Values',
|
||||
and: 'AND',
|
||||
or: 'OR',
|
||||
empty: '(empty)',
|
||||
'?': 'Use “?” to represent a single character.',
|
||||
'*': 'Use “*” to represent multiple characters.',
|
||||
},
|
||||
conditions: {
|
||||
none: 'None',
|
||||
empty: 'Is Empty',
|
||||
'not-empty': 'Is Not Empty',
|
||||
'text-contains': 'Text Contains',
|
||||
'does-not-contain': 'Text Does Not Contain',
|
||||
'starts-with': 'Text Starts With',
|
||||
'ends-with': 'Text Ends With',
|
||||
equals: 'Text Equals',
|
||||
'greater-than': 'Greater Than',
|
||||
'greater-than-or-equal': 'Greater Than Or Equal To',
|
||||
'less-than': 'Less Than',
|
||||
'less-than-or-equal': 'Less Than Or Equal To',
|
||||
equal: 'Equal',
|
||||
'not-equal': 'Not Equal',
|
||||
between: 'Between',
|
||||
'not-between': 'Not Between',
|
||||
custom: 'Custom',
|
||||
},
|
||||
date: {
|
||||
1: 'January',
|
||||
2: 'February',
|
||||
3: 'March',
|
||||
4: 'April',
|
||||
5: 'May',
|
||||
6: 'June',
|
||||
7: 'July',
|
||||
8: 'August',
|
||||
9: 'September',
|
||||
10: 'October',
|
||||
11: 'November',
|
||||
12: 'December',
|
||||
},
|
||||
sync: {
|
||||
title: 'Filter is visible to everyone',
|
||||
statusTips: {
|
||||
on: 'Once on, all collaborators will see the filter results',
|
||||
off: 'Once off, only you will see the filter results',
|
||||
},
|
||||
switchTips: {
|
||||
on: '"Filter is visible to everyone" is turned on, all collaborators will see the filter results',
|
||||
off: '"Filter is visible to everyone" is turned off, only you will see the filter results',
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default locale;
|
||||
106
packages/sheets-filter-ui/src/locale/es-ES.ts
Normal file
106
packages/sheets-filter-ui/src/locale/es-ES.ts
Normal file
@@ -0,0 +1,106 @@
|
||||
/**
|
||||
* Copyright 2023-present DreamNum Co., Ltd.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import type enUS from './en-US';
|
||||
|
||||
const locale: typeof enUS = {
|
||||
'sheets-filter-ui': {
|
||||
toolbar: {
|
||||
'smart-toggle-filter-tooltip': 'Alternar filtro',
|
||||
'clear-filter-criteria': 'Borrar condiciones de filtro',
|
||||
're-calc-filter-conditions': 'Recalcular condiciones de filtro',
|
||||
},
|
||||
shortcut: {
|
||||
'smart-toggle-filter': 'Alternar filtro',
|
||||
},
|
||||
permission: {
|
||||
filterErr: 'No tiene permiso para usar el filtro.',
|
||||
},
|
||||
panel: {
|
||||
'clear-filter': 'Borrar filtro',
|
||||
cancel: 'Cancelar',
|
||||
confirm: 'Confirmar',
|
||||
mobile: {
|
||||
resize: 'Cambiar el tamaño del panel de filtro',
|
||||
back: 'Volver',
|
||||
column: 'Columna {0}',
|
||||
'previous-column': 'Columna anterior',
|
||||
'next-column': 'Columna siguiente',
|
||||
},
|
||||
'by-values': 'Por valores',
|
||||
'by-colors': 'Por colores',
|
||||
'filter-by-cell-fill-color': 'Filtrar por color de relleno de celda',
|
||||
'filter-by-cell-text-color': 'Filtrar por color de texto de celda',
|
||||
'filter-by-color-none': 'La columna contiene solo un color',
|
||||
'by-conditions': 'Por condiciones',
|
||||
'filter-only': 'Solo filtrar',
|
||||
'search-placeholder': 'Usa espacio para separar palabras clave',
|
||||
'select-all': 'Seleccionar todo',
|
||||
'input-values-placeholder': 'Introducir valores',
|
||||
and: 'Y',
|
||||
or: 'O',
|
||||
empty: '(vacío)',
|
||||
'?': 'Usa “?” para representar un solo carácter.',
|
||||
'*': 'Usa “*” para representar varios caracteres.',
|
||||
},
|
||||
conditions: {
|
||||
none: 'Ninguno',
|
||||
empty: 'Está vacío',
|
||||
'not-empty': 'No está vacío',
|
||||
'text-contains': 'El texto contiene',
|
||||
'does-not-contain': 'El texto no contiene',
|
||||
'starts-with': 'El texto comienza con',
|
||||
'ends-with': 'El texto termina con',
|
||||
equals: 'El texto es igual a',
|
||||
'greater-than': 'Mayor que',
|
||||
'greater-than-or-equal': 'Mayor o igual que',
|
||||
'less-than': 'Menor que',
|
||||
'less-than-or-equal': 'Menor o igual que',
|
||||
equal: 'Igual',
|
||||
'not-equal': 'No igual',
|
||||
between: 'Entre',
|
||||
'not-between': 'No entre',
|
||||
custom: 'Personalizado',
|
||||
},
|
||||
date: {
|
||||
1: 'Enero',
|
||||
2: 'Febrero',
|
||||
3: 'Marzo',
|
||||
4: 'Abril',
|
||||
5: 'Mayo',
|
||||
6: 'Junio',
|
||||
7: 'Julio',
|
||||
8: 'Agosto',
|
||||
9: 'Septiembre',
|
||||
10: 'Octubre',
|
||||
11: 'Noviembre',
|
||||
12: 'Diciembre',
|
||||
},
|
||||
sync: {
|
||||
title: 'El filtro es visible para todos',
|
||||
statusTips: {
|
||||
on: 'Una vez activado, todos los colaboradores verán los resultados del filtro',
|
||||
off: 'Una vez desactivado, solo tú verás los resultados del filtro',
|
||||
},
|
||||
switchTips: {
|
||||
on: '"El filtro es visible para todos" está activado, todos los colaboradores verán los resultados del filtro',
|
||||
off: '"El filtro es visible para todos" está desactivado, solo tú verás los resultados del filtro',
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default locale;
|
||||
106
packages/sheets-filter-ui/src/locale/fa-IR.ts
Normal file
106
packages/sheets-filter-ui/src/locale/fa-IR.ts
Normal file
@@ -0,0 +1,106 @@
|
||||
/**
|
||||
* Copyright 2023-present DreamNum Co., Ltd.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import type enUS from './en-US';
|
||||
|
||||
const locale: typeof enUS = {
|
||||
'sheets-filter-ui': {
|
||||
toolbar: {
|
||||
'smart-toggle-filter-tooltip': 'تغییر حالت فیلتر',
|
||||
'clear-filter-criteria': 'پاک کردن شرایط فیلتر',
|
||||
're-calc-filter-conditions': 'بازمحاسبه شرایط فیلتر',
|
||||
},
|
||||
shortcut: {
|
||||
'smart-toggle-filter': 'تغییر حالت فیلتر',
|
||||
},
|
||||
permission: {
|
||||
filterErr: 'شما مجوز استفاده از فیلتر را ندارید.',
|
||||
},
|
||||
panel: {
|
||||
'clear-filter': 'پاک کردن فیلتر',
|
||||
cancel: 'انصراف',
|
||||
confirm: 'تایید',
|
||||
mobile: {
|
||||
resize: 'تغییر اندازه پنل فیلتر',
|
||||
back: 'بازگشت',
|
||||
column: 'ستون {0}',
|
||||
'previous-column': 'ستون قبلی',
|
||||
'next-column': 'ستون بعدی',
|
||||
},
|
||||
'by-values': 'بر اساس مقادیر',
|
||||
'by-colors': 'بر اساس رنگها',
|
||||
'filter-by-cell-fill-color': 'فیلتر بر اساس رنگ پر کردن سلول',
|
||||
'filter-by-cell-text-color': 'فیلتر بر اساس رنگ متن سلول',
|
||||
'filter-by-color-none': 'این ستون فقط شامل یک رنگ است',
|
||||
'by-conditions': 'بر اساس شرایط',
|
||||
'filter-only': 'فقط فیلتر',
|
||||
'search-placeholder': 'برای جداسازی کلیدواژهها از فاصله استفاده کنید',
|
||||
'select-all': 'انتخاب همه',
|
||||
'input-values-placeholder': 'ورود مقادیر',
|
||||
and: 'و',
|
||||
or: 'یا',
|
||||
empty: '(خالی)',
|
||||
'?': 'برای نمایش یک کاراکتر از “?” استفاده کنید.',
|
||||
'*': 'برای نمایش چندین کاراکتر از “*” استفاده کنید.',
|
||||
},
|
||||
conditions: {
|
||||
none: 'هیچ کدام',
|
||||
empty: 'خالی است',
|
||||
'not-empty': 'خالی نیست',
|
||||
'text-contains': 'شامل متن است',
|
||||
'does-not-contain': 'شامل متن نیست',
|
||||
'starts-with': 'با متن شروع میشود',
|
||||
'ends-with': 'با متن پایان مییابد',
|
||||
equals: 'برابر است با',
|
||||
'greater-than': 'بزرگتر از',
|
||||
'greater-than-or-equal': 'بزرگتر از یا برابر با',
|
||||
'less-than': 'کوچکتر از',
|
||||
'less-than-or-equal': 'کوچکتر از یا برابر با',
|
||||
equal: 'برابر است با',
|
||||
'not-equal': 'برابر نیست با',
|
||||
between: 'بین',
|
||||
'not-between': 'بین نیست',
|
||||
custom: 'سفارشی',
|
||||
},
|
||||
date: {
|
||||
1: 'ژانویه',
|
||||
2: 'فوریه',
|
||||
3: 'مارس',
|
||||
4: 'آوریل',
|
||||
5: 'مه',
|
||||
6: 'ژوئن',
|
||||
7: 'جولای',
|
||||
8: 'آگوست',
|
||||
9: 'سپتامبر',
|
||||
10: 'اکتبر',
|
||||
11: 'نوامبر',
|
||||
12: 'دسامبر',
|
||||
},
|
||||
sync: {
|
||||
title: 'فیلتر برای همه قابل مشاهده است',
|
||||
statusTips: {
|
||||
on: 'پس از روشن شدن، همه همکاران نتایج فیلتر را مشاهده خواهند کرد',
|
||||
off: 'پس از خاموش شدن، فقط شما نتایج فیلتر را مشاهده خواهید کرد',
|
||||
},
|
||||
switchTips: {
|
||||
on: 'گزینه "فیلتر برای همه قابل مشاهده است" روشن است، همه همکاران نتایج فیلتر را مشاهده خواهند کرد',
|
||||
off: 'گزینه "فیلتر برای همه قابل مشاهده است" خاموش است، فقط شما نتایج فیلتر را مشاهده خواهید کرد',
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default locale;
|
||||
106
packages/sheets-filter-ui/src/locale/fr-FR.ts
Normal file
106
packages/sheets-filter-ui/src/locale/fr-FR.ts
Normal file
@@ -0,0 +1,106 @@
|
||||
/**
|
||||
* Copyright 2023-present DreamNum Co., Ltd.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import type enUS from './en-US';
|
||||
|
||||
const locale: typeof enUS = {
|
||||
'sheets-filter-ui': {
|
||||
toolbar: {
|
||||
'smart-toggle-filter-tooltip': 'Inverser le filtre',
|
||||
'clear-filter-criteria': 'Effacer les conditions de filtre',
|
||||
're-calc-filter-conditions': 'Recalculer les conditions de filtre',
|
||||
},
|
||||
shortcut: {
|
||||
'smart-toggle-filter': 'Inverser le filtre',
|
||||
},
|
||||
permission: {
|
||||
filterErr: 'Vous n’avez pas l’autorisation d’utiliser le filtre.',
|
||||
},
|
||||
panel: {
|
||||
'clear-filter': 'Effacer le filtre',
|
||||
cancel: 'Annuler',
|
||||
confirm: 'Confirmer',
|
||||
mobile: {
|
||||
resize: 'Redimensionner le panneau de filtre',
|
||||
back: 'Retour',
|
||||
column: 'Colonne {0}',
|
||||
'previous-column': 'Colonne précédente',
|
||||
'next-column': 'Colonne suivante',
|
||||
},
|
||||
'by-values': 'Par valeurs',
|
||||
'by-colors': 'Par couleurs',
|
||||
'filter-by-cell-fill-color': 'Filtrer par couleur de remplissage de cellule',
|
||||
'filter-by-cell-text-color': 'Filtrer par couleur de texte de cellule',
|
||||
'filter-by-color-none': 'Cette colonne ne contient qu\'une seule couleur',
|
||||
'by-conditions': 'Par conditions',
|
||||
'filter-only': 'Filtrer uniquement',
|
||||
'search-placeholder': 'Utilisez un espace pour séparer les mots-clés',
|
||||
'select-all': 'Tout sélectionner',
|
||||
'input-values-placeholder': 'Saisir des valeurs',
|
||||
and: 'ET',
|
||||
or: 'OU',
|
||||
empty: '(vide)',
|
||||
'?': 'Utilisez “?” pour représenter un seul caractère.',
|
||||
'*': 'Utilisez “*” pour représenter plusieurs caractères.',
|
||||
},
|
||||
conditions: {
|
||||
none: 'Aucun',
|
||||
empty: 'Est vide',
|
||||
'not-empty': 'N\'est pas vide',
|
||||
'text-contains': 'Le texte contient',
|
||||
'does-not-contain': 'Le texte ne contient pas',
|
||||
'starts-with': 'Le texte commence par',
|
||||
'ends-with': 'Le texte se termine par',
|
||||
equals: 'Le texte est égal à',
|
||||
'greater-than': 'Supérieur à',
|
||||
'greater-than-or-equal': 'Supérieur ou égal à',
|
||||
'less-than': 'Inférieur à',
|
||||
'less-than-or-equal': 'Inférieur ou égal à',
|
||||
equal: 'Égal à',
|
||||
'not-equal': 'Différent de',
|
||||
between: 'Entre',
|
||||
'not-between': 'Pas entre',
|
||||
custom: 'Personnalisé',
|
||||
},
|
||||
date: {
|
||||
1: 'Jan',
|
||||
2: 'Fév',
|
||||
3: 'Mar',
|
||||
4: 'Avr',
|
||||
5: 'Mai',
|
||||
6: 'Juin',
|
||||
7: 'Juil',
|
||||
8: 'Août',
|
||||
9: 'Sept',
|
||||
10: 'Oct',
|
||||
11: 'Nov',
|
||||
12: 'Déc',
|
||||
},
|
||||
sync: {
|
||||
title: 'Le filtre est visible par tous',
|
||||
statusTips: {
|
||||
on: 'Une fois activé, tous les collaborateurs verront les résultats du filtre',
|
||||
off: 'Une fois désactivé, seul vous verrez les résultats du filtre',
|
||||
},
|
||||
switchTips: {
|
||||
on: '“Le filtre est visible par tous” est activé, tous les collaborateurs verront les résultats du filtre',
|
||||
off: '“Le filtre est visible par tous” est désactivé, seul vous verrez les résultats du filtre',
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default locale;
|
||||
106
packages/sheets-filter-ui/src/locale/id-ID.ts
Normal file
106
packages/sheets-filter-ui/src/locale/id-ID.ts
Normal file
@@ -0,0 +1,106 @@
|
||||
/**
|
||||
* Copyright 2023-present DreamNum Co., Ltd.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import type enUS from './en-US';
|
||||
|
||||
const locale: typeof enUS = {
|
||||
'sheets-filter-ui': {
|
||||
toolbar: {
|
||||
'smart-toggle-filter-tooltip': 'Aktifkan/Nonaktifkan Filter',
|
||||
'clear-filter-criteria': 'Hapus Kondisi Filter',
|
||||
're-calc-filter-conditions': 'Hitung Ulang Kondisi Filter',
|
||||
},
|
||||
shortcut: {
|
||||
'smart-toggle-filter': 'Aktifkan/Nonaktifkan Filter',
|
||||
},
|
||||
permission: {
|
||||
filterErr: 'Anda tidak memiliki izin untuk menggunakan filter.',
|
||||
},
|
||||
panel: {
|
||||
'clear-filter': 'Hapus Filter',
|
||||
cancel: 'Batal',
|
||||
confirm: 'Konfirmasi',
|
||||
mobile: {
|
||||
resize: 'Ubah ukuran panel filter',
|
||||
back: 'Kembali',
|
||||
column: 'Kolom {0}',
|
||||
'previous-column': 'Kolom sebelumnya',
|
||||
'next-column': 'Kolom berikutnya',
|
||||
},
|
||||
'by-values': 'Berdasarkan Nilai',
|
||||
'by-colors': 'Berdasarkan Warna',
|
||||
'filter-by-cell-fill-color': 'Filter berdasarkan warna isi sel',
|
||||
'filter-by-cell-text-color': 'Filter berdasarkan warna teks sel',
|
||||
'filter-by-color-none': 'Kolom hanya berisi satu warna',
|
||||
'by-conditions': 'Berdasarkan Kondisi',
|
||||
'filter-only': 'Hanya Filter',
|
||||
'search-placeholder': 'Gunakan spasi untuk memisahkan kata kunci',
|
||||
'select-all': 'Pilih Semua',
|
||||
'input-values-placeholder': 'Masukkan Nilai',
|
||||
and: 'DAN',
|
||||
or: 'ATAU',
|
||||
empty: '(kosong)',
|
||||
'?': 'Gunakan "?" untuk mewakili satu karakter.',
|
||||
'*': 'Gunakan "*" untuk mewakili banyak karakter.',
|
||||
},
|
||||
conditions: {
|
||||
none: 'Tidak ada',
|
||||
empty: 'Kosong',
|
||||
'not-empty': 'Tidak Kosong',
|
||||
'text-contains': 'Teks Mengandung',
|
||||
'does-not-contain': 'Teks Tidak Mengandung',
|
||||
'starts-with': 'Teks Dimulai Dengan',
|
||||
'ends-with': 'Teks Diakhiri Dengan',
|
||||
equals: 'Teks Sama Dengan',
|
||||
'greater-than': 'Lebih Besar Dari',
|
||||
'greater-than-or-equal': 'Lebih Besar Dari Atau Sama Dengan',
|
||||
'less-than': 'Lebih Kecil Dari',
|
||||
'less-than-or-equal': 'Lebih Kecil Dari Atau Sama Dengan',
|
||||
equal: 'Sama Dengan',
|
||||
'not-equal': 'Tidak Sama Dengan',
|
||||
between: 'Di Antara',
|
||||
'not-between': 'Tidak Di Antara',
|
||||
custom: 'Kustom',
|
||||
},
|
||||
date: {
|
||||
1: 'Januari',
|
||||
2: 'Februari',
|
||||
3: 'Maret',
|
||||
4: 'April',
|
||||
5: 'Mei',
|
||||
6: 'Juni',
|
||||
7: 'Juli',
|
||||
8: 'Agustus',
|
||||
9: 'September',
|
||||
10: 'Oktober',
|
||||
11: 'November',
|
||||
12: 'Desember',
|
||||
},
|
||||
sync: {
|
||||
title: 'Filter terlihat oleh semua orang',
|
||||
statusTips: {
|
||||
on: 'Jika diaktifkan, semua kolaborator akan melihat hasil filter',
|
||||
off: 'Jika dinonaktifkan, hanya Anda yang akan melihat hasil filter',
|
||||
},
|
||||
switchTips: {
|
||||
on: '"Filter terlihat oleh semua orang" diaktifkan, semua kolaborator akan melihat hasil filter',
|
||||
off: '"Filter terlihat oleh semua orang" dinonaktifkan, hanya Anda yang akan melihat hasil filter',
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default locale;
|
||||
106
packages/sheets-filter-ui/src/locale/it-IT.ts
Normal file
106
packages/sheets-filter-ui/src/locale/it-IT.ts
Normal file
@@ -0,0 +1,106 @@
|
||||
/**
|
||||
* Copyright 2023-present DreamNum Co., Ltd.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import type enUS from './en-US';
|
||||
|
||||
const locale: typeof enUS = {
|
||||
'sheets-filter-ui': {
|
||||
toolbar: {
|
||||
'smart-toggle-filter-tooltip': 'Attiva/disattiva filtro',
|
||||
'clear-filter-criteria': 'Cancella condizioni filtro',
|
||||
're-calc-filter-conditions': 'Ricalcola condizioni filtro',
|
||||
},
|
||||
shortcut: {
|
||||
'smart-toggle-filter': 'Attiva/disattiva filtro',
|
||||
},
|
||||
permission: {
|
||||
filterErr: 'Non hai l’autorizzazione per usare il filtro.',
|
||||
},
|
||||
panel: {
|
||||
'clear-filter': 'Cancella filtro',
|
||||
cancel: 'Annulla',
|
||||
confirm: 'Conferma',
|
||||
mobile: {
|
||||
resize: 'Ridimensiona il pannello filtro',
|
||||
back: 'Indietro',
|
||||
column: 'Colonna {0}',
|
||||
'previous-column': 'Colonna precedente',
|
||||
'next-column': 'Colonna successiva',
|
||||
},
|
||||
'by-values': 'Per valori',
|
||||
'by-colors': 'Per colori',
|
||||
'filter-by-cell-fill-color': 'Filtra per colore di riempimento cella',
|
||||
'filter-by-cell-text-color': 'Filtra per colore testo cella',
|
||||
'filter-by-color-none': 'La colonna contiene un solo colore',
|
||||
'by-conditions': 'Per condizioni',
|
||||
'filter-only': 'Solo filtro',
|
||||
'search-placeholder': 'Usa lo spazio per separare le parole chiave',
|
||||
'select-all': 'Seleziona tutto',
|
||||
'input-values-placeholder': 'Inserisci valori',
|
||||
and: 'E',
|
||||
or: 'O',
|
||||
empty: '(vuoto)',
|
||||
'?': 'Usa "?" per rappresentare un singolo carattere.',
|
||||
'*': 'Usa "*" per rappresentare più caratteri.',
|
||||
},
|
||||
conditions: {
|
||||
none: 'Nessuna',
|
||||
empty: 'È vuoto',
|
||||
'not-empty': 'Non è vuoto',
|
||||
'text-contains': 'Il testo contiene',
|
||||
'does-not-contain': 'Il testo non contiene',
|
||||
'starts-with': 'Il testo inizia con',
|
||||
'ends-with': 'Il testo finisce con',
|
||||
equals: 'Il testo è uguale a',
|
||||
'greater-than': 'Maggiore di',
|
||||
'greater-than-or-equal': 'Maggiore o uguale a',
|
||||
'less-than': 'Minore di',
|
||||
'less-than-or-equal': 'Minore o uguale a',
|
||||
equal: 'Uguale',
|
||||
'not-equal': 'Diverso',
|
||||
between: 'Tra',
|
||||
'not-between': 'Non tra',
|
||||
custom: 'Personalizzato',
|
||||
},
|
||||
date: {
|
||||
1: 'Gennaio',
|
||||
2: 'Febbraio',
|
||||
3: 'Marzo',
|
||||
4: 'Aprile',
|
||||
5: 'Maggio',
|
||||
6: 'Giugno',
|
||||
7: 'Luglio',
|
||||
8: 'Agosto',
|
||||
9: 'Settembre',
|
||||
10: 'Ottobre',
|
||||
11: 'Novembre',
|
||||
12: 'Dicembre',
|
||||
},
|
||||
sync: {
|
||||
title: 'Il filtro è visibile a tutti',
|
||||
statusTips: {
|
||||
on: 'Se attivato, tutti i collaboratori vedranno i risultati del filtro',
|
||||
off: 'Se disattivato, solo tu vedrai i risultati del filtro',
|
||||
},
|
||||
switchTips: {
|
||||
on: '"Il filtro è visibile a tutti" è attivato, tutti i collaboratori vedranno i risultati del filtro',
|
||||
off: '"Il filtro è visibile a tutti" è disattivato, solo tu vedrai i risultati del filtro',
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default locale;
|
||||
106
packages/sheets-filter-ui/src/locale/ja-JP.ts
Normal file
106
packages/sheets-filter-ui/src/locale/ja-JP.ts
Normal file
@@ -0,0 +1,106 @@
|
||||
/**
|
||||
* Copyright 2023-present DreamNum Co., Ltd.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import type enUS from './en-US';
|
||||
|
||||
const locale: typeof enUS = {
|
||||
'sheets-filter-ui': {
|
||||
toolbar: {
|
||||
'smart-toggle-filter-tooltip': 'フィルタの切替',
|
||||
'clear-filter-criteria': 'フィルタ条件をクリア',
|
||||
're-calc-filter-conditions': 'フィルタ条件を再計算',
|
||||
},
|
||||
shortcut: {
|
||||
'smart-toggle-filter': 'フィルタの切替',
|
||||
},
|
||||
permission: {
|
||||
filterErr: 'フィルタを使用する権限がありません。',
|
||||
},
|
||||
panel: {
|
||||
'clear-filter': 'フィルタをクリア',
|
||||
cancel: 'キャンセル',
|
||||
confirm: '確認',
|
||||
mobile: {
|
||||
resize: 'フィルターパネルのサイズを変更',
|
||||
back: '戻る',
|
||||
column: '{0} 列',
|
||||
'previous-column': '前の列',
|
||||
'next-column': '次の列',
|
||||
},
|
||||
'by-values': '値でフィルタ',
|
||||
'by-colors': '色でフィルタ',
|
||||
'filter-by-cell-fill-color': 'セルの塗りつぶし色でフィルタ',
|
||||
'filter-by-cell-text-color': 'セルの文字色でフィルタ',
|
||||
'filter-by-color-none': 'この列は単一の色のみを含んでいます',
|
||||
'by-conditions': '条件でフィルタ',
|
||||
'filter-only': 'フィルタのみ表示',
|
||||
'search-placeholder': '検索語はスペースで区切る',
|
||||
'select-all': 'すべて選択',
|
||||
'input-values-placeholder': '値を入力',
|
||||
and: 'AND',
|
||||
or: 'OR',
|
||||
empty: '(空白)',
|
||||
'?': '「?」は任意の1文字',
|
||||
'*': '「*」は任意の複数文字',
|
||||
},
|
||||
conditions: {
|
||||
none: 'なし',
|
||||
empty: '空白セル',
|
||||
'not-empty': '空白ではない',
|
||||
'text-contains': '文字列を含む',
|
||||
'does-not-contain': '文字列を含まない',
|
||||
'starts-with': '前方一致',
|
||||
'ends-with': '後方一致',
|
||||
equals: '一致',
|
||||
'greater-than': 'より大きい',
|
||||
'greater-than-or-equal': '以上',
|
||||
'less-than': 'より小さい',
|
||||
'less-than-or-equal': '以下',
|
||||
equal: '等しい',
|
||||
'not-equal': '等しくない',
|
||||
between: '範囲内',
|
||||
'not-between': '範囲外',
|
||||
custom: 'カスタム',
|
||||
},
|
||||
date: {
|
||||
1: '1月',
|
||||
2: '2月',
|
||||
3: '3月',
|
||||
4: '4月',
|
||||
5: '5月',
|
||||
6: '6月',
|
||||
7: '7月',
|
||||
8: '8月',
|
||||
9: '9月',
|
||||
10: '10月',
|
||||
11: '11月',
|
||||
12: '12月',
|
||||
},
|
||||
sync: {
|
||||
title: 'フィルタを全員が表示可能',
|
||||
statusTips: {
|
||||
on: '有効にすると、すべての共同作業者がフィルタ結果を表示できます。',
|
||||
off: '無効にすると、自分のみフィルタ結果を表示できます。',
|
||||
},
|
||||
switchTips: {
|
||||
on: '「フィルタを全員が表示可能」が有効な場合、すべての共同作業者がフィルタ結果を表示できます。',
|
||||
off: '「フィルタを全員が表示可能」が無効な場合、自分のみフィルタ結果を表示できます。',
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default locale;
|
||||
106
packages/sheets-filter-ui/src/locale/ko-KR.ts
Normal file
106
packages/sheets-filter-ui/src/locale/ko-KR.ts
Normal file
@@ -0,0 +1,106 @@
|
||||
/**
|
||||
* Copyright 2023-present DreamNum Co., Ltd.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import type enUS from './en-US';
|
||||
|
||||
const locale: typeof enUS = {
|
||||
'sheets-filter-ui': {
|
||||
toolbar: {
|
||||
'smart-toggle-filter-tooltip': '필터 토글',
|
||||
'clear-filter-criteria': '필터 조건 지우기',
|
||||
're-calc-filter-conditions': '필터 조건 재계산',
|
||||
},
|
||||
shortcut: {
|
||||
'smart-toggle-filter': '필터 토글',
|
||||
},
|
||||
permission: {
|
||||
filterErr: '필터를 사용할 권한이 없습니다.',
|
||||
},
|
||||
panel: {
|
||||
'clear-filter': '필터 지우기',
|
||||
cancel: '취소',
|
||||
confirm: '확인',
|
||||
mobile: {
|
||||
resize: '필터 패널 크기 조정',
|
||||
back: '뒤로',
|
||||
column: '{0} 열',
|
||||
'previous-column': '이전 열',
|
||||
'next-column': '다음 열',
|
||||
},
|
||||
'by-values': '값으로 필터',
|
||||
'by-colors': '색상으로 필터',
|
||||
'filter-by-cell-fill-color': '셀 채우기 색상으로 필터',
|
||||
'filter-by-cell-text-color': '셀 텍스트 색상으로 필터',
|
||||
'filter-by-color-none': '이 열은 단일 색상만 포함합니다',
|
||||
'by-conditions': '조건으로 필터',
|
||||
'filter-only': '필터만 보기',
|
||||
'search-placeholder': '검색어는 공백으로 구분',
|
||||
'select-all': '전체 선택',
|
||||
'input-values-placeholder': '값 입력',
|
||||
and: 'AND',
|
||||
or: 'OR',
|
||||
empty: '(빈 값)',
|
||||
'?': '“?”는 임의의 한 문자',
|
||||
'*': '“*”는 임의의 여러 문자',
|
||||
},
|
||||
conditions: {
|
||||
none: '없음',
|
||||
empty: '빈 셀',
|
||||
'not-empty': '빈 셀 아님',
|
||||
'text-contains': '텍스트 포함',
|
||||
'does-not-contain': '텍스트 미포함',
|
||||
'starts-with': '텍스트 시작',
|
||||
'ends-with': '텍스트 끝',
|
||||
equals: '텍스트 일치',
|
||||
'greater-than': '크다',
|
||||
'greater-than-or-equal': '크거나 같다',
|
||||
'less-than': '작다',
|
||||
'less-than-or-equal': '작거나 같다',
|
||||
equal: '같음',
|
||||
'not-equal': '같지 않음',
|
||||
between: '범위 내',
|
||||
'not-between': '범위 밖',
|
||||
custom: '사용자 지정',
|
||||
},
|
||||
date: {
|
||||
1: '1월',
|
||||
2: '2월',
|
||||
3: '3월',
|
||||
4: '4월',
|
||||
5: '5월',
|
||||
6: '6월',
|
||||
7: '7월',
|
||||
8: '8월',
|
||||
9: '9월',
|
||||
10: '10월',
|
||||
11: '11월',
|
||||
12: '12월',
|
||||
},
|
||||
sync: {
|
||||
title: '필터를 모든 사용자가 볼 수 있음',
|
||||
statusTips: {
|
||||
on: '활성화하면 모든 협업자가 필터 결과를 볼 수 있습니다.',
|
||||
off: '비활성화하면 나만 필터 결과를 볼 수 있습니다.',
|
||||
},
|
||||
switchTips: {
|
||||
on: '"필터를 모든 사용자가 볼 수 있음"이 활성화되어 모든 협업자가 필터 결과를 볼 수 있습니다.',
|
||||
off: '"필터를 모든 사용자가 볼 수 있음"이 비활성화되어 나만 필터 결과를 볼 수 있습니다.',
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default locale;
|
||||
106
packages/sheets-filter-ui/src/locale/pl-PL.ts
Normal file
106
packages/sheets-filter-ui/src/locale/pl-PL.ts
Normal file
@@ -0,0 +1,106 @@
|
||||
/**
|
||||
* Copyright 2023-present DreamNum Co., Ltd.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import type enUS from './en-US';
|
||||
|
||||
const locale: typeof enUS = {
|
||||
'sheets-filter-ui': {
|
||||
toolbar: {
|
||||
'smart-toggle-filter-tooltip': 'Przełącz filtr',
|
||||
'clear-filter-criteria': 'Wyczyść warunki filtrowania',
|
||||
're-calc-filter-conditions': 'Przelicz warunki filtrowania',
|
||||
},
|
||||
shortcut: {
|
||||
'smart-toggle-filter': 'Przełącz filtr',
|
||||
},
|
||||
permission: {
|
||||
filterErr: 'Nie masz uprawnień do używania filtra.',
|
||||
},
|
||||
panel: {
|
||||
'clear-filter': 'Wyczyść filtr',
|
||||
cancel: 'Anuluj',
|
||||
confirm: 'Potwierdź',
|
||||
mobile: {
|
||||
resize: 'Zmień rozmiar panelu filtra',
|
||||
back: 'Wstecz',
|
||||
column: 'Kolumna {0}',
|
||||
'previous-column': 'Poprzednia kolumna',
|
||||
'next-column': 'Następna kolumna',
|
||||
},
|
||||
'by-values': 'Według wartości',
|
||||
'by-colors': 'Według kolorów',
|
||||
'filter-by-cell-fill-color': 'Filtruj według koloru wypełnienia komórki',
|
||||
'filter-by-cell-text-color': 'Filtruj według koloru tekstu komórki',
|
||||
'filter-by-color-none': 'Kolumna zawiera tylko jeden kolor',
|
||||
'by-conditions': 'Według warunków',
|
||||
'filter-only': 'Tylko filtr',
|
||||
'search-placeholder': 'Użyj spacji do oddzielenia słów kluczowych',
|
||||
'select-all': 'Zaznacz wszystko',
|
||||
'input-values-placeholder': 'Wprowadź wartości',
|
||||
and: 'ORAZ',
|
||||
or: 'LUB',
|
||||
empty: '(puste)',
|
||||
'?': 'Użyj „?”, aby reprezentować pojedynczy znak.',
|
||||
'*': 'Użyj „*”, aby reprezentować dowolną liczbę znaków.',
|
||||
},
|
||||
conditions: {
|
||||
none: 'Brak',
|
||||
empty: 'Jest puste',
|
||||
'not-empty': 'Nie jest puste',
|
||||
'text-contains': 'Tekst zawiera',
|
||||
'does-not-contain': 'Tekst nie zawiera',
|
||||
'starts-with': 'Tekst zaczyna się od',
|
||||
'ends-with': 'Tekst kończy się na',
|
||||
equals: 'Tekst równa się',
|
||||
'greater-than': 'Większe niż',
|
||||
'greater-than-or-equal': 'Większe lub równe',
|
||||
'less-than': 'Mniejsze niż',
|
||||
'less-than-or-equal': 'Mniejsze lub równe',
|
||||
equal: 'Równe',
|
||||
'not-equal': 'Różne od',
|
||||
between: 'Między',
|
||||
'not-between': 'Nie między',
|
||||
custom: 'Niestandardowe',
|
||||
},
|
||||
date: {
|
||||
1: 'Styczeń',
|
||||
2: 'Luty',
|
||||
3: 'Marzec',
|
||||
4: 'Kwiecień',
|
||||
5: 'Maj',
|
||||
6: 'Czerwiec',
|
||||
7: 'Lipiec',
|
||||
8: 'Sierpień',
|
||||
9: 'Wrzesień',
|
||||
10: 'Październik',
|
||||
11: 'Listopad',
|
||||
12: 'Grudzień',
|
||||
},
|
||||
sync: {
|
||||
title: 'Filtr jest widoczny dla wszystkich',
|
||||
statusTips: {
|
||||
on: 'Po włączeniu wszyscy współpracownicy zobaczą wyniki filtrowania',
|
||||
off: 'Po wyłączeniu tylko Ty zobaczysz wyniki filtrowania',
|
||||
},
|
||||
switchTips: {
|
||||
on: '„Filtr jest widoczny dla wszystkich” jest włączony, wszyscy współpracownicy zobaczą wyniki filtrowania',
|
||||
off: '„Filtr jest widoczny dla wszystkich” jest wyłączony, tylko Ty zobaczysz wyniki filtrowania',
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default locale;
|
||||
106
packages/sheets-filter-ui/src/locale/pt-BR.ts
Normal file
106
packages/sheets-filter-ui/src/locale/pt-BR.ts
Normal file
@@ -0,0 +1,106 @@
|
||||
/**
|
||||
* Copyright 2023-present DreamNum Co., Ltd.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import type enUS from './en-US';
|
||||
|
||||
const locale: typeof enUS = {
|
||||
'sheets-filter-ui': {
|
||||
toolbar: {
|
||||
'smart-toggle-filter-tooltip': 'Alternar Filtro',
|
||||
'clear-filter-criteria': 'Limpar Condições de Filtro',
|
||||
're-calc-filter-conditions': 'Recalcular Condições de Filtro',
|
||||
},
|
||||
shortcut: {
|
||||
'smart-toggle-filter': 'Alternar Filtro',
|
||||
},
|
||||
permission: {
|
||||
filterErr: 'Você não tem permissão para usar o filtro.',
|
||||
},
|
||||
panel: {
|
||||
'clear-filter': 'Limpar Filtro',
|
||||
cancel: 'Cancelar',
|
||||
confirm: 'Confirmar',
|
||||
mobile: {
|
||||
resize: 'Redimensionar o painel de filtro',
|
||||
back: 'Voltar',
|
||||
column: 'Coluna {0}',
|
||||
'previous-column': 'Coluna anterior',
|
||||
'next-column': 'Próxima coluna',
|
||||
},
|
||||
'by-values': 'Por Valores',
|
||||
'by-colors': 'Por Cores',
|
||||
'filter-by-cell-fill-color': 'Filtrar por cor de preenchimento da célula',
|
||||
'filter-by-cell-text-color': 'Filtrar por cor do texto da célula',
|
||||
'filter-by-color-none': 'A coluna contém apenas uma cor',
|
||||
'by-conditions': 'Por Condições',
|
||||
'filter-only': 'Apenas Filtrar',
|
||||
'search-placeholder': 'Use espaço para separar palavras-chave',
|
||||
'select-all': 'Selecionar Todos',
|
||||
'input-values-placeholder': 'Inserir Valores',
|
||||
and: 'E',
|
||||
or: 'OU',
|
||||
empty: '(vazio)',
|
||||
'?': 'Use "?" para representar um único caractere.',
|
||||
'*': 'Use "*" para representar vários caracteres.',
|
||||
},
|
||||
conditions: {
|
||||
none: 'Nenhum',
|
||||
empty: 'Está Vazio',
|
||||
'not-empty': 'Não Está Vazio',
|
||||
'text-contains': 'O Texto Contém',
|
||||
'does-not-contain': 'O Texto Não Contém',
|
||||
'starts-with': 'O Texto Começa Com',
|
||||
'ends-with': 'O Texto Termina Com',
|
||||
equals: 'O Texto É Igual A',
|
||||
'greater-than': 'Maior Que',
|
||||
'greater-than-or-equal': 'Maior Ou Igual A',
|
||||
'less-than': 'Menor Que',
|
||||
'less-than-or-equal': 'Menor Ou Igual A',
|
||||
equal: 'Igual',
|
||||
'not-equal': 'Diferente',
|
||||
between: 'Entre',
|
||||
'not-between': 'Não Está Entre',
|
||||
custom: 'Personalizado',
|
||||
},
|
||||
date: {
|
||||
1: 'Janeiro',
|
||||
2: 'Fevereiro',
|
||||
3: 'Março',
|
||||
4: 'Abril',
|
||||
5: 'Maio',
|
||||
6: 'Junho',
|
||||
7: 'Julho',
|
||||
8: 'Agosto',
|
||||
9: 'Setembro',
|
||||
10: 'Outubro',
|
||||
11: 'Novembro',
|
||||
12: 'Dezembro',
|
||||
},
|
||||
sync: {
|
||||
title: 'O filtro é visível para todos',
|
||||
statusTips: {
|
||||
on: 'Uma vez ativado, todos os colaboradores verão os resultados do filtro',
|
||||
off: 'Uma vez desativado, apenas você verá os resultados do filtro',
|
||||
},
|
||||
switchTips: {
|
||||
on: '"O filtro é visível para todos" está ativado, todos os colaboradores verão os resultados do filtro',
|
||||
off: '"O filtro é visível para todos" está desativado, apenas você verá os resultados do filtro',
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default locale;
|
||||
106
packages/sheets-filter-ui/src/locale/ru-RU.ts
Normal file
106
packages/sheets-filter-ui/src/locale/ru-RU.ts
Normal file
@@ -0,0 +1,106 @@
|
||||
/**
|
||||
* Copyright 2023-present DreamNum Co., Ltd.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import type enUS from './en-US';
|
||||
|
||||
const locale: typeof enUS = {
|
||||
'sheets-filter-ui': {
|
||||
toolbar: {
|
||||
'smart-toggle-filter-tooltip': 'Переключить фильтр',
|
||||
'clear-filter-criteria': 'Очистить условия фильтра',
|
||||
're-calc-filter-conditions': 'Пересчитать условия фильтра',
|
||||
},
|
||||
shortcut: {
|
||||
'smart-toggle-filter': 'Переключить фильтр',
|
||||
},
|
||||
permission: {
|
||||
filterErr: 'У вас нет разрешения на использование фильтра.',
|
||||
},
|
||||
panel: {
|
||||
'clear-filter': 'Очистить фильтр',
|
||||
cancel: 'Отмена',
|
||||
confirm: 'Подтвердить',
|
||||
mobile: {
|
||||
resize: 'Изменить размер панели фильтра',
|
||||
back: 'Назад',
|
||||
column: 'Столбец {0}',
|
||||
'previous-column': 'Предыдущий столбец',
|
||||
'next-column': 'Следующий столбец',
|
||||
},
|
||||
'by-values': 'По значениям',
|
||||
'by-colors': 'По цветам',
|
||||
'filter-by-cell-fill-color': 'Фильтр по цвету заливки ячейки',
|
||||
'filter-by-cell-text-color': 'Фильтр по цвету текста ячейки',
|
||||
'filter-by-color-none': 'Этот столбец содержит только один цвет',
|
||||
'by-conditions': 'По условиям',
|
||||
'filter-only': 'Только фильтр',
|
||||
'search-placeholder': 'Используйте пробел для разделения ключевых слов',
|
||||
'select-all': 'Выбрать все',
|
||||
'input-values-placeholder': 'Введите значения',
|
||||
and: 'И',
|
||||
or: 'ИЛИ',
|
||||
empty: '(пусто)',
|
||||
'?': 'Используйте “?” для обозначения одного символа.',
|
||||
'*': 'Используйте “*” для обозначения нескольких символов.',
|
||||
},
|
||||
conditions: {
|
||||
none: 'Нет',
|
||||
empty: 'Пусто',
|
||||
'not-empty': 'Не пусто',
|
||||
'text-contains': 'Содержит текст',
|
||||
'does-not-contain': 'Не содержит текст',
|
||||
'starts-with': 'Начинается с',
|
||||
'ends-with': 'Заканчивается на',
|
||||
equals: 'Равно',
|
||||
'greater-than': 'Больше чем',
|
||||
'greater-than-or-equal': 'Больше или равно',
|
||||
'less-than': 'Меньше чем',
|
||||
'less-than-or-equal': 'Меньше или равно',
|
||||
equal: 'Равно',
|
||||
'not-equal': 'Не равно',
|
||||
between: 'Между',
|
||||
'not-between': 'Не между',
|
||||
custom: 'Пользовательский',
|
||||
},
|
||||
date: {
|
||||
1: 'Январь',
|
||||
2: 'Февраль',
|
||||
3: 'Март',
|
||||
4: 'Апрель',
|
||||
5: 'Май',
|
||||
6: 'Июнь',
|
||||
7: 'Июль',
|
||||
8: 'Август',
|
||||
9: 'Сентябрь',
|
||||
10: 'Октябрь',
|
||||
11: 'Ноябрь',
|
||||
12: 'Декабрь',
|
||||
},
|
||||
sync: {
|
||||
title: 'Фильтр виден всем',
|
||||
statusTips: {
|
||||
on: 'После включения все сотрудники увидят результаты фильтрации',
|
||||
off: 'После отключения только вы увидите результаты фильтрации',
|
||||
},
|
||||
switchTips: {
|
||||
on: 'Функция "Фильтр виден всем" включена, все сотрудники увидят результаты фильтрации',
|
||||
off: 'Функция "Фильтр виден всем" отключена, только вы увидите результаты фильтрации',
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default locale;
|
||||
106
packages/sheets-filter-ui/src/locale/sk-SK.ts
Normal file
106
packages/sheets-filter-ui/src/locale/sk-SK.ts
Normal file
@@ -0,0 +1,106 @@
|
||||
/**
|
||||
* Copyright 2023-present DreamNum Co., Ltd.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import type enUS from './en-US';
|
||||
|
||||
const locale: typeof enUS = {
|
||||
'sheets-filter-ui': {
|
||||
toolbar: {
|
||||
'smart-toggle-filter-tooltip': 'Prepnúť filter',
|
||||
'clear-filter-criteria': 'Vymazať podmienky filtra',
|
||||
're-calc-filter-conditions': 'Prepočítať podmienky filtra',
|
||||
},
|
||||
shortcut: {
|
||||
'smart-toggle-filter': 'Prepnúť filter',
|
||||
},
|
||||
permission: {
|
||||
filterErr: 'Nemáte povolenie používať filter.',
|
||||
},
|
||||
panel: {
|
||||
'clear-filter': 'Vymazať filter',
|
||||
cancel: 'Zrušiť',
|
||||
confirm: 'Potvrdiť',
|
||||
mobile: {
|
||||
resize: 'Zmeniť veľkosť panela filtra',
|
||||
back: 'Späť',
|
||||
column: 'Stĺpec {0}',
|
||||
'previous-column': 'Predchádzajúci stĺpec',
|
||||
'next-column': 'Nasledujúci stĺpec',
|
||||
},
|
||||
'by-values': 'Podľa hodnôt',
|
||||
'by-colors': 'Podľa farieb',
|
||||
'filter-by-cell-fill-color': 'Filtrovať podľa farby výplne bunky',
|
||||
'filter-by-cell-text-color': 'Filtrovať podľa farby textu bunky',
|
||||
'filter-by-color-none': 'Stĺpec obsahuje iba jednu farbu',
|
||||
'by-conditions': 'Podľa podmienok',
|
||||
'filter-only': 'Len filtrovať',
|
||||
'search-placeholder': 'Použite medzeru na oddelenie kľúčových slov',
|
||||
'select-all': 'Vybrať všetko',
|
||||
'input-values-placeholder': 'Zadajte hodnoty',
|
||||
and: 'A',
|
||||
or: 'ALEBO',
|
||||
empty: '(prázdne)',
|
||||
'?': 'Použite „?“ na označenie jedného znaku.',
|
||||
'*': 'Použite „*“ na označenie viacerých znakov.',
|
||||
},
|
||||
conditions: {
|
||||
none: 'Žiadne',
|
||||
empty: 'Je prázdne',
|
||||
'not-empty': 'Nie je prázdne',
|
||||
'text-contains': 'Text obsahuje',
|
||||
'does-not-contain': 'Text neobsahuje',
|
||||
'starts-with': 'Text začína na',
|
||||
'ends-with': 'Text končí na',
|
||||
equals: 'Text sa rovná',
|
||||
'greater-than': 'Väčšie ako',
|
||||
'greater-than-or-equal': 'Väčšie alebo rovné',
|
||||
'less-than': 'Menšie ako',
|
||||
'less-than-or-equal': 'Menšie alebo rovné',
|
||||
equal: 'Rovné',
|
||||
'not-equal': 'Nie je rovné',
|
||||
between: 'Medzi',
|
||||
'not-between': 'Nie medzi',
|
||||
custom: 'Vlastné',
|
||||
},
|
||||
date: {
|
||||
1: 'Január',
|
||||
2: 'Február',
|
||||
3: 'Marec',
|
||||
4: 'Apríl',
|
||||
5: 'Máj',
|
||||
6: 'Jún',
|
||||
7: 'Júl',
|
||||
8: 'August',
|
||||
9: 'September',
|
||||
10: 'Október',
|
||||
11: 'November',
|
||||
12: 'December',
|
||||
},
|
||||
sync: {
|
||||
title: 'Filter je viditeľný pre všetkých',
|
||||
statusTips: {
|
||||
on: 'Po zapnutí budú všetci spolupracovníci vidieť výsledky filtra',
|
||||
off: 'Po vypnutí budete výsledky filtra vidieť iba vy',
|
||||
},
|
||||
switchTips: {
|
||||
on: '"Filter je viditeľný pre všetkých" je zapnutý, všetci spolupracovníci uvidia výsledky filtra',
|
||||
off: '"Filter je viditeľný pre všetkých" je vypnutý, výsledky filtra uvidíte iba vy',
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default locale;
|
||||
20
packages/sheets-filter-ui/src/locale/types.ts
Normal file
20
packages/sheets-filter-ui/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>;
|
||||
107
packages/sheets-filter-ui/src/locale/vi-VN.ts
Normal file
107
packages/sheets-filter-ui/src/locale/vi-VN.ts
Normal file
@@ -0,0 +1,107 @@
|
||||
/**
|
||||
* 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-ui': {
|
||||
toolbar: {
|
||||
'smart-toggle-filter-tooltip': 'Lọc',
|
||||
'clear-filter-criteria': 'Xóa điều kiện lọc',
|
||||
're-calc-filter-conditions': 'Tính toán lại',
|
||||
},
|
||||
shortcut: {
|
||||
'smart-toggle-filter': 'Chuyển đổi lọc',
|
||||
},
|
||||
permission: {
|
||||
filterErr: 'Bạn không có quyền sử dụng bộ lọc.',
|
||||
},
|
||||
panel: {
|
||||
'clear-filter': 'Xóa lọc',
|
||||
cancel: 'Hủy bỏ',
|
||||
confirm: 'Xác nhận',
|
||||
mobile: {
|
||||
resize: 'Thay đổi kích thước bảng lọc',
|
||||
back: 'Quay lại',
|
||||
column: 'Cột {0}',
|
||||
'previous-column': 'Cột trước',
|
||||
'next-column': 'Cột tiếp theo',
|
||||
},
|
||||
'by-values': 'Theo giá trị',
|
||||
'by-colors': 'Theo màu sắc',
|
||||
'filter-by-cell-fill-color': 'Lọc theo màu nền ô',
|
||||
'filter-by-cell-text-color': 'Lọc theo màu chữ ô',
|
||||
'filter-by-color-none': 'Cột này chỉ chứa một màu',
|
||||
'by-conditions': 'Theo điều kiện',
|
||||
'filter-only': 'Chỉ lọc',
|
||||
'search-placeholder': 'Sử dụng khoảng trắng để tách các từ khóa',
|
||||
'select-all': 'Chọn tất cả',
|
||||
'input-values-placeholder': 'Vui lòng nhập',
|
||||
or: 'hoặc',
|
||||
and: 'và',
|
||||
empty: '(Trống)',
|
||||
'?': 'Sử dụng ? để đại diện cho một ký tự',
|
||||
'*': 'Sử dụng * để đại diện cho nhiều ký tự',
|
||||
},
|
||||
conditions: {
|
||||
none: 'Không',
|
||||
empty: 'Trống',
|
||||
'not-empty': 'Không trống',
|
||||
'text-contains': 'Văn bản chứa',
|
||||
'does-not-contain': 'Văn bản không chứa',
|
||||
'starts-with': 'Văn bản bắt đầu với',
|
||||
'ends-with': 'Văn bản kết thúc với',
|
||||
equals: 'Văn bản khớp',
|
||||
'greater-than': 'Lớn hơn',
|
||||
'greater-than-or-equal': 'Lớn hơn hoặc bằng',
|
||||
'less-than': 'Nhỏ hơn',
|
||||
'less-than-or-equal': 'Nhỏ hơn hoặc bằng',
|
||||
equal: 'Bằng',
|
||||
'not-equal': 'Không bằng',
|
||||
between: 'Giữa',
|
||||
'not-between': 'Không giữa',
|
||||
custom: 'Tùy chỉnh',
|
||||
},
|
||||
date: {
|
||||
1: 'Tháng 1',
|
||||
2: 'Tháng 2',
|
||||
3: 'Tháng 3',
|
||||
4: 'Tháng 4',
|
||||
5: 'Tháng 5',
|
||||
6: 'Tháng 6',
|
||||
7: 'Tháng 7',
|
||||
8: 'Tháng 8',
|
||||
9: 'Tháng 9',
|
||||
10: 'Tháng 10',
|
||||
11: 'Tháng 11',
|
||||
12: 'Tháng 12',
|
||||
},
|
||||
sync: {
|
||||
title: 'Lọc cho tất cả mọi người đều thấy',
|
||||
statusTips: {
|
||||
on: 'Khi bật, tất cả cộng tác viên sẽ thấy kết quả lọc',
|
||||
off: 'Khi tắt, chỉ bạn mới thấy kết quả lọc',
|
||||
},
|
||||
switchTips: {
|
||||
on: 'Đã bật "Lọc cho tất cả mọi người đều thấy", tất cả cộng tác viên sẽ thấy kết quả lọc',
|
||||
off: 'Đã tắt "Lọc cho tất cả mọi người đều thấy", chỉ bạn mới thấy kết quả lọc',
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
};
|
||||
|
||||
export default locale;
|
||||
106
packages/sheets-filter-ui/src/locale/zh-CN.ts
Normal file
106
packages/sheets-filter-ui/src/locale/zh-CN.ts
Normal file
@@ -0,0 +1,106 @@
|
||||
/**
|
||||
* Copyright 2023-present DreamNum Co., Ltd.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import type enUS from './en-US';
|
||||
|
||||
const locale: typeof enUS = {
|
||||
'sheets-filter-ui': {
|
||||
toolbar: {
|
||||
'smart-toggle-filter-tooltip': '筛选',
|
||||
'clear-filter-criteria': '清除筛选条件',
|
||||
're-calc-filter-conditions': '重新计算',
|
||||
},
|
||||
shortcut: {
|
||||
'smart-toggle-filter': '切换筛选',
|
||||
},
|
||||
permission: {
|
||||
filterErr: '你没有权限使用筛选。',
|
||||
},
|
||||
panel: {
|
||||
'clear-filter': '清除筛选',
|
||||
cancel: '取消',
|
||||
confirm: '确认',
|
||||
mobile: {
|
||||
resize: '调整筛选面板',
|
||||
back: '返回',
|
||||
column: '{0} 列',
|
||||
'previous-column': '上一列',
|
||||
'next-column': '下一列',
|
||||
},
|
||||
'by-values': '按值',
|
||||
'by-colors': '按颜色',
|
||||
'filter-by-cell-fill-color': '按单元格填充颜色筛选',
|
||||
'filter-by-cell-text-color': '按单元格文本颜色筛选',
|
||||
'filter-by-color-none': '该列仅包含一种颜色',
|
||||
'by-conditions': '按条件',
|
||||
'filter-only': '仅筛选',
|
||||
'search-placeholder': '使用空格分隔关键字',
|
||||
'select-all': '全选',
|
||||
'input-values-placeholder': '请输入',
|
||||
or: '或',
|
||||
and: '和',
|
||||
empty: '(空白)',
|
||||
'?': '可用 ? 代表单个字符',
|
||||
'*': '可用 * 代表任意多个字符',
|
||||
},
|
||||
conditions: {
|
||||
none: '无',
|
||||
empty: '为空',
|
||||
'not-empty': '不为空',
|
||||
'text-contains': '文本包含',
|
||||
'does-not-contain': '文本不包含',
|
||||
'starts-with': '文本开头',
|
||||
'ends-with': '文本结尾',
|
||||
equals: '文本相符',
|
||||
'greater-than': '大于',
|
||||
'greater-than-or-equal': '大于等于',
|
||||
'less-than': '小于',
|
||||
'less-than-or-equal': '小于等于',
|
||||
equal: '等于',
|
||||
'not-equal': '不等于',
|
||||
between: '介于',
|
||||
'not-between': '不介于',
|
||||
custom: '自定义',
|
||||
},
|
||||
date: {
|
||||
1: '1月',
|
||||
2: '2月',
|
||||
3: '3月',
|
||||
4: '4月',
|
||||
5: '5月',
|
||||
6: '6月',
|
||||
7: '7月',
|
||||
8: '8月',
|
||||
9: '9月',
|
||||
10: '10月',
|
||||
11: '11月',
|
||||
12: '12月',
|
||||
},
|
||||
sync: {
|
||||
title: '筛选对所有人可见',
|
||||
statusTips: {
|
||||
on: '开启后,所有协作者都将看到此次筛选结果',
|
||||
off: '关闭后,仅你可见此次筛选结果',
|
||||
},
|
||||
switchTips: {
|
||||
on: '已开启“筛选对所有人可见”,所有协作者都将看到此次筛选结果',
|
||||
off: '已关闭“筛选对所有人可见”,仅你可见此次筛选结果',
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default locale;
|
||||
106
packages/sheets-filter-ui/src/locale/zh-HK.ts
Normal file
106
packages/sheets-filter-ui/src/locale/zh-HK.ts
Normal file
@@ -0,0 +1,106 @@
|
||||
/**
|
||||
* Copyright 2023-present DreamNum Co., Ltd.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import type enUS from './en-US';
|
||||
|
||||
const locale: typeof enUS = {
|
||||
'sheets-filter-ui': {
|
||||
toolbar: {
|
||||
'smart-toggle-filter-tooltip': '篩選',
|
||||
'clear-filter-criteria': '清除篩選條件',
|
||||
're-calc-filter-conditions': '重新計算',
|
||||
},
|
||||
shortcut: {
|
||||
'smart-toggle-filter': '切換篩選',
|
||||
},
|
||||
permission: {
|
||||
filterErr: '你沒有權限使用篩選。',
|
||||
},
|
||||
panel: {
|
||||
'clear-filter': '清除篩選',
|
||||
cancel: '取消',
|
||||
confirm: '確認',
|
||||
mobile: {
|
||||
resize: '調整篩選面板',
|
||||
back: '返回',
|
||||
column: '{0} 欄',
|
||||
'previous-column': '上一欄',
|
||||
'next-column': '下一欄',
|
||||
},
|
||||
'by-values': '按值',
|
||||
'by-colors': '按顏色',
|
||||
'filter-by-cell-fill-color': '按儲存格填充顏色篩選',
|
||||
'filter-by-cell-text-color': '按儲存格文字顏色篩選',
|
||||
'filter-by-color-none': '該欄僅包含一種顏色',
|
||||
'by-conditions': '按條件',
|
||||
'filter-only': '僅篩選',
|
||||
'search-placeholder': '使用空格分隔關鍵字',
|
||||
'select-all': '全選',
|
||||
'input-values-placeholder': '請輸入',
|
||||
or: '或',
|
||||
and: '和',
|
||||
empty: '(空白)',
|
||||
'?': '可用 ? 代表單一字元',
|
||||
'*': '可用 * 代表任意多個字元',
|
||||
},
|
||||
conditions: {
|
||||
none: '無',
|
||||
empty: '為空',
|
||||
'not-empty': '不為空',
|
||||
'text-contains': '文字包含',
|
||||
'does-not-contain': '文字不包含',
|
||||
'starts-with': '文字開頭',
|
||||
'ends-with': '文字結尾',
|
||||
equals: '文字相符',
|
||||
'greater-than': '大於',
|
||||
'greater-than-or-equal': '大於等於',
|
||||
'less-than': '小於',
|
||||
'less-than-or-equal': '小於等於',
|
||||
equal: '等於',
|
||||
'not-equal': '不等於',
|
||||
between: '介於',
|
||||
'not-between': '不介於',
|
||||
custom: '自訂',
|
||||
},
|
||||
date: {
|
||||
1: '1月',
|
||||
2: '2月',
|
||||
3: '3月',
|
||||
4: '4月',
|
||||
5: '5月',
|
||||
6: '6月',
|
||||
7: '7月',
|
||||
8: '8月',
|
||||
9: '9月',
|
||||
10: '10月',
|
||||
11: '11月',
|
||||
12: '12月',
|
||||
},
|
||||
sync: {
|
||||
title: '篩選對所有人可見',
|
||||
statusTips: {
|
||||
on: '開啟後,所有協作者都將看到此次篩選結果',
|
||||
off: '關閉後,僅你可見此次篩選結果',
|
||||
},
|
||||
switchTips: {
|
||||
on: '已開啟“篩選對所有人可見”,所有協作者都將看到此次篩選結果',
|
||||
off: '已關閉“篩選對所有人可見”,僅你可見此次篩選結果',
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default locale;
|
||||
106
packages/sheets-filter-ui/src/locale/zh-TW.ts
Normal file
106
packages/sheets-filter-ui/src/locale/zh-TW.ts
Normal file
@@ -0,0 +1,106 @@
|
||||
/**
|
||||
* Copyright 2023-present DreamNum Co., Ltd.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import type enUS from './en-US';
|
||||
|
||||
const locale: typeof enUS = {
|
||||
'sheets-filter-ui': {
|
||||
toolbar: {
|
||||
'smart-toggle-filter-tooltip': '篩選',
|
||||
'clear-filter-criteria': '清除篩選條件',
|
||||
're-calc-filter-conditions': '重新計算',
|
||||
},
|
||||
shortcut: {
|
||||
'smart-toggle-filter': '切換篩選',
|
||||
},
|
||||
permission: {
|
||||
filterErr: '你沒有權限使用篩選。',
|
||||
},
|
||||
panel: {
|
||||
'clear-filter': '清除篩選',
|
||||
cancel: '取消',
|
||||
confirm: '確認',
|
||||
mobile: {
|
||||
resize: '調整篩選面板',
|
||||
back: '返回',
|
||||
column: '{0} 欄',
|
||||
'previous-column': '上一欄',
|
||||
'next-column': '下一欄',
|
||||
},
|
||||
'by-values': '按值',
|
||||
'by-colors': '按顏色',
|
||||
'filter-by-cell-fill-color': '按儲存格填充顏色篩選',
|
||||
'filter-by-cell-text-color': '按儲存格文字顏色篩選',
|
||||
'filter-by-color-none': '該欄僅包含一種顏色',
|
||||
'by-conditions': '按條件',
|
||||
'filter-only': '僅篩選',
|
||||
'search-placeholder': '使用空格分隔關鍵字',
|
||||
'select-all': '全選',
|
||||
'input-values-placeholder': '請輸入',
|
||||
or: '或',
|
||||
and: '和',
|
||||
empty: '(空白)',
|
||||
'?': '可用 ? 代表單一字元',
|
||||
'*': '可用 * 代表任意多個字元',
|
||||
},
|
||||
conditions: {
|
||||
none: '無',
|
||||
empty: '為空',
|
||||
'not-empty': '不為空',
|
||||
'text-contains': '文字包含',
|
||||
'does-not-contain': '文字不包含',
|
||||
'starts-with': '文字開頭',
|
||||
'ends-with': '文字結尾',
|
||||
equals: '文字相符',
|
||||
'greater-than': '大於',
|
||||
'greater-than-or-equal': '大於等於',
|
||||
'less-than': '小於',
|
||||
'less-than-or-equal': '小於等於',
|
||||
equal: '等於',
|
||||
'not-equal': '不等於',
|
||||
between: '介於',
|
||||
'not-between': '不介於',
|
||||
custom: '自訂',
|
||||
},
|
||||
date: {
|
||||
1: '1月',
|
||||
2: '2月',
|
||||
3: '3月',
|
||||
4: '4月',
|
||||
5: '5月',
|
||||
6: '6月',
|
||||
7: '7月',
|
||||
8: '8月',
|
||||
9: '9月',
|
||||
10: '10月',
|
||||
11: '11月',
|
||||
12: '12月',
|
||||
},
|
||||
sync: {
|
||||
title: '篩選對所有人可見',
|
||||
statusTips: {
|
||||
on: '開啟後,所有協作者都將看到此次篩選結果',
|
||||
off: '關閉後,僅你可見此次篩選結果',
|
||||
},
|
||||
switchTips: {
|
||||
on: '已開啟“篩選對所有人可見”,所有協作者都將看到此次篩選結果',
|
||||
off: '已關閉“篩選對所有人可見”,僅你可見此次篩選結果',
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default locale;
|
||||
46
packages/sheets-filter-ui/src/menu/schema.ts
Normal file
46
packages/sheets-filter-ui/src/menu/schema.ts
Normal file
@@ -0,0 +1,46 @@
|
||||
/**
|
||||
* 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 { MenuSchemaType } from '@univerjs/ui';
|
||||
import {
|
||||
ClearSheetsFilterCriteriaCommand,
|
||||
ReCalcSheetsFilterCommand,
|
||||
SmartToggleSheetsFilterCommand,
|
||||
} from '@univerjs/sheets-filter';
|
||||
import { RibbonDataGroup } from '@univerjs/ui';
|
||||
import {
|
||||
ClearFilterCriteriaMenuItemFactory,
|
||||
ReCalcFilterMenuItemFactory,
|
||||
SmartToggleFilterMenuItemFactory,
|
||||
} from './sheets-filter.menu';
|
||||
|
||||
export const menuSchema: MenuSchemaType = {
|
||||
[RibbonDataGroup.ORGANIZATION]: {
|
||||
[SmartToggleSheetsFilterCommand.id]: {
|
||||
order: 2,
|
||||
gridLayout: { row: 1, column: 2 },
|
||||
menuItemFactory: SmartToggleFilterMenuItemFactory,
|
||||
[ClearSheetsFilterCriteriaCommand.id]: {
|
||||
order: 0,
|
||||
menuItemFactory: ClearFilterCriteriaMenuItemFactory,
|
||||
},
|
||||
[ReCalcSheetsFilterCommand.id]: {
|
||||
order: 1,
|
||||
menuItemFactory: ReCalcFilterMenuItemFactory,
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
85
packages/sheets-filter-ui/src/menu/sheets-filter.menu.ts
Normal file
85
packages/sheets-filter-ui/src/menu/sheets-filter.menu.ts
Normal file
@@ -0,0 +1,85 @@
|
||||
/**
|
||||
* 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 } from '@univerjs/core';
|
||||
import type { IMenuButtonItem, IMenuSelectorItem } from '@univerjs/ui';
|
||||
import type { LocaleKey } from '../locale/types';
|
||||
import { UniverInstanceType } from '@univerjs/core';
|
||||
import {
|
||||
RangeProtectionPermissionViewPoint,
|
||||
WorksheetFilterPermission,
|
||||
WorksheetViewPermission,
|
||||
} from '@univerjs/sheets';
|
||||
import {
|
||||
ClearSheetsFilterCriteriaCommand,
|
||||
ReCalcSheetsFilterCommand,
|
||||
SheetsFilterService,
|
||||
SmartToggleSheetsFilterCommand,
|
||||
} from '@univerjs/sheets-filter';
|
||||
import { getCurrentRangeDisable$, getObservableWithExclusiveRange$ } from '@univerjs/sheets-ui';
|
||||
import { getMenuHiddenObservable, MenuItemType } from '@univerjs/ui';
|
||||
import { map, of, switchMap } from 'rxjs';
|
||||
|
||||
export function SmartToggleFilterMenuItemFactory(accessor: IAccessor): IMenuSelectorItem<LocaleKey> {
|
||||
const sheetsFilterService = accessor.get(SheetsFilterService);
|
||||
|
||||
return {
|
||||
id: SmartToggleSheetsFilterCommand.id,
|
||||
type: MenuItemType.BUTTON_SELECTOR,
|
||||
icon: 'FilterIcon',
|
||||
tooltip: 'sheets-filter-ui.toolbar.smart-toggle-filter-tooltip',
|
||||
hidden$: getMenuHiddenObservable(accessor, UniverInstanceType.UNIVER_SHEET),
|
||||
activated$: sheetsFilterService.activeFilterModel$.pipe(map((model) => !!model)),
|
||||
disabled$: getObservableWithExclusiveRange$(
|
||||
accessor,
|
||||
getCurrentRangeDisable$(
|
||||
accessor,
|
||||
{
|
||||
worksheetTypes: [WorksheetFilterPermission, WorksheetViewPermission],
|
||||
rangeTypes: [RangeProtectionPermissionViewPoint],
|
||||
}
|
||||
)
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
export function ClearFilterCriteriaMenuItemFactory(accessor: IAccessor): IMenuButtonItem<LocaleKey> {
|
||||
const sheetsFilterService = accessor.get(SheetsFilterService);
|
||||
|
||||
return {
|
||||
id: ClearSheetsFilterCriteriaCommand.id,
|
||||
type: MenuItemType.BUTTON,
|
||||
title: 'sheets-filter-ui.toolbar.clear-filter-criteria',
|
||||
hidden$: getMenuHiddenObservable(accessor, UniverInstanceType.UNIVER_SHEET),
|
||||
disabled$: sheetsFilterService
|
||||
.activeFilterModel$
|
||||
.pipe(switchMap((model) => model?.hasCriteria$.pipe(map((m) => !m)) ?? of(true))),
|
||||
};
|
||||
}
|
||||
|
||||
export function ReCalcFilterMenuItemFactory(accessor: IAccessor): IMenuButtonItem<LocaleKey> {
|
||||
const sheetsFilterService = accessor.get(SheetsFilterService);
|
||||
|
||||
return {
|
||||
id: ReCalcSheetsFilterCommand.id,
|
||||
type: MenuItemType.BUTTON,
|
||||
title: 'sheets-filter-ui.toolbar.re-calc-filter-conditions',
|
||||
hidden$: getMenuHiddenObservable(accessor, UniverInstanceType.UNIVER_SHEET),
|
||||
disabled$: sheetsFilterService
|
||||
.activeFilterModel$
|
||||
.pipe(switchMap((model) => model?.hasCriteria$.pipe(map((m) => !m)) ?? of(true))),
|
||||
};
|
||||
}
|
||||
84
packages/sheets-filter-ui/src/mobile-plugin.ts
Normal file
84
packages/sheets-filter-ui/src/mobile-plugin.ts
Normal file
@@ -0,0 +1,84 @@
|
||||
/**
|
||||
* 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 { IUniverSheetsFilterUIConfig } from './config/config';
|
||||
import { DependentOn, IConfigService, Inject, Injector, merge, Plugin, UniverInstanceType } from '@univerjs/core';
|
||||
import { UniverRenderEnginePlugin } from '@univerjs/engine-render';
|
||||
import { UniverSheetsPlugin } from '@univerjs/sheets';
|
||||
import { UniverSheetsFilterPlugin } from '@univerjs/sheets-filter';
|
||||
import { UniverSheetsMobileUIPlugin } from '@univerjs/sheets-ui';
|
||||
import { UniverMobileUIPlugin } from '@univerjs/ui';
|
||||
import pkg from '../package.json';
|
||||
import { defaultPluginConfig, SHEETS_FILTER_UI_PLUGIN_CONFIG_KEY } from './config/config';
|
||||
import { SheetsFilterMobileMenuController } from './controllers/mobile-menu.controller';
|
||||
import { MobileComponentsController } from './controllers/mobile/components.controller';
|
||||
import { SheetsFilterUIMobileController } from './controllers/mobile/ui.controller';
|
||||
import { SheetsFilterPermissionController } from './controllers/sheets-filter-permission.controller';
|
||||
import { ISheetsFilterPanelService, SheetsFilterPanelService } from './services/sheets-filter-panel.service';
|
||||
|
||||
@DependentOn(
|
||||
UniverRenderEnginePlugin,
|
||||
UniverSheetsPlugin,
|
||||
UniverMobileUIPlugin,
|
||||
UniverSheetsFilterPlugin,
|
||||
UniverSheetsMobileUIPlugin
|
||||
)
|
||||
export class UniverSheetsFilterMobileUIPlugin extends Plugin {
|
||||
static override type = UniverInstanceType.UNIVER_SHEET;
|
||||
static override pluginName = 'SHEET_FILTER_UI_PLUGIN';
|
||||
static override packageName = pkg.name;
|
||||
static override version = pkg.version;
|
||||
|
||||
constructor(
|
||||
private readonly _config: Partial<IUniverSheetsFilterUIConfig> = defaultPluginConfig,
|
||||
@Inject(Injector) protected readonly _injector: Injector,
|
||||
@IConfigService private readonly _configService: IConfigService
|
||||
) {
|
||||
super();
|
||||
|
||||
// Manage the plugin configuration.
|
||||
const { menu, ...rest } = merge(
|
||||
{},
|
||||
defaultPluginConfig,
|
||||
this._config
|
||||
);
|
||||
if (menu) {
|
||||
this._configService.setConfig('menu', menu, { merge: true });
|
||||
}
|
||||
this._configService.setConfig(SHEETS_FILTER_UI_PLUGIN_CONFIG_KEY, rest);
|
||||
}
|
||||
|
||||
override onStarting(): void {
|
||||
this._injector.add([MobileComponentsController]);
|
||||
this._injector.get(MobileComponentsController);
|
||||
([
|
||||
[ISheetsFilterPanelService, { useClass: SheetsFilterPanelService }],
|
||||
[SheetsFilterPermissionController],
|
||||
[SheetsFilterMobileMenuController],
|
||||
[SheetsFilterUIMobileController],
|
||||
] as Dependency[]).forEach((d) => this._injector.add(d));
|
||||
}
|
||||
|
||||
override onReady(): void {
|
||||
this._injector.get(SheetsFilterPermissionController);
|
||||
this._injector.get(SheetsFilterMobileMenuController);
|
||||
}
|
||||
|
||||
override onRendered(): void {
|
||||
this._injector.get(SheetsFilterUIMobileController);
|
||||
}
|
||||
}
|
||||
@@ -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 { BooleanNumber } from '@univerjs/core';
|
||||
import { CustomFilterOperator } from '@univerjs/sheets-filter';
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { FilterConditionItems } from '../conditions';
|
||||
import { ExtendCustomFilterOperator } from '../extended-operators';
|
||||
|
||||
describe('test "FilterConditionItems"', () => {
|
||||
describe('test utils work as expected in different condition format situations', () => {
|
||||
describe('test NONE condition', () => {
|
||||
it('should load NONE condition from empty filter column', () => {
|
||||
expect(FilterConditionItems.testMappingFilterColumn()).toEqual([FilterConditionItems.NONE, {}]);
|
||||
expect(FilterConditionItems.testMappingFilterColumn({ customFilters: undefined })).toEqual([FilterConditionItems.NONE, {}]);
|
||||
});
|
||||
|
||||
it('should throw error when trying to get default params', () => {
|
||||
expect(() => FilterConditionItems.NONE.getDefaultFormParams()).toThrowError();
|
||||
});
|
||||
|
||||
it('should mapped to its own operator', () => {
|
||||
expect(FilterConditionItems.NONE.testMappingParams({ operator1: ExtendCustomFilterOperator.NONE })).toBeTruthy();
|
||||
expect(FilterConditionItems.NONE.testMappingParams({ operator1: ExtendCustomFilterOperator.NOT_EQUALS })).toBeFalsy();
|
||||
});
|
||||
|
||||
it('should map to empty filter column', () => {
|
||||
// clear filter column
|
||||
expect(FilterConditionItems.NONE.mapToFilterColumn({ operator1: ExtendCustomFilterOperator.NONE })).toEqual(null);
|
||||
});
|
||||
});
|
||||
|
||||
describe('test EMPTY condition', () => {
|
||||
it('should load EMPTY condition from custom filter with empty string', () => {
|
||||
expect(FilterConditionItems.testMappingFilterColumn({ customFilters: { customFilters: [{ val: '' }] } }))
|
||||
.toEqual([FilterConditionItems.EMPTY, { operator1: ExtendCustomFilterOperator.EMPTY }]);
|
||||
});
|
||||
|
||||
it('should throw error when trying to get default params', () => {
|
||||
expect(() => FilterConditionItems.EMPTY.getDefaultFormParams()).toThrowError();
|
||||
});
|
||||
|
||||
it('should map to its own operator', () => {
|
||||
expect(FilterConditionItems.EMPTY.testMappingParams({ operator1: ExtendCustomFilterOperator.EMPTY })).toBeTruthy();
|
||||
expect(FilterConditionItems.EMPTY.testMappingParams({ operator1: ExtendCustomFilterOperator.NONE })).toBeFalsy();
|
||||
});
|
||||
|
||||
it('should map to filter column with empty string', () => {
|
||||
expect(FilterConditionItems.EMPTY.mapToFilterColumn({ operator1: ExtendCustomFilterOperator.EMPTY }))
|
||||
.toEqual({ customFilters: { customFilters: [{ val: '' }] } });
|
||||
});
|
||||
});
|
||||
|
||||
describe('test NOT_EMPTY condition', () => {
|
||||
it('should load NOT_EMPTY condition from custom filter with space string', () => {
|
||||
expect(FilterConditionItems.testMappingFilterColumn({ customFilters: { customFilters: [{ operator: CustomFilterOperator.NOT_EQUALS, val: ' ' }] } }))
|
||||
.toEqual([FilterConditionItems.NOT_EMPTY, { operator1: ExtendCustomFilterOperator.NOT_EMPTY }]);
|
||||
|
||||
// when the val is not a space char, it would map to NOT_EQUAL
|
||||
expect(FilterConditionItems.testMappingFilterColumn({
|
||||
customFilters: { customFilters: [{ operator: CustomFilterOperator.NOT_EQUALS, val: '123' }] },
|
||||
})).toEqual([FilterConditionItems.NOT_EQUAL, { operator1: CustomFilterOperator.NOT_EQUALS, val1: '123' }]);
|
||||
});
|
||||
|
||||
it('should throw error when trying to get default params', () => {
|
||||
expect(() => FilterConditionItems.NOT_EMPTY.getDefaultFormParams()).toThrowError();
|
||||
});
|
||||
|
||||
it('should map to its own operator', () => {
|
||||
expect(FilterConditionItems.NOT_EMPTY.testMappingParams({ operator1: ExtendCustomFilterOperator.NOT_EMPTY })).toBeTruthy();
|
||||
expect(FilterConditionItems.NOT_EMPTY.testMappingParams({ operator1: ExtendCustomFilterOperator.NONE })).toBeFalsy();
|
||||
});
|
||||
|
||||
it('should map to filter column with space string', () => {
|
||||
expect(FilterConditionItems.NOT_EMPTY.mapToFilterColumn({ operator1: ExtendCustomFilterOperator.NOT_EMPTY }))
|
||||
.toEqual({ customFilters: { customFilters: [{ operator: CustomFilterOperator.NOT_EQUALS, val: '' }] } });
|
||||
});
|
||||
});
|
||||
|
||||
describe('test TEXT_CONTAINS condition', () => {
|
||||
it('should load TEXT_CONTAINS condition from char set "*xxx*"', () => {
|
||||
expect(FilterConditionItems.testMappingFilterColumn({ customFilters: { customFilters: [{ val: '*xxx*' }] } }))
|
||||
.toEqual([FilterConditionItems.TEXT_CONTAINS, { operator1: ExtendCustomFilterOperator.CONTAINS, val1: 'xxx' }]);
|
||||
|
||||
expect(FilterConditionItems.testMappingFilterColumn({ customFilters: { customFilters: [{ val: '*xxx' }] } }))
|
||||
.not
|
||||
.toEqual([FilterConditionItems.TEXT_CONTAINS, { operator1: ExtendCustomFilterOperator.CONTAINS, val1: 'xxx' }]);
|
||||
});
|
||||
|
||||
it('should support default params with empty string', () => {
|
||||
expect(FilterConditionItems.TEXT_CONTAINS.getDefaultFormParams()).toEqual({ operator1: ExtendCustomFilterOperator.CONTAINS, val1: '' });
|
||||
});
|
||||
|
||||
it('should mapped params with only one operator CONTAINS', () => {
|
||||
expect(FilterConditionItems.TEXT_CONTAINS.testMappingParams({ operator1: ExtendCustomFilterOperator.CONTAINS })).toBeTruthy();
|
||||
expect(FilterConditionItems.TEXT_CONTAINS.testMappingParams({ operator1: ExtendCustomFilterOperator.DOES_NOT_CONTAIN })).toBeFalsy();
|
||||
expect(() => FilterConditionItems.TEXT_CONTAINS.testMappingParams({ operator1: ExtendCustomFilterOperator.CONTAINS, operator2: ExtendCustomFilterOperator.CONTAINS }))
|
||||
.toThrowError();
|
||||
|
||||
expect(FilterConditionItems.TEXT_CONTAINS.testMappingParams({ operator2: ExtendCustomFilterOperator.CONTAINS })).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should map to filter column with char set "*xxx*"', () => {
|
||||
expect(FilterConditionItems.TEXT_CONTAINS.mapToFilterColumn({ operator1: ExtendCustomFilterOperator.CONTAINS, val1: 'univer' }))
|
||||
.toEqual({ customFilters: { customFilters: [{ val: '*univer*' }] } });
|
||||
|
||||
expect(FilterConditionItems.TEXT_CONTAINS.mapToFilterColumn({ operator1: ExtendCustomFilterOperator.CONTAINS, val1: ' ' }))
|
||||
.toEqual({ customFilters: { customFilters: [{ val: '* *' }] } });
|
||||
});
|
||||
});
|
||||
|
||||
describe('test DOES_NOT_CONTAIN condition', () => {
|
||||
it('should load DOES_NOT_CONTAIN condition from char set "*xxx*"', () => {
|
||||
expect(FilterConditionItems.testMappingFilterColumn({ customFilters: { customFilters: [{ val: '*xxx*', operator: CustomFilterOperator.NOT_EQUALS }] } }))
|
||||
.toEqual([FilterConditionItems.DOES_NOT_CONTAIN, { operator1: ExtendCustomFilterOperator.DOES_NOT_CONTAIN, val1: 'xxx' }]);
|
||||
});
|
||||
|
||||
it('should support default params with empty string', () => {
|
||||
expect(FilterConditionItems.DOES_NOT_CONTAIN.getDefaultFormParams()).toEqual({ operator1: ExtendCustomFilterOperator.DOES_NOT_CONTAIN, val1: '' });
|
||||
});
|
||||
|
||||
it('should mapped params with only one operator DOES_NOT_CONTAIN', () => {
|
||||
expect(FilterConditionItems.DOES_NOT_CONTAIN.testMappingParams({ operator1: ExtendCustomFilterOperator.DOES_NOT_CONTAIN })).toBeTruthy();
|
||||
expect(FilterConditionItems.DOES_NOT_CONTAIN.testMappingParams({ operator1: ExtendCustomFilterOperator.CONTAINS })).toBeFalsy();
|
||||
expect(() => FilterConditionItems.DOES_NOT_CONTAIN.testMappingParams({ operator1: ExtendCustomFilterOperator.DOES_NOT_CONTAIN, operator2: ExtendCustomFilterOperator.DOES_NOT_CONTAIN }))
|
||||
.toThrowError();
|
||||
|
||||
expect(FilterConditionItems.DOES_NOT_CONTAIN.testMappingParams({ operator2: ExtendCustomFilterOperator.DOES_NOT_CONTAIN })).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should map to filter column with char set "*xxx*"', () => {
|
||||
expect(FilterConditionItems.DOES_NOT_CONTAIN.mapToFilterColumn({ operator1: ExtendCustomFilterOperator.DOES_NOT_CONTAIN, val1: 'univer' }))
|
||||
.toEqual({ customFilters: { customFilters: [{ val: '*univer*', operator: CustomFilterOperator.NOT_EQUALS }] } });
|
||||
|
||||
expect(FilterConditionItems.DOES_NOT_CONTAIN.mapToFilterColumn({ operator1: ExtendCustomFilterOperator.DOES_NOT_CONTAIN, val1: ' ' }))
|
||||
.toEqual({ customFilters: { customFilters: [{ val: '* *', operator: CustomFilterOperator.NOT_EQUALS }] } });
|
||||
});
|
||||
});
|
||||
|
||||
describe('test STARTS_WITH', () => {
|
||||
it('should load STARTS_WITH condition from char set "xxx*"', () => {
|
||||
expect(FilterConditionItems.testMappingFilterColumn({ customFilters: { customFilters: [{ val: 'xxx*' }] } }))
|
||||
.toEqual([FilterConditionItems.STARTS_WITH, { operator1: ExtendCustomFilterOperator.STARTS_WITH, val1: 'xxx' }]);
|
||||
});
|
||||
|
||||
it('should support default params with empty string', () => {
|
||||
expect(FilterConditionItems.STARTS_WITH.getDefaultFormParams()).toEqual({ operator1: ExtendCustomFilterOperator.STARTS_WITH, val1: '' });
|
||||
});
|
||||
|
||||
it('should mapped params with only one operator STARTS_WITH', () => {
|
||||
expect(FilterConditionItems.STARTS_WITH.testMappingParams({ operator1: ExtendCustomFilterOperator.STARTS_WITH })).toBeTruthy();
|
||||
expect(FilterConditionItems.STARTS_WITH.testMappingParams({ operator1: ExtendCustomFilterOperator.DOES_NOT_START_WITH })).toBeFalsy();
|
||||
expect(() => FilterConditionItems.STARTS_WITH.testMappingParams({ operator1: ExtendCustomFilterOperator.STARTS_WITH, operator2: ExtendCustomFilterOperator.STARTS_WITH }))
|
||||
.toThrowError();
|
||||
|
||||
expect(FilterConditionItems.STARTS_WITH.testMappingParams({ operator2: ExtendCustomFilterOperator.STARTS_WITH })).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should map to filter column with char set "xxx*"', () => {
|
||||
expect(FilterConditionItems.STARTS_WITH.mapToFilterColumn({ operator1: ExtendCustomFilterOperator.STARTS_WITH, val1: 'univer' }))
|
||||
.toEqual({ customFilters: { customFilters: [{ val: 'univer*' }] } });
|
||||
|
||||
expect(FilterConditionItems.STARTS_WITH.mapToFilterColumn({ operator1: ExtendCustomFilterOperator.STARTS_WITH, val1: ' ' }))
|
||||
.toEqual({ customFilters: { customFilters: [{ val: ' *' }] } });
|
||||
});
|
||||
});
|
||||
|
||||
describe('test ENDS_WITH', () => {
|
||||
it('should load ENDS_WITH condition from char set "*xxx"', () => {
|
||||
expect(FilterConditionItems.testMappingFilterColumn({ customFilters: { customFilters: [{ val: '*xxx' }] } }))
|
||||
.toEqual([FilterConditionItems.ENDS_WITH, { operator1: ExtendCustomFilterOperator.ENDS_WITH, val1: 'xxx' }]);
|
||||
});
|
||||
|
||||
it('should support default params with empty string', () => {
|
||||
expect(FilterConditionItems.ENDS_WITH.getDefaultFormParams()).toEqual({ operator1: ExtendCustomFilterOperator.ENDS_WITH, val1: '' });
|
||||
});
|
||||
|
||||
it('should mapped params with only one operator ENDS_WITH', () => {
|
||||
expect(FilterConditionItems.ENDS_WITH.testMappingParams({ operator1: ExtendCustomFilterOperator.ENDS_WITH })).toBeTruthy();
|
||||
expect(FilterConditionItems.ENDS_WITH.testMappingParams({ operator1: ExtendCustomFilterOperator.DOES_NOT_END_WITH })).toBeFalsy();
|
||||
expect(() => FilterConditionItems.ENDS_WITH.testMappingParams({ operator1: ExtendCustomFilterOperator.ENDS_WITH, operator2: ExtendCustomFilterOperator.ENDS_WITH }))
|
||||
.toThrowError();
|
||||
|
||||
expect(FilterConditionItems.ENDS_WITH.testMappingParams({ operator2: ExtendCustomFilterOperator.ENDS_WITH })).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should map to filter column with char set "*xxx"', () => {
|
||||
expect(FilterConditionItems.ENDS_WITH.mapToFilterColumn({ operator1: ExtendCustomFilterOperator.ENDS_WITH, val1: 'univer' }))
|
||||
.toEqual({ customFilters: { customFilters: [{ val: '*univer' }] } });
|
||||
|
||||
expect(FilterConditionItems.ENDS_WITH.mapToFilterColumn({ operator1: ExtendCustomFilterOperator.ENDS_WITH, val1: ' ' }))
|
||||
.toEqual({ customFilters: { customFilters: [{ val: '* ' }] } });
|
||||
});
|
||||
});
|
||||
|
||||
describe('test EQUALS', () => {
|
||||
it('should load EQUALS condition from char set "xxx"', () => {
|
||||
expect(FilterConditionItems.testMappingFilterColumn({ customFilters: { customFilters: [{ val: 'xxx' }] } }))
|
||||
.toEqual([FilterConditionItems.EQUALS, { operator1: ExtendCustomFilterOperator.EQUALS, val1: 'xxx' }]);
|
||||
|
||||
expect(FilterConditionItems.testMappingFilterColumn({ filters: { filters: [''] } }))
|
||||
.toEqual([FilterConditionItems.EQUALS, { operator1: ExtendCustomFilterOperator.EQUALS, val1: '' }]);
|
||||
});
|
||||
|
||||
it('should support default params with empty string', () => {
|
||||
expect(FilterConditionItems.EQUALS.getDefaultFormParams()).toEqual({ operator1: ExtendCustomFilterOperator.EQUALS, val1: '' });
|
||||
});
|
||||
|
||||
it('should mapped params with only one operator EQUALS', () => {
|
||||
expect(FilterConditionItems.EQUALS.testMappingParams({ operator1: ExtendCustomFilterOperator.EQUALS })).toBeTruthy();
|
||||
expect(FilterConditionItems.EQUALS.testMappingParams({ operator1: ExtendCustomFilterOperator.NOT_EQUALS })).toBeFalsy();
|
||||
expect(() => FilterConditionItems.EQUALS.testMappingParams({ operator1: ExtendCustomFilterOperator.EQUALS, operator2: ExtendCustomFilterOperator.EQUALS }))
|
||||
.toThrowError();
|
||||
|
||||
expect(FilterConditionItems.EQUALS.testMappingParams({ operator2: ExtendCustomFilterOperator.EQUALS })).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should map to filter column with char set "xxx"', () => {
|
||||
expect(FilterConditionItems.EQUALS.mapToFilterColumn({ operator1: ExtendCustomFilterOperator.EQUALS, val1: 'univer' }))
|
||||
.toEqual({ customFilters: { customFilters: [{ val: 'univer' }] } });
|
||||
|
||||
expect(FilterConditionItems.EQUALS.mapToFilterColumn({ operator1: ExtendCustomFilterOperator.EQUALS, val1: ' ' }))
|
||||
.toEqual({ customFilters: { customFilters: [{ val: ' ' }] } });
|
||||
});
|
||||
});
|
||||
|
||||
describe('test GREATER_THAN', () => {
|
||||
it('should load GREATER_THAN condition from operator"', () => {
|
||||
expect(FilterConditionItems.testMappingFilterColumn({ customFilters: { customFilters: [{ val: '123', operator: CustomFilterOperator.GREATER_THAN }] } }))
|
||||
.toEqual([FilterConditionItems.GREATER_THAN, { operator1: CustomFilterOperator.GREATER_THAN, val1: '123' }]);
|
||||
});
|
||||
|
||||
it('should support default params with empty string', () => {
|
||||
expect(FilterConditionItems.GREATER_THAN.getDefaultFormParams()).toEqual({ operator1: CustomFilterOperator.GREATER_THAN, val1: '' });
|
||||
});
|
||||
|
||||
it('should mapped params with only one operator GREATER_THAN', () => {
|
||||
expect(FilterConditionItems.GREATER_THAN.testMappingParams({ operator1: CustomFilterOperator.GREATER_THAN })).toBeTruthy();
|
||||
expect(FilterConditionItems.GREATER_THAN.testMappingParams({ operator1: CustomFilterOperator.LESS_THAN })).toBeFalsy();
|
||||
expect(() => FilterConditionItems.GREATER_THAN.testMappingParams({ operator1: CustomFilterOperator.GREATER_THAN, operator2: CustomFilterOperator.GREATER_THAN }))
|
||||
.toThrowError();
|
||||
|
||||
expect(FilterConditionItems.GREATER_THAN.testMappingParams({ operator2: CustomFilterOperator.GREATER_THAN })).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should map to filter column with operator GREATER_THAN', () => {
|
||||
expect(FilterConditionItems.GREATER_THAN.mapToFilterColumn({ operator1: CustomFilterOperator.GREATER_THAN, val1: '123' }))
|
||||
.toEqual({ customFilters: { customFilters: [{ val: '123', operator: CustomFilterOperator.GREATER_THAN }] } });
|
||||
});
|
||||
});
|
||||
|
||||
describe('test GREATER_THAN_OR_EQUAL', () => {
|
||||
it('should load GREATER_THAN_OR_EQUAL condition from operator"', () => {
|
||||
expect(FilterConditionItems.testMappingFilterColumn({ customFilters: { customFilters: [{ val: '123', operator: CustomFilterOperator.GREATER_THAN_OR_EQUAL }] } }))
|
||||
.toEqual([FilterConditionItems.GREATER_THAN_OR_EQUAL, { operator1: CustomFilterOperator.GREATER_THAN_OR_EQUAL, val1: '123' }]);
|
||||
});
|
||||
|
||||
it('should support default params with empty string', () => {
|
||||
expect(FilterConditionItems.GREATER_THAN_OR_EQUAL.getDefaultFormParams()).toEqual({ operator1: CustomFilterOperator.GREATER_THAN_OR_EQUAL, val1: '' });
|
||||
});
|
||||
|
||||
it('should mapped params with only one operator GREATER_THAN_OR_EQUAL', () => {
|
||||
expect(FilterConditionItems.GREATER_THAN_OR_EQUAL.testMappingParams({ operator1: CustomFilterOperator.GREATER_THAN_OR_EQUAL })).toBeTruthy();
|
||||
expect(FilterConditionItems.GREATER_THAN_OR_EQUAL.testMappingParams({ operator1: CustomFilterOperator.LESS_THAN_OR_EQUAL })).toBeFalsy();
|
||||
expect(() => FilterConditionItems.GREATER_THAN_OR_EQUAL.testMappingParams({ operator1: CustomFilterOperator.GREATER_THAN_OR_EQUAL, operator2: CustomFilterOperator.GREATER_THAN_OR_EQUAL }))
|
||||
.toThrowError();
|
||||
|
||||
expect(FilterConditionItems.GREATER_THAN_OR_EQUAL.testMappingParams({ operator2: CustomFilterOperator.GREATER_THAN_OR_EQUAL })).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should map to filter column with operator GREATER_THAN_OR_EQUAL', () => {
|
||||
expect(FilterConditionItems.GREATER_THAN_OR_EQUAL.mapToFilterColumn({ operator1: CustomFilterOperator.GREATER_THAN_OR_EQUAL, val1: '123' }))
|
||||
.toEqual({ customFilters: { customFilters: [{ val: '123', operator: CustomFilterOperator.GREATER_THAN_OR_EQUAL }] } });
|
||||
});
|
||||
});
|
||||
|
||||
describe('test LESS_THAN', () => {
|
||||
it('should load LESS_THAN condition from operator"', () => {
|
||||
expect(FilterConditionItems.testMappingFilterColumn({ customFilters: { customFilters: [{ val: '123', operator: CustomFilterOperator.LESS_THAN }] } }))
|
||||
.toEqual([FilterConditionItems.LESS_THAN, { operator1: CustomFilterOperator.LESS_THAN, val1: '123' }]);
|
||||
});
|
||||
|
||||
it('should support default params with empty string', () => {
|
||||
expect(FilterConditionItems.LESS_THAN.getDefaultFormParams()).toEqual({ operator1: CustomFilterOperator.LESS_THAN, val1: '' });
|
||||
});
|
||||
|
||||
it('should mapped params with only one operator LESS_THAN', () => {
|
||||
expect(FilterConditionItems.LESS_THAN.testMappingParams({ operator1: CustomFilterOperator.LESS_THAN })).toBeTruthy();
|
||||
expect(FilterConditionItems.LESS_THAN.testMappingParams({ operator1: CustomFilterOperator.GREATER_THAN })).toBeFalsy();
|
||||
expect(() => FilterConditionItems.LESS_THAN.testMappingParams({ operator1: CustomFilterOperator.LESS_THAN, operator2: CustomFilterOperator.LESS_THAN }))
|
||||
.toThrowError();
|
||||
|
||||
expect(FilterConditionItems.LESS_THAN.testMappingParams({ operator2: CustomFilterOperator.LESS_THAN })).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should map to filter column with operator LESS_THAN', () => {
|
||||
expect(FilterConditionItems.LESS_THAN.mapToFilterColumn({ operator1: CustomFilterOperator.LESS_THAN, val1: '123' }))
|
||||
.toEqual({ customFilters: { customFilters: [{ val: '123', operator: CustomFilterOperator.LESS_THAN }] } });
|
||||
});
|
||||
});
|
||||
|
||||
describe('test LESS_THAN_OR_EQUAL', () => {
|
||||
it('should load LESS_THAN_OR_EQUAL condition from operator"', () => {
|
||||
expect(FilterConditionItems.testMappingFilterColumn({ customFilters: { customFilters: [{ val: '123', operator: CustomFilterOperator.LESS_THAN_OR_EQUAL }] } }))
|
||||
.toEqual([FilterConditionItems.LESS_THAN_OR_EQUAL, { operator1: CustomFilterOperator.LESS_THAN_OR_EQUAL, val1: '123' }]);
|
||||
});
|
||||
|
||||
it('should support default params with empty string', () => {
|
||||
expect(FilterConditionItems.LESS_THAN_OR_EQUAL.getDefaultFormParams()).toEqual({ operator1: CustomFilterOperator.LESS_THAN_OR_EQUAL, val1: '' });
|
||||
});
|
||||
|
||||
it('should mapped params with only one operator LESS_THAN_OR_EQUAL', () => {
|
||||
expect(FilterConditionItems.LESS_THAN_OR_EQUAL.testMappingParams({ operator1: CustomFilterOperator.LESS_THAN_OR_EQUAL })).toBeTruthy();
|
||||
expect(FilterConditionItems.LESS_THAN_OR_EQUAL.testMappingParams({ operator1: CustomFilterOperator.GREATER_THAN_OR_EQUAL })).toBeFalsy();
|
||||
expect(() => FilterConditionItems.LESS_THAN_OR_EQUAL.testMappingParams({ operator1: CustomFilterOperator.LESS_THAN_OR_EQUAL, operator2: CustomFilterOperator.LESS_THAN_OR_EQUAL }))
|
||||
.toThrowError();
|
||||
|
||||
expect(FilterConditionItems.LESS_THAN_OR_EQUAL.testMappingParams({ operator2: CustomFilterOperator.LESS_THAN_OR_EQUAL })).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should map to filter column with operator LESS_THAN_OR_EQUAL', () => {
|
||||
expect(FilterConditionItems.LESS_THAN_OR_EQUAL.mapToFilterColumn({ operator1: CustomFilterOperator.LESS_THAN_OR_EQUAL, val1: '123' }))
|
||||
.toEqual({ customFilters: { customFilters: [{ val: '123', operator: CustomFilterOperator.LESS_THAN_OR_EQUAL }] } });
|
||||
});
|
||||
});
|
||||
|
||||
describe('test EQUAL condition', () => {
|
||||
it('should load EQUAL condition from operator"', () => {
|
||||
expect(FilterConditionItems.testMappingFilterColumn({ customFilters: { customFilters: [{ val: '123', operator: CustomFilterOperator.EQUAL }] } }))
|
||||
.toEqual([FilterConditionItems.EQUAL, { operator1: CustomFilterOperator.EQUAL, val1: '123' }]);
|
||||
});
|
||||
|
||||
it('should support default params with empty string', () => {
|
||||
expect(FilterConditionItems.EQUAL.getDefaultFormParams()).toEqual({ operator1: CustomFilterOperator.EQUAL, val1: '' });
|
||||
});
|
||||
|
||||
it('should mapped params with only one operator EQUAL', () => {
|
||||
expect(FilterConditionItems.EQUAL.testMappingParams({ operator1: CustomFilterOperator.EQUAL })).toBeTruthy();
|
||||
expect(FilterConditionItems.EQUAL.testMappingParams({ operator1: CustomFilterOperator.NOT_EQUALS })).toBeFalsy();
|
||||
expect(() => FilterConditionItems.EQUAL.testMappingParams({ operator1: CustomFilterOperator.EQUAL, operator2: CustomFilterOperator.EQUAL }))
|
||||
.toThrowError();
|
||||
|
||||
expect(FilterConditionItems.EQUAL.testMappingParams({ operator2: CustomFilterOperator.EQUAL })).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should map to filter column with operator EQUAL', () => {
|
||||
expect(FilterConditionItems.EQUAL.mapToFilterColumn({ operator1: CustomFilterOperator.EQUAL, val1: '123' }))
|
||||
.toEqual({ customFilters: { customFilters: [{ val: '123', operator: CustomFilterOperator.EQUAL }] } });
|
||||
});
|
||||
});
|
||||
|
||||
describe('test NOT_EQUAL', () => {
|
||||
it('should load NOT_EQUAL condition from operator"', () => {
|
||||
expect(FilterConditionItems.testMappingFilterColumn({ customFilters: { customFilters: [{ val: '123', operator: CustomFilterOperator.NOT_EQUALS }] } }))
|
||||
.toEqual([FilterConditionItems.NOT_EQUAL, { operator1: CustomFilterOperator.NOT_EQUALS, val1: '123' }]);
|
||||
});
|
||||
|
||||
it('should support default params with empty string', () => {
|
||||
expect(FilterConditionItems.NOT_EQUAL.getDefaultFormParams()).toEqual({ operator1: CustomFilterOperator.NOT_EQUALS, val1: '' });
|
||||
});
|
||||
|
||||
it('should mapped params with only one operator NOT_EQUAL', () => {
|
||||
expect(FilterConditionItems.NOT_EQUAL.testMappingParams({ operator1: CustomFilterOperator.NOT_EQUALS })).toBeTruthy();
|
||||
expect(FilterConditionItems.NOT_EQUAL.testMappingParams({ operator1: CustomFilterOperator.EQUAL })).toBeFalsy();
|
||||
expect(() => FilterConditionItems.NOT_EQUAL.testMappingParams({ operator1: CustomFilterOperator.NOT_EQUALS, operator2: CustomFilterOperator.NOT_EQUALS }))
|
||||
.toThrowError();
|
||||
|
||||
expect(FilterConditionItems.NOT_EQUAL.testMappingParams({ operator2: CustomFilterOperator.NOT_EQUALS })).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should map to filter column with operator NOT_EQUAL', () => {
|
||||
expect(FilterConditionItems.NOT_EQUAL.mapToFilterColumn({ operator1: CustomFilterOperator.NOT_EQUALS, val1: '123' }))
|
||||
.toEqual({ customFilters: { customFilters: [{ val: '123', operator: CustomFilterOperator.NOT_EQUALS }] } });
|
||||
});
|
||||
});
|
||||
|
||||
describe('test BETWEEN', () => {
|
||||
it('should load BETWEEN condition from correct combination of two operators"', () => {
|
||||
expect(FilterConditionItems.testMappingFilterColumn({
|
||||
customFilters: {
|
||||
and: BooleanNumber.TRUE,
|
||||
customFilters: [
|
||||
{ val: '123', operator: CustomFilterOperator.GREATER_THAN_OR_EQUAL },
|
||||
{ val: '456', operator: CustomFilterOperator.LESS_THAN_OR_EQUAL },
|
||||
],
|
||||
},
|
||||
})).toEqual([FilterConditionItems.BETWEEN, {
|
||||
and: true,
|
||||
operator1: CustomFilterOperator.GREATER_THAN_OR_EQUAL,
|
||||
val1: '123',
|
||||
operator2: CustomFilterOperator.LESS_THAN_OR_EQUAL,
|
||||
val2: '456',
|
||||
}]);
|
||||
|
||||
expect(FilterConditionItems.testMappingFilterColumn({
|
||||
customFilters: {
|
||||
and: BooleanNumber.TRUE,
|
||||
customFilters: [
|
||||
{ val: '456', operator: CustomFilterOperator.LESS_THAN_OR_EQUAL },
|
||||
{ val: '123', operator: CustomFilterOperator.GREATER_THAN_OR_EQUAL },
|
||||
],
|
||||
},
|
||||
})).toEqual([FilterConditionItems.BETWEEN, {
|
||||
and: true,
|
||||
operator1: CustomFilterOperator.GREATER_THAN_OR_EQUAL,
|
||||
val1: '123',
|
||||
operator2: CustomFilterOperator.LESS_THAN_OR_EQUAL,
|
||||
val2: '456',
|
||||
}]);
|
||||
|
||||
expect(FilterConditionItems.testMappingFilterColumn({
|
||||
customFilters: {
|
||||
customFilters: [
|
||||
{ val: '456', operator: CustomFilterOperator.LESS_THAN_OR_EQUAL },
|
||||
{ val: '123', operator: CustomFilterOperator.GREATER_THAN_OR_EQUAL },
|
||||
],
|
||||
},
|
||||
})).not.toEqual([FilterConditionItems.BETWEEN, {
|
||||
and: true,
|
||||
operator1: CustomFilterOperator.GREATER_THAN_OR_EQUAL,
|
||||
val1: '123',
|
||||
operator2: CustomFilterOperator.LESS_THAN_OR_EQUAL,
|
||||
val2: '456',
|
||||
}]);
|
||||
});
|
||||
|
||||
it('should support default params with empty string', () => {
|
||||
expect(FilterConditionItems.BETWEEN.getDefaultFormParams()).toEqual({
|
||||
and: true,
|
||||
operator1: CustomFilterOperator.GREATER_THAN_OR_EQUAL,
|
||||
val1: '',
|
||||
operator2: CustomFilterOperator.LESS_THAN_OR_EQUAL,
|
||||
val2: '',
|
||||
});
|
||||
});
|
||||
|
||||
it('should mapped params with only two operators GREATER_THAN_OR_EQUAL and LESS_THAN_OR_EQUAL', () => {
|
||||
expect(FilterConditionItems.BETWEEN.testMappingParams({
|
||||
and: true,
|
||||
operator1: CustomFilterOperator.GREATER_THAN_OR_EQUAL,
|
||||
operator2: CustomFilterOperator.LESS_THAN_OR_EQUAL,
|
||||
})).toBeTruthy();
|
||||
|
||||
expect(FilterConditionItems.BETWEEN.testMappingParams({
|
||||
and: true,
|
||||
operator1: CustomFilterOperator.LESS_THAN_OR_EQUAL,
|
||||
operator2: CustomFilterOperator.GREATER_THAN_OR_EQUAL,
|
||||
})).toBeTruthy();
|
||||
|
||||
expect(FilterConditionItems.BETWEEN.testMappingParams({
|
||||
and: true,
|
||||
operator1: CustomFilterOperator.GREATER_THAN_OR_EQUAL,
|
||||
})).toBeFalsy();
|
||||
|
||||
expect(FilterConditionItems.BETWEEN.testMappingParams({
|
||||
and: true,
|
||||
operator2: CustomFilterOperator.LESS_THAN_OR_EQUAL,
|
||||
})).toBeFalsy();
|
||||
|
||||
expect(FilterConditionItems.BETWEEN.testMappingParams({
|
||||
and: true,
|
||||
operator1: CustomFilterOperator.GREATER_THAN_OR_EQUAL,
|
||||
operator2: CustomFilterOperator.GREATER_THAN_OR_EQUAL,
|
||||
})).toBeFalsy();
|
||||
});
|
||||
|
||||
it('should map to filter column with correct combination of two operators', () => {
|
||||
expect(FilterConditionItems.BETWEEN.mapToFilterColumn({
|
||||
and: true,
|
||||
operator1: CustomFilterOperator.GREATER_THAN_OR_EQUAL,
|
||||
val1: '123',
|
||||
operator2: CustomFilterOperator.LESS_THAN_OR_EQUAL,
|
||||
val2: '456',
|
||||
})).toEqual({
|
||||
customFilters: {
|
||||
and: BooleanNumber.TRUE,
|
||||
customFilters: [
|
||||
{ val: '123', operator: CustomFilterOperator.GREATER_THAN_OR_EQUAL },
|
||||
{ val: '456', operator: CustomFilterOperator.LESS_THAN_OR_EQUAL },
|
||||
],
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('test NOT_BETWEEN', () => {
|
||||
it('should load NOT_BETWEEN condition from correct combination of two operators"', () => {
|
||||
expect(FilterConditionItems.testMappingFilterColumn({
|
||||
customFilters: {
|
||||
customFilters: [
|
||||
{ val: '123', operator: CustomFilterOperator.LESS_THAN },
|
||||
{ val: '456', operator: CustomFilterOperator.GREATER_THAN },
|
||||
],
|
||||
},
|
||||
})).toEqual([FilterConditionItems.NOT_BETWEEN, {
|
||||
operator1: CustomFilterOperator.LESS_THAN,
|
||||
val1: '123',
|
||||
operator2: CustomFilterOperator.GREATER_THAN,
|
||||
val2: '456',
|
||||
}]);
|
||||
});
|
||||
|
||||
it('should support default params with empty string', () => {
|
||||
expect(FilterConditionItems.NOT_BETWEEN.getDefaultFormParams()).toEqual({
|
||||
operator1: CustomFilterOperator.LESS_THAN,
|
||||
val1: '',
|
||||
operator2: CustomFilterOperator.GREATER_THAN,
|
||||
val2: '',
|
||||
});
|
||||
});
|
||||
|
||||
it('should mapped params with only two operators GREATER_THAN and LESS_THAN', () => {
|
||||
expect(FilterConditionItems.NOT_BETWEEN.testMappingParams({
|
||||
operator1: CustomFilterOperator.GREATER_THAN,
|
||||
operator2: CustomFilterOperator.LESS_THAN,
|
||||
})).toBeTruthy();
|
||||
|
||||
expect(FilterConditionItems.NOT_BETWEEN.testMappingParams({
|
||||
operator1: CustomFilterOperator.LESS_THAN,
|
||||
operator2: CustomFilterOperator.GREATER_THAN,
|
||||
})).toBeTruthy();
|
||||
|
||||
expect(FilterConditionItems.NOT_BETWEEN.testMappingParams({
|
||||
operator1: CustomFilterOperator.GREATER_THAN,
|
||||
})).toBeFalsy();
|
||||
|
||||
expect(FilterConditionItems.NOT_BETWEEN.testMappingParams({
|
||||
operator2: CustomFilterOperator.LESS_THAN,
|
||||
})).toBeFalsy();
|
||||
|
||||
expect(FilterConditionItems.NOT_BETWEEN.testMappingParams({
|
||||
operator1: CustomFilterOperator.GREATER_THAN,
|
||||
operator2: CustomFilterOperator.GREATER_THAN,
|
||||
})).toBeFalsy();
|
||||
});
|
||||
|
||||
it('should map to filter column with correct combination of two operators', () => {
|
||||
expect(FilterConditionItems.NOT_BETWEEN.mapToFilterColumn({
|
||||
operator1: CustomFilterOperator.GREATER_THAN,
|
||||
val1: '123',
|
||||
operator2: CustomFilterOperator.LESS_THAN,
|
||||
val2: '456',
|
||||
})).toEqual({
|
||||
customFilters: {
|
||||
customFilters: [
|
||||
{ val: '123', operator: CustomFilterOperator.GREATER_THAN },
|
||||
{ val: '456', operator: CustomFilterOperator.LESS_THAN },
|
||||
],
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('test CUSTOM', () => {
|
||||
it('should load CUSTOM condition from two custom filters that cannot map to previous', () => {
|
||||
expect(FilterConditionItems.testMappingFilterColumn({ customFilters: { and: BooleanNumber.TRUE, customFilters: [
|
||||
{ val: '*123' },
|
||||
{ val: '123*' },
|
||||
] } })).toEqual([FilterConditionItems.CUSTOM, {
|
||||
and: true,
|
||||
operator1: ExtendCustomFilterOperator.ENDS_WITH,
|
||||
val1: '123',
|
||||
operator2: ExtendCustomFilterOperator.STARTS_WITH,
|
||||
val2: '123',
|
||||
}]);
|
||||
});
|
||||
|
||||
it('should support default params with empty string', () => {
|
||||
expect(FilterConditionItems.CUSTOM.getDefaultFormParams()).toEqual({
|
||||
operator1: ExtendCustomFilterOperator.NONE,
|
||||
val1: '',
|
||||
operator2: ExtendCustomFilterOperator.NONE,
|
||||
val2: '',
|
||||
});
|
||||
});
|
||||
|
||||
it('should mapped by any params change', () => {
|
||||
expect(FilterConditionItems.CUSTOM.testMappingParams({ operator1: ExtendCustomFilterOperator.CUSTOM })).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should map to filter column with empty string', () => {
|
||||
expect(FilterConditionItems.CUSTOM.mapToFilterColumn({
|
||||
and: true,
|
||||
operator1: ExtendCustomFilterOperator.ENDS_WITH,
|
||||
val1: '123',
|
||||
operator2: CustomFilterOperator.GREATER_THAN,
|
||||
val2: '456',
|
||||
}))
|
||||
.toEqual({ customFilters: {
|
||||
and: BooleanNumber.TRUE,
|
||||
customFilters: [{ val: '*123' }, { val: '456', operator: CustomFilterOperator.GREATER_THAN }],
|
||||
} });
|
||||
});
|
||||
|
||||
it('should fallback to NONE when no custom filter is provided', () => {
|
||||
expect(FilterConditionItems.CUSTOM.mapToFilterColumn({ operator1: ExtendCustomFilterOperator.NONE })).toEqual(null);
|
||||
});
|
||||
|
||||
it('should fallback to one conditional filter when only one is provided', () => {
|
||||
expect(FilterConditionItems.CUSTOM.mapToFilterColumn({ operator1: ExtendCustomFilterOperator.ENDS_WITH, val1: '123' }))
|
||||
.toEqual({ customFilters: { customFilters: [{ val: '*123' }] } });
|
||||
|
||||
expect(FilterConditionItems.CUSTOM.mapToFilterColumn({ operator2: ExtendCustomFilterOperator.STARTS_WITH, val2: '123' }))
|
||||
.toEqual({ customFilters: { customFilters: [{ val: '123*' }] } });
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
822
packages/sheets-filter-ui/src/models/conditions.ts
Normal file
822
packages/sheets-filter-ui/src/models/conditions.ts
Normal file
@@ -0,0 +1,822 @@
|
||||
/**
|
||||
* 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 { ICustomFilters, IFilterColumn } from '@univerjs/sheets-filter';
|
||||
import type { LocaleKey } from '../locale/types';
|
||||
import { BooleanNumber } from '@univerjs/core';
|
||||
import { CustomFilterOperator } from '@univerjs/sheets-filter';
|
||||
import { ExtendCustomFilterOperator, OperatorOrder } from './extended-operators';
|
||||
|
||||
// This file implements a simple form schema system for condition filters (and only for it!)
|
||||
|
||||
export type FilterOperator = ExtendCustomFilterOperator | CustomFilterOperator;
|
||||
|
||||
export interface IFilterConditionFormParams {
|
||||
and?: true;
|
||||
|
||||
operator1?: FilterOperator;
|
||||
val1?: string;
|
||||
|
||||
operator2?: FilterOperator;
|
||||
val2?: string;
|
||||
}
|
||||
|
||||
export interface IFilterConditionItem {
|
||||
operator: FilterOperator;
|
||||
numOfParameters: number;
|
||||
order: OperatorOrder;
|
||||
|
||||
/**
|
||||
* Name of the filter condition. Should be an i18n key.
|
||||
*/
|
||||
label: LocaleKey;
|
||||
|
||||
and?: true;
|
||||
|
||||
/**
|
||||
* Get the initial form parameters for this condition item. This should only be called when `numOfParameters` is not `0`.
|
||||
*/
|
||||
getDefaultFormParams(): IFilterConditionFormParams;
|
||||
/**
|
||||
* Test if the form params can be mapped to this condition item. This should be called when the
|
||||
* condition form changes and `numOfParameters` is not `0`.
|
||||
* @param params
|
||||
*/
|
||||
testMappingParams(params: IFilterConditionFormParams): boolean;
|
||||
|
||||
/**
|
||||
* When user confirm changing filter condition, this method will be called to map the form params
|
||||
* to the filter column data.
|
||||
* @param mapParams
|
||||
*/
|
||||
mapToFilterColumn(mapParams: IFilterConditionFormParams): Nullable<Omit<IFilterColumn, 'colId'>>;
|
||||
/**
|
||||
* Test if the filter column data can be mapped to this condition item.
|
||||
* It should return the mapping parameters if it can be mapped, otherwise `false`.
|
||||
* This should be called when the filter panel opens.
|
||||
*
|
||||
* @param filterColumn
|
||||
* @returns the mapping parameters if it can be mapped, otherwise `false`
|
||||
*/
|
||||
testMappingFilterColumn(filterColumn: Omit<IFilterColumn, 'colId'>): IFilterConditionFormParams | false;
|
||||
}
|
||||
|
||||
// eslint-disable-next-line ts/no-namespace
|
||||
export namespace FilterConditionItems {
|
||||
export const NONE: IFilterConditionItem = {
|
||||
label: 'sheets-filter-ui.conditions.none',
|
||||
|
||||
operator: ExtendCustomFilterOperator.NONE,
|
||||
order: OperatorOrder.SECOND,
|
||||
numOfParameters: 0,
|
||||
|
||||
getDefaultFormParams: () => { throw new Error('[FilterConditionItems.NONE]: should not have initial form params!'); },
|
||||
testMappingParams: (params) => {
|
||||
return params.operator1 === ExtendCustomFilterOperator.NONE;
|
||||
},
|
||||
|
||||
mapToFilterColumn: () => null,
|
||||
testMappingFilterColumn: (filterColumn) => {
|
||||
if (!filterColumn.customFilters && !filterColumn.filters) return {};
|
||||
return false;
|
||||
},
|
||||
};
|
||||
|
||||
// ------------------------------
|
||||
|
||||
export const EMPTY: IFilterConditionItem = {
|
||||
label: 'sheets-filter-ui.conditions.empty',
|
||||
|
||||
operator: ExtendCustomFilterOperator.EMPTY,
|
||||
order: OperatorOrder.SECOND,
|
||||
numOfParameters: 0,
|
||||
|
||||
getDefaultFormParams: () => { throw new Error('[FilterConditionItems.EMPTY]: should not have initial form params!'); },
|
||||
testMappingParams: ({ operator1 }) => operator1 === ExtendCustomFilterOperator.EMPTY,
|
||||
|
||||
mapToFilterColumn: () => ({ customFilters: { customFilters: [{ val: '' }] } }),
|
||||
testMappingFilterColumn: (filterColumn) => {
|
||||
if (filterColumn.customFilters?.customFilters.length !== 1) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const firstCustomFilter = filterColumn.customFilters.customFilters[0];
|
||||
const mapped = firstCustomFilter.val === '' && firstCustomFilter.operator === undefined;
|
||||
if (!mapped) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return { operator1: ExtendCustomFilterOperator.EMPTY };
|
||||
},
|
||||
};
|
||||
|
||||
export const NOT_EMPTY: IFilterConditionItem = {
|
||||
label: 'sheets-filter-ui.conditions.not-empty',
|
||||
|
||||
operator: ExtendCustomFilterOperator.NOT_EMPTY,
|
||||
order: OperatorOrder.SECOND,
|
||||
numOfParameters: 0,
|
||||
|
||||
getDefaultFormParams: () => { throw new Error('[FilterConditionItems.NOT_EMPTY]: should not have initial form params!'); },
|
||||
testMappingParams: ({ operator1 }) => operator1 === ExtendCustomFilterOperator.NOT_EMPTY,
|
||||
|
||||
mapToFilterColumn: () => ({ customFilters: { customFilters: [{ val: '', operator: CustomFilterOperator.NOT_EQUALS }] } }),
|
||||
testMappingFilterColumn: (filterColumn) => {
|
||||
if (filterColumn.customFilters?.customFilters.length !== 1) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const firstCustomFilter = filterColumn.customFilters.customFilters[0];
|
||||
const canMap = firstCustomFilter.val === ' ' && firstCustomFilter.operator === CustomFilterOperator.NOT_EQUALS;
|
||||
if (!canMap) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return { operator1: ExtendCustomFilterOperator.NOT_EMPTY };
|
||||
},
|
||||
};
|
||||
|
||||
// ------------------------------
|
||||
|
||||
export const TEXT_CONTAINS: IFilterConditionItem = {
|
||||
label: 'sheets-filter-ui.conditions.text-contains',
|
||||
|
||||
operator: ExtendCustomFilterOperator.CONTAINS,
|
||||
order: OperatorOrder.FIRST,
|
||||
numOfParameters: 1,
|
||||
|
||||
getDefaultFormParams: () => ({ operator1: ExtendCustomFilterOperator.CONTAINS, val1: '' }),
|
||||
testMappingParams: (params) => {
|
||||
const [op] = getOnlyOperatorAndVal(params);
|
||||
return op === ExtendCustomFilterOperator.CONTAINS;
|
||||
},
|
||||
|
||||
mapToFilterColumn: (mapParams) => {
|
||||
const { val1 } = mapParams;
|
||||
if (val1 === '') return null;
|
||||
|
||||
return {
|
||||
customFilters: { customFilters: [{ val: `*${val1}*` }] },
|
||||
};
|
||||
},
|
||||
testMappingFilterColumn: (filterColumn) => {
|
||||
if (filterColumn.customFilters?.customFilters.length !== 1) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const firstCustomFilter = filterColumn.customFilters.customFilters[0];
|
||||
const valAsString = firstCustomFilter.val.toString();
|
||||
if (!firstCustomFilter.operator && valAsString.startsWith('*') && valAsString.endsWith('*')) {
|
||||
return { operator1: ExtendCustomFilterOperator.CONTAINS, val1: valAsString.slice(1, -1) };
|
||||
}
|
||||
|
||||
return false;
|
||||
},
|
||||
};
|
||||
|
||||
export const DOES_NOT_CONTAIN: IFilterConditionItem = {
|
||||
label: 'sheets-filter-ui.conditions.does-not-contain',
|
||||
|
||||
operator: ExtendCustomFilterOperator.DOES_NOT_CONTAIN,
|
||||
order: OperatorOrder.FIRST,
|
||||
numOfParameters: 1,
|
||||
|
||||
getDefaultFormParams: () => ({ operator1: ExtendCustomFilterOperator.DOES_NOT_CONTAIN, val1: '' }),
|
||||
mapToFilterColumn: (mapParams) => ({
|
||||
customFilters: { customFilters: [{ val: `*${mapParams.val1}*`, operator: CustomFilterOperator.NOT_EQUALS }] },
|
||||
}),
|
||||
testMappingParams: (params) => {
|
||||
const [op] = getOnlyOperatorAndVal(params);
|
||||
return op === ExtendCustomFilterOperator.DOES_NOT_CONTAIN;
|
||||
},
|
||||
testMappingFilterColumn: (filterColumn) => {
|
||||
if (filterColumn.customFilters?.customFilters.length !== 1) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const firstCustomFilter = filterColumn.customFilters.customFilters[0];
|
||||
const valAsString = firstCustomFilter.val.toString();
|
||||
if (
|
||||
firstCustomFilter.operator === CustomFilterOperator.NOT_EQUALS
|
||||
&& valAsString.startsWith('*')
|
||||
&& valAsString.endsWith('*')
|
||||
) {
|
||||
return { operator1: ExtendCustomFilterOperator.DOES_NOT_CONTAIN, val1: valAsString.slice(1, -1) };
|
||||
}
|
||||
|
||||
return false;
|
||||
},
|
||||
};
|
||||
|
||||
export const STARTS_WITH: IFilterConditionItem = {
|
||||
label: 'sheets-filter-ui.conditions.starts-with',
|
||||
|
||||
operator: ExtendCustomFilterOperator.STARTS_WITH,
|
||||
order: OperatorOrder.FIRST,
|
||||
numOfParameters: 1,
|
||||
|
||||
getDefaultFormParams: () => ({ operator1: ExtendCustomFilterOperator.STARTS_WITH, val1: '' }),
|
||||
mapToFilterColumn: (mapParams) => ({
|
||||
customFilters: { customFilters: [{ val: `${mapParams.val1}*` }] },
|
||||
}),
|
||||
testMappingParams: (params) => {
|
||||
const [op] = getOnlyOperatorAndVal(params);
|
||||
return op === ExtendCustomFilterOperator.STARTS_WITH;
|
||||
},
|
||||
testMappingFilterColumn: (filterColumn) => {
|
||||
if (filterColumn.customFilters?.customFilters.length !== 1) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const firstCustomFilter = filterColumn.customFilters.customFilters[0];
|
||||
const valAsString = firstCustomFilter.val.toString();
|
||||
if (!firstCustomFilter.operator && valAsString.endsWith('*') && !valAsString.startsWith('*')) {
|
||||
return { operator1: ExtendCustomFilterOperator.STARTS_WITH, val1: valAsString.slice(0, -1) };
|
||||
}
|
||||
|
||||
return false;
|
||||
},
|
||||
};
|
||||
|
||||
export const ENDS_WITH: IFilterConditionItem = {
|
||||
label: 'sheets-filter-ui.conditions.ends-with',
|
||||
|
||||
operator: ExtendCustomFilterOperator.ENDS_WITH,
|
||||
order: OperatorOrder.FIRST,
|
||||
numOfParameters: 1,
|
||||
|
||||
getDefaultFormParams: () => ({ operator1: ExtendCustomFilterOperator.ENDS_WITH, val1: '' }),
|
||||
mapToFilterColumn: (mapParams) => ({
|
||||
customFilters: { customFilters: [{ val: `*${mapParams.val1}` }] },
|
||||
}),
|
||||
testMappingParams: (params) => {
|
||||
const [op] = getOnlyOperatorAndVal(params);
|
||||
return op === ExtendCustomFilterOperator.ENDS_WITH;
|
||||
},
|
||||
testMappingFilterColumn: (filterColumn) => {
|
||||
if (filterColumn.customFilters?.customFilters.length !== 1) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const firstCustomFilter = filterColumn.customFilters.customFilters[0];
|
||||
const valAsString = firstCustomFilter.val.toString();
|
||||
if (!firstCustomFilter.operator && valAsString.startsWith('*') && !valAsString.endsWith('*')) {
|
||||
return { operator1: ExtendCustomFilterOperator.ENDS_WITH, val1: valAsString.slice(1) };
|
||||
}
|
||||
|
||||
return false;
|
||||
},
|
||||
};
|
||||
|
||||
export const EQUALS: IFilterConditionItem = {
|
||||
label: 'sheets-filter-ui.conditions.equals',
|
||||
|
||||
operator: ExtendCustomFilterOperator.EQUALS,
|
||||
order: OperatorOrder.FIRST,
|
||||
numOfParameters: 1,
|
||||
|
||||
getDefaultFormParams: () => ({ operator1: ExtendCustomFilterOperator.EQUALS, val1: '' }),
|
||||
testMappingParams: (params) => {
|
||||
const [op] = getOnlyOperatorAndVal(params);
|
||||
return op === ExtendCustomFilterOperator.EQUALS;
|
||||
},
|
||||
|
||||
mapToFilterColumn: (mapParams) => {
|
||||
const { val1 } = mapParams;
|
||||
if (val1 === '') return null;
|
||||
|
||||
return {
|
||||
customFilters: { customFilters: [{ val: val1! }] },
|
||||
};
|
||||
},
|
||||
testMappingFilterColumn: (filterColumn) => {
|
||||
if (filterColumn.filters?.filters?.length === 1) {
|
||||
return { operator1: ExtendCustomFilterOperator.EQUALS, val1: '' };
|
||||
}
|
||||
|
||||
if (filterColumn.customFilters?.customFilters.length === 1 && !filterColumn.customFilters.customFilters[0].operator) {
|
||||
return { operator1: ExtendCustomFilterOperator.EQUALS, val1: filterColumn.customFilters.customFilters[0].val.toString() };
|
||||
}
|
||||
|
||||
return false;
|
||||
},
|
||||
};
|
||||
|
||||
// #region number conditions
|
||||
|
||||
export const GREATER_THAN: IFilterConditionItem = {
|
||||
label: 'sheets-filter-ui.conditions.greater-than',
|
||||
|
||||
operator: CustomFilterOperator.GREATER_THAN,
|
||||
numOfParameters: 1,
|
||||
order: OperatorOrder.FIRST,
|
||||
|
||||
getDefaultFormParams: () => ({ operator1: CustomFilterOperator.GREATER_THAN, val1: '' }),
|
||||
mapToFilterColumn: (mapParams) => ({
|
||||
customFilters: { customFilters: [{ val: mapParams.val1!, operator: CustomFilterOperator.GREATER_THAN }] },
|
||||
}),
|
||||
testMappingParams: (params) => {
|
||||
const [op] = getOnlyOperatorAndVal(params);
|
||||
return op === CustomFilterOperator.GREATER_THAN;
|
||||
},
|
||||
testMappingFilterColumn: (filterColumn) => {
|
||||
if (filterColumn.customFilters?.customFilters.length !== 1) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const firstCustomFilter = filterColumn.customFilters.customFilters[0];
|
||||
if (firstCustomFilter.operator !== CustomFilterOperator.GREATER_THAN) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return { operator1: CustomFilterOperator.GREATER_THAN, val1: firstCustomFilter.val.toString() };
|
||||
},
|
||||
};
|
||||
|
||||
export const GREATER_THAN_OR_EQUAL: IFilterConditionItem = {
|
||||
label: 'sheets-filter-ui.conditions.greater-than-or-equal',
|
||||
|
||||
operator: CustomFilterOperator.GREATER_THAN_OR_EQUAL,
|
||||
numOfParameters: 1,
|
||||
order: OperatorOrder.FIRST,
|
||||
|
||||
getDefaultFormParams: () => ({ operator1: CustomFilterOperator.GREATER_THAN_OR_EQUAL, val1: '' }),
|
||||
testMappingParams: (params) => {
|
||||
const [op] = getOnlyOperatorAndVal(params);
|
||||
return op === CustomFilterOperator.GREATER_THAN_OR_EQUAL;
|
||||
},
|
||||
|
||||
mapToFilterColumn: (mapParams) => ({
|
||||
customFilters: { customFilters: [{ val: mapParams.val1!, operator: CustomFilterOperator.GREATER_THAN_OR_EQUAL }] },
|
||||
}),
|
||||
testMappingFilterColumn: (filterColumn) => {
|
||||
if (filterColumn.customFilters?.customFilters.length !== 1) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const firstCustomFilter = filterColumn.customFilters.customFilters[0];
|
||||
if (firstCustomFilter.operator !== CustomFilterOperator.GREATER_THAN_OR_EQUAL) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return { operator1: CustomFilterOperator.GREATER_THAN_OR_EQUAL, val1: firstCustomFilter.val.toString() };
|
||||
},
|
||||
};
|
||||
|
||||
export const LESS_THAN: IFilterConditionItem = {
|
||||
label: 'sheets-filter-ui.conditions.less-than',
|
||||
|
||||
operator: CustomFilterOperator.LESS_THAN,
|
||||
numOfParameters: 1,
|
||||
order: OperatorOrder.FIRST,
|
||||
|
||||
getDefaultFormParams: () => ({ operator1: CustomFilterOperator.LESS_THAN, val1: '' }),
|
||||
testMappingParams: (params) => {
|
||||
const [op] = getOnlyOperatorAndVal(params);
|
||||
return op === CustomFilterOperator.LESS_THAN;
|
||||
},
|
||||
|
||||
mapToFilterColumn: (mapParams) => ({
|
||||
customFilters: { customFilters: [{ val: mapParams.val1!, operator: CustomFilterOperator.LESS_THAN }] },
|
||||
}),
|
||||
testMappingFilterColumn: (filterColumn) => {
|
||||
if (filterColumn.customFilters?.customFilters.length !== 1) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const firstCustomFilter = filterColumn.customFilters.customFilters[0];
|
||||
if (firstCustomFilter.operator !== CustomFilterOperator.LESS_THAN) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return { operator1: CustomFilterOperator.LESS_THAN, val1: firstCustomFilter.val.toString() };
|
||||
},
|
||||
};
|
||||
|
||||
export const LESS_THAN_OR_EQUAL: IFilterConditionItem = {
|
||||
label: 'sheets-filter-ui.conditions.less-than-or-equal',
|
||||
|
||||
operator: CustomFilterOperator.LESS_THAN_OR_EQUAL,
|
||||
numOfParameters: 1,
|
||||
order: OperatorOrder.FIRST,
|
||||
|
||||
getDefaultFormParams: () => ({ operator1: CustomFilterOperator.LESS_THAN_OR_EQUAL, val1: '' }),
|
||||
testMappingParams: (params) => {
|
||||
const [op] = getOnlyOperatorAndVal(params);
|
||||
return op === CustomFilterOperator.LESS_THAN_OR_EQUAL;
|
||||
},
|
||||
|
||||
mapToFilterColumn: (mapParams) => ({
|
||||
customFilters: { customFilters: [{ val: mapParams.val1!, operator: CustomFilterOperator.LESS_THAN_OR_EQUAL }] },
|
||||
}),
|
||||
testMappingFilterColumn: (filterColumn) => {
|
||||
if (filterColumn.customFilters?.customFilters.length !== 1) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const firstCustomFilter = filterColumn.customFilters.customFilters[0];
|
||||
if (firstCustomFilter.operator !== CustomFilterOperator.LESS_THAN_OR_EQUAL) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return { operator1: CustomFilterOperator.LESS_THAN_OR_EQUAL, val1: firstCustomFilter.val.toString() };
|
||||
},
|
||||
};
|
||||
|
||||
export const EQUAL: IFilterConditionItem = {
|
||||
label: 'sheets-filter-ui.conditions.equal',
|
||||
|
||||
operator: CustomFilterOperator.EQUAL,
|
||||
numOfParameters: 1,
|
||||
order: OperatorOrder.FIRST,
|
||||
|
||||
getDefaultFormParams: () => ({ operator1: CustomFilterOperator.EQUAL, val1: '' }),
|
||||
testMappingParams: (params) => {
|
||||
const [op] = getOnlyOperatorAndVal(params);
|
||||
return op === CustomFilterOperator.EQUAL;
|
||||
},
|
||||
|
||||
mapToFilterColumn: (mapParams) => ({
|
||||
customFilters: { customFilters: [{ val: mapParams.val1!, operator: CustomFilterOperator.EQUAL }] },
|
||||
}),
|
||||
testMappingFilterColumn: (filterColumn) => {
|
||||
if (filterColumn.customFilters?.customFilters.length !== 1) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const firstCustomFilter = filterColumn.customFilters.customFilters[0];
|
||||
if (firstCustomFilter.operator !== CustomFilterOperator.EQUAL) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return { operator1: CustomFilterOperator.EQUAL, val1: firstCustomFilter.val.toString() };
|
||||
},
|
||||
};
|
||||
|
||||
export const NOT_EQUAL: IFilterConditionItem = {
|
||||
label: 'sheets-filter-ui.conditions.not-equal',
|
||||
|
||||
operator: CustomFilterOperator.NOT_EQUALS,
|
||||
numOfParameters: 1,
|
||||
order: OperatorOrder.FIRST,
|
||||
|
||||
getDefaultFormParams: () => ({ operator1: CustomFilterOperator.NOT_EQUALS, val1: '' }),
|
||||
testMappingParams: (params) => {
|
||||
const [op] = getOnlyOperatorAndVal(params);
|
||||
return op === CustomFilterOperator.NOT_EQUALS;
|
||||
},
|
||||
|
||||
mapToFilterColumn: (mapParams) => ({
|
||||
customFilters: { customFilters: [{ val: mapParams.val1!, operator: CustomFilterOperator.NOT_EQUALS }] },
|
||||
}),
|
||||
testMappingFilterColumn: (filterColumn) => {
|
||||
if (filterColumn.customFilters?.customFilters.length !== 1) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const firstCustomFilter = filterColumn.customFilters.customFilters[0];
|
||||
if (firstCustomFilter.operator !== CustomFilterOperator.NOT_EQUALS) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return { operator1: CustomFilterOperator.NOT_EQUALS, val1: firstCustomFilter.val.toString() };
|
||||
},
|
||||
};
|
||||
|
||||
// #endregion
|
||||
|
||||
export const BETWEEN: IFilterConditionItem = {
|
||||
label: 'sheets-filter-ui.conditions.between',
|
||||
|
||||
operator: ExtendCustomFilterOperator.BETWEEN,
|
||||
order: OperatorOrder.SECOND,
|
||||
numOfParameters: 2,
|
||||
|
||||
getDefaultFormParams: () => ({
|
||||
and: true,
|
||||
operator1: CustomFilterOperator.GREATER_THAN_OR_EQUAL,
|
||||
val1: '',
|
||||
operator2: CustomFilterOperator.LESS_THAN_OR_EQUAL,
|
||||
val2: '',
|
||||
}),
|
||||
testMappingParams: (params) => {
|
||||
const { and, operator1, operator2 } = params;
|
||||
if (!and) return false;
|
||||
|
||||
const operators = [operator1, operator2];
|
||||
return operators.includes(CustomFilterOperator.GREATER_THAN_OR_EQUAL)
|
||||
&& operators.includes(CustomFilterOperator.LESS_THAN_OR_EQUAL);
|
||||
},
|
||||
|
||||
mapToFilterColumn: (mapParams) => {
|
||||
const { val1, val2, operator1 } = mapParams;
|
||||
const operator1IsGreater = operator1 === CustomFilterOperator.GREATER_THAN_OR_EQUAL;
|
||||
return {
|
||||
customFilters: {
|
||||
and: BooleanNumber.TRUE,
|
||||
customFilters: [
|
||||
{ val: operator1IsGreater ? val1! : val2!, operator: CustomFilterOperator.GREATER_THAN_OR_EQUAL },
|
||||
{ val: operator1IsGreater ? val2! : val1!, operator: CustomFilterOperator.LESS_THAN_OR_EQUAL },
|
||||
],
|
||||
},
|
||||
};
|
||||
},
|
||||
testMappingFilterColumn: (filterColumn) => {
|
||||
if (filterColumn.customFilters?.customFilters.length !== 2) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const [firstCustomFilter, secondCustomFilter] = filterColumn.customFilters.customFilters;
|
||||
if (
|
||||
firstCustomFilter.operator === CustomFilterOperator.GREATER_THAN_OR_EQUAL
|
||||
&& secondCustomFilter.operator === CustomFilterOperator.LESS_THAN_OR_EQUAL
|
||||
&& filterColumn.customFilters.and
|
||||
) {
|
||||
return {
|
||||
and: true,
|
||||
operator1: CustomFilterOperator.GREATER_THAN_OR_EQUAL,
|
||||
val1: firstCustomFilter.val.toString(),
|
||||
operator2: CustomFilterOperator.LESS_THAN_OR_EQUAL,
|
||||
val2: secondCustomFilter.val.toString(),
|
||||
};
|
||||
}
|
||||
|
||||
if (
|
||||
secondCustomFilter.operator === CustomFilterOperator.GREATER_THAN_OR_EQUAL
|
||||
&& firstCustomFilter.operator === CustomFilterOperator.LESS_THAN_OR_EQUAL
|
||||
&& filterColumn.customFilters.and
|
||||
) {
|
||||
return {
|
||||
and: true,
|
||||
operator1: CustomFilterOperator.GREATER_THAN_OR_EQUAL,
|
||||
val1: secondCustomFilter.val.toString(),
|
||||
operator2: CustomFilterOperator.LESS_THAN_OR_EQUAL,
|
||||
val2: firstCustomFilter.val.toLocaleString(),
|
||||
};
|
||||
}
|
||||
|
||||
return false;
|
||||
},
|
||||
};
|
||||
|
||||
export const NOT_BETWEEN: IFilterConditionItem = {
|
||||
label: 'sheets-filter-ui.conditions.not-between',
|
||||
|
||||
operator: ExtendCustomFilterOperator.NOT_BETWEEN,
|
||||
order: OperatorOrder.SECOND,
|
||||
numOfParameters: 2,
|
||||
|
||||
getDefaultFormParams: () => ({
|
||||
operator1: CustomFilterOperator.LESS_THAN,
|
||||
val1: '',
|
||||
operator2: CustomFilterOperator.GREATER_THAN,
|
||||
val2: '',
|
||||
}),
|
||||
testMappingParams: (params) => {
|
||||
const { and, operator1, operator2 } = params;
|
||||
if (and) return false;
|
||||
|
||||
const operators = [operator1, operator2];
|
||||
return operators.includes(CustomFilterOperator.GREATER_THAN) && operators.includes(CustomFilterOperator.LESS_THAN);
|
||||
},
|
||||
|
||||
mapToFilterColumn: (mapParams) => {
|
||||
const { val1, val2, operator1 } = mapParams;
|
||||
const operator1IsGreater = operator1 === CustomFilterOperator.GREATER_THAN;
|
||||
return {
|
||||
customFilters: {
|
||||
customFilters: [
|
||||
{ val: operator1IsGreater ? val1! : val2!, operator: CustomFilterOperator.GREATER_THAN },
|
||||
{ val: operator1IsGreater ? val2! : val1!, operator: CustomFilterOperator.LESS_THAN },
|
||||
],
|
||||
},
|
||||
};
|
||||
},
|
||||
testMappingFilterColumn: (filterColumn) => {
|
||||
if (filterColumn.customFilters?.customFilters.length !== 2) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const [firstCustomFilter, secondCustomFilter] = filterColumn.customFilters.customFilters;
|
||||
if (
|
||||
firstCustomFilter.operator === CustomFilterOperator.LESS_THAN
|
||||
&& secondCustomFilter.operator === CustomFilterOperator.GREATER_THAN
|
||||
&& !filterColumn.customFilters.and
|
||||
) {
|
||||
return {
|
||||
operator1: CustomFilterOperator.LESS_THAN,
|
||||
val1: firstCustomFilter.val.toString(),
|
||||
operator2: CustomFilterOperator.GREATER_THAN,
|
||||
val2: secondCustomFilter.val.toString(),
|
||||
};
|
||||
}
|
||||
|
||||
if (
|
||||
secondCustomFilter.operator === CustomFilterOperator.LESS_THAN
|
||||
&& firstCustomFilter.operator === CustomFilterOperator.GREATER_THAN
|
||||
&& !filterColumn.customFilters.and
|
||||
) {
|
||||
return {
|
||||
operator1: CustomFilterOperator.GREATER_THAN,
|
||||
val1: secondCustomFilter.val.toString(),
|
||||
operator2: CustomFilterOperator.LESS_THAN,
|
||||
val2: firstCustomFilter.val.toLocaleString(),
|
||||
};
|
||||
}
|
||||
|
||||
return false;
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* This should be test last. If no other condition item can be mapped, then it should be mapped.
|
||||
*/
|
||||
export const CUSTOM: IFilterConditionItem = {
|
||||
label: 'sheets-filter-ui.conditions.custom',
|
||||
|
||||
operator: ExtendCustomFilterOperator.CUSTOM,
|
||||
order: OperatorOrder.SECOND,
|
||||
numOfParameters: 2,
|
||||
|
||||
getDefaultFormParams: () => {
|
||||
return {
|
||||
operator1: ExtendCustomFilterOperator.NONE,
|
||||
val1: '',
|
||||
operator2: ExtendCustomFilterOperator.NONE,
|
||||
val2: '',
|
||||
};
|
||||
},
|
||||
testMappingParams: () => true,
|
||||
|
||||
mapToFilterColumn: (mapParams) => {
|
||||
const { and, val1, val2, operator1, operator2 } = mapParams;
|
||||
|
||||
function mapOperator(operator: FilterOperator | undefined, val: string) {
|
||||
// eslint-disable-next-line ts/no-use-before-define
|
||||
for (const condition of ALL_CONDITIONS) {
|
||||
if (condition.operator === operator) {
|
||||
return condition.mapToFilterColumn({ val1: val, operator1: operator });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const operator1IsNone = !operator1 || operator1 === FilterConditionItems.NONE.operator;
|
||||
const operator2IsNone = !operator2 || operator2 === FilterConditionItems.NONE.operator;
|
||||
|
||||
if (operator1IsNone && operator2IsNone) {
|
||||
return NONE.mapToFilterColumn({});
|
||||
}
|
||||
|
||||
if (operator1IsNone) {
|
||||
return mapOperator(operator2!, val2!)!;
|
||||
}
|
||||
|
||||
if (operator2IsNone) {
|
||||
return mapOperator(operator1!, val1!)!;
|
||||
}
|
||||
|
||||
const mappedCustomFilter1 = mapOperator(operator1!, val1!)!;
|
||||
const mappedCustomFilter2 = mapOperator(operator2!, val2!)!;
|
||||
const customFilters: ICustomFilters = {
|
||||
customFilters: [
|
||||
mappedCustomFilter1.customFilters!.customFilters[0],
|
||||
mappedCustomFilter2.customFilters!.customFilters[0],
|
||||
],
|
||||
};
|
||||
|
||||
if (and) customFilters.and = BooleanNumber.TRUE;
|
||||
return { customFilters };
|
||||
},
|
||||
testMappingFilterColumn: (filterColumn) => {
|
||||
if (filterColumn.customFilters?.customFilters.length !== 2) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const params = filterColumn.customFilters.customFilters.map((customFilter) => {
|
||||
return testMappingFilterColumn({ customFilters: { customFilters: [customFilter] } });
|
||||
});
|
||||
|
||||
const result: IFilterConditionFormParams = {
|
||||
operator1: params[0][0].operator,
|
||||
val1: params[0][1].val1,
|
||||
operator2: params[1][0].operator,
|
||||
val2: params[1][1].val1,
|
||||
};
|
||||
|
||||
if (filterColumn.customFilters.and) {
|
||||
result.and = true;
|
||||
}
|
||||
|
||||
return result;
|
||||
},
|
||||
};
|
||||
|
||||
export const ALL_CONDITIONS: IFilterConditionItem[] = [
|
||||
// ------------------------------
|
||||
NONE,
|
||||
// ------------------------------
|
||||
EMPTY,
|
||||
NOT_EMPTY,
|
||||
// ------------------------------
|
||||
TEXT_CONTAINS,
|
||||
DOES_NOT_CONTAIN,
|
||||
STARTS_WITH,
|
||||
ENDS_WITH,
|
||||
EQUALS,
|
||||
// ------------------------------
|
||||
GREATER_THAN,
|
||||
GREATER_THAN_OR_EQUAL,
|
||||
LESS_THAN,
|
||||
LESS_THAN_OR_EQUAL,
|
||||
EQUAL,
|
||||
NOT_EQUAL,
|
||||
BETWEEN,
|
||||
NOT_BETWEEN,
|
||||
// ------------------------------
|
||||
CUSTOM,
|
||||
];
|
||||
|
||||
export function getItemByOperator(operator: FilterOperator): IFilterConditionItem {
|
||||
const item = ALL_CONDITIONS.find((condition) => condition.operator === operator);
|
||||
if (!item) {
|
||||
throw new Error(`[SheetsFilter]: no condition item found for operator: ${operator}`);
|
||||
}
|
||||
|
||||
return item;
|
||||
}
|
||||
|
||||
export function testMappingParams(mapParams: IFilterConditionFormParams, numOfParameters: number): IFilterConditionItem {
|
||||
// TODO@wzhudev: iteration here can be optimized
|
||||
// We match operators with same count of parameters first.
|
||||
for (const condition of ALL_CONDITIONS.filter((condition) => condition.numOfParameters === numOfParameters)) {
|
||||
if (condition.numOfParameters !== 0 && condition.testMappingParams(mapParams)) {
|
||||
return condition;
|
||||
}
|
||||
}
|
||||
|
||||
// And fallback to others.
|
||||
for (const condition of ALL_CONDITIONS) {
|
||||
if (condition.testMappingParams(mapParams)) {
|
||||
return condition;
|
||||
}
|
||||
}
|
||||
|
||||
throw new Error('[SheetsFilter]: no condition item can be mapped from the filter map params!');
|
||||
}
|
||||
|
||||
export function getInitialFormParams(operator: FilterOperator): IFilterConditionFormParams {
|
||||
const condition = ALL_CONDITIONS.find((condition) => condition.operator === operator)!;
|
||||
if (condition?.numOfParameters === 0) {
|
||||
return { operator1: condition.operator };
|
||||
}
|
||||
|
||||
return condition.getDefaultFormParams();
|
||||
}
|
||||
|
||||
export function mapToFilterColumn(condition: IFilterConditionItem, mapParams: IFilterConditionFormParams): Nullable<Omit<IFilterColumn, 'colId'>> {
|
||||
return condition.mapToFilterColumn(mapParams);
|
||||
}
|
||||
|
||||
export function testMappingFilterColumn(filterColumn: Nullable<Omit<IFilterColumn, 'colId'>>): [IFilterConditionItem, IFilterConditionFormParams] {
|
||||
if (!filterColumn) {
|
||||
return [NONE, {}];
|
||||
}
|
||||
|
||||
for (const condition of ALL_CONDITIONS) {
|
||||
const mapParams = condition.testMappingFilterColumn(filterColumn);
|
||||
if (mapParams) {
|
||||
return [condition, mapParams];
|
||||
}
|
||||
}
|
||||
|
||||
// Return NONE by default.
|
||||
return [NONE, {}];
|
||||
}
|
||||
}
|
||||
|
||||
function getOnlyOperatorAndVal(mapParams: IFilterConditionFormParams): [FilterOperator, string | undefined] {
|
||||
const { operator1, operator2, val1, val2 } = mapParams;
|
||||
if (operator1 && operator2) {
|
||||
throw new Error('Both operator1 and operator2 are set!');
|
||||
}
|
||||
|
||||
if (!operator1 && !operator2) {
|
||||
throw new Error('Neither operator1 and operator2 and both not set!');
|
||||
}
|
||||
|
||||
return operator1 ? [operator1, val1] : [operator2!, val2];
|
||||
}
|
||||
101
packages/sheets-filter-ui/src/models/extended-operators.ts
Normal file
101
packages/sheets-filter-ui/src/models/extended-operators.ts
Normal file
@@ -0,0 +1,101 @@
|
||||
/**
|
||||
* Copyright 2023-present DreamNum Co., Ltd.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
export enum OperatorOrder {
|
||||
FIRST,
|
||||
SECOND,
|
||||
}
|
||||
|
||||
/**
|
||||
* Extended custom filter operators.
|
||||
*
|
||||
* These operators are not defined in OOXML,
|
||||
* so when exporting these operators we need to fallback to `CustomFilterOperator` and
|
||||
* other possible filter types.
|
||||
*/
|
||||
export enum ExtendCustomFilterOperator {
|
||||
NONE = 'none',
|
||||
|
||||
/** Fallback to `<customFilter val="123*" />` */
|
||||
STARTS_WITH = 'startsWith',
|
||||
/** Fallback to `<customFilter operator="notEqual" val="123*" />` */
|
||||
DOES_NOT_START_WITH = 'doesNotStartWith',
|
||||
|
||||
/** Fallback to `<customFilter val="*123" />` */
|
||||
ENDS_WITH = 'endsWith',
|
||||
/** Fallback to `<customFilter operator="notEqual" val="*123" />` */
|
||||
DOES_NOT_END_WITH = 'doesNotEndWith',
|
||||
|
||||
/** Fallback to `<customFilter val="*123*" />` */
|
||||
CONTAINS = 'contains',
|
||||
/** Fallback to `<customFilter operator="notEqual" val="*123*" />` */
|
||||
DOES_NOT_CONTAIN = 'doesNotContain',
|
||||
|
||||
/**
|
||||
* Text equals. It is not same as CustomFilterOperator.EQUAL.
|
||||
*
|
||||
* When equals empty, it will be mapped to.
|
||||
*
|
||||
* <autoFilter>
|
||||
* <filterColumn colId="0">
|
||||
* <filters blank="1"/>
|
||||
* </filterColumn>
|
||||
* </autoFilter>
|
||||
*/
|
||||
EQUALS = 'equals',
|
||||
|
||||
/** Fallback to `<customFilter operator="notEqual" val="" />` */
|
||||
NOT_EQUALS = 'notEquals',
|
||||
|
||||
/**
|
||||
* Fallback to `<filter blank="1" />`.
|
||||
*
|
||||
* It can also fallback to `<customFilter val="" />`, when with another custom filter.
|
||||
*/
|
||||
EMPTY = 'empty',
|
||||
|
||||
/** Fallback to `<customFilter operator="notEqual" val=" " />` */
|
||||
NOT_EMPTY = 'notEmpty',
|
||||
|
||||
/**
|
||||
* Falls back to the following XML:
|
||||
*
|
||||
* ```xml
|
||||
* <customFilters and="1">
|
||||
* <customFilter operator="greaterThanOrEqual" val="123"/>
|
||||
* <customFilter operator="lessThanOrEqual" val="456"/>
|
||||
* </customFilters>
|
||||
* ```
|
||||
*
|
||||
* Actually in Microsoft Excel, `NOT_BETWEEN` is still `BETWEEN`, as long as the two operators
|
||||
* are `greaterThanOrEqual` and `lessThanOrEqual`.
|
||||
*/
|
||||
BETWEEN = 'between',
|
||||
|
||||
/**
|
||||
* Falls back to the following XML:
|
||||
*
|
||||
* ```xml
|
||||
* <customFilters> <!-- no `and="1"` means `OR` -->
|
||||
* <customFilter operator="lessThan" val="456"/>
|
||||
* <customFilter operator="greaterThan" val="123"/>
|
||||
* </customFilters>
|
||||
* ```
|
||||
*/
|
||||
NOT_BETWEEN = 'notBetween',
|
||||
|
||||
CUSTOM = 'custom',
|
||||
}
|
||||
54
packages/sheets-filter-ui/src/models/utils.ts
Normal file
54
packages/sheets-filter-ui/src/models/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 { IFilterByValueWithTreeItem } from '../services/sheets-filter-panel.service';
|
||||
|
||||
export function statisticFilterByValueItems(items: IFilterByValueWithTreeItem[]): {
|
||||
checked: number;
|
||||
unchecked: number;
|
||||
checkedItems: IFilterByValueWithTreeItem[];
|
||||
uncheckedItems: IFilterByValueWithTreeItem[];
|
||||
} {
|
||||
const checkedItems: IFilterByValueWithTreeItem[] = [];
|
||||
const uncheckedItems: IFilterByValueWithTreeItem[] = [];
|
||||
let checked = 0;
|
||||
let unchecked = 0;
|
||||
|
||||
function traverse(node: IFilterByValueWithTreeItem) {
|
||||
if (node.leaf) {
|
||||
if (node.checked) {
|
||||
checkedItems.push(node);
|
||||
checked += node.count;
|
||||
} else {
|
||||
uncheckedItems.push(node);
|
||||
unchecked += node.count;
|
||||
}
|
||||
}
|
||||
|
||||
if (node.children) {
|
||||
node.children.forEach(traverse);
|
||||
}
|
||||
}
|
||||
|
||||
items.forEach(traverse);
|
||||
|
||||
return {
|
||||
checkedItems,
|
||||
uncheckedItems,
|
||||
checked,
|
||||
unchecked,
|
||||
};
|
||||
}
|
||||
111
packages/sheets-filter-ui/src/plugin.ts
Normal file
111
packages/sheets-filter-ui/src/plugin.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 { IUniverSheetsFilterUIConfig } from './config/config';
|
||||
import {
|
||||
DependentOn,
|
||||
IConfigService,
|
||||
Inject,
|
||||
Injector,
|
||||
merge,
|
||||
Optional,
|
||||
Plugin,
|
||||
registerDependencies,
|
||||
touchDependencies,
|
||||
UniverInstanceType,
|
||||
} from '@univerjs/core';
|
||||
import { UniverRenderEnginePlugin } from '@univerjs/engine-render';
|
||||
import { IRPCChannelService, toModule } from '@univerjs/rpc';
|
||||
import { UniverSheetsPlugin } from '@univerjs/sheets';
|
||||
import { UniverSheetsFilterPlugin } from '@univerjs/sheets-filter';
|
||||
import { UniverSheetsUIPlugin } from '@univerjs/sheets-ui';
|
||||
import pkg from '../package.json';
|
||||
import { defaultPluginConfig, SHEETS_FILTER_UI_PLUGIN_CONFIG_KEY } from './config/config';
|
||||
import { ComponentsController } from './controllers/components.controller';
|
||||
import { SheetsFilterPermissionController } from './controllers/sheets-filter-permission.controller';
|
||||
import { SheetsFilterUIDesktopController } from './controllers/ui.controller';
|
||||
import { ISheetsFilterPanelService, SheetsFilterPanelService } from './services/sheets-filter-panel.service';
|
||||
import {
|
||||
ISheetsGenerateFilterValuesService,
|
||||
SHEETS_GENERATE_FILTER_VALUES_SERVICE_NAME,
|
||||
} from './worker/generate-filter-values.service';
|
||||
|
||||
/**
|
||||
* The plugin for the desktop version of the sheets filter UI. Its type is {@link UniverInstanceType.UNIVER_SHEET}.
|
||||
*/
|
||||
@DependentOn(
|
||||
UniverRenderEnginePlugin,
|
||||
UniverSheetsPlugin,
|
||||
UniverSheetsFilterPlugin,
|
||||
UniverSheetsUIPlugin
|
||||
)
|
||||
export class UniverSheetsFilterUIPlugin extends Plugin {
|
||||
static override type = UniverInstanceType.UNIVER_SHEET;
|
||||
static override pluginName = 'SHEET_FILTER_UI_PLUGIN';
|
||||
static override packageName = pkg.name;
|
||||
static override version = pkg.version;
|
||||
|
||||
constructor(
|
||||
private readonly _config: Partial<IUniverSheetsFilterUIConfig> = defaultPluginConfig,
|
||||
@Inject(Injector) protected readonly _injector: Injector,
|
||||
@IConfigService private readonly _configService: IConfigService,
|
||||
@Optional(IRPCChannelService) private readonly _rpcChannelService?: IRPCChannelService
|
||||
) {
|
||||
super();
|
||||
|
||||
// Manage the plugin configuration.
|
||||
const { menu, ...rest } = merge(
|
||||
{},
|
||||
defaultPluginConfig,
|
||||
this._config
|
||||
);
|
||||
if (menu) {
|
||||
this._configService.setConfig('menu', menu, { merge: true });
|
||||
}
|
||||
this._configService.setConfig(SHEETS_FILTER_UI_PLUGIN_CONFIG_KEY, rest);
|
||||
}
|
||||
|
||||
override onStarting(): void {
|
||||
this._injector.add([ComponentsController]);
|
||||
this._injector.get(ComponentsController);
|
||||
registerDependencies(this._injector, [
|
||||
[ISheetsFilterPanelService, { useClass: SheetsFilterPanelService }],
|
||||
[SheetsFilterPermissionController],
|
||||
[SheetsFilterUIDesktopController],
|
||||
]);
|
||||
|
||||
if (this._config.useRemoteFilterValuesGenerator && this._rpcChannelService) {
|
||||
this._injector.add([ISheetsGenerateFilterValuesService, {
|
||||
useFactory: (): ISheetsGenerateFilterValuesService =>
|
||||
toModule<ISheetsGenerateFilterValuesService>(
|
||||
this._rpcChannelService!.requestChannel(SHEETS_GENERATE_FILTER_VALUES_SERVICE_NAME)
|
||||
),
|
||||
}]);
|
||||
}
|
||||
}
|
||||
|
||||
override onReady(): void {
|
||||
touchDependencies(this._injector, [
|
||||
[SheetsFilterPermissionController],
|
||||
]);
|
||||
}
|
||||
|
||||
override onRendered(): void {
|
||||
touchDependencies(this._injector, [
|
||||
[SheetsFilterUIDesktopController],
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,711 @@
|
||||
/**
|
||||
* 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 { IOperation, IWorkbookData, Workbook } from '@univerjs/core';
|
||||
import type { IInsertColMutationParams } from '@univerjs/sheets';
|
||||
import type { IEditorBridgeServiceVisibleParam } from '@univerjs/sheets-ui';
|
||||
import type { IOpenFilterPanelOperationParams } from '../../commands/operations/sheets-filter.operation';
|
||||
import type { IFilterConditionFormParams } from '../../models/conditions';
|
||||
import type { IFilterByValueWithTreeItem } from '../sheets-filter-panel.service';
|
||||
import {
|
||||
awaitTime,
|
||||
CommandType,
|
||||
ICommandService,
|
||||
Inject,
|
||||
Injector,
|
||||
LocaleService,
|
||||
LocaleType,
|
||||
Plugin,
|
||||
Univer,
|
||||
UniverInstanceType,
|
||||
} from '@univerjs/core';
|
||||
import { InsertColMutation } from '@univerjs/sheets';
|
||||
import { CustomFilterOperator, FilterBy, SheetsFilterService, UniverSheetsFilterPlugin } from '@univerjs/sheets-filter';
|
||||
import { afterEach, beforeEach, describe, expect, it, vitest } from 'vitest';
|
||||
import {
|
||||
E_ITEMS,
|
||||
ITEMS,
|
||||
ITEMS_WITH_EMPTY,
|
||||
WithCustomFilterModelFactory,
|
||||
WithMergedCellFilterFactory,
|
||||
WithMultiEmptyCellsModelFactory,
|
||||
WithTwoFilterColumnsFactory,
|
||||
WithValuesAndEmptyFilterModelFactory,
|
||||
WithValuesFilterModelFactory,
|
||||
} from '../../__tests__/data';
|
||||
import { CloseFilterPanelOperation, OpenFilterPanelOperation } from '../../commands/operations/sheets-filter.operation';
|
||||
import { FilterConditionItems } from '../../models/conditions';
|
||||
import { ExtendCustomFilterOperator } from '../../models/extended-operators';
|
||||
import {
|
||||
ByColorsModel,
|
||||
ByConditionsModel,
|
||||
ByValuesModel,
|
||||
ISheetsFilterPanelService,
|
||||
SheetsFilterPanelService,
|
||||
} from '../sheets-filter-panel.service';
|
||||
|
||||
const SetCellEditVisibleOperation: IOperation<IEditorBridgeServiceVisibleParam> = {
|
||||
id: 'sheet.operation.set-cell-edit-visible',
|
||||
type: CommandType.OPERATION,
|
||||
handler: () => {
|
||||
return true;
|
||||
},
|
||||
};
|
||||
|
||||
function countLeafNodesByCheckedStatus(
|
||||
items: IFilterByValueWithTreeItem[],
|
||||
checkedStatus: boolean
|
||||
): number {
|
||||
let count = 0;
|
||||
|
||||
function traverse(node: IFilterByValueWithTreeItem) {
|
||||
if (node.leaf && node.checked === checkedStatus) {
|
||||
count++;
|
||||
}
|
||||
if (node.children) {
|
||||
node.children.forEach(traverse);
|
||||
}
|
||||
}
|
||||
|
||||
items.forEach(traverse);
|
||||
return count;
|
||||
}
|
||||
|
||||
function getAllLeafNodes(
|
||||
items: IFilterByValueWithTreeItem[]
|
||||
): IFilterByValueWithTreeItem[] {
|
||||
const leafNodes: IFilterByValueWithTreeItem[] = [];
|
||||
|
||||
function traverse(node: IFilterByValueWithTreeItem) {
|
||||
if (node.leaf) {
|
||||
leafNodes.push(node);
|
||||
}
|
||||
if (node.children) {
|
||||
node.children.forEach(traverse);
|
||||
}
|
||||
}
|
||||
|
||||
items.forEach(traverse);
|
||||
return leafNodes;
|
||||
}
|
||||
|
||||
function createSheetsFilterPanelServiceTestBed(workbookData: IWorkbookData) {
|
||||
const univer = new Univer();
|
||||
const injector = univer.__getInjector();
|
||||
const get = injector.get.bind(injector);
|
||||
|
||||
class SheetsFilterPanelTestPlugin extends Plugin {
|
||||
static override type = UniverInstanceType.UNIVER_SHEET;
|
||||
static override pluginName = 'sheets-filter-panel-test';
|
||||
|
||||
constructor(_config: unknown, @Inject(Injector) protected readonly _injector: Injector) {
|
||||
super();
|
||||
}
|
||||
|
||||
override onStarting(): void {
|
||||
this._injector.add([ISheetsFilterPanelService, { useClass: SheetsFilterPanelService }]);
|
||||
}
|
||||
}
|
||||
|
||||
get(LocaleService).load({});
|
||||
|
||||
univer.registerPlugin(UniverSheetsFilterPlugin);
|
||||
univer.registerPlugin(SheetsFilterPanelTestPlugin);
|
||||
|
||||
univer.createUnit<IWorkbookData, Workbook>(UniverInstanceType.UNIVER_SHEET, workbookData);
|
||||
|
||||
const commandService = get(ICommandService);
|
||||
|
||||
[
|
||||
OpenFilterPanelOperation,
|
||||
CloseFilterPanelOperation,
|
||||
SetCellEditVisibleOperation,
|
||||
].forEach((command) => commandService.registerCommand(command));
|
||||
|
||||
return { univer, get };
|
||||
}
|
||||
|
||||
describe('test "SheetsFilterPanelService"', () => {
|
||||
let univer: Univer;
|
||||
let get: Injector['get'];
|
||||
let commandService: ICommandService;
|
||||
let sheetsFilterService: SheetsFilterService;
|
||||
let sheetsFilterPanelService: ISheetsFilterPanelService;
|
||||
|
||||
function prepare(workbookData: IWorkbookData) {
|
||||
const testBed = createSheetsFilterPanelServiceTestBed(workbookData);
|
||||
|
||||
univer = testBed.univer;
|
||||
get = testBed.get;
|
||||
|
||||
commandService = get(ICommandService);
|
||||
sheetsFilterService = get(SheetsFilterService);
|
||||
sheetsFilterPanelService = get(ISheetsFilterPanelService);
|
||||
}
|
||||
|
||||
afterEach(() => {
|
||||
univer.dispose();
|
||||
});
|
||||
|
||||
describe('test filter by conditions', () => {
|
||||
describe('test initialization behavior', () => {
|
||||
it('should initialize by conditional model when the filter column is by custom filters', () => {
|
||||
prepare(WithCustomFilterModelFactory());
|
||||
|
||||
expect(commandService.syncExecuteCommand(OpenFilterPanelOperation.id, {
|
||||
unitId: 'test',
|
||||
subUnitId: 'sheet1',
|
||||
col: 0,
|
||||
} as IOpenFilterPanelOperationParams)).toBe(true);
|
||||
|
||||
expect(sheetsFilterPanelService.filterBy).toBe(FilterBy.CONDITIONS);
|
||||
const filterByModel = sheetsFilterPanelService.filterByModel as ByConditionsModel;
|
||||
expect(filterByModel instanceof ByConditionsModel).toBeTruthy();
|
||||
expect(filterByModel.conditionItem.operator).toBe(FilterConditionItems.GREATER_THAN.operator);
|
||||
expect(filterByModel.filterConditionFormParams).toEqual({ operator1: CustomFilterOperator.GREATER_THAN, val1: '123' });
|
||||
});
|
||||
});
|
||||
|
||||
describe('test behavior when changing the primary operator', () => {
|
||||
it('should update the filter condition item when the primary operator changes', () => {
|
||||
prepare(WithCustomFilterModelFactory());
|
||||
|
||||
expect(commandService.syncExecuteCommand(OpenFilterPanelOperation.id, {
|
||||
unitId: 'test',
|
||||
subUnitId: 'sheet1',
|
||||
col: 0,
|
||||
} as IOpenFilterPanelOperationParams)).toBe(true);
|
||||
|
||||
const filterByModel = sheetsFilterPanelService.filterByModel as ByConditionsModel;
|
||||
|
||||
filterByModel.onPrimaryConditionChange(ExtendCustomFilterOperator.BETWEEN);
|
||||
expect(filterByModel.conditionItem.operator).toBe(FilterConditionItems.BETWEEN.operator);
|
||||
expect(filterByModel.filterConditionFormParams).toEqual({
|
||||
and: true,
|
||||
operator1: CustomFilterOperator.GREATER_THAN_OR_EQUAL,
|
||||
val1: '',
|
||||
operator2: CustomFilterOperator.LESS_THAN_OR_EQUAL,
|
||||
val2: '',
|
||||
} as IFilterConditionFormParams);
|
||||
|
||||
filterByModel.onPrimaryConditionChange(ExtendCustomFilterOperator.ENDS_WITH);
|
||||
expect(filterByModel.conditionItem.operator).toBe(FilterConditionItems.ENDS_WITH.operator);
|
||||
expect(filterByModel.filterConditionFormParams).toEqual({
|
||||
operator1: ExtendCustomFilterOperator.ENDS_WITH,
|
||||
val1: '',
|
||||
} as IFilterConditionFormParams);
|
||||
});
|
||||
});
|
||||
|
||||
describe('test behavior when changing the secondary operators', () => {
|
||||
it('should update primary operator when the secondary operator changes (in some conditions)', () => {
|
||||
prepare(WithCustomFilterModelFactory());
|
||||
|
||||
expect(commandService.syncExecuteCommand(OpenFilterPanelOperation.id, {
|
||||
unitId: 'test',
|
||||
subUnitId: 'sheet1',
|
||||
col: 0,
|
||||
} as IOpenFilterPanelOperationParams)).toBe(true);
|
||||
|
||||
const filterByModel = sheetsFilterPanelService.filterByModel as ByConditionsModel;
|
||||
|
||||
filterByModel.onConditionFormChange({ operator1: ExtendCustomFilterOperator.DOES_NOT_CONTAIN });
|
||||
expect(filterByModel.conditionItem.operator).toBe(FilterConditionItems.DOES_NOT_CONTAIN.operator);
|
||||
expect(filterByModel.filterConditionFormParams).toEqual({
|
||||
operator1: ExtendCustomFilterOperator.DOES_NOT_CONTAIN,
|
||||
val1: '123',
|
||||
} as IFilterConditionFormParams);
|
||||
|
||||
filterByModel.onPrimaryConditionChange(ExtendCustomFilterOperator.BETWEEN);
|
||||
filterByModel.onConditionFormChange({ operator1: ExtendCustomFilterOperator.ENDS_WITH });
|
||||
expect(filterByModel.conditionItem.operator).toBe(FilterConditionItems.CUSTOM.operator);
|
||||
expect(filterByModel.filterConditionFormParams).toEqual({
|
||||
and: true,
|
||||
operator1: ExtendCustomFilterOperator.ENDS_WITH,
|
||||
val1: '',
|
||||
operator2: CustomFilterOperator.LESS_THAN_OR_EQUAL,
|
||||
val2: '',
|
||||
} as IFilterConditionFormParams);
|
||||
});
|
||||
});
|
||||
|
||||
it('should execute command when "apply" is called', async () => {
|
||||
prepare(WithCustomFilterModelFactory());
|
||||
|
||||
expect(commandService.syncExecuteCommand(OpenFilterPanelOperation.id, {
|
||||
unitId: 'test',
|
||||
subUnitId: 'sheet1',
|
||||
col: 0,
|
||||
} as IOpenFilterPanelOperationParams)).toBe(true);
|
||||
|
||||
const filterByModel = sheetsFilterPanelService.filterByModel as ByConditionsModel;
|
||||
filterByModel.onConditionFormChange({ val1: '5' });
|
||||
expect(await filterByModel.apply());
|
||||
|
||||
const filterModel = sheetsFilterService.activeFilterModel;
|
||||
expect(filterModel!.filteredOutRows).toEqual(new Set([1, 2, 3, 4, 5]));
|
||||
});
|
||||
});
|
||||
|
||||
describe('test filter type switching and color filters', () => {
|
||||
beforeEach(() => {
|
||||
vitest.useFakeTimers();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vitest.useRealTimers();
|
||||
});
|
||||
|
||||
it('should switch the active panel model by filter type for the same filter column', async () => {
|
||||
prepare(WithValuesFilterModelFactory());
|
||||
|
||||
expect(commandService.syncExecuteCommand(OpenFilterPanelOperation.id, {
|
||||
unitId: 'test',
|
||||
subUnitId: 'sheet1',
|
||||
col: 0,
|
||||
} as IOpenFilterPanelOperationParams)).toBe(true);
|
||||
await tick();
|
||||
|
||||
expect(sheetsFilterPanelService.filterModel).toBe(sheetsFilterService.activeFilterModel);
|
||||
expect(sheetsFilterPanelService.col).toBe(0);
|
||||
expect(sheetsFilterPanelService.filterByModel).toBeInstanceOf(ByValuesModel);
|
||||
|
||||
expect(sheetsFilterPanelService.changeFilterBy(FilterBy.CONDITIONS)).toBe(true);
|
||||
expect(sheetsFilterPanelService.filterBy).toBe(FilterBy.CONDITIONS);
|
||||
expect(sheetsFilterPanelService.filterByModel).toBeInstanceOf(ByConditionsModel);
|
||||
|
||||
expect(sheetsFilterPanelService.changeFilterBy(FilterBy.COLORS)).toBe(true);
|
||||
await tick();
|
||||
expect(sheetsFilterPanelService.filterBy).toBe(FilterBy.COLORS);
|
||||
expect(sheetsFilterPanelService.filterByModel).toBeInstanceOf(ByColorsModel);
|
||||
|
||||
expect(sheetsFilterPanelService.changeFilterBy(FilterBy.VALUES)).toBe(true);
|
||||
await tick();
|
||||
expect(sheetsFilterPanelService.filterBy).toBe(FilterBy.VALUES);
|
||||
expect(sheetsFilterPanelService.filterByModel).toBeInstanceOf(ByValuesModel);
|
||||
});
|
||||
|
||||
it('should build and apply color filter options from cell fill and text colors', async () => {
|
||||
prepare(WithColorFilterModelFactory());
|
||||
|
||||
expect(commandService.syncExecuteCommand(OpenFilterPanelOperation.id, {
|
||||
unitId: 'test',
|
||||
subUnitId: 'sheet1',
|
||||
col: 0,
|
||||
} as IOpenFilterPanelOperationParams)).toBe(true);
|
||||
expect(sheetsFilterPanelService.changeFilterBy(FilterBy.COLORS)).toBe(true);
|
||||
await tick();
|
||||
|
||||
const filterByModel = sheetsFilterPanelService.filterByModel as ByColorsModel;
|
||||
expect(filterByModel).toBeInstanceOf(ByColorsModel);
|
||||
expect(filterByModel.cellFillColors).toEqual([
|
||||
{ color: 'rgb(255,0,0)', checked: false },
|
||||
{ color: null, checked: false },
|
||||
]);
|
||||
expect(filterByModel.cellTextColors).toEqual([
|
||||
{ color: 'rgb(0,0,0)', checked: false },
|
||||
{ color: 'rgb(0,0,255)', checked: false },
|
||||
]);
|
||||
|
||||
filterByModel.onFilterCheckToggled({ color: 'rgb(255,0,0)', checked: false });
|
||||
expect(filterByModel.cellFillColors).toEqual([
|
||||
{ color: 'rgb(255,0,0)', checked: true },
|
||||
{ color: null, checked: false },
|
||||
]);
|
||||
|
||||
expect(await filterByModel.apply()).toBe(true);
|
||||
expect(sheetsFilterService.activeFilterModel!.getFilterColumn(0)!.getColumnData().colorFilters).toEqual({
|
||||
cellFillColors: ['rgb(255,0,0)'],
|
||||
});
|
||||
|
||||
filterByModel.onFilterCheckToggled({ color: 'rgb(0,0,255)', checked: false }, false);
|
||||
expect(filterByModel.cellFillColors).toEqual([
|
||||
{ color: 'rgb(255,0,0)', checked: false },
|
||||
{ color: null, checked: false },
|
||||
]);
|
||||
expect(filterByModel.cellTextColors).toEqual([
|
||||
{ color: 'rgb(0,0,0)', checked: false },
|
||||
{ color: 'rgb(0,0,255)', checked: true },
|
||||
]);
|
||||
|
||||
expect(await filterByModel.apply()).toBe(true);
|
||||
expect(sheetsFilterService.activeFilterModel!.getFilterColumn(0)!.getColumnData().colorFilters).toEqual({
|
||||
cellTextColors: ['rgb(0,0,255)'],
|
||||
});
|
||||
|
||||
filterByModel.onFilterCheckToggled({ color: 'rgb(0,0,255)', checked: true }, false);
|
||||
expect(await filterByModel.apply()).toBe(true);
|
||||
expect(sheetsFilterService.activeFilterModel!.getFilterColumn(0)).toBeNull();
|
||||
});
|
||||
|
||||
it('should keep the panel target column aligned when columns are inserted before the filter header', async () => {
|
||||
prepare(WithValuesFilterModelFactory());
|
||||
|
||||
expect(commandService.syncExecuteCommand(OpenFilterPanelOperation.id, {
|
||||
unitId: 'test',
|
||||
subUnitId: 'sheet1',
|
||||
col: 0,
|
||||
} as IOpenFilterPanelOperationParams)).toBe(true);
|
||||
await tick();
|
||||
|
||||
const filterByModel = sheetsFilterPanelService.filterByModel as ByValuesModel;
|
||||
expect(filterByModel.col).toBe(0);
|
||||
|
||||
expect(commandService.syncExecuteCommand(InsertColMutation.id, {
|
||||
unitId: 'test',
|
||||
subUnitId: 'sheet1',
|
||||
range: {
|
||||
startRow: 0,
|
||||
endRow: 999,
|
||||
startColumn: 0,
|
||||
endColumn: 0,
|
||||
},
|
||||
} as IInsertColMutationParams)).toBe(true);
|
||||
|
||||
expect(filterByModel.col).toBe(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('test filter by values', () => {
|
||||
beforeEach(() => {
|
||||
vitest.useFakeTimers();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vitest.useRealTimers();
|
||||
});
|
||||
|
||||
it('should initialize by filters when the filter column holds "filters"', async () => {
|
||||
prepare(WithValuesFilterModelFactory());
|
||||
|
||||
expect(commandService.syncExecuteCommand(OpenFilterPanelOperation.id, {
|
||||
unitId: 'test',
|
||||
subUnitId: 'sheet1',
|
||||
col: 0,
|
||||
} as IOpenFilterPanelOperationParams)).toBeTruthy();
|
||||
await tick();
|
||||
|
||||
expect(sheetsFilterPanelService.filterBy).toBe(FilterBy.VALUES);
|
||||
const filterByModel = sheetsFilterPanelService.filterByModel as ByValuesModel;
|
||||
expect(filterByModel instanceof ByValuesModel).toBeTruthy();
|
||||
expect(filterByModel.filterItems).toEqual(ITEMS);
|
||||
});
|
||||
|
||||
it('should initialize with blank content', async () => {
|
||||
prepare(WithValuesAndEmptyFilterModelFactory());
|
||||
|
||||
expect(commandService.syncExecuteCommand(OpenFilterPanelOperation.id, {
|
||||
unitId: 'test',
|
||||
subUnitId: 'sheet1',
|
||||
col: 0,
|
||||
} as IOpenFilterPanelOperationParams)).toBeTruthy();
|
||||
await tick();
|
||||
|
||||
expect(sheetsFilterPanelService.filterBy).toBe(FilterBy.VALUES);
|
||||
const filterByModel = sheetsFilterPanelService.filterByModel as ByValuesModel;
|
||||
expect(filterByModel instanceof ByValuesModel).toBeTruthy();
|
||||
expect(filterByModel.filterItems).toEqual(ITEMS_WITH_EMPTY);
|
||||
});
|
||||
|
||||
it('should count empty cells', async () => {
|
||||
prepare(WithMultiEmptyCellsModelFactory());
|
||||
|
||||
expect(commandService.syncExecuteCommand(OpenFilterPanelOperation.id, {
|
||||
unitId: 'test',
|
||||
subUnitId: 'sheet1',
|
||||
col: 0,
|
||||
} as IOpenFilterPanelOperationParams)).toBeTruthy();
|
||||
await tick();
|
||||
|
||||
expect(sheetsFilterPanelService.filterBy).toBe(FilterBy.VALUES);
|
||||
const filterByModel = sheetsFilterPanelService.filterByModel as ByValuesModel;
|
||||
expect(filterByModel instanceof ByValuesModel).toBeTruthy();
|
||||
const filterItems = filterByModel.filterItems;
|
||||
const emptyItem = filterItems.find((item) => item.key === 'empty');
|
||||
expect(emptyItem).toEqual({
|
||||
title: 'sheets-filter-ui.panel.empty',
|
||||
count: 4,
|
||||
leaf: true,
|
||||
checked: true,
|
||||
key: 'empty',
|
||||
});
|
||||
});
|
||||
|
||||
it('should filter out the items from the panel if its row is already filtered out by other column', async () => {
|
||||
prepare(WithTwoFilterColumnsFactory());
|
||||
|
||||
expect(commandService.syncExecuteCommand(OpenFilterPanelOperation.id, {
|
||||
unitId: 'test',
|
||||
subUnitId: 'sheet1',
|
||||
col: 1,
|
||||
} as IOpenFilterPanelOperationParams)).toBeTruthy();
|
||||
await tick();
|
||||
|
||||
expect(sheetsFilterPanelService.filterBy).toBe(FilterBy.VALUES);
|
||||
const filterByModel = sheetsFilterPanelService.filterByModel as ByValuesModel;
|
||||
expect(filterByModel instanceof ByValuesModel).toBeTruthy();
|
||||
const filterItems = filterByModel.filterItems;
|
||||
expect(filterItems).toEqual([
|
||||
{
|
||||
title: 'a',
|
||||
leaf: true,
|
||||
checked: true,
|
||||
key: 'a',
|
||||
count: 1,
|
||||
},
|
||||
{
|
||||
title: 'b',
|
||||
leaf: true,
|
||||
checked: true,
|
||||
key: 'b',
|
||||
count: 1,
|
||||
},
|
||||
{
|
||||
title: 'c',
|
||||
leaf: true,
|
||||
checked: true,
|
||||
key: 'c',
|
||||
count: 1,
|
||||
},
|
||||
{
|
||||
title: 'sheets-filter-ui.panel.empty',
|
||||
count: 3,
|
||||
leaf: true,
|
||||
checked: true,
|
||||
key: 'empty',
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it('merged cell should use value of the top left corner', async () => {
|
||||
prepare(WithMergedCellFilterFactory());
|
||||
|
||||
expect(commandService.syncExecuteCommand(OpenFilterPanelOperation.id, {
|
||||
unitId: 'test',
|
||||
subUnitId: 'sheet1',
|
||||
col: 1,
|
||||
} as IOpenFilterPanelOperationParams)).toBeTruthy();
|
||||
await tick();
|
||||
|
||||
expect(sheetsFilterPanelService.filterBy).toBe(FilterBy.VALUES);
|
||||
const filterByModel = sheetsFilterPanelService.filterByModel as ByValuesModel;
|
||||
expect(filterByModel instanceof ByValuesModel).toBeTruthy();
|
||||
const filterItems = filterByModel.filterItems;
|
||||
expect(filterItems).toEqual([
|
||||
{
|
||||
title: '3',
|
||||
leaf: true,
|
||||
checked: true,
|
||||
key: '3',
|
||||
count: 2,
|
||||
},
|
||||
{
|
||||
title: 'e',
|
||||
leaf: true,
|
||||
checked: true,
|
||||
key: 'e',
|
||||
count: 1,
|
||||
},
|
||||
{
|
||||
title: 'sheets-filter-ui.panel.empty',
|
||||
count: 3,
|
||||
leaf: true,
|
||||
checked: true,
|
||||
key: 'empty',
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it('should update the filter items when toggle checked status', async () => {
|
||||
prepare(WithValuesFilterModelFactory());
|
||||
|
||||
expect(commandService.syncExecuteCommand(OpenFilterPanelOperation.id, {
|
||||
unitId: 'test',
|
||||
subUnitId: 'sheet1',
|
||||
col: 0,
|
||||
} as IOpenFilterPanelOperationParams)).toBe(true);
|
||||
await tick();
|
||||
|
||||
const filterByModel = sheetsFilterPanelService.filterByModel as ByValuesModel;
|
||||
filterByModel.onFilterOnly([ITEMS[5].key]);
|
||||
expect(filterByModel.filterItems[0].checked).toBeFalsy();
|
||||
expect(filterByModel.filterItems[1].checked).toBeFalsy();
|
||||
expect(filterByModel.filterItems[5].checked).toBeTruthy();
|
||||
|
||||
filterByModel.onCheckAllToggled(true);
|
||||
expect(countLeafNodesByCheckedStatus(filterByModel.filterItems, true)).toBe(10);
|
||||
|
||||
filterByModel.onCheckAllToggled(false);
|
||||
expect(countLeafNodesByCheckedStatus(filterByModel.filterItems, true)).toBe(0);
|
||||
});
|
||||
|
||||
it('should execute command when "apply" is called', async () => {
|
||||
prepare(WithValuesFilterModelFactory());
|
||||
|
||||
expect(commandService.syncExecuteCommand(OpenFilterPanelOperation.id, {
|
||||
unitId: 'test',
|
||||
subUnitId: 'sheet1',
|
||||
col: 0,
|
||||
} as IOpenFilterPanelOperationParams)).toBe(true);
|
||||
await tick();
|
||||
|
||||
const filterByModel = sheetsFilterPanelService.filterByModel as ByValuesModel;
|
||||
filterByModel.onFilterCheckToggled(ITEMS[0]);
|
||||
expect(filterByModel.filterItems[0].checked).toBeFalsy();
|
||||
|
||||
filterByModel.onFilterOnly([ITEMS[5].key]);
|
||||
expect(await filterByModel.apply()).toBeTruthy();
|
||||
|
||||
const filterModel = sheetsFilterService.activeFilterModel;
|
||||
expect(filterModel!.filteredOutRows).toEqual(new Set([1, 10, 2, 4, 5, 6, 7, 8, 9, 10]));
|
||||
});
|
||||
|
||||
describe('with searching', async () => {
|
||||
// use fake timers
|
||||
// for performance reasons, search string are throttled to change the search results
|
||||
beforeEach(() => {
|
||||
vitest.useFakeTimers();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vitest.useRealTimers();
|
||||
});
|
||||
|
||||
it('should update the filter items when searching', async () => {
|
||||
prepare(WithValuesFilterModelFactory());
|
||||
|
||||
expect(commandService.syncExecuteCommand(OpenFilterPanelOperation.id, {
|
||||
unitId: 'test',
|
||||
subUnitId: 'sheet1',
|
||||
col: 0,
|
||||
} as IOpenFilterPanelOperationParams)).toBe(true);
|
||||
await tick();
|
||||
|
||||
const filterByModel = sheetsFilterPanelService.filterByModel as ByValuesModel;
|
||||
|
||||
filterByModel.setSearchString('e');
|
||||
vitest.advanceTimersByTime(600);
|
||||
expect(filterByModel.filterItems).toEqual(E_ITEMS);
|
||||
});
|
||||
|
||||
it('should toggle all applied to searched results', async () => {
|
||||
prepare(WithValuesFilterModelFactory());
|
||||
|
||||
expect(commandService.syncExecuteCommand(OpenFilterPanelOperation.id, {
|
||||
unitId: 'test',
|
||||
subUnitId: 'sheet1',
|
||||
col: 0,
|
||||
} as IOpenFilterPanelOperationParams)).toBe(true);
|
||||
await tick();
|
||||
|
||||
const filterByModel = sheetsFilterPanelService.filterByModel as ByValuesModel;
|
||||
|
||||
filterByModel.setSearchString('e');
|
||||
vitest.advanceTimersByTime(600);
|
||||
expect(filterByModel.filterItems.length).toBe(3);
|
||||
expect(getAllLeafNodes(filterByModel.filterItems).length).toBe(7);
|
||||
filterByModel.onCheckAllToggled(true);
|
||||
expect(countLeafNodesByCheckedStatus(filterByModel.filterItems, true)).toBe(7); // the original "1" should be checked as well
|
||||
});
|
||||
|
||||
it('should filter only applied to searched results', async () => {
|
||||
prepare(WithValuesFilterModelFactory());
|
||||
|
||||
expect(commandService.syncExecuteCommand(OpenFilterPanelOperation.id, {
|
||||
unitId: 'test',
|
||||
subUnitId: 'sheet1',
|
||||
col: 0,
|
||||
} as IOpenFilterPanelOperationParams)).toBe(true);
|
||||
await tick();
|
||||
|
||||
const filterByModel = sheetsFilterPanelService.filterByModel as ByValuesModel;
|
||||
|
||||
filterByModel.setSearchString('e');
|
||||
vitest.advanceTimersByTime(600);
|
||||
expect(filterByModel.filterItems.length).toBe(3);
|
||||
filterByModel.onFilterOnly([filterByModel.filterItems[2].key]);
|
||||
expect(countLeafNodesByCheckedStatus(filterByModel.filterItems, true)).toBe(1); // the original "1" should be checked as well
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
function tick(milliseconds: number = 0): Promise<void> {
|
||||
const result = awaitTime(milliseconds);
|
||||
vitest.advanceTimersByTime(milliseconds + 1);
|
||||
return result;
|
||||
}
|
||||
|
||||
function WithColorFilterModelFactory(): IWorkbookData {
|
||||
return {
|
||||
id: 'test',
|
||||
sheetOrder: ['sheet1'],
|
||||
name: '',
|
||||
appVersion: '3.0.0-alpha',
|
||||
locale: LocaleType.ZH_CN,
|
||||
styles: {
|
||||
redFill: {
|
||||
bg: {
|
||||
rgb: '#ff0000',
|
||||
},
|
||||
},
|
||||
blueText: {
|
||||
cl: {
|
||||
rgb: '#0000ff',
|
||||
},
|
||||
},
|
||||
},
|
||||
sheets: {
|
||||
sheet1: {
|
||||
id: 'sheet1',
|
||||
name: 'Sheet1',
|
||||
rowCount: 20,
|
||||
columnCount: 5,
|
||||
cellData: {
|
||||
0: {
|
||||
0: { v: 'Status' },
|
||||
},
|
||||
1: {
|
||||
0: { v: 'Blocked', s: 'redFill' },
|
||||
},
|
||||
2: {
|
||||
0: { v: 'Ready', s: 'blueText' },
|
||||
},
|
||||
3: {
|
||||
0: { v: 'Backlog' },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
resources: [
|
||||
{
|
||||
name: 'SHEET_FILTER_PLUGIN',
|
||||
data: JSON.stringify({
|
||||
sheet1: {
|
||||
ref: {
|
||||
startRow: 0,
|
||||
startColumn: 0,
|
||||
endRow: 3,
|
||||
endColumn: 0,
|
||||
},
|
||||
filterColumns: [],
|
||||
cachedFilteredOut: [],
|
||||
},
|
||||
}),
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,910 @@
|
||||
/**
|
||||
* 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 { IDisposable, IRange, Nullable, Workbook } from '@univerjs/core';
|
||||
import type { FilterColumn, FilterModel, IFilterColumn, ISetSheetsFilterCriteriaCommandParams } from '@univerjs/sheets-filter';
|
||||
import type { Observable } from 'rxjs';
|
||||
import type { FilterOperator, IFilterConditionFormParams, IFilterConditionItem } from '../models/conditions';
|
||||
import { ColorKit, createIdentifier, Disposable, ICommandService, Inject, Injector, IUniverInstanceService, LocaleService, Quantity, Tools, UniverInstanceType } from '@univerjs/core';
|
||||
import { COLOR_BLACK_RGB } from '@univerjs/engine-render';
|
||||
import { RefRangeService } from '@univerjs/sheets';
|
||||
import { FilterBy, SetSheetsFilterCriteriaCommand } from '@univerjs/sheets-filter';
|
||||
import { BehaviorSubject, combineLatest, map, merge, of, ReplaySubject, shareReplay, startWith, Subject, throttleTime } from 'rxjs';
|
||||
import { FilterConditionItems } from '../models/conditions';
|
||||
import { statisticFilterByValueItems } from '../models/utils';
|
||||
import { getFilterTreeByValueItems, ISheetsGenerateFilterValuesService } from '../worker/generate-filter-values.service';
|
||||
import { areAllLeafNodesChecked, findObjectByKey, searchTree, updateLeafNodesCheckedStatus } from './util';
|
||||
|
||||
export interface IFilterByValueItem {
|
||||
value: string;
|
||||
checked: boolean;
|
||||
count: number;
|
||||
index: number;
|
||||
|
||||
/**
|
||||
* This property indicates that this is a special item which maps to empty strings or empty cells.
|
||||
*/
|
||||
isEmpty: boolean;
|
||||
}
|
||||
|
||||
export interface IFilterByValueWithTreeItem {
|
||||
title: string;
|
||||
key: string;
|
||||
count: number;
|
||||
checked: boolean;
|
||||
leaf: boolean;
|
||||
originValues?: Set<string>;
|
||||
children?: IFilterByValueWithTreeItem[];
|
||||
}
|
||||
|
||||
export interface IFilterByColorItem {
|
||||
color: string | null;
|
||||
checked?: boolean;
|
||||
}
|
||||
|
||||
export interface ISheetsFilterPanelService {
|
||||
readonly col: number;
|
||||
readonly col$: Observable<number>;
|
||||
readonly filterBy: FilterBy;
|
||||
readonly filterBy$: Observable<FilterBy>;
|
||||
readonly filterByModel: Nullable<IFilterByModel>;
|
||||
readonly filterByModel$: Observable<Nullable<IFilterByModel>>;
|
||||
readonly filterModel: Nullable<FilterModel>;
|
||||
readonly hasCriteria$: Observable<boolean>;
|
||||
|
||||
setupCol(filterModel: FilterModel, col: number): void;
|
||||
changeFilterBy(filterBy: FilterBy): boolean;
|
||||
terminate(): boolean;
|
||||
}
|
||||
export const ISheetsFilterPanelService = createIdentifier<ISheetsFilterPanelService>('sheets-filter-ui.sheets-filter-panel.service');
|
||||
|
||||
export interface IFilterByModel extends IDisposable {
|
||||
canApply$: Observable<boolean>;
|
||||
|
||||
deltaCol(offset: number): void;
|
||||
|
||||
clear(): Promise<boolean>;
|
||||
apply(): Promise<boolean>;
|
||||
}
|
||||
|
||||
/**
|
||||
* This service controls the state of the filter panel. There should be only one instance of the filter panel
|
||||
* at one time.
|
||||
*/
|
||||
export class SheetsFilterPanelService extends Disposable implements ISheetsFilterPanelService {
|
||||
private readonly _filterBy$ = new BehaviorSubject<FilterBy>(FilterBy.VALUES);
|
||||
readonly filterBy$ = this._filterBy$.asObservable();
|
||||
get filterBy(): FilterBy { return this._filterBy$.getValue(); }
|
||||
|
||||
private readonly _filterByModel$ = new ReplaySubject<Nullable<IFilterByModel>>(1);
|
||||
readonly filterByModel$ = this._filterByModel$.asObservable();
|
||||
private _filterByModel: Nullable<IFilterByModel> = null;
|
||||
get filterByModel(): Nullable<IFilterByModel> { return this._filterByModel; }
|
||||
private set filterByModel(model: Nullable<IFilterByModel>) {
|
||||
this._filterByModel = model;
|
||||
this._filterByModel$.next(model);
|
||||
}
|
||||
|
||||
private readonly _hasCriteria$ = new BehaviorSubject<boolean>(false);
|
||||
readonly hasCriteria$ = this._hasCriteria$.asObservable();
|
||||
|
||||
private _filterModel: Nullable<FilterModel> = null;
|
||||
get filterModel() { return this._filterModel; }
|
||||
|
||||
private readonly _col$ = new BehaviorSubject<number>(-1);
|
||||
readonly col$ = this._col$.asObservable();
|
||||
get col(): number { return this._col$.getValue(); }
|
||||
|
||||
constructor(
|
||||
@Inject(Injector) private readonly _injector: Injector,
|
||||
@Inject(RefRangeService) private readonly _refRangeService: RefRangeService
|
||||
) {
|
||||
super();
|
||||
}
|
||||
|
||||
override dispose(): void {
|
||||
this._filterBy$.complete();
|
||||
this._filterByModel$.complete();
|
||||
this._hasCriteria$.complete();
|
||||
}
|
||||
|
||||
setupCol(filterModel: FilterModel, col: number): void {
|
||||
this.terminate();
|
||||
|
||||
this._filterModel = filterModel;
|
||||
this._col$.next(col);
|
||||
|
||||
// We use filter type that (if) has been set on the column as the default filter type.
|
||||
const filterColumn = filterModel.getFilterColumn(col);
|
||||
if (filterColumn) {
|
||||
const info = filterColumn.getColumnData();
|
||||
if (info.customFilters) {
|
||||
this._hasCriteria$.next(true);
|
||||
this._setupByConditions(filterModel, col);
|
||||
return;
|
||||
}
|
||||
|
||||
if (info.colorFilters) {
|
||||
this._hasCriteria$.next(true);
|
||||
this._setupByColors(filterModel, col);
|
||||
return;
|
||||
}
|
||||
|
||||
if (info.filters) {
|
||||
this._hasCriteria$.next(true);
|
||||
this._setupByValues(filterModel, col);
|
||||
return;
|
||||
}
|
||||
|
||||
// Use value values by default.
|
||||
this._hasCriteria$.next(false);
|
||||
this._setupByValues(filterModel, col);
|
||||
return;
|
||||
}
|
||||
|
||||
// By default we filter by values.
|
||||
this._hasCriteria$.next(false);
|
||||
this._setupByValues(filterModel, col);
|
||||
};
|
||||
|
||||
changeFilterBy(filterBy: FilterBy): boolean {
|
||||
if (!this._filterModel || this.col === -1) {
|
||||
return false;
|
||||
}
|
||||
|
||||
switch (filterBy) {
|
||||
case FilterBy.VALUES:
|
||||
this._setupByValues(this._filterModel, this.col);
|
||||
break;
|
||||
case FilterBy.COLORS:
|
||||
this._setupByColors(this._filterModel, this.col);
|
||||
break;
|
||||
case FilterBy.CONDITIONS:
|
||||
this._setupByConditions(this._filterModel, this.col);
|
||||
break;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
terminate(): boolean {
|
||||
this._filterModel = null;
|
||||
this._col$.next(-1);
|
||||
|
||||
this._disposeFilterHeaderChangeListener();
|
||||
return true;
|
||||
}
|
||||
|
||||
private _filterHeaderListener: Nullable<IDisposable> = null;
|
||||
private _disposeFilterHeaderChangeListener(): void {
|
||||
this._filterHeaderListener?.dispose();
|
||||
this._filterHeaderListener = null;
|
||||
}
|
||||
|
||||
private _listenToFilterHeaderChange(filterModel: FilterModel, col: number): void {
|
||||
this._disposeFilterHeaderChangeListener();
|
||||
|
||||
const unitId = filterModel.unitId;
|
||||
const subUnitId = filterModel.subUnitId;
|
||||
const filterRange = filterModel.getRange();
|
||||
const columnHeaderRange: IRange = {
|
||||
startColumn: col,
|
||||
startRow: filterRange.startRow,
|
||||
endRow: filterRange.startRow,
|
||||
endColumn: col,
|
||||
};
|
||||
|
||||
this._filterHeaderListener = this._refRangeService.watchRange(unitId, subUnitId, columnHeaderRange, (before, after) => {
|
||||
if (!after) {
|
||||
// If the range collapsed, the column header must be deleted.
|
||||
this.terminate();
|
||||
} else {
|
||||
const offset = after.startColumn - before.startColumn;
|
||||
if (offset !== 0) {
|
||||
this._filterByModel!.deltaCol(offset);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private async _setupByValues(filterModel: FilterModel, col: number): Promise<boolean> {
|
||||
this._disposePreviousModel();
|
||||
|
||||
const range = filterModel.getRange();
|
||||
if (range.startRow === range.endRow) return false;
|
||||
|
||||
const filterByModel = await ByValuesModel.fromFilterColumn(
|
||||
this._injector,
|
||||
filterModel,
|
||||
col
|
||||
);
|
||||
|
||||
this.filterByModel = filterByModel;
|
||||
this._filterBy$.next(FilterBy.VALUES);
|
||||
|
||||
this._listenToFilterHeaderChange(filterModel, col);
|
||||
return true;
|
||||
}
|
||||
|
||||
private async _setupByColors(filterModel: FilterModel, col: number): Promise<boolean> {
|
||||
this._disposePreviousModel();
|
||||
|
||||
const range = filterModel.getRange();
|
||||
if (range.startRow === range.endRow) return false;
|
||||
|
||||
const filterByModel = await ByColorsModel.fromFilterColumn(
|
||||
this._injector,
|
||||
filterModel,
|
||||
col
|
||||
);
|
||||
|
||||
this.filterByModel = filterByModel;
|
||||
this._filterBy$.next(FilterBy.COLORS);
|
||||
|
||||
this._listenToFilterHeaderChange(filterModel, col);
|
||||
return true;
|
||||
}
|
||||
|
||||
private _setupByConditions(filterModel: FilterModel, col: number): boolean {
|
||||
this._disposePreviousModel();
|
||||
|
||||
const range = filterModel.getRange();
|
||||
if (range.startRow === range.endRow) return false;
|
||||
|
||||
const model = ByConditionsModel.fromFilterColumn(
|
||||
this._injector,
|
||||
filterModel,
|
||||
col,
|
||||
filterModel.getFilterColumn(col)
|
||||
);
|
||||
|
||||
this.filterByModel = model;
|
||||
this._filterBy$.next(FilterBy.CONDITIONS);
|
||||
|
||||
this._listenToFilterHeaderChange(filterModel, col);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private _disposePreviousModel(): void {
|
||||
this._filterByModel?.dispose();
|
||||
this.filterByModel = null;
|
||||
}
|
||||
}
|
||||
|
||||
// #region ByConditionsModel
|
||||
|
||||
/**
|
||||
* This model would be used to control the "Filter By Conditions" panel. It should be reconstructed in the following
|
||||
* situations:
|
||||
*
|
||||
* 1. The target `FilterColumn` object is changed
|
||||
* 2. User toggles "Filter By"
|
||||
*/
|
||||
export class ByConditionsModel extends Disposable implements IFilterByModel {
|
||||
/**
|
||||
* Create a model with targeting filter column. If there is not a filter column, the model would be created with
|
||||
* default values.
|
||||
*
|
||||
* @param injector
|
||||
* @param filterModel
|
||||
* @param col
|
||||
* @param filterColumn
|
||||
*
|
||||
* @returns the model to control the panel's state
|
||||
*/
|
||||
static fromFilterColumn(injector: Injector, filterModel: FilterModel, col: number, filterColumn?: Nullable<FilterColumn>): ByConditionsModel {
|
||||
const [conditionItem, conditionParams] = FilterConditionItems.testMappingFilterColumn(filterColumn?.getColumnData());
|
||||
const model = injector.createInstance(ByConditionsModel, filterModel, col, conditionItem, conditionParams);
|
||||
return model;
|
||||
}
|
||||
|
||||
canApply$: Observable<boolean> = of(true);
|
||||
|
||||
private readonly _conditionItem$: BehaviorSubject<IFilterConditionItem>;
|
||||
readonly conditionItem$: Observable<IFilterConditionItem>;
|
||||
get conditionItem(): IFilterConditionItem { return this._conditionItem$.getValue(); }
|
||||
|
||||
private readonly _filterConditionFormParams$: BehaviorSubject<IFilterConditionFormParams>;
|
||||
readonly filterConditionFormParams$: Observable<IFilterConditionFormParams>;
|
||||
get filterConditionFormParams(): IFilterConditionFormParams { return this._filterConditionFormParams$.getValue(); }
|
||||
|
||||
constructor(
|
||||
private readonly _filterModel: FilterModel,
|
||||
public col: number,
|
||||
conditionItem: IFilterConditionItem,
|
||||
conditionParams: IFilterConditionFormParams,
|
||||
@ICommandService private readonly _commandService: ICommandService
|
||||
) {
|
||||
super();
|
||||
|
||||
this._conditionItem$ = new BehaviorSubject<IFilterConditionItem>(conditionItem);
|
||||
this.conditionItem$ = this._conditionItem$.asObservable();
|
||||
|
||||
this._filterConditionFormParams$ = new BehaviorSubject(conditionParams);
|
||||
this.filterConditionFormParams$ = this._filterConditionFormParams$.asObservable();
|
||||
}
|
||||
|
||||
override dispose(): void {
|
||||
super.dispose();
|
||||
|
||||
this._conditionItem$.complete();
|
||||
this._filterConditionFormParams$.complete();
|
||||
}
|
||||
|
||||
deltaCol(offset: number): void {
|
||||
this.col += offset;
|
||||
}
|
||||
|
||||
clear(): Promise<boolean> {
|
||||
if (this._disposed) return Promise.resolve(false);
|
||||
|
||||
return this._commandService.executeCommand(SetSheetsFilterCriteriaCommand.id, {
|
||||
unitId: this._filterModel.unitId,
|
||||
subUnitId: this._filterModel.subUnitId,
|
||||
col: this.col,
|
||||
criteria: null,
|
||||
} as ISetSheetsFilterCriteriaCommandParams);
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply the filter condition to the target filter column.
|
||||
*/
|
||||
async apply(): Promise<boolean> {
|
||||
if (this._disposed) return false;
|
||||
|
||||
const filterColumn = FilterConditionItems.mapToFilterColumn(this.conditionItem, this.filterConditionFormParams);
|
||||
return this._commandService.executeCommand(SetSheetsFilterCriteriaCommand.id, {
|
||||
unitId: this._filterModel.unitId,
|
||||
subUnitId: this._filterModel.subUnitId,
|
||||
col: this.col,
|
||||
criteria: filterColumn,
|
||||
} as ISetSheetsFilterCriteriaCommandParams);
|
||||
}
|
||||
|
||||
/**
|
||||
* This method would be called when user changes the primary condition. The model would load the corresponding
|
||||
* `IFilterConditionFormParams` and load default condition form params.
|
||||
*/
|
||||
onPrimaryConditionChange(operator: FilterOperator): void {
|
||||
const conditionItem = FilterConditionItems.ALL_CONDITIONS.find((item) => item.operator === operator);
|
||||
if (!conditionItem) {
|
||||
throw new Error(`[ByConditionsModel]: condition item not found for operator: ${operator}!`);
|
||||
}
|
||||
|
||||
this._conditionItem$.next(conditionItem);
|
||||
this._filterConditionFormParams$.next(FilterConditionItems.getInitialFormParams(operator));
|
||||
}
|
||||
|
||||
/**
|
||||
* This method would be called when user changes the primary conditions, the input values or "AND" "OR" ratio.
|
||||
* If the primary conditions or the ratio is changed, the method would load the corresponding `IFilterCondition`.
|
||||
*
|
||||
* When the panel call this method, it only has to pass the changed keys.
|
||||
*
|
||||
* @param params
|
||||
*/
|
||||
onConditionFormChange(params: Partial<Omit<IFilterConditionFormParams, 'and'> & { and: boolean }>): void {
|
||||
const newParams = { ...this.filterConditionFormParams, ...params };
|
||||
if (newParams.and !== true) {
|
||||
delete newParams.and;
|
||||
}
|
||||
|
||||
if (typeof params.and !== 'undefined' || typeof params.operator1 !== 'undefined' || typeof params.operator2 !== 'undefined') {
|
||||
const conditionItem = FilterConditionItems.testMappingParams(newParams as IFilterConditionFormParams, this.conditionItem.numOfParameters);
|
||||
this._conditionItem$.next(conditionItem);
|
||||
}
|
||||
|
||||
this._filterConditionFormParams$.next(newParams as IFilterConditionFormParams);
|
||||
}
|
||||
}
|
||||
|
||||
// #endregion
|
||||
|
||||
// #region - ByValuesModel
|
||||
|
||||
/**
|
||||
* This model would be used to control the "Filter By Values" panel. It should be reconstructed in the following
|
||||
* situations:
|
||||
*
|
||||
* 1. The target `FilterColumn` object is changed
|
||||
* 2. User toggles "Filter By"
|
||||
*/
|
||||
export class ByValuesModel extends Disposable implements IFilterByModel {
|
||||
/**
|
||||
* Create a model with targeting filter column. If there is not a filter column, the model would be created with
|
||||
* default values.
|
||||
*
|
||||
* @param injector
|
||||
* @param filterModel
|
||||
* @param col
|
||||
*
|
||||
* @returns the model to control the panel's state
|
||||
*/
|
||||
static async fromFilterColumn(injector: Injector, filterModel: FilterModel, col: number): Promise<ByValuesModel> {
|
||||
const univerInstanceService = injector.get(IUniverInstanceService);
|
||||
const localeService = injector.get(LocaleService);
|
||||
const generateFilterValuesService = injector.get(ISheetsGenerateFilterValuesService, Quantity.OPTIONAL);
|
||||
|
||||
const { unitId, subUnitId } = filterModel;
|
||||
const workbook = univerInstanceService.getUnit<Workbook>(unitId, UniverInstanceType.UNIVER_SHEET);
|
||||
if (!workbook) throw new Error(`[ByValuesModel]: Workbook not found for filter model with unitId: ${unitId}!`);
|
||||
|
||||
const worksheet = workbook?.getSheetBySheetId(subUnitId);
|
||||
if (!worksheet) throw new Error(`[ByValuesModel]: Worksheet not found for filter model with unitId: ${unitId} and subUnitId: ${subUnitId}!`);
|
||||
|
||||
const range = filterModel.getRange();
|
||||
const column = col;
|
||||
const filterColumn = filterModel.getFilterColumn(col);
|
||||
const filters = filterColumn?.getColumnData().filters;
|
||||
const alreadyChecked = new Set(filters?.filters);
|
||||
const blankChecked = !!(filters && filters.blank);
|
||||
const filteredOutRowsByOtherColumns = filterModel.getFilteredOutRowsExceptCol(col);
|
||||
const iterateRange: IRange = { ...range, startRow: range.startRow + 1, startColumn: column, endColumn: column };
|
||||
|
||||
let items: IFilterByValueWithTreeItem[];
|
||||
let cache: Map<string, string[]> | undefined;
|
||||
if (generateFilterValuesService) {
|
||||
const res = await generateFilterValuesService.getFilterValues({
|
||||
unitId,
|
||||
subUnitId,
|
||||
filteredOutRowsByOtherColumns: Array.from(filteredOutRowsByOtherColumns),
|
||||
filterColumn,
|
||||
filters: !!filters,
|
||||
blankChecked,
|
||||
iterateRange,
|
||||
alreadyChecked: Array.from(alreadyChecked),
|
||||
});
|
||||
items = res.filterTreeItems;
|
||||
cache = res.filterTreeMapCache;
|
||||
} else {
|
||||
// the first row is filter header and should be added to options
|
||||
const res = getFilterTreeByValueItems(
|
||||
!!filters,
|
||||
localeService,
|
||||
iterateRange,
|
||||
worksheet,
|
||||
filteredOutRowsByOtherColumns,
|
||||
filterColumn,
|
||||
alreadyChecked,
|
||||
blankChecked,
|
||||
workbook.getStyles()
|
||||
);
|
||||
items = res.filterTreeItems;
|
||||
cache = res.filterTreeMapCache;
|
||||
}
|
||||
|
||||
return injector.createInstance(ByValuesModel, filterModel, col, items, cache);
|
||||
}
|
||||
|
||||
private readonly _rawFilterItems$: BehaviorSubject<IFilterByValueWithTreeItem[]>;
|
||||
readonly rawFilterItems$: Observable<IFilterByValueWithTreeItem[]>;
|
||||
get rawFilterItems(): IFilterByValueWithTreeItem[] { return this._rawFilterItems$.getValue(); }
|
||||
|
||||
readonly filterItems$: Observable<IFilterByValueWithTreeItem[]>;
|
||||
private _filterItems: IFilterByValueWithTreeItem[] = [];
|
||||
get filterItems() { return this._filterItems; }
|
||||
|
||||
private _treeMapCache: Map<string, string[]>;
|
||||
get treeMapCache() { return this._treeMapCache; }
|
||||
|
||||
readonly canApply$: Observable<boolean>;
|
||||
|
||||
private readonly _manuallyUpdateFilterItems$: Subject<IFilterByValueWithTreeItem[]>;
|
||||
|
||||
private readonly _searchString$: BehaviorSubject<string>;
|
||||
readonly searchString$: Observable<string>;
|
||||
|
||||
constructor(
|
||||
private readonly _filterModel: FilterModel,
|
||||
public col: number,
|
||||
/**
|
||||
* Filter items would remain unchanged after we create them,
|
||||
* though data may change after.
|
||||
*/
|
||||
items: IFilterByValueWithTreeItem[],
|
||||
cache: Map<string, string[]>,
|
||||
@ICommandService private readonly _commandService: ICommandService
|
||||
) {
|
||||
super();
|
||||
|
||||
this._treeMapCache = cache;
|
||||
this._searchString$ = new BehaviorSubject<string>('');
|
||||
this.searchString$ = this._searchString$.asObservable();
|
||||
|
||||
this._rawFilterItems$ = new BehaviorSubject<IFilterByValueWithTreeItem[]>(items);
|
||||
this.rawFilterItems$ = this._rawFilterItems$.asObservable();
|
||||
|
||||
this._manuallyUpdateFilterItems$ = new Subject<IFilterByValueWithTreeItem[]>();
|
||||
|
||||
this.filterItems$ = merge(
|
||||
combineLatest([
|
||||
this._searchString$.pipe(
|
||||
throttleTime(500, undefined, { leading: true, trailing: true }),
|
||||
startWith(void 0)
|
||||
),
|
||||
this._rawFilterItems$,
|
||||
]).pipe(
|
||||
map(([searchString, items]) => {
|
||||
if (!searchString) return items;
|
||||
|
||||
const lowerSearchString = searchString.toLowerCase();
|
||||
const searchKeyWords = lowerSearchString.split(/\s+/).filter((s) => !!s);
|
||||
return searchTree(items, searchKeyWords);
|
||||
})
|
||||
),
|
||||
this._manuallyUpdateFilterItems$
|
||||
).pipe(shareReplay({ bufferSize: 1, refCount: true }));
|
||||
|
||||
this.canApply$ = this.filterItems$.pipe(map((items) => {
|
||||
const stat = statisticFilterByValueItems(items);
|
||||
return stat.checked > 0;
|
||||
}));
|
||||
|
||||
this.disposeWithMe(this.filterItems$.subscribe((items) => this._filterItems = items));
|
||||
}
|
||||
|
||||
override dispose(): void {
|
||||
this._rawFilterItems$.complete();
|
||||
this._searchString$.complete();
|
||||
}
|
||||
|
||||
deltaCol(offset: number): void {
|
||||
this.col += offset;
|
||||
}
|
||||
|
||||
setSearchString(str: string): void {
|
||||
this._searchString$.next(str);
|
||||
}
|
||||
|
||||
onCheckAllToggled(checked: boolean) {
|
||||
const items = Tools.deepClone(this._filterItems);
|
||||
items.forEach((item) => updateLeafNodesCheckedStatus(item, checked));
|
||||
this._manuallyUpdateFilterItems(items);
|
||||
}
|
||||
|
||||
/**
|
||||
* Toggle a filter item.
|
||||
*/
|
||||
onFilterCheckToggled(item: IFilterByValueWithTreeItem): void {
|
||||
const items = Tools.deepClone(this._filterItems);
|
||||
const changedItem = findObjectByKey(items, item.key);
|
||||
if (!changedItem) {
|
||||
return;
|
||||
}
|
||||
const allLeafChecked = areAllLeafNodesChecked(changedItem);
|
||||
updateLeafNodesCheckedStatus(changedItem, !allLeafChecked);
|
||||
this._manuallyUpdateFilterItems(items);
|
||||
}
|
||||
|
||||
onFilterOnly(itemKeys: string[]) {
|
||||
const items = Tools.deepClone(this._filterItems);
|
||||
items.forEach((item) => updateLeafNodesCheckedStatus(item, false));
|
||||
itemKeys.forEach((key) => {
|
||||
const changedItem = findObjectByKey(items, key);
|
||||
if (changedItem) {
|
||||
updateLeafNodesCheckedStatus(changedItem, true);
|
||||
}
|
||||
});
|
||||
this._manuallyUpdateFilterItems(items);
|
||||
}
|
||||
|
||||
private _manuallyUpdateFilterItems(items: IFilterByValueWithTreeItem[]): void {
|
||||
this._manuallyUpdateFilterItems$.next(items);
|
||||
}
|
||||
|
||||
// expose method here to let the panel change filter items
|
||||
|
||||
// #region ByValuesModel apply methods
|
||||
clear(): Promise<boolean> {
|
||||
if (this._disposed) return Promise.resolve(false);
|
||||
|
||||
return this._commandService.executeCommand(SetSheetsFilterCriteriaCommand.id, {
|
||||
unitId: this._filterModel.unitId,
|
||||
subUnitId: this._filterModel.subUnitId,
|
||||
col: this.col,
|
||||
criteria: null,
|
||||
} as ISetSheetsFilterCriteriaCommandParams);
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply the filter condition to the target filter column.
|
||||
*/
|
||||
async apply(): Promise<boolean> {
|
||||
if (this._disposed) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const statistics = statisticFilterByValueItems(this._filterItems);
|
||||
const { checked, checkedItems } = statistics;
|
||||
const rawFilterItems = this.rawFilterItems;
|
||||
let rawFilterCount = 0;
|
||||
for (const item of rawFilterItems) {
|
||||
rawFilterCount += item.count;
|
||||
}
|
||||
|
||||
const noChecked = checked === 0;
|
||||
const allChecked = statistics.checked === rawFilterCount;
|
||||
|
||||
const criteria: IFilterColumn = { colId: this.col };
|
||||
if (noChecked) {
|
||||
throw new Error('[ByValuesModel]: no checked items!');
|
||||
} else if (allChecked) {
|
||||
return this._commandService.executeCommand(SetSheetsFilterCriteriaCommand.id, {
|
||||
unitId: this._filterModel.unitId,
|
||||
subUnitId: this._filterModel.subUnitId,
|
||||
col: this.col,
|
||||
criteria: null,
|
||||
} as ISetSheetsFilterCriteriaCommandParams);
|
||||
} else {
|
||||
criteria.filters = {};
|
||||
|
||||
const nonEmptyItems = checkedItems.filter((item) => item.key !== 'empty');
|
||||
if (nonEmptyItems.length > 0) {
|
||||
criteria.filters = {
|
||||
filters: nonEmptyItems.flatMap((item) => item.originValues ? Array.from(item.originValues) : [item.title]),
|
||||
};
|
||||
}
|
||||
|
||||
const hasEmpty = nonEmptyItems.length !== checkedItems.length;
|
||||
if (hasEmpty) {
|
||||
criteria.filters.blank = true;
|
||||
}
|
||||
}
|
||||
|
||||
return this._commandService.executeCommand(SetSheetsFilterCriteriaCommand.id, {
|
||||
unitId: this._filterModel.unitId,
|
||||
subUnitId: this._filterModel.subUnitId,
|
||||
col: this.col,
|
||||
criteria,
|
||||
} as ISetSheetsFilterCriteriaCommandParams);
|
||||
}
|
||||
|
||||
// #endregion
|
||||
}
|
||||
|
||||
// #endregion
|
||||
|
||||
// #region - ByColorsModel
|
||||
|
||||
/**
|
||||
* This model would be used to control the "Filter By Colors" panel. It should be reconstructed in the following
|
||||
* situations:
|
||||
*
|
||||
* 1. The target `FilterColumn` object is changed
|
||||
* 2. User toggles "Filter By"
|
||||
*/
|
||||
export class ByColorsModel extends Disposable implements IFilterByModel {
|
||||
/**
|
||||
* Create a model with targeting filter column. If there is not a filter column, the model would be created with
|
||||
* default values.
|
||||
*
|
||||
* @param injector
|
||||
* @param filterModel
|
||||
* @param col
|
||||
*
|
||||
* @returns the model to control the panel's state
|
||||
*/
|
||||
static async fromFilterColumn(injector: Injector, filterModel: FilterModel, col: number): Promise<ByColorsModel> {
|
||||
const univerInstanceService = injector.get(IUniverInstanceService);
|
||||
|
||||
const { unitId, subUnitId } = filterModel;
|
||||
const workbook = univerInstanceService.getUnit<Workbook>(unitId, UniverInstanceType.UNIVER_SHEET);
|
||||
if (!workbook) throw new Error(`[ByColorsModel]: Workbook not found for filter model with unitId: ${unitId}!`);
|
||||
|
||||
const worksheet = workbook?.getSheetBySheetId(subUnitId);
|
||||
if (!worksheet) throw new Error(`[ByColorsModel]: Worksheet not found for filter model with unitId: ${unitId} and subUnitId: ${subUnitId}!`);
|
||||
|
||||
const range = filterModel.getRange();
|
||||
const column = col;
|
||||
const colorFilters = filterModel.getFilterColumn(col)?.getColumnData().colorFilters;
|
||||
const filteredOutRowsByOtherColumns = filterModel.getFilteredOutRowsExceptCol(col);
|
||||
const iterateRange: IRange = { ...range, startRow: range.startRow + 1, startColumn: column, endColumn: column };
|
||||
|
||||
const cellFillColors = new Map<string, IFilterByColorItem>();
|
||||
const cellFillColorsChecked = new Set<string | null>(colorFilters?.cellFillColors ?? []);
|
||||
const cellTextColors = new Map<string, IFilterByColorItem>();
|
||||
const cellTextColorsChecked = new Set<string>(colorFilters?.cellTextColors ?? []);
|
||||
|
||||
for (const cell of worksheet.iterateByColumn(iterateRange, false, true)) {
|
||||
const { row, col, value } = cell;
|
||||
|
||||
if (filteredOutRowsByOtherColumns.has(row)) {
|
||||
// If the row is filtered out by other columns, we skip it.
|
||||
continue;
|
||||
}
|
||||
|
||||
const style = worksheet.getComposedCellStyleByCellData(row, col, value);
|
||||
|
||||
if (style.bg && style.bg.rgb) {
|
||||
const bg = new ColorKit(style.bg.rgb).toRgbString();
|
||||
|
||||
if (!cellFillColors.has(bg)) {
|
||||
cellFillColors.set(bg, { color: bg, checked: cellFillColorsChecked.has(bg) });
|
||||
}
|
||||
} else {
|
||||
cellFillColors.set('default-fill-color', { color: null, checked: cellFillColorsChecked.has(null) });
|
||||
}
|
||||
|
||||
if (style.cl && style.cl.rgb) {
|
||||
const cl = new ColorKit(style.cl.rgb).toRgbString();
|
||||
|
||||
if (!cellTextColors.has(cl)) {
|
||||
cellTextColors.set(cl, { color: cl, checked: cellTextColorsChecked.has(cl) });
|
||||
}
|
||||
} else {
|
||||
cellTextColors.set('default-font-color', { color: COLOR_BLACK_RGB, checked: cellTextColorsChecked.has(COLOR_BLACK_RGB) });
|
||||
}
|
||||
}
|
||||
|
||||
return injector.createInstance(ByColorsModel, filterModel, col, cellFillColors, cellTextColors);
|
||||
}
|
||||
|
||||
canApply$: Observable<boolean> = of(true);
|
||||
|
||||
private readonly _cellFillColors$: BehaviorSubject<IFilterByColorItem[]>;
|
||||
readonly cellFillColors$: Observable<IFilterByColorItem[]>;
|
||||
get cellFillColors(): IFilterByColorItem[] { return this._cellFillColors$.getValue(); }
|
||||
|
||||
private readonly _cellTextColors$: BehaviorSubject<IFilterByColorItem[]>;
|
||||
readonly cellTextColors$: Observable<IFilterByColorItem[]>;
|
||||
get cellTextColors(): IFilterByColorItem[] { return this._cellTextColors$.getValue(); }
|
||||
|
||||
constructor(
|
||||
private readonly _filterModel: FilterModel,
|
||||
public col: number,
|
||||
/**
|
||||
* Filter items would remain unchanged after we create them,
|
||||
* though data may change after.
|
||||
*/
|
||||
cellFillColors: Map<string, IFilterByColorItem>,
|
||||
cellTextColors: Map<string, IFilterByColorItem>,
|
||||
@ICommandService private readonly _commandService: ICommandService
|
||||
) {
|
||||
super();
|
||||
|
||||
this._cellFillColors$ = new BehaviorSubject<IFilterByColorItem[]>(Array.from(cellFillColors.values()));
|
||||
this.cellFillColors$ = this._cellFillColors$.asObservable();
|
||||
|
||||
this._cellTextColors$ = new BehaviorSubject<IFilterByColorItem[]>(Array.from(cellTextColors.values()));
|
||||
this.cellTextColors$ = this._cellTextColors$.asObservable();
|
||||
}
|
||||
|
||||
override dispose(): void {
|
||||
super.dispose();
|
||||
|
||||
this._cellFillColors$.complete();
|
||||
}
|
||||
|
||||
deltaCol(offset: number): void {
|
||||
this.col += offset;
|
||||
}
|
||||
|
||||
// expose method here to let the panel change filter items
|
||||
|
||||
// #region ByColorsModel apply methods
|
||||
clear(): Promise<boolean> {
|
||||
if (this._disposed) return Promise.resolve(false);
|
||||
|
||||
return this._commandService.executeCommand(SetSheetsFilterCriteriaCommand.id, {
|
||||
unitId: this._filterModel.unitId,
|
||||
subUnitId: this._filterModel.subUnitId,
|
||||
col: this.col,
|
||||
criteria: null,
|
||||
} as ISetSheetsFilterCriteriaCommandParams);
|
||||
}
|
||||
|
||||
onFilterCheckToggled(item: IFilterByColorItem, isFillColor: boolean = true): void {
|
||||
const colors = isFillColor ? this.cellFillColors : this.cellTextColors;
|
||||
const items: IFilterByColorItem[] = [];
|
||||
|
||||
let found = false;
|
||||
|
||||
for (let i = 0; i < colors.length; i++) {
|
||||
const colorItem = colors[i];
|
||||
|
||||
if (colorItem.color === item.color) {
|
||||
found = true;
|
||||
items.push({
|
||||
color: colorItem.color,
|
||||
checked: !colorItem.checked,
|
||||
});
|
||||
continue;
|
||||
}
|
||||
|
||||
items.push({
|
||||
color: colorItem.color,
|
||||
checked: colorItem.checked,
|
||||
});
|
||||
}
|
||||
|
||||
if (!found) {
|
||||
return;
|
||||
}
|
||||
|
||||
// fill color and text color are mutually exclusive
|
||||
this._resetColorsCheckedStatus(!isFillColor);
|
||||
|
||||
if (isFillColor) {
|
||||
this._cellFillColors$.next([...items]);
|
||||
} else {
|
||||
this._cellTextColors$.next([...items]);
|
||||
}
|
||||
}
|
||||
|
||||
private _resetColorsCheckedStatus(isFillColor: boolean = true): void {
|
||||
const colors = isFillColor ? this.cellFillColors : this.cellTextColors;
|
||||
const items: IFilterByColorItem[] = [];
|
||||
|
||||
for (let i = 0; i < colors.length; i++) {
|
||||
items.push({
|
||||
color: colors[i].color,
|
||||
checked: false,
|
||||
});
|
||||
}
|
||||
|
||||
if (isFillColor) {
|
||||
this._cellFillColors$.next([...items]);
|
||||
} else {
|
||||
this._cellTextColors$.next([...items]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply the filter condition to the target filter column.
|
||||
*/
|
||||
async apply(): Promise<boolean> {
|
||||
if (this._disposed) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const cellFillColorsChecked = this.cellFillColors.filter((item) => item.checked).map((item) => item.color);
|
||||
const cellTextColorsChecked = this.cellTextColors.filter((item) => item.checked).map((item) => item.color);
|
||||
|
||||
if (cellFillColorsChecked.length === 0 && cellTextColorsChecked.length === 0) {
|
||||
// If no colors are checked, we clear the filter.
|
||||
return this._commandService.executeCommand(SetSheetsFilterCriteriaCommand.id, {
|
||||
unitId: this._filterModel.unitId,
|
||||
subUnitId: this._filterModel.subUnitId,
|
||||
col: this.col,
|
||||
criteria: null,
|
||||
} as ISetSheetsFilterCriteriaCommandParams);
|
||||
}
|
||||
|
||||
const criteria: IFilterColumn = { colId: this.col };
|
||||
|
||||
if (cellFillColorsChecked.length > 0) {
|
||||
criteria.colorFilters = {
|
||||
cellFillColors: cellFillColorsChecked,
|
||||
};
|
||||
} else if (cellTextColorsChecked.length > 0) {
|
||||
criteria.colorFilters = {
|
||||
cellTextColors: cellTextColorsChecked as string[],
|
||||
};
|
||||
}
|
||||
|
||||
return this._commandService.executeCommand(SetSheetsFilterCriteriaCommand.id, {
|
||||
unitId: this._filterModel.unitId,
|
||||
subUnitId: this._filterModel.subUnitId,
|
||||
col: this.col,
|
||||
criteria,
|
||||
} as ISetSheetsFilterCriteriaCommandParams);
|
||||
}
|
||||
|
||||
// #endregion
|
||||
}
|
||||
|
||||
// #endregion
|
||||
87
packages/sheets-filter-ui/src/services/util.ts
Normal file
87
packages/sheets-filter-ui/src/services/util.ts
Normal file
@@ -0,0 +1,87 @@
|
||||
/**
|
||||
* Copyright 2023-present DreamNum Co., Ltd.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import type { IFilterByValueWithTreeItem } from './sheets-filter-panel.service';
|
||||
|
||||
export function findObjectByKey(data: IFilterByValueWithTreeItem[], targetKey: string): IFilterByValueWithTreeItem | null {
|
||||
for (const node of data) {
|
||||
if (node.key === targetKey) {
|
||||
return node;
|
||||
}
|
||||
if (node.children) {
|
||||
const result = findObjectByKey(node.children, targetKey);
|
||||
if (result) {
|
||||
return result;
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
export function areAllLeafNodesChecked(node: IFilterByValueWithTreeItem): boolean {
|
||||
if (node.leaf) {
|
||||
return node.checked;
|
||||
}
|
||||
return node.children ? node.children.every((child) => areAllLeafNodesChecked(child)) : true;
|
||||
}
|
||||
|
||||
export function updateLeafNodesCheckedStatus(node: IFilterByValueWithTreeItem, status?: boolean) {
|
||||
if (node.leaf) {
|
||||
if (status !== undefined) {
|
||||
node.checked = status;
|
||||
} else {
|
||||
node.checked = !node.checked;
|
||||
}
|
||||
}
|
||||
|
||||
if (node.children) {
|
||||
node.children.forEach((child) => updateLeafNodesCheckedStatus(child, status));
|
||||
}
|
||||
}
|
||||
|
||||
export function searchTree(items: IFilterByValueWithTreeItem[], searchKeywords: string[]): IFilterByValueWithTreeItem[] {
|
||||
const result: IFilterByValueWithTreeItem[] = [];
|
||||
|
||||
items.forEach((item) => {
|
||||
const originMatches = item.originValues
|
||||
? searchKeywords.some((keyword) =>
|
||||
Array.from(item.originValues!).some((value) =>
|
||||
value.toLowerCase().includes(keyword.toLowerCase())
|
||||
)
|
||||
)
|
||||
: false;
|
||||
|
||||
const titleMatches = !originMatches
|
||||
&& searchKeywords.some((keyword) =>
|
||||
item.title.toLowerCase().includes(keyword.toLowerCase())
|
||||
);
|
||||
|
||||
const matches = originMatches || titleMatches;
|
||||
|
||||
if (matches) {
|
||||
result.push({ ...item });
|
||||
} else if (item.children) {
|
||||
const filteredChildren = searchTree(item.children, searchKeywords);
|
||||
|
||||
if (filteredChildren.length > 0) {
|
||||
const aggregatedCount = filteredChildren.reduce((sum, child) => sum + child.count, 0);
|
||||
result.push({ ...item, count: aggregatedCount, children: filteredChildren });
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return result;
|
||||
}
|
||||
@@ -0,0 +1,175 @@
|
||||
/**
|
||||
* 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 { LocaleKey } from '../../locale/types';
|
||||
import type { ByColorsModel, IFilterByColorItem } from '../../services/sheets-filter-panel.service';
|
||||
import { LocaleService } from '@univerjs/core';
|
||||
import { borderClassName, clsx } from '@univerjs/design';
|
||||
import { AIcon, BanIcon, SuccessIcon } from '@univerjs/icons';
|
||||
import { useDependency, useObservable } from '@univerjs/ui';
|
||||
import { useCallback } from 'react';
|
||||
|
||||
/**
|
||||
* Filter by colors.
|
||||
*/
|
||||
export function FilterByColor(props: { model: ByColorsModel }) {
|
||||
const { model } = props;
|
||||
const localeService = useDependency(LocaleService);
|
||||
const cellFillColors = useObservable(model.cellFillColors$, [], true);
|
||||
const cellTextColors = useObservable(model.cellTextColors$, [], true);
|
||||
|
||||
const handleSelectCellFillColor = useCallback((color: IFilterByColorItem) => {
|
||||
model.onFilterCheckToggled(color);
|
||||
}, [model]);
|
||||
|
||||
const handleSelectCellTextColor = useCallback((color: IFilterByColorItem) => {
|
||||
model.onFilterCheckToggled(color, false);
|
||||
}, [model]);
|
||||
|
||||
return (
|
||||
<div
|
||||
data-u-comp="sheets-filter-panel-colors-container"
|
||||
className="univer-flex univer-h-full univer-min-h-[300px] univer-flex-col"
|
||||
>
|
||||
<div
|
||||
data-u-comp="sheets-filter-panel"
|
||||
className={clsx(`
|
||||
univer-mt-2 univer-box-border univer-flex univer-h-[300px] univer-flex-grow univer-flex-col
|
||||
univer-gap-4 univer-overflow-auto univer-rounded-md univer-px-2 univer-py-2.5
|
||||
`, borderClassName)}
|
||||
>
|
||||
{cellFillColors.length > 1 && (
|
||||
<div>
|
||||
<div
|
||||
className={`
|
||||
univer-mb-2 univer-text-sm univer-text-gray-900
|
||||
dark:!univer-text-gray-0
|
||||
`}
|
||||
>
|
||||
{localeService.t<LocaleKey>('sheets-filter-ui.panel.filter-by-cell-fill-color')}
|
||||
</div>
|
||||
<div
|
||||
className={`
|
||||
univer-grid univer-grid-cols-8 univer-items-center univer-justify-start univer-gap-2
|
||||
`}
|
||||
>
|
||||
{cellFillColors.map((color, index) => (
|
||||
<div
|
||||
key={`sheets-filter-cell-fill-color-${index}`}
|
||||
className="univer-relative univer-size-6"
|
||||
onClick={() => handleSelectCellFillColor(color)}
|
||||
>
|
||||
{!color.color
|
||||
? (
|
||||
<BanIcon
|
||||
className={`
|
||||
univer-size-6 univer-cursor-pointer univer-rounded-full
|
||||
hover:univer-ring-2 hover:univer-ring-offset-2
|
||||
hover:univer-ring-offset-gray-0
|
||||
`}
|
||||
/>
|
||||
)
|
||||
: (
|
||||
<button
|
||||
type="button"
|
||||
className={clsx(`
|
||||
univer-box-border univer-size-6 univer-cursor-pointer
|
||||
univer-rounded-full univer-border univer-border-solid
|
||||
univer-border-transparent univer-bg-gray-300 univer-transition-shadow
|
||||
hover:univer-ring-2 hover:univer-ring-offset-2
|
||||
hover:univer-ring-offset-gray-0
|
||||
`)}
|
||||
style={{ backgroundColor: color.color }}
|
||||
/>
|
||||
)}
|
||||
|
||||
{color.checked && <CheckedIcon />}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{cellTextColors.length > 1 && (
|
||||
<div>
|
||||
<div
|
||||
className={`
|
||||
univer-mb-2 univer-text-sm univer-text-gray-900
|
||||
dark:!univer-text-gray-0
|
||||
`}
|
||||
>
|
||||
{localeService.t<LocaleKey>('sheets-filter-ui.panel.filter-by-cell-text-color')}
|
||||
</div>
|
||||
<div
|
||||
className={`
|
||||
univer-grid univer-grid-cols-8 univer-items-center univer-justify-start univer-gap-2
|
||||
`}
|
||||
>
|
||||
{cellTextColors.map((color, index) => (
|
||||
<div
|
||||
key={`sheets-filter-cell-text-color-${index}`}
|
||||
className="univer-relative univer-size-6"
|
||||
onClick={() => handleSelectCellTextColor(color)}
|
||||
>
|
||||
<div
|
||||
className={`
|
||||
univer-box-border univer-flex univer-size-full univer-cursor-pointer
|
||||
univer-items-center univer-justify-center univer-rounded-full univer-border
|
||||
univer-border-solid univer-border-[rgba(13,13,13,0.06)] univer-p-0.5
|
||||
hover:univer-ring-2 hover:univer-ring-offset-2 hover:univer-ring-offset-gray-0
|
||||
dark:!univer-border-[rgba(255,255,255,0.06)]
|
||||
`}
|
||||
>
|
||||
<AIcon style={{ color: color.color as string }} />
|
||||
</div>
|
||||
|
||||
{color.checked && <CheckedIcon />}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{cellFillColors.length <= 1 && cellTextColors.length <= 1 && (
|
||||
<div
|
||||
className={`
|
||||
univer-flex univer-size-full univer-items-center univer-justify-center univer-text-sm
|
||||
univer-text-gray-900
|
||||
dark:!univer-text-gray-200
|
||||
`}
|
||||
>
|
||||
{localeService.t<LocaleKey>('sheets-filter-ui.panel.filter-by-color-none')}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function CheckedIcon() {
|
||||
return (
|
||||
<div
|
||||
className={`
|
||||
univer-absolute -univer-bottom-0.5 -univer-right-0.5 univer-flex univer-size-3 univer-cursor-pointer
|
||||
univer-items-center univer-justify-center univer-rounded-full univer-bg-gray-0
|
||||
`}
|
||||
>
|
||||
<SuccessIcon
|
||||
className="univer-size-full univer-font-bold univer-text-[#418F1F]"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,150 @@
|
||||
/**
|
||||
* 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 { LocaleKey } from '../../locale/types';
|
||||
import type { FilterOperator, IFilterConditionFormParams } from '../../models/conditions';
|
||||
import type { ByConditionsModel } from '../../services/sheets-filter-panel.service';
|
||||
import { LocaleService } from '@univerjs/core';
|
||||
import { borderClassName, clsx, Input, Radio, RadioGroup, Select } from '@univerjs/design';
|
||||
import { useDependency, useObservable } from '@univerjs/ui';
|
||||
import { useCallback } from 'react';
|
||||
import { FilterConditionItems } from '../../models/conditions';
|
||||
|
||||
const PRIMARY_CONDITION_GROUPS = [
|
||||
[FilterConditionItems.NONE],
|
||||
[FilterConditionItems.EMPTY, FilterConditionItems.NOT_EMPTY],
|
||||
[
|
||||
FilterConditionItems.TEXT_CONTAINS,
|
||||
FilterConditionItems.DOES_NOT_CONTAIN,
|
||||
FilterConditionItems.STARTS_WITH,
|
||||
FilterConditionItems.ENDS_WITH,
|
||||
FilterConditionItems.EQUALS,
|
||||
],
|
||||
[
|
||||
FilterConditionItems.GREATER_THAN,
|
||||
FilterConditionItems.GREATER_THAN_OR_EQUAL,
|
||||
FilterConditionItems.LESS_THAN,
|
||||
FilterConditionItems.LESS_THAN_OR_EQUAL,
|
||||
FilterConditionItems.EQUAL,
|
||||
FilterConditionItems.NOT_EQUAL,
|
||||
FilterConditionItems.BETWEEN,
|
||||
FilterConditionItems.NOT_BETWEEN,
|
||||
],
|
||||
[FilterConditionItems.CUSTOM],
|
||||
];
|
||||
|
||||
/**
|
||||
* Filter by conditions.
|
||||
*/
|
||||
export function FilterByCondition(props: { model: ByConditionsModel }) {
|
||||
const { model } = props;
|
||||
|
||||
const localeService = useDependency(LocaleService);
|
||||
|
||||
// form state is from the model
|
||||
const condition = useObservable(model.conditionItem$, undefined);
|
||||
const formParams = useObservable(model.filterConditionFormParams$, undefined);
|
||||
|
||||
const radioValue = formParams?.and ? 'AND' : 'OR';
|
||||
const onRadioChange = useCallback((key: string | number | boolean) => {
|
||||
model.onConditionFormChange({ and: key === 'AND' });
|
||||
}, [model]);
|
||||
|
||||
const primaryOptions = PRIMARY_CONDITION_GROUPS.map((group) => ({
|
||||
options: group.map((conditionItem) => ({
|
||||
label: localeService.t(conditionItem.label),
|
||||
value: conditionItem.operator,
|
||||
})),
|
||||
}));
|
||||
const onPrimaryConditionChange = useCallback((value: string) => {
|
||||
model.onPrimaryConditionChange(value as FilterOperator);
|
||||
}, [model]);
|
||||
|
||||
const secondaryOptions = FilterConditionItems.ALL_CONDITIONS
|
||||
.filter((conditionItem) => conditionItem.numOfParameters !== 2)
|
||||
.map((conditionItem) => ({
|
||||
label: localeService.t(conditionItem.label),
|
||||
value: conditionItem.operator,
|
||||
}));
|
||||
const onFormParamsChange = useCallback((diffParams: Partial<IFilterConditionFormParams>) => {
|
||||
model.onConditionFormChange(diffParams);
|
||||
}, [model]);
|
||||
|
||||
const placeholder = localeService.t<LocaleKey>('sheets-filter-ui.panel.input-values-placeholder');
|
||||
function renderSecondaryCondition(operator: FilterOperator, val: string, name: 'operator1' | 'operator2') {
|
||||
const shouldRenderInput = FilterConditionItems.getItemByOperator(operator).numOfParameters === 1;
|
||||
return (
|
||||
<>
|
||||
{name === 'operator2' && (
|
||||
<RadioGroup value={radioValue} onChange={onRadioChange}>
|
||||
<Radio value="AND">{localeService.t<LocaleKey>('sheets-filter-ui.panel.and')}</Radio>
|
||||
<Radio value="OR">{localeService.t<LocaleKey>('sheets-filter-ui.panel.or')}</Radio>
|
||||
</RadioGroup>
|
||||
)}
|
||||
<Select
|
||||
value={operator}
|
||||
options={secondaryOptions}
|
||||
onChange={(operator) => onFormParamsChange({ [name]: operator as FilterOperator })}
|
||||
/>
|
||||
{shouldRenderInput && (
|
||||
<div>
|
||||
<Input
|
||||
className="univer-mt-2"
|
||||
value={val}
|
||||
placeholder={placeholder}
|
||||
onChange={(value) => onFormParamsChange({ [name === 'operator1' ? 'val1' : 'val2']: value })}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
data-u-comp="sheets-filter-panel-conditions-container"
|
||||
className="univer-flex univer-h-full univer-min-h-[300px] univer-flex-col"
|
||||
>
|
||||
{/* primary condition */}
|
||||
{(condition && formParams) && (
|
||||
<>
|
||||
<Select value={condition.operator} options={primaryOptions} onChange={onPrimaryConditionChange} />
|
||||
{FilterConditionItems.getItemByOperator(condition.operator).numOfParameters !== 0
|
||||
? (
|
||||
<div
|
||||
data-u-comp="sheets-filter-panel-conditions-container-inner"
|
||||
className={clsx(`
|
||||
univer-mt-2 univer-flex-grow univer-overflow-hidden univer-rounded-md univer-p-2
|
||||
`, borderClassName)}
|
||||
>
|
||||
{condition.numOfParameters >= 1 && renderSecondaryCondition(formParams.operator1!, formParams.val1 ?? '', 'operator1')}
|
||||
{condition.numOfParameters >= 2 && renderSecondaryCondition(formParams.operator2!, formParams.val2 ?? '', 'operator2')}
|
||||
<div
|
||||
data-u-comp="sheets-filter-panel-conditions-desc"
|
||||
className="univer-mt-2 univer-text-xs univer-text-gray-500"
|
||||
>
|
||||
{localeService.t<LocaleKey>('sheets-filter-ui.panel.?')}
|
||||
<br />
|
||||
{localeService.t<LocaleKey>('sheets-filter-ui.panel.*')}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
: null}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,185 @@
|
||||
/**
|
||||
* 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 { LocaleKey } from '../../locale/types';
|
||||
import type { ByValuesModel, IFilterByValueWithTreeItem } from '../../services/sheets-filter-panel.service';
|
||||
import { LocaleService } from '@univerjs/core';
|
||||
import { borderClassName, Checkbox, clsx, Input, Tree } from '@univerjs/design';
|
||||
import { useDependency, useObservable } from '@univerjs/ui';
|
||||
import { useCallback } from 'react';
|
||||
import { statisticFilterByValueItems } from '../../models/utils';
|
||||
|
||||
/**
|
||||
* Filter by values.
|
||||
*/
|
||||
export function FilterByValue(props: { model: ByValuesModel }) {
|
||||
const { model } = props;
|
||||
|
||||
const localeService = useDependency(LocaleService);
|
||||
|
||||
const searchText = useObservable(model.searchString$, '', true);
|
||||
const items = useObservable(model.filterItems$, undefined, true);
|
||||
const filterOnly = localeService.t<LocaleKey>('sheets-filter-ui.panel.filter-only');
|
||||
|
||||
const stat = statisticFilterByValueItems(items);
|
||||
const allChecked = stat.checked > 0 && stat.unchecked === 0;
|
||||
const indeterminate = stat.checked > 0 && stat.unchecked > 0;
|
||||
|
||||
const treeMap = model.treeMapCache;
|
||||
|
||||
const onCheckAllToggled = useCallback(() => {
|
||||
model.onCheckAllToggled(!allChecked);
|
||||
}, [model, allChecked]);
|
||||
|
||||
const onSearchValueChange = useCallback((str: string) => {
|
||||
model.setSearchString(str);
|
||||
}, [model]);
|
||||
|
||||
function extractCheckedKeys(items: IFilterByValueWithTreeItem[]): string[] {
|
||||
let checkedKeys: string[] = [];
|
||||
items.forEach((item) => {
|
||||
if (item.checked) {
|
||||
checkedKeys.push(item.key);
|
||||
}
|
||||
if (item.children) {
|
||||
checkedKeys = checkedKeys.concat(extractCheckedKeys(item.children));
|
||||
}
|
||||
});
|
||||
return checkedKeys;
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
data-u-comp="sheets-filter-panel-values-container"
|
||||
className="univer-flex univer-h-full univer-min-h-[300px] univer-flex-col"
|
||||
>
|
||||
<Input
|
||||
autoFocus
|
||||
value={searchText}
|
||||
placeholder={localeService.t<LocaleKey>('sheets-filter-ui.panel.search-placeholder')}
|
||||
onChange={onSearchValueChange}
|
||||
/>
|
||||
<div
|
||||
data-u-comp="sheets-filter-panel"
|
||||
className={clsx(`
|
||||
univer-mt-2 univer-box-border univer-flex univer-flex-grow univer-flex-col univer-overflow-hidden
|
||||
univer-rounded-md univer-px-2 univer-py-2.5
|
||||
`, borderClassName)}
|
||||
>
|
||||
{/* The on-top "Select All" button */}
|
||||
<div
|
||||
data-u-comp="sheets-filter-panel-values-item"
|
||||
className="univer-box-border univer-h-8 univer-w-full univer-py-0.5"
|
||||
>
|
||||
<div
|
||||
data-u-comp="sheets-filter-panel-values-item-inner"
|
||||
className={`
|
||||
univer-box-border univer-flex univer-h-7 univer-items-center univer-rounded-md univer-py-0
|
||||
univer-pl-5 univer-pr-0.5 univer-text-sm
|
||||
`}
|
||||
>
|
||||
<Checkbox
|
||||
indeterminate={indeterminate}
|
||||
disabled={items.length === 0}
|
||||
checked={allChecked}
|
||||
onChange={onCheckAllToggled}
|
||||
/>
|
||||
<span
|
||||
data-u-comp="sheets-filter-panel-values-item-text"
|
||||
className={`
|
||||
univer-mx-1 univer-inline-block univer-flex-shrink univer-truncate univer-text-gray-900
|
||||
dark:!univer-text-gray-0
|
||||
`}
|
||||
>
|
||||
{`${localeService.t<LocaleKey>('sheets-filter-ui.panel.select-all')}`}
|
||||
</span>
|
||||
<span
|
||||
data-u-comp="sheets-filter-panel-values-item-count"
|
||||
className={`
|
||||
univer-text-gray-400
|
||||
dark:!univer-text-gray-500
|
||||
`}
|
||||
>
|
||||
{`(${stat.checked}/${stat.checked + stat.unchecked})`}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div data-u-comp="sheets-filter-panel-values-virtual" className="univer-flex-grow">
|
||||
<Tree
|
||||
data={items}
|
||||
defaultExpandAll={false}
|
||||
valueGroup={extractCheckedKeys(items)}
|
||||
// height={180}
|
||||
onChange={(node) => {
|
||||
model.onFilterCheckToggled(node as IFilterByValueWithTreeItem);
|
||||
}}
|
||||
defaultCache={treeMap}
|
||||
itemHeight={28}
|
||||
treeNodeClassName={`
|
||||
univer-pr-2 univer-border-box univer-rounded-md
|
||||
[&:hover_a]:univer-inline-block
|
||||
hover:univer-bg-gray-50 univer-h-full
|
||||
univer-text-gray-900 dark:hover:!univer-bg-gray-900
|
||||
dark:!univer-text-gray-0
|
||||
`}
|
||||
attachRender={(item) => (
|
||||
<div
|
||||
className={`
|
||||
univer-ml-1 univer-flex univer-h-5 univer-flex-1 univer-cursor-pointer
|
||||
univer-items-center univer-justify-between univer-text-sm univer-text-primary-500
|
||||
`}
|
||||
>
|
||||
<span
|
||||
data-u-comp="sheets-filter-panel-values-item-count"
|
||||
className={`
|
||||
univer-text-gray-400
|
||||
dark:!univer-text-gray-500
|
||||
`}
|
||||
>
|
||||
{`(${item.count})`}
|
||||
</span>
|
||||
<a
|
||||
className={`
|
||||
univer-box-border univer-hidden univer-h-4 univer-whitespace-nowrap univer-px-1.5
|
||||
`}
|
||||
onClick={() => {
|
||||
const filterValues = [];
|
||||
if (item.children) {
|
||||
item.children.forEach((child) => {
|
||||
if (child.children) {
|
||||
child.children.forEach((subChild) => {
|
||||
filterValues.push(subChild.key);
|
||||
});
|
||||
} else {
|
||||
filterValues.push(child.key);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
filterValues.push(item.key);
|
||||
}
|
||||
model.onFilterOnly(filterValues);
|
||||
}}
|
||||
>
|
||||
{filterOnly}
|
||||
</a>
|
||||
</div>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,146 @@
|
||||
/**
|
||||
* Copyright 2023-present DreamNum Co., Ltd.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import type { Meta } from '@storybook/react';
|
||||
import type { IWorkbookData } from '@univerjs/core';
|
||||
import type { IOpenFilterPanelOperationParams } from '../../commands/operations/sheets-filter.operation';
|
||||
import { CommandType, ICommandService, ILogService, Inject, Injector, LocaleService, LocaleType, LogLevel, Plugin, registerDependencies, Univer, UniverInstanceType } from '@univerjs/core';
|
||||
import { RefRangeService, SheetInterceptorService, SheetsSelectionsService, WorksheetProtectionPointModel } from '@univerjs/sheets';
|
||||
import { ClearSheetsFilterCriteriaCommand, ReCalcSheetsFilterCommand, SetSheetsFilterCriteriaCommand, SmartToggleSheetsFilterCommand, UniverSheetsFilterPlugin } from '@univerjs/sheets-filter';
|
||||
import { SetCellEditVisibleOperation } from '@univerjs/sheets-ui';
|
||||
import { IShortcutService, IUIPartsService, RediContext, ShortcutService, UIPartsService } from '@univerjs/ui';
|
||||
import { useState } from 'react';
|
||||
import { WithCustomFilterModelFactory, WithValuesFilterModelFactory } from '../../__tests__/data';
|
||||
import { ChangeFilterByOperation, CloseFilterPanelOperation, OpenFilterPanelOperation } from '../../commands/operations/sheets-filter.operation';
|
||||
import enUS from '../../locale/en-US';
|
||||
import ruRU from '../../locale/ru-RU';
|
||||
import zhCN from '../../locale/zh-CN';
|
||||
import { ISheetsFilterPanelService, SheetsFilterPanelService } from '../../services/sheets-filter-panel.service';
|
||||
import { FilterPanel } from './SheetsFilterPanel';
|
||||
|
||||
const meta: Meta<typeof FilterPanel> = {
|
||||
title: 'Components / FilterPanel',
|
||||
component: FilterPanel,
|
||||
tags: ['autodocs'],
|
||||
};
|
||||
|
||||
export default meta;
|
||||
|
||||
const FakeCetCellEditVisibleOperation = {
|
||||
id: SetCellEditVisibleOperation.id,
|
||||
type: CommandType.OPERATION,
|
||||
handler: () => true,
|
||||
};
|
||||
|
||||
function createFilterStorybookBed(workbookData: IWorkbookData, locale: LocaleType = LocaleType.EN_US) {
|
||||
const univer = new Univer();
|
||||
const injector = univer.__getInjector();
|
||||
|
||||
class TestPlugin extends Plugin {
|
||||
static override type = UniverInstanceType.UNIVER_SHEET;
|
||||
static override pluginName = 'test-plugin';
|
||||
|
||||
constructor(
|
||||
_config: unknown,
|
||||
@Inject(Injector) protected readonly _injector: Injector
|
||||
) {
|
||||
super();
|
||||
}
|
||||
|
||||
override onStarting(): void {
|
||||
const injector = this._injector;
|
||||
|
||||
registerDependencies(injector, [
|
||||
[SheetsSelectionsService],
|
||||
[IShortcutService, { useClass: ShortcutService }],
|
||||
[IUIPartsService, { useClass: UIPartsService }],
|
||||
[WorksheetProtectionPointModel],
|
||||
[SheetInterceptorService],
|
||||
[ISheetsFilterPanelService, { useClass: SheetsFilterPanelService }],
|
||||
[RefRangeService],
|
||||
]);
|
||||
|
||||
const commandService = injector.get(ICommandService);
|
||||
[
|
||||
FakeCetCellEditVisibleOperation,
|
||||
SmartToggleSheetsFilterCommand,
|
||||
SetSheetsFilterCriteriaCommand,
|
||||
ClearSheetsFilterCriteriaCommand,
|
||||
ReCalcSheetsFilterCommand,
|
||||
OpenFilterPanelOperation,
|
||||
CloseFilterPanelOperation,
|
||||
ChangeFilterByOperation,
|
||||
].forEach((command) => commandService.registerCommand(command));
|
||||
}
|
||||
|
||||
override onReady(): void {
|
||||
const commandService = injector.get(ICommandService);
|
||||
commandService.syncExecuteCommand(OpenFilterPanelOperation.id, {
|
||||
unitId: 'test',
|
||||
subUnitId: 'sheet1',
|
||||
col: 0,
|
||||
} as IOpenFilterPanelOperationParams);
|
||||
}
|
||||
}
|
||||
|
||||
univer.registerPlugin(TestPlugin);
|
||||
univer.registerPlugin(UniverSheetsFilterPlugin);
|
||||
|
||||
injector.get(LocaleService).setLocale(locale);
|
||||
injector.get(LocaleService).load({ enUS, zhCN, ruRU });
|
||||
injector.get(ILogService).setLogLevel(LogLevel.VERBOSE);
|
||||
|
||||
const sheet = univer.createUnit(UniverInstanceType.UNIVER_SHEET, workbookData);
|
||||
|
||||
return { univer, injector, sheet };
|
||||
}
|
||||
|
||||
export const FilterWithConditions = {
|
||||
render() {
|
||||
const [bed] = useState(() => createFilterStorybookBed(WithCustomFilterModelFactory()));
|
||||
const { injector } = bed;
|
||||
|
||||
return (
|
||||
<RediContext.Provider value={{ injector }}>
|
||||
<FilterPanel />
|
||||
</RediContext.Provider>
|
||||
);
|
||||
},
|
||||
};
|
||||
|
||||
export const FilterWithValues = {
|
||||
render() {
|
||||
const [bed] = useState(() => createFilterStorybookBed(WithValuesFilterModelFactory()));
|
||||
|
||||
return (
|
||||
<RediContext.Provider value={{ injector: bed.injector }}>
|
||||
<FilterPanel />
|
||||
</RediContext.Provider>
|
||||
);
|
||||
},
|
||||
};
|
||||
|
||||
export const FilterWithChinese = {
|
||||
render() {
|
||||
const [bed] = useState(() => createFilterStorybookBed(WithValuesFilterModelFactory(), LocaleType.ZH_CN));
|
||||
|
||||
return (
|
||||
<RediContext.Provider value={{ injector: bed.injector }}>
|
||||
<FilterPanel />
|
||||
</RediContext.Provider>
|
||||
);
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,139 @@
|
||||
/**
|
||||
* Copyright 2023-present DreamNum Co., Ltd.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import type { LocaleKey } from '../../locale/types';
|
||||
import type { ByColorsModel, ByConditionsModel, ByValuesModel } from '../../services/sheets-filter-panel.service';
|
||||
import { ICommandService, LocaleService } from '@univerjs/core';
|
||||
import { Button, Segmented } from '@univerjs/design';
|
||||
import { FilterBy, SheetsFilterService } from '@univerjs/sheets-filter';
|
||||
import { SheetsUIPart } from '@univerjs/sheets-ui';
|
||||
import { ComponentContainer, useComponentsOfPart, useDependency, useObservable } from '@univerjs/ui';
|
||||
import { useCallback } from 'react';
|
||||
import { of } from 'rxjs';
|
||||
import { ChangeFilterByOperation, CloseFilterPanelOperation } from '../../commands/operations/sheets-filter.operation';
|
||||
import { ISheetsFilterPanelService } from '../../services/sheets-filter-panel.service';
|
||||
import { FilterByColor } from './SheetsFilterByColorsPanel';
|
||||
import { FilterByCondition } from './SheetsFilterByConditionsPanel';
|
||||
import { FilterByValue } from './SheetsFilterByValuesPanel';
|
||||
import { FilterSyncSwitch } from './SheetsFilterSyncSwitch';
|
||||
|
||||
const FILTER_BY_OPTIONS: Array<{ label: LocaleKey; value: FilterBy }> = [
|
||||
{ label: 'sheets-filter-ui.panel.by-values', value: FilterBy.VALUES },
|
||||
{ label: 'sheets-filter-ui.panel.by-colors', value: FilterBy.COLORS },
|
||||
{ label: 'sheets-filter-ui.panel.by-conditions', value: FilterBy.CONDITIONS },
|
||||
];
|
||||
|
||||
/**
|
||||
* This Filter Panel component is used to filter the data in the sheet.
|
||||
*
|
||||
* @returns React element
|
||||
*/
|
||||
export function FilterPanel() {
|
||||
const sheetsFilterPanelService = useDependency(ISheetsFilterPanelService);
|
||||
const localeService = useDependency(LocaleService);
|
||||
const commandService = useDependency(ICommandService);
|
||||
|
||||
const filterBy = useObservable(sheetsFilterPanelService.filterBy$, undefined, true);
|
||||
const filterByModel = useObservable(sheetsFilterPanelService.filterByModel$, undefined, false);
|
||||
const canApply = useObservable(() => filterByModel?.canApply$ || of(false), undefined, false, [filterByModel]);
|
||||
const items = FILTER_BY_OPTIONS.map((option) => ({ ...option, label: localeService.t(option.label) }));
|
||||
|
||||
// only can disable clear when there is no criteria
|
||||
const clearFilterDisabled = !useObservable(sheetsFilterPanelService.hasCriteria$);
|
||||
|
||||
const onFilterByTypeChange = useCallback((value: FilterBy) => {
|
||||
commandService.executeCommand(ChangeFilterByOperation.id, { filterBy: value });
|
||||
}, [commandService]);
|
||||
|
||||
const onClearCriteria = useCallback(async () => {
|
||||
await filterByModel?.clear();
|
||||
commandService.executeCommand(CloseFilterPanelOperation.id);
|
||||
}, [filterByModel, commandService]);
|
||||
|
||||
const onCancel = useCallback(() => {
|
||||
commandService.executeCommand(CloseFilterPanelOperation.id);
|
||||
}, [commandService]);
|
||||
|
||||
const onApply = useCallback(async () => {
|
||||
await filterByModel?.apply();
|
||||
commandService.executeCommand(CloseFilterPanelOperation.id);
|
||||
}, [filterByModel, commandService]);
|
||||
|
||||
const filterService = useDependency(SheetsFilterService);
|
||||
const range = filterService.activeFilterModel?.getRange();
|
||||
const colIndex = sheetsFilterPanelService.col;
|
||||
const FilterPanelEmbedPointPart = useComponentsOfPart(SheetsUIPart.FILTER_PANEL_EMBED_POINT);
|
||||
|
||||
return (
|
||||
<div
|
||||
data-u-comp="sheets-filter-panel"
|
||||
className={`
|
||||
univer-box-border univer-flex univer-max-h-[500px] univer-w-[400px] univer-flex-col univer-rounded-lg
|
||||
univer-bg-gray-0 univer-p-4 univer-shadow-lg
|
||||
dark:!univer-border-gray-600 dark:!univer-bg-gray-700
|
||||
`}
|
||||
>
|
||||
<ComponentContainer
|
||||
components={FilterPanelEmbedPointPart}
|
||||
sharedProps={{ range, colIndex, onClose: onCancel }}
|
||||
/>
|
||||
<div className="univer-mb-1 univer-flex-shrink-0 univer-flex-grow-0">
|
||||
<Segmented
|
||||
value={filterBy}
|
||||
items={items}
|
||||
onChange={(value) => onFilterByTypeChange(value as FilterBy)}
|
||||
/>
|
||||
</div>
|
||||
{filterByModel
|
||||
? (
|
||||
<div
|
||||
data-u-comp="sheets-filter-panel-content"
|
||||
className="univer-flex-shrink univer-flex-grow univer-pt-2"
|
||||
>
|
||||
{filterBy === FilterBy.VALUES
|
||||
? <FilterByValue model={filterByModel as ByValuesModel} />
|
||||
: filterBy === FilterBy.COLORS
|
||||
? <FilterByColor model={filterByModel as ByColorsModel} />
|
||||
: <FilterByCondition model={filterByModel as ByConditionsModel} />}
|
||||
</div>
|
||||
)
|
||||
: (
|
||||
<div className="univer-flex-1" />
|
||||
)}
|
||||
<FilterSyncSwitch />
|
||||
<div
|
||||
data-u-comp="sheets-filter-panel-footer"
|
||||
className={`
|
||||
univer-mt-4 univer-inline-flex univer-flex-shrink-0 univer-flex-grow-0 univer-flex-nowrap
|
||||
univer-justify-between univer-overflow-hidden
|
||||
`}
|
||||
>
|
||||
<Button variant="link" onClick={onClearCriteria} disabled={clearFilterDisabled}>
|
||||
{localeService.t<LocaleKey>('sheets-filter-ui.panel.clear-filter')}
|
||||
</Button>
|
||||
<span className="univer-flex univer-gap-2">
|
||||
<Button variant="default" onClick={onCancel}>
|
||||
{localeService.t<LocaleKey>('sheets-filter-ui.panel.cancel')}
|
||||
</Button>
|
||||
<Button disabled={!canApply} variant="primary" onClick={onApply}>
|
||||
{localeService.t<LocaleKey>('sheets-filter-ui.panel.confirm')}
|
||||
</Button>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
export const FILTER_PANEL_POPUP_KEY = 'FILTER_PANEL_POPUP';
|
||||
@@ -0,0 +1,67 @@
|
||||
/**
|
||||
* 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 { LocaleKey } from '../../locale/types';
|
||||
import { LocaleService } from '@univerjs/core';
|
||||
import { MessageType, Switch, Tooltip } from '@univerjs/design';
|
||||
import { InfoIcon } from '@univerjs/icons';
|
||||
import { SheetsFilterSyncController } from '@univerjs/sheets-filter';
|
||||
import { IMessageService, useDependency, useObservable } from '@univerjs/ui';
|
||||
|
||||
export function FilterSyncSwitch() {
|
||||
const sheetsFilterSyncController = useDependency(SheetsFilterSyncController);
|
||||
const visible = useObservable(sheetsFilterSyncController.visible$, undefined, true);
|
||||
const localeService = useDependency(LocaleService);
|
||||
const messageService = useDependency(IMessageService);
|
||||
const enabled = useObservable(sheetsFilterSyncController.enabled$, undefined, true);
|
||||
|
||||
if (!visible) return null;
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`
|
||||
univer-mt-2 univer-flex univer-items-center univer-justify-between univer-text-sm univer-text-gray-900
|
||||
dark:!univer-text-gray-200
|
||||
`}
|
||||
>
|
||||
<div className="univer-flex univer-items-center univer-gap-1">
|
||||
<span>{localeService.t<LocaleKey>('sheets-filter-ui.sync.title')}</span>
|
||||
<Tooltip
|
||||
title={enabled
|
||||
? localeService.t<LocaleKey>('sheets-filter-ui.sync.statusTips.off')
|
||||
: localeService.t<LocaleKey>('sheets-filter-ui.sync.statusTips.on')}
|
||||
asChild
|
||||
>
|
||||
<InfoIcon className="univer-block" />
|
||||
</Tooltip>
|
||||
</div>
|
||||
<Switch
|
||||
defaultChecked={enabled}
|
||||
onChange={(checked) => {
|
||||
const message = checked
|
||||
? localeService.t<LocaleKey>('sheets-filter-ui.sync.switchTips.on')
|
||||
: localeService.t<LocaleKey>('sheets-filter-ui.sync.switchTips.off');
|
||||
sheetsFilterSyncController.setEnabled(checked);
|
||||
messageService.show({
|
||||
content: message,
|
||||
type: MessageType.Success,
|
||||
duration: 2000,
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,578 @@
|
||||
/**
|
||||
* 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 { IDisposable, IOperation, IWorkbookData, Workbook } from '@univerjs/core';
|
||||
import type { IUniverSheetsFilterConfig } from '@univerjs/sheets-filter';
|
||||
import type { IEditorBridgeServiceVisibleParam } from '@univerjs/sheets-ui';
|
||||
import type { Root } from 'react-dom/client';
|
||||
import {
|
||||
awaitTime,
|
||||
CommandType,
|
||||
ICommandService,
|
||||
IContextService,
|
||||
ILogService,
|
||||
Inject,
|
||||
Injector,
|
||||
LocaleService,
|
||||
LocaleType,
|
||||
LogLevel,
|
||||
Plugin,
|
||||
toDisposable,
|
||||
Univer,
|
||||
UniverInstanceType,
|
||||
} from '@univerjs/core';
|
||||
import { FilterBy, SheetsFilterService, SheetsFilterSyncController, UniverSheetsFilterPlugin } from '@univerjs/sheets-filter';
|
||||
import { ILayoutService, IMessageService, IUIPartsService, RediContext, UIPartsService } from '@univerjs/ui';
|
||||
import { act } from 'react';
|
||||
import { createRoot } from 'react-dom/client';
|
||||
import { afterEach, describe, expect, it } from 'vitest';
|
||||
import { WithValuesAndEmptyFilterModelFactory, WithValuesFilterModelFactory } from '../../../__tests__/data';
|
||||
import {
|
||||
ChangeFilterByOperation,
|
||||
CloseFilterPanelOperation,
|
||||
FILTER_PANEL_OPENED_KEY,
|
||||
OpenFilterPanelOperation,
|
||||
} from '../../../commands/operations/sheets-filter.operation';
|
||||
import enUS from '../../../locale/en-US';
|
||||
import { ISheetsFilterPanelService, SheetsFilterPanelService } from '../../../services/sheets-filter-panel.service';
|
||||
import { FilterPanel } from '../SheetsFilterPanel';
|
||||
|
||||
(globalThis as { IS_REACT_ACT_ENVIRONMENT?: boolean }).IS_REACT_ACT_ENVIRONMENT = true;
|
||||
|
||||
const UNIT_ID = 'test';
|
||||
const SUB_UNIT_ID = 'sheet1';
|
||||
|
||||
const SetCellEditVisibleOperation: IOperation<IEditorBridgeServiceVisibleParam> = {
|
||||
id: 'sheet.operation.set-cell-edit-visible',
|
||||
type: CommandType.OPERATION,
|
||||
handler: () => true,
|
||||
};
|
||||
|
||||
class TestLayoutService {
|
||||
private _focused = false;
|
||||
readonly rootContainerElement = document.body;
|
||||
|
||||
get isFocused() {
|
||||
return this._focused;
|
||||
}
|
||||
|
||||
focus() {
|
||||
this._focused = true;
|
||||
}
|
||||
|
||||
registerFocusHandler(): IDisposable {
|
||||
return toDisposable(() => undefined);
|
||||
}
|
||||
|
||||
registerRootContainerElement(): IDisposable {
|
||||
return toDisposable(() => undefined);
|
||||
}
|
||||
|
||||
registerContentElement(): IDisposable {
|
||||
return toDisposable(() => undefined);
|
||||
}
|
||||
|
||||
registerContainerElement(): IDisposable {
|
||||
return toDisposable(() => undefined);
|
||||
}
|
||||
|
||||
getContentElement() {
|
||||
return document.body;
|
||||
}
|
||||
|
||||
checkElementInCurrentContainers() {
|
||||
return true;
|
||||
}
|
||||
|
||||
checkContentIsFocused() {
|
||||
return this._focused;
|
||||
}
|
||||
}
|
||||
|
||||
class TestMessageService {
|
||||
readonly messages: unknown[] = [];
|
||||
|
||||
show(options: unknown): IDisposable {
|
||||
this.messages.push(options);
|
||||
return toDisposable(() => undefined);
|
||||
}
|
||||
|
||||
remove() {
|
||||
// The switch UI is not the behavior under test here.
|
||||
}
|
||||
|
||||
removeAll() {
|
||||
this.messages.length = 0;
|
||||
}
|
||||
}
|
||||
|
||||
function createFilterPanelViewTestBed(workbookData: IWorkbookData, filterConfig?: Partial<IUniverSheetsFilterConfig>) {
|
||||
const univer = new Univer();
|
||||
const injector = univer.__getInjector();
|
||||
|
||||
class TestPlugin extends Plugin {
|
||||
static override type = UniverInstanceType.UNIVER_SHEET;
|
||||
static override pluginName = 'sheets-filter-ui-panel-view-test-plugin';
|
||||
|
||||
constructor(
|
||||
_config: unknown,
|
||||
@Inject(Injector) protected readonly _injector: Injector
|
||||
) {
|
||||
super();
|
||||
}
|
||||
|
||||
override onStarting(): void {
|
||||
this._injector.add([ISheetsFilterPanelService, { useClass: SheetsFilterPanelService }]);
|
||||
this._injector.add([ILayoutService, { useClass: TestLayoutService as never }]);
|
||||
this._injector.add([IMessageService, { useClass: TestMessageService as never }]);
|
||||
this._injector.add([IUIPartsService, { useClass: UIPartsService }]);
|
||||
}
|
||||
}
|
||||
|
||||
injector.get(LocaleService).load({ [LocaleType.EN_US]: enUS });
|
||||
injector.get(LocaleService).setLocale(LocaleType.EN_US);
|
||||
|
||||
if (filterConfig) {
|
||||
univer.registerPlugin(UniverSheetsFilterPlugin, filterConfig);
|
||||
} else {
|
||||
univer.registerPlugin(UniverSheetsFilterPlugin);
|
||||
}
|
||||
univer.registerPlugin(TestPlugin);
|
||||
|
||||
const workbook = univer.createUnit<IWorkbookData, Workbook>(UniverInstanceType.UNIVER_SHEET, workbookData);
|
||||
injector.get(ILogService).setLogLevel(LogLevel.SILENT);
|
||||
|
||||
const commandService = injector.get(ICommandService);
|
||||
[
|
||||
OpenFilterPanelOperation,
|
||||
CloseFilterPanelOperation,
|
||||
ChangeFilterByOperation,
|
||||
SetCellEditVisibleOperation,
|
||||
].forEach((command) => commandService.registerCommand(command));
|
||||
|
||||
return {
|
||||
univer,
|
||||
injector,
|
||||
workbook,
|
||||
commandService,
|
||||
contextService: injector.get(IContextService),
|
||||
filterService: injector.get(SheetsFilterService),
|
||||
panelService: injector.get(ISheetsFilterPanelService),
|
||||
messageService: injector.get(IMessageService) as TestMessageService,
|
||||
};
|
||||
}
|
||||
|
||||
async function openPanel(testBed: ReturnType<typeof createFilterPanelViewTestBed>) {
|
||||
expect(testBed.commandService.syncExecuteCommand(OpenFilterPanelOperation.id, {
|
||||
unitId: UNIT_ID,
|
||||
subUnitId: SUB_UNIT_ID,
|
||||
col: 0,
|
||||
})).toBe(true);
|
||||
await awaitTime(20);
|
||||
}
|
||||
|
||||
async function renderPanel(root: Root, testBed: ReturnType<typeof createFilterPanelViewTestBed>) {
|
||||
await act(async () => {
|
||||
root.render(
|
||||
<RediContext.Provider value={{ injector: testBed.injector }}>
|
||||
<FilterPanel />
|
||||
</RediContext.Provider>
|
||||
);
|
||||
await awaitTime(20);
|
||||
});
|
||||
}
|
||||
|
||||
function clickElement(element: Element) {
|
||||
element.dispatchEvent(new MouseEvent('click', { bubbles: true }));
|
||||
}
|
||||
|
||||
function openSelect(element: Element) {
|
||||
element.dispatchEvent(new PointerEvent('pointerdown', { bubbles: true, button: 0 }));
|
||||
}
|
||||
|
||||
function changeCheckbox(input: HTMLInputElement) {
|
||||
input.dispatchEvent(new MouseEvent('click', { bubbles: true }));
|
||||
}
|
||||
|
||||
function changeInput(input: HTMLInputElement, value: string) {
|
||||
const valueSetter = Object.getOwnPropertyDescriptor(HTMLInputElement.prototype, 'value')?.set;
|
||||
valueSetter?.call(input, value);
|
||||
input.dispatchEvent(new Event('input', { bubbles: true }));
|
||||
}
|
||||
|
||||
function getButton(container: HTMLElement, text: string): HTMLElement {
|
||||
const button = Array.from(container.querySelectorAll('[data-u-comp="button"]'))
|
||||
.find((button) => button.textContent === text);
|
||||
|
||||
if (!button) {
|
||||
throw new Error(`Button "${text}" was not rendered.`);
|
||||
}
|
||||
|
||||
return button as HTMLElement;
|
||||
}
|
||||
|
||||
function getSelect(container: HTMLElement, index = 0): HTMLElement {
|
||||
const select = container.querySelectorAll<HTMLElement>('[data-u-comp="select"]').item(index);
|
||||
if (!select) {
|
||||
throw new Error(`Select at index "${index}" was not rendered.`);
|
||||
}
|
||||
|
||||
return select;
|
||||
}
|
||||
|
||||
function getDropdownOption(text: string): HTMLElement {
|
||||
const option = Array.from(document.querySelectorAll<HTMLElement>('[data-slot="dropdown-menu-radio-item"]'))
|
||||
.find((item) => item.textContent === text);
|
||||
if (!option) {
|
||||
const renderedOptions = Array.from(document.querySelectorAll<HTMLElement>('[data-slot="dropdown-menu-radio-item"]'))
|
||||
.map((item) => item.textContent)
|
||||
.join(', ');
|
||||
throw new Error(`Dropdown option "${text}" was not rendered. Options: ${renderedOptions}`);
|
||||
}
|
||||
|
||||
return option;
|
||||
}
|
||||
|
||||
function getValueItemCheckbox(container: HTMLElement, title: string): HTMLInputElement {
|
||||
const titleElement = Array.from(container.querySelectorAll('span'))
|
||||
.find((element) => element.textContent === title);
|
||||
|
||||
if (!titleElement) {
|
||||
throw new Error(`Filter value "${title}" was not rendered.`);
|
||||
}
|
||||
|
||||
let row: Element | null = titleElement;
|
||||
while (row && !row.querySelector('[data-u-comp="checkbox"] input')) {
|
||||
row = row.parentElement;
|
||||
}
|
||||
|
||||
const input = row?.querySelector<HTMLInputElement>('[data-u-comp="checkbox"] input');
|
||||
if (!input) {
|
||||
throw new Error(`Filter value "${title}" did not render a checkbox.`);
|
||||
}
|
||||
|
||||
return input;
|
||||
}
|
||||
|
||||
function getColorSwatch(container: HTMLElement, color: string): HTMLElement {
|
||||
const button = Array.from(container.querySelectorAll<HTMLButtonElement>('button'))
|
||||
.find((button) => button.style.backgroundColor === color);
|
||||
|
||||
const swatch = button?.parentElement;
|
||||
if (!swatch) {
|
||||
throw new Error(`Color swatch "${color}" was not rendered.`);
|
||||
}
|
||||
|
||||
return swatch;
|
||||
}
|
||||
|
||||
function WithColorOptionsFilterModelFactory(): IWorkbookData {
|
||||
return {
|
||||
id: UNIT_ID,
|
||||
sheetOrder: [SUB_UNIT_ID],
|
||||
name: '',
|
||||
appVersion: '3.0.0-alpha',
|
||||
locale: LocaleType.ZH_CN,
|
||||
styles: {
|
||||
redFill: {
|
||||
bg: {
|
||||
rgb: '#ff0000',
|
||||
},
|
||||
},
|
||||
blueText: {
|
||||
cl: {
|
||||
rgb: '#0000ff',
|
||||
},
|
||||
},
|
||||
},
|
||||
sheets: {
|
||||
[SUB_UNIT_ID]: {
|
||||
id: SUB_UNIT_ID,
|
||||
name: 'Sheet1',
|
||||
rowCount: 20,
|
||||
columnCount: 5,
|
||||
cellData: {
|
||||
0: {
|
||||
0: { v: 'Status' },
|
||||
},
|
||||
1: {
|
||||
0: { v: 'Blocked', s: 'redFill' },
|
||||
},
|
||||
2: {
|
||||
0: { v: 'Ready', s: 'blueText' },
|
||||
},
|
||||
3: {
|
||||
0: { v: 'Backlog' },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
resources: [
|
||||
{
|
||||
name: 'SHEET_FILTER_PLUGIN',
|
||||
data: JSON.stringify({
|
||||
[SUB_UNIT_ID]: {
|
||||
ref: {
|
||||
startRow: 0,
|
||||
startColumn: 0,
|
||||
endRow: 3,
|
||||
endColumn: 0,
|
||||
},
|
||||
filterColumns: [],
|
||||
cachedFilteredOut: [],
|
||||
},
|
||||
}),
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
describe('FilterPanel', () => {
|
||||
let root: Root | undefined;
|
||||
let container: HTMLDivElement | undefined;
|
||||
let currentTestBed: ReturnType<typeof createFilterPanelViewTestBed> | undefined;
|
||||
|
||||
afterEach(() => {
|
||||
act(() => {
|
||||
root?.unmount();
|
||||
});
|
||||
container?.remove();
|
||||
currentTestBed?.univer.dispose();
|
||||
root = undefined;
|
||||
container = undefined;
|
||||
currentTestBed = undefined;
|
||||
});
|
||||
|
||||
it('clears existing value criteria and closes the panel from the footer action', async () => {
|
||||
currentTestBed = createFilterPanelViewTestBed(WithValuesAndEmptyFilterModelFactory());
|
||||
await openPanel(currentTestBed);
|
||||
container = document.createElement('div');
|
||||
document.body.appendChild(container);
|
||||
root = createRoot(container);
|
||||
|
||||
await renderPanel(root, currentTestBed);
|
||||
|
||||
const filterColumnBefore = currentTestBed.filterService.activeFilterModel?.getFilterColumn(0);
|
||||
expect(filterColumnBefore?.getColumnData().filters).toBeDefined();
|
||||
|
||||
const clearButton = Array.from(container.querySelectorAll('[data-u-comp="button"]'))
|
||||
.find((button) => button.textContent === 'Clear Filter') as HTMLElement;
|
||||
|
||||
await act(async () => {
|
||||
clearButton.dispatchEvent(new MouseEvent('click', { bubbles: true }));
|
||||
await awaitTime(20);
|
||||
});
|
||||
|
||||
expect(currentTestBed.filterService.activeFilterModel?.getFilterColumn(0)).toBeNull();
|
||||
expect(currentTestBed.contextService.getContextValue(FILTER_PANEL_OPENED_KEY)).toBe(false);
|
||||
expect(currentTestBed.panelService.col).toBe(-1);
|
||||
});
|
||||
|
||||
it('switches from values to conditions through the segmented control', async () => {
|
||||
currentTestBed = createFilterPanelViewTestBed(WithValuesFilterModelFactory());
|
||||
await openPanel(currentTestBed);
|
||||
container = document.createElement('div');
|
||||
document.body.appendChild(container);
|
||||
root = createRoot(container);
|
||||
|
||||
await renderPanel(root, currentTestBed);
|
||||
|
||||
expect(currentTestBed.panelService.filterBy).toBe(FilterBy.VALUES);
|
||||
|
||||
const conditionButton = Array.from(container.querySelectorAll('[data-u-comp="segmented"] button'))
|
||||
.find((button) => button.textContent === 'By Conditions') as HTMLElement;
|
||||
|
||||
await act(async () => {
|
||||
conditionButton.dispatchEvent(new MouseEvent('click', { bubbles: true }));
|
||||
await awaitTime(20);
|
||||
});
|
||||
|
||||
expect(currentTestBed.panelService.filterBy).toBe(FilterBy.CONDITIONS);
|
||||
expect(container.querySelector('[data-u-comp="sheets-filter-panel-conditions-container"]')).toBeTruthy();
|
||||
});
|
||||
|
||||
it('applies value criteria after unchecking a value from the tree', async () => {
|
||||
currentTestBed = createFilterPanelViewTestBed(WithValuesFilterModelFactory());
|
||||
await openPanel(currentTestBed);
|
||||
container = document.createElement('div');
|
||||
document.body.appendChild(container);
|
||||
root = createRoot(container);
|
||||
|
||||
await renderPanel(root, currentTestBed);
|
||||
|
||||
const michaelCheckbox = getValueItemCheckbox(container, 'Michael');
|
||||
const confirmButton = getButton(container, 'Confirm');
|
||||
|
||||
await act(async () => {
|
||||
changeCheckbox(michaelCheckbox);
|
||||
await awaitTime(20);
|
||||
});
|
||||
|
||||
await act(async () => {
|
||||
clickElement(confirmButton);
|
||||
await awaitTime(20);
|
||||
});
|
||||
|
||||
const filters = currentTestBed.filterService.activeFilterModel?.getFilterColumn(0)?.getColumnData().filters?.filters;
|
||||
expect(filters).toContain('Jackson');
|
||||
expect(filters).not.toContain('Michael');
|
||||
expect(currentTestBed.contextService.getContextValue(FILTER_PANEL_OPENED_KEY)).toBe(false);
|
||||
});
|
||||
|
||||
it('keeps value criteria unchanged when a changed candidate is cancelled', async () => {
|
||||
currentTestBed = createFilterPanelViewTestBed(WithValuesAndEmptyFilterModelFactory());
|
||||
await openPanel(currentTestBed);
|
||||
container = document.createElement('div');
|
||||
document.body.appendChild(container);
|
||||
root = createRoot(container);
|
||||
|
||||
await renderPanel(root, currentTestBed);
|
||||
|
||||
const michaelCheckbox = getValueItemCheckbox(container, 'Michael');
|
||||
expect(michaelCheckbox.checked).toBe(false);
|
||||
|
||||
await act(async () => {
|
||||
changeCheckbox(michaelCheckbox);
|
||||
await awaitTime(20);
|
||||
});
|
||||
|
||||
expect(getValueItemCheckbox(container, 'Michael').checked).toBe(true);
|
||||
|
||||
await act(async () => {
|
||||
clickElement(getButton(container!, 'Cancel'));
|
||||
await awaitTime(20);
|
||||
});
|
||||
|
||||
expect(currentTestBed.filterService.activeFilterModel?.getFilterColumn(0)?.getColumnData().filters).toEqual({
|
||||
blank: true,
|
||||
filters: ['1'],
|
||||
});
|
||||
expect(currentTestBed.contextService.getContextValue(FILTER_PANEL_OPENED_KEY)).toBe(false);
|
||||
});
|
||||
|
||||
it('applies text contains criteria from the conditions panel', async () => {
|
||||
currentTestBed = createFilterPanelViewTestBed(WithValuesFilterModelFactory());
|
||||
await openPanel(currentTestBed);
|
||||
container = document.createElement('div');
|
||||
document.body.appendChild(container);
|
||||
root = createRoot(container);
|
||||
|
||||
await renderPanel(root, currentTestBed);
|
||||
|
||||
const conditionButton = Array.from(container.querySelectorAll('[data-u-comp="segmented"] button'))
|
||||
.find((button) => button.textContent === 'By Conditions') as HTMLElement;
|
||||
|
||||
await act(async () => {
|
||||
clickElement(conditionButton);
|
||||
await awaitTime(20);
|
||||
});
|
||||
|
||||
await act(async () => {
|
||||
openSelect(getSelect(container!));
|
||||
await awaitTime(20);
|
||||
});
|
||||
|
||||
await act(async () => {
|
||||
clickElement(getDropdownOption('Text Contains'));
|
||||
await awaitTime(20);
|
||||
});
|
||||
|
||||
const input = container.querySelector<HTMLInputElement>('[data-u-comp="sheets-filter-panel-conditions-container-inner"] input');
|
||||
if (!input) {
|
||||
throw new Error('Condition value input was not rendered.');
|
||||
}
|
||||
|
||||
await act(async () => {
|
||||
changeInput(input, 'Michael');
|
||||
await awaitTime(20);
|
||||
});
|
||||
|
||||
await act(async () => {
|
||||
clickElement(getButton(container!, 'Confirm'));
|
||||
await awaitTime(20);
|
||||
});
|
||||
|
||||
const columnData = currentTestBed.filterService.activeFilterModel?.getFilterColumn(0)?.getColumnData();
|
||||
expect(columnData?.customFilters).toEqual({
|
||||
customFilters: [{ val: '*Michael*' }],
|
||||
});
|
||||
expect(columnData?.filters).toBeUndefined();
|
||||
expect(currentTestBed.contextService.getContextValue(FILTER_PANEL_OPENED_KEY)).toBe(false);
|
||||
});
|
||||
|
||||
it('applies the selected cell fill color as color criteria', async () => {
|
||||
currentTestBed = createFilterPanelViewTestBed(WithColorOptionsFilterModelFactory());
|
||||
await openPanel(currentTestBed);
|
||||
container = document.createElement('div');
|
||||
document.body.appendChild(container);
|
||||
root = createRoot(container);
|
||||
|
||||
await renderPanel(root, currentTestBed);
|
||||
|
||||
const colorButton = Array.from(container.querySelectorAll('[data-u-comp="segmented"] button'))
|
||||
.find((button) => button.textContent === 'By Colors') as HTMLElement;
|
||||
|
||||
await act(async () => {
|
||||
clickElement(colorButton);
|
||||
await awaitTime(20);
|
||||
});
|
||||
|
||||
await act(async () => {
|
||||
clickElement(getColorSwatch(container!, 'rgb(255, 0, 0)'));
|
||||
await awaitTime(20);
|
||||
});
|
||||
|
||||
await act(async () => {
|
||||
clickElement(getButton(container!, 'Confirm'));
|
||||
await awaitTime(20);
|
||||
});
|
||||
|
||||
expect(currentTestBed.filterService.activeFilterModel?.getFilterColumn(0)?.getColumnData().colorFilters).toEqual({
|
||||
cellFillColors: ['rgb(255,0,0)'],
|
||||
});
|
||||
expect(currentTestBed.contextService.getContextValue(FILTER_PANEL_OPENED_KEY)).toBe(false);
|
||||
});
|
||||
|
||||
it('turns sync off through the switch and reports the change', async () => {
|
||||
currentTestBed = createFilterPanelViewTestBed(WithValuesFilterModelFactory(), {
|
||||
enableSyncSwitch: { defaultValue: true },
|
||||
});
|
||||
await openPanel(currentTestBed);
|
||||
container = document.createElement('div');
|
||||
document.body.appendChild(container);
|
||||
root = createRoot(container);
|
||||
|
||||
await renderPanel(root, currentTestBed);
|
||||
|
||||
const syncController = currentTestBed.injector.get(SheetsFilterSyncController);
|
||||
const switchInput = Array.from(container.querySelectorAll<HTMLInputElement>('input[type="checkbox"]')).at(-1);
|
||||
if (!switchInput) {
|
||||
throw new Error('Sync switch input was not rendered.');
|
||||
}
|
||||
|
||||
await act(async () => {
|
||||
changeCheckbox(switchInput);
|
||||
await awaitTime(20);
|
||||
});
|
||||
|
||||
expect(syncController.enabled).toBe(false);
|
||||
expect(currentTestBed.messageService.messages).toEqual([{
|
||||
content: '"Filter is visible to everyone" is turned off, only you will see the filter results',
|
||||
type: 'success',
|
||||
duration: 2000,
|
||||
}]);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,115 @@
|
||||
/**
|
||||
* 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 { LocaleKey } from '../../locale/types';
|
||||
import type { ByColorsModel, IFilterByColorItem } from '../../services/sheets-filter-panel.service';
|
||||
import { LocaleService } from '@univerjs/core';
|
||||
import { clsx, resetButtonClassName } from '@univerjs/design';
|
||||
import { AIcon, BanIcon, CheckMarkIcon } from '@univerjs/icons';
|
||||
import { useDependency, useObservable } from '@univerjs/ui';
|
||||
|
||||
function MobileColorOption(props: {
|
||||
item: IFilterByColorItem;
|
||||
label: string;
|
||||
textColor?: boolean;
|
||||
onSelect: () => void;
|
||||
}) {
|
||||
const { item, label, textColor = false, onSelect } = props;
|
||||
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
aria-label={item.color ?? label}
|
||||
aria-pressed={item.checked}
|
||||
className={clsx(resetButtonClassName, `
|
||||
univer-relative univer-flex univer-size-12 univer-items-center univer-justify-center univer-rounded-full
|
||||
univer-border univer-border-solid univer-border-gray-200 univer-bg-gray-0
|
||||
dark:!univer-border-gray-600 dark:!univer-bg-gray-800
|
||||
`)}
|
||||
style={!textColor && item.color ? { backgroundColor: item.color } : undefined}
|
||||
onClick={onSelect}
|
||||
>
|
||||
{!item.color && <BanIcon className="univer-size-7" />}
|
||||
{textColor && item.color && <AIcon className="univer-size-7" style={{ color: item.color }} />}
|
||||
{item.checked && (
|
||||
<span
|
||||
className="
|
||||
univer-absolute -univer-bottom-1 -univer-right-1 univer-flex univer-size-5 univer-items-center
|
||||
univer-justify-center univer-rounded-full univer-bg-primary-600 univer-text-gray-0
|
||||
"
|
||||
>
|
||||
<CheckMarkIcon className="univer-size-3" />
|
||||
</span>
|
||||
)}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
export function MobileFilterByColor(props: { model: ByColorsModel }) {
|
||||
const { model } = props;
|
||||
const localeService = useDependency(LocaleService);
|
||||
const cellFillColors = useObservable(model.cellFillColors$, [], true);
|
||||
const cellTextColors = useObservable(model.cellTextColors$, [], true);
|
||||
const fillLabel = localeService.t<LocaleKey>('sheets-filter-ui.panel.filter-by-cell-fill-color');
|
||||
const textLabel = localeService.t<LocaleKey>('sheets-filter-ui.panel.filter-by-cell-text-color');
|
||||
const hasMultipleColors = cellFillColors.length > 1 || cellTextColors.length > 1;
|
||||
|
||||
return (
|
||||
<div
|
||||
data-u-comp="mobile-sheets-filter-by-colors"
|
||||
className="univer-flex univer-min-h-full univer-flex-col univer-gap-6"
|
||||
>
|
||||
{cellFillColors.length > 1 && (
|
||||
<section>
|
||||
<h3 className="univer-m-0 univer-mb-4 univer-text-base univer-font-medium">{fillLabel}</h3>
|
||||
<div className="univer-flex univer-flex-wrap univer-gap-4">
|
||||
{cellFillColors.map((item) => (
|
||||
<MobileColorOption
|
||||
key={item.color ?? 'none'}
|
||||
item={item}
|
||||
label={fillLabel}
|
||||
onSelect={() => model.onFilterCheckToggled(item)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
)}
|
||||
{cellTextColors.length > 1 && (
|
||||
<section>
|
||||
<h3 className="univer-m-0 univer-mb-4 univer-text-base univer-font-medium">{textLabel}</h3>
|
||||
<div className="univer-flex univer-flex-wrap univer-gap-4">
|
||||
{cellTextColors.map((item) => (
|
||||
<MobileColorOption
|
||||
key={item.color ?? 'none'}
|
||||
item={item}
|
||||
label={textLabel}
|
||||
textColor
|
||||
onSelect={() => model.onFilterCheckToggled(item, false)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
)}
|
||||
{!hasMultipleColors && (
|
||||
<div
|
||||
className="univer-flex univer-flex-1 univer-items-center univer-justify-center univer-text-gray-500"
|
||||
>
|
||||
{localeService.t<LocaleKey>('sheets-filter-ui.panel.filter-by-color-none')}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,137 @@
|
||||
/**
|
||||
* 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 { LocaleKey } from '../../locale/types';
|
||||
import type { FilterOperator } from '../../models/conditions';
|
||||
import type { ByConditionsModel } from '../../services/sheets-filter-panel.service';
|
||||
import { LocaleService } from '@univerjs/core';
|
||||
import { Input, MobileSelect, Radio, RadioGroup } from '@univerjs/design';
|
||||
import { useDependency, useObservable } from '@univerjs/ui';
|
||||
import { FilterConditionItems } from '../../models/conditions';
|
||||
|
||||
export function MobileFilterByCondition(props: { model: ByConditionsModel }) {
|
||||
const { model } = props;
|
||||
const localeService = useDependency(LocaleService);
|
||||
const condition = useObservable(model.conditionItem$, undefined, true);
|
||||
const formParams = useObservable(model.filterConditionFormParams$, undefined, true);
|
||||
const secondaryOptions = FilterConditionItems.ALL_CONDITIONS
|
||||
.filter((item) => item.numOfParameters !== 2)
|
||||
.map((item) => ({ label: localeService.t(item.label), value: item.operator }));
|
||||
|
||||
if (!condition || !formParams) {
|
||||
return null;
|
||||
}
|
||||
|
||||
function renderSecondaryCondition(operator: FilterOperator, value: string, name: 'operator1' | 'operator2') {
|
||||
const needsValue = FilterConditionItems.getItemByOperator(operator).numOfParameters === 1;
|
||||
return (
|
||||
<div className="univer-grid univer-gap-3">
|
||||
{name === 'operator2' && (
|
||||
<RadioGroup
|
||||
value={formParams?.and ? 'AND' : 'OR'}
|
||||
onChange={(join) => model.onConditionFormChange({ and: join === 'AND' })}
|
||||
>
|
||||
<Radio value="AND">{localeService.t<LocaleKey>('sheets-filter-ui.panel.and')}</Radio>
|
||||
<Radio value="OR">{localeService.t<LocaleKey>('sheets-filter-ui.panel.or')}</Radio>
|
||||
</RadioGroup>
|
||||
)}
|
||||
<MobileSelect
|
||||
value={operator}
|
||||
options={secondaryOptions}
|
||||
onChange={(nextOperator) => model.onConditionFormChange({
|
||||
[name]: nextOperator as FilterOperator,
|
||||
})}
|
||||
/>
|
||||
{needsValue && (
|
||||
<Input
|
||||
value={value}
|
||||
placeholder={localeService.t<LocaleKey>('sheets-filter-ui.panel.input-values-placeholder')}
|
||||
onChange={(nextValue) => model.onConditionFormChange({
|
||||
[name === 'operator1' ? 'val1' : 'val2']: nextValue,
|
||||
})}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function renderConditionEditor() {
|
||||
return (
|
||||
<div className="univer-grid univer-gap-3 univer-pb-4 univer-pl-7">
|
||||
{condition!.numOfParameters === 1 && (
|
||||
<Input
|
||||
value={formParams!.val1 ?? ''}
|
||||
placeholder={localeService.t<LocaleKey>('sheets-filter-ui.panel.input-values-placeholder')}
|
||||
onChange={(value) => model.onConditionFormChange({ val1: value })}
|
||||
/>
|
||||
)}
|
||||
{condition!.numOfParameters >= 2 && renderSecondaryCondition(
|
||||
formParams!.operator1!,
|
||||
formParams!.val1 ?? '',
|
||||
'operator1'
|
||||
)}
|
||||
{condition!.numOfParameters >= 2 && renderSecondaryCondition(
|
||||
formParams!.operator2!,
|
||||
formParams!.val2 ?? '',
|
||||
'operator2'
|
||||
)}
|
||||
<p className="univer-m-0 univer-text-xs univer-text-gray-500">
|
||||
{localeService.t<LocaleKey>('sheets-filter-ui.panel.?')}
|
||||
<br />
|
||||
{localeService.t<LocaleKey>('sheets-filter-ui.panel.*')}
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div data-u-comp="mobile-sheets-filter-by-conditions" className="univer-grid univer-gap-4">
|
||||
<div
|
||||
className="
|
||||
univer-overflow-hidden univer-rounded-xl univer-bg-gray-0
|
||||
dark:!univer-bg-gray-800
|
||||
"
|
||||
>
|
||||
{FilterConditionItems.ALL_CONDITIONS.map((item) => {
|
||||
const selected = condition.operator === item.operator;
|
||||
return (
|
||||
<div
|
||||
key={item.operator}
|
||||
data-u-comp="mobile-filter-condition-item"
|
||||
data-selected={selected}
|
||||
className="
|
||||
univer-border-0 univer-border-b univer-border-solid univer-border-gray-200 univer-px-4
|
||||
last:univer-border-b-0
|
||||
dark:!univer-border-gray-700
|
||||
"
|
||||
>
|
||||
<Radio
|
||||
value={item.operator}
|
||||
checked={selected}
|
||||
onChange={(operator) => model.onPrimaryConditionChange(operator as FilterOperator)}
|
||||
>
|
||||
<span className="univer-flex univer-min-h-12 univer-items-center univer-text-base">
|
||||
{localeService.t(item.label)}
|
||||
</span>
|
||||
</Radio>
|
||||
{selected && condition.numOfParameters > 0 && renderConditionEditor()}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -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 type { LocaleKey } from '../../locale/types';
|
||||
import type { ByValuesModel, IFilterByValueWithTreeItem } from '../../services/sheets-filter-panel.service';
|
||||
import { LocaleService } from '@univerjs/core';
|
||||
import { Checkbox, Input, resetButtonClassName, Tree } from '@univerjs/design';
|
||||
import { CloseIcon, SearchIcon } from '@univerjs/icons';
|
||||
import { useDependency, useObservable } from '@univerjs/ui';
|
||||
import { useState } from 'react';
|
||||
import { statisticFilterByValueItems } from '../../models/utils';
|
||||
|
||||
function extractCheckedKeys(items: IFilterByValueWithTreeItem[]): string[] {
|
||||
let checkedKeys: string[] = [];
|
||||
items.forEach((item) => {
|
||||
if (item.checked) {
|
||||
checkedKeys.push(item.key);
|
||||
}
|
||||
if (item.children) {
|
||||
checkedKeys = checkedKeys.concat(extractCheckedKeys(item.children));
|
||||
}
|
||||
});
|
||||
return checkedKeys;
|
||||
}
|
||||
|
||||
export function MobileFilterByValue(props: { model: ByValuesModel }) {
|
||||
const { model } = props;
|
||||
const localeService = useDependency(LocaleService);
|
||||
const [searching, setSearching] = useState(false);
|
||||
const searchText = useObservable(model.searchString$, '', true);
|
||||
const items = useObservable(model.filterItems$, [], true);
|
||||
const stat = statisticFilterByValueItems(items);
|
||||
const allChecked = stat.checked > 0 && stat.unchecked === 0;
|
||||
const indeterminate = stat.checked > 0 && stat.unchecked > 0;
|
||||
const searchLabel = localeService.t<LocaleKey>('sheets-filter-ui.panel.search-placeholder');
|
||||
|
||||
return (
|
||||
<div data-u-comp="mobile-sheets-filter-by-values" className="univer-flex univer-min-h-full univer-flex-col">
|
||||
<div
|
||||
className="
|
||||
univer-flex univer-min-h-14 univer-items-center univer-border-0 univer-border-b univer-border-solid
|
||||
univer-border-gray-200 univer-px-2
|
||||
dark:!univer-border-gray-700
|
||||
"
|
||||
>
|
||||
{searching
|
||||
? (
|
||||
<>
|
||||
<Input
|
||||
autoFocus
|
||||
className="univer-flex-1"
|
||||
value={searchText}
|
||||
placeholder={searchLabel}
|
||||
onChange={(value) => model.setSearchString(value)}
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
aria-label={localeService.t<LocaleKey>('sheets-filter-ui.panel.cancel')}
|
||||
className={`
|
||||
${resetButtonClassName}
|
||||
univer-ml-2 univer-flex univer-size-10 univer-items-center univer-justify-center
|
||||
`}
|
||||
onClick={() => {
|
||||
model.setSearchString('');
|
||||
setSearching(false);
|
||||
}}
|
||||
>
|
||||
<CloseIcon className="univer-size-5" />
|
||||
</button>
|
||||
</>
|
||||
)
|
||||
: (
|
||||
<>
|
||||
<Checkbox
|
||||
className="univer-min-h-12 univer-flex-1 univer-text-base"
|
||||
contentClassName="univer-flex univer-items-center univer-gap-1"
|
||||
indeterminate={indeterminate}
|
||||
disabled={items.length === 0}
|
||||
checked={allChecked}
|
||||
onChange={() => model.onCheckAllToggled(!allChecked)}
|
||||
>
|
||||
<span>{localeService.t<LocaleKey>('sheets-filter-ui.panel.select-all')}</span>
|
||||
<span className="univer-text-sm univer-text-gray-500">
|
||||
{`(${stat.checked}/${stat.checked + stat.unchecked})`}
|
||||
</span>
|
||||
</Checkbox>
|
||||
<button
|
||||
type="button"
|
||||
aria-label={searchLabel}
|
||||
className={`
|
||||
${resetButtonClassName}
|
||||
univer-flex univer-size-12 univer-items-center univer-justify-center
|
||||
`}
|
||||
onClick={() => setSearching(true)}
|
||||
>
|
||||
<SearchIcon className="univer-size-6" />
|
||||
</button>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
<div className="univer-min-h-0 univer-flex-1 univer-py-2">
|
||||
<Tree
|
||||
data={items}
|
||||
defaultExpandAll={false}
|
||||
valueGroup={extractCheckedKeys(items)}
|
||||
onChange={(node) => model.onFilterCheckToggled(node as IFilterByValueWithTreeItem)}
|
||||
defaultCache={model.treeMapCache}
|
||||
itemHeight={48}
|
||||
treeNodeClassName="univer-h-full univer-pr-2 univer-text-base univer-text-gray-900 dark:!univer-text-gray-0"
|
||||
attachRender={(item) => (
|
||||
<span className="univer-ml-auto univer-text-sm univer-text-gray-500">{`(${item.count})`}</span>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,320 @@
|
||||
/**
|
||||
* 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 { MobileDrawerSnap } from '@univerjs/ui';
|
||||
import type { LocaleKey } from '../../locale/types';
|
||||
import type {
|
||||
ByColorsModel,
|
||||
ByConditionsModel,
|
||||
ByValuesModel,
|
||||
} from '../../services/sheets-filter-panel.service';
|
||||
import { ICommandService, IContextService, LocaleService, Tools } from '@univerjs/core';
|
||||
import {
|
||||
Button,
|
||||
MessageType,
|
||||
MobileActionRow,
|
||||
MobileActionRowGroup,
|
||||
resetButtonClassName,
|
||||
Switch,
|
||||
} from '@univerjs/design';
|
||||
import { MoreLeftIcon, MoreRightIcon } from '@univerjs/icons';
|
||||
import { FilterBy, SheetsFilterSyncController } from '@univerjs/sheets-filter';
|
||||
import { IMessageService, MobileDrawer, useDependency, useObservable } from '@univerjs/ui';
|
||||
import { useRef, useState } from 'react';
|
||||
import { map, of } from 'rxjs';
|
||||
import {
|
||||
ChangeFilterByOperation,
|
||||
CloseFilterPanelOperation,
|
||||
FILTER_PANEL_OPENED_KEY,
|
||||
} from '../../commands/operations/sheets-filter.operation';
|
||||
import { ISheetsFilterPanelService } from '../../services/sheets-filter-panel.service';
|
||||
import { MobileFilterByColor } from './MobileFilterByColor';
|
||||
import { MobileFilterByCondition } from './MobileFilterByCondition';
|
||||
import { MobileFilterByValue } from './MobileFilterByValue';
|
||||
|
||||
const FILTER_BY_OPTIONS: Array<{ label: LocaleKey; value: FilterBy }> = [
|
||||
{ label: 'sheets-filter-ui.panel.by-values', value: FilterBy.VALUES },
|
||||
{ label: 'sheets-filter-ui.panel.by-colors', value: FilterBy.COLORS },
|
||||
{ label: 'sheets-filter-ui.panel.by-conditions', value: FilterBy.CONDITIONS },
|
||||
];
|
||||
|
||||
function MobileFilterSyncSwitch() {
|
||||
const localeService = useDependency(LocaleService);
|
||||
const messageService = useDependency(IMessageService);
|
||||
const filterSyncController = useDependency(SheetsFilterSyncController);
|
||||
const visible = useObservable(filterSyncController.visible$, false, true);
|
||||
const enabled = useObservable(filterSyncController.enabled$, true, true);
|
||||
|
||||
if (!visible) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className="
|
||||
univer-flex univer-min-h-20 univer-items-center univer-gap-4 univer-rounded-xl univer-bg-gray-0
|
||||
univer-px-4 univer-py-3
|
||||
dark:!univer-bg-gray-800
|
||||
"
|
||||
>
|
||||
<div className="univer-min-w-0 univer-flex-1">
|
||||
<div
|
||||
className="
|
||||
univer-text-base univer-font-medium univer-text-gray-900
|
||||
dark:!univer-text-gray-100
|
||||
"
|
||||
>
|
||||
{localeService.t<LocaleKey>('sheets-filter-ui.sync.title')}
|
||||
</div>
|
||||
<div
|
||||
className="
|
||||
univer-mt-1 univer-text-sm univer-text-gray-500
|
||||
dark:!univer-text-gray-400
|
||||
"
|
||||
>
|
||||
{localeService.t<LocaleKey>(enabled
|
||||
? 'sheets-filter-ui.sync.statusTips.on'
|
||||
: 'sheets-filter-ui.sync.statusTips.off')}
|
||||
</div>
|
||||
</div>
|
||||
<Switch
|
||||
defaultChecked={enabled}
|
||||
onChange={(checked) => {
|
||||
filterSyncController.setEnabled(checked);
|
||||
messageService.show({
|
||||
content: localeService.t<LocaleKey>(checked
|
||||
? 'sheets-filter-ui.sync.switchTips.on'
|
||||
: 'sheets-filter-ui.sync.switchTips.off'),
|
||||
type: MessageType.Success,
|
||||
duration: 2000,
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function MobileSheetsFilterPanel() {
|
||||
const contextService = useDependency(IContextService);
|
||||
const visible = useObservable(
|
||||
() => contextService.subscribeContextValue$(FILTER_PANEL_OPENED_KEY).pipe(map(Boolean)),
|
||||
Boolean(contextService.getContextValue(FILTER_PANEL_OPENED_KEY)),
|
||||
false,
|
||||
[contextService]
|
||||
);
|
||||
|
||||
return visible ? <MobileSheetsFilterPanelContent /> : null;
|
||||
}
|
||||
|
||||
function MobileSheetsFilterPanelContent() {
|
||||
const commandService = useDependency(ICommandService);
|
||||
const localeService = useDependency(LocaleService);
|
||||
const panelService = useDependency(ISheetsFilterPanelService);
|
||||
const filterBy = useObservable(panelService.filterBy$, FilterBy.VALUES, true);
|
||||
const filterByModel = useObservable(panelService.filterByModel$, null);
|
||||
const canApply = useObservable(() => filterByModel?.canApply$ ?? of(false), false, true, [filterByModel]);
|
||||
const hasCriteria = useObservable(panelService.hasCriteria$, false, true);
|
||||
const col = useObservable(panelService.col$, panelService.col, true);
|
||||
const [detailOpen, setDetailOpen] = useState(false);
|
||||
const [drawerSnap, setDrawerSnap] = useState<MobileDrawerSnap>('compact');
|
||||
const layerRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
const filterRange = panelService.filterModel?.getRange();
|
||||
const title = detailOpen
|
||||
? localeService.t<LocaleKey>(FILTER_BY_OPTIONS.find((option) => option.value === filterBy)!.label)
|
||||
: localeService.t<LocaleKey>('sheets-filter-ui.toolbar.smart-toggle-filter-tooltip');
|
||||
|
||||
async function closePanel() {
|
||||
await commandService.executeCommand(CloseFilterPanelOperation.id);
|
||||
}
|
||||
|
||||
async function openFilterBy(nextFilterBy: FilterBy) {
|
||||
const changed = await commandService.executeCommand(ChangeFilterByOperation.id, { filterBy: nextFilterBy });
|
||||
if (changed) {
|
||||
setDetailOpen(true);
|
||||
setDrawerSnap('expanded');
|
||||
}
|
||||
}
|
||||
|
||||
async function clearFilter() {
|
||||
await filterByModel?.clear();
|
||||
await closePanel();
|
||||
}
|
||||
|
||||
async function applyFilter() {
|
||||
await filterByModel?.apply();
|
||||
await closePanel();
|
||||
}
|
||||
|
||||
function goBack() {
|
||||
if (detailOpen) {
|
||||
setDetailOpen(false);
|
||||
setDrawerSnap('compact');
|
||||
return;
|
||||
}
|
||||
|
||||
closePanel().catch(() => undefined);
|
||||
}
|
||||
|
||||
function changeColumn(offset: number) {
|
||||
const nextCol = col + offset;
|
||||
if (!filterRange || nextCol < filterRange.startColumn || nextCol > filterRange.endColumn) {
|
||||
return;
|
||||
}
|
||||
|
||||
panelService.setupCol(panelService.filterModel!, nextCol);
|
||||
}
|
||||
|
||||
return (
|
||||
<div ref={layerRef} className="univer-pointer-events-none univer-fixed univer-inset-0 univer-z-[1100]">
|
||||
<MobileDrawer
|
||||
layerRef={layerRef}
|
||||
componentName="mobile-sheets-filter-drawer"
|
||||
snap={drawerSnap}
|
||||
expandLabel={localeService.t<LocaleKey>('sheets-filter-ui.panel.mobile.resize')}
|
||||
collapseLabel={localeService.t<LocaleKey>('sheets-filter-ui.panel.mobile.resize')}
|
||||
onSnapChange={setDrawerSnap}
|
||||
onClose={() => closePanel().catch(() => undefined)}
|
||||
role="dialog"
|
||||
ariaLabel={title}
|
||||
panelClassName="univer-pointer-events-auto"
|
||||
contentClassName="univer-min-h-0 univer-px-4 univer-pb-4"
|
||||
header={(
|
||||
<div
|
||||
className="
|
||||
univer-grid univer-h-12 univer-flex-1 univer-grid-cols-[56px_minmax(0,1fr)_56px]
|
||||
univer-items-center univer-px-2
|
||||
"
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
aria-label={localeService.t<LocaleKey>('sheets-filter-ui.panel.mobile.back')}
|
||||
className={`
|
||||
${resetButtonClassName}
|
||||
univer-flex univer-size-12 univer-items-center univer-justify-center
|
||||
`}
|
||||
onClick={goBack}
|
||||
>
|
||||
<MoreLeftIcon className="univer-size-6" />
|
||||
</button>
|
||||
<div className="univer-truncate univer-text-center univer-text-base univer-font-semibold">
|
||||
{title}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
footer={detailOpen
|
||||
? (
|
||||
<div
|
||||
className="
|
||||
univer-shrink-0 univer-border-0 univer-border-t univer-border-solid univer-border-gray-200
|
||||
univer-p-4
|
||||
dark:!univer-border-gray-700
|
||||
"
|
||||
>
|
||||
<MobileActionRowGroup>
|
||||
<Button onClick={() => closePanel().catch(() => undefined)}>
|
||||
{localeService.t<LocaleKey>('sheets-filter-ui.panel.cancel')}
|
||||
</Button>
|
||||
<Button
|
||||
variant="primary"
|
||||
disabled={!canApply || !filterByModel}
|
||||
onClick={() => applyFilter().catch(() => undefined)}
|
||||
>
|
||||
{localeService.t<LocaleKey>('sheets-filter-ui.panel.confirm')}
|
||||
</Button>
|
||||
</MobileActionRowGroup>
|
||||
</div>
|
||||
)
|
||||
: undefined}
|
||||
>
|
||||
{detailOpen
|
||||
? (
|
||||
<div className="univer-flex univer-min-h-full univer-flex-col">
|
||||
<div
|
||||
className="
|
||||
univer-flex univer-min-h-14 univer-items-center univer-justify-between univer-px-2
|
||||
"
|
||||
>
|
||||
<span className="univer-text-base univer-font-medium">
|
||||
{localeService.t<LocaleKey>(
|
||||
'sheets-filter-ui.panel.mobile.column',
|
||||
Tools.chatAtABC(col)
|
||||
)}
|
||||
</span>
|
||||
<div className="univer-flex univer-gap-2">
|
||||
<Button
|
||||
disabled={!filterRange || col <= filterRange.startColumn}
|
||||
onClick={() => changeColumn(-1)}
|
||||
>
|
||||
{localeService.t<LocaleKey>('sheets-filter-ui.panel.mobile.previous-column')}
|
||||
</Button>
|
||||
<Button
|
||||
disabled={!filterRange || col >= filterRange.endColumn}
|
||||
onClick={() => changeColumn(1)}
|
||||
>
|
||||
{localeService.t<LocaleKey>('sheets-filter-ui.panel.mobile.next-column')}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="univer-min-h-0 univer-flex-1">
|
||||
{filterBy === FilterBy.VALUES && filterByModel
|
||||
? <MobileFilterByValue model={filterByModel as ByValuesModel} />
|
||||
: null}
|
||||
{filterBy === FilterBy.COLORS && filterByModel
|
||||
? <MobileFilterByColor model={filterByModel as ByColorsModel} />
|
||||
: null}
|
||||
{filterBy === FilterBy.CONDITIONS && filterByModel
|
||||
? <MobileFilterByCondition model={filterByModel as ByConditionsModel} />
|
||||
: null}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
: (
|
||||
<div className="univer-grid univer-gap-4">
|
||||
<div
|
||||
className="
|
||||
univer-overflow-hidden univer-rounded-xl univer-bg-gray-0
|
||||
dark:!univer-bg-gray-800
|
||||
"
|
||||
>
|
||||
{FILTER_BY_OPTIONS.map((option) => {
|
||||
const label = localeService.t<LocaleKey>(option.label);
|
||||
return (
|
||||
<MobileActionRow
|
||||
key={option.value}
|
||||
title={label}
|
||||
aria-label={label}
|
||||
className="univer-rounded-none"
|
||||
trailing={<MoreRightIcon className="univer-size-5 univer-text-gray-400" />}
|
||||
onClick={() => openFilterBy(option.value).catch(() => undefined)}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
<MobileFilterSyncSwitch />
|
||||
<Button
|
||||
size="large"
|
||||
disabled={!hasCriteria || !filterByModel}
|
||||
onClick={() => clearFilter().catch(() => undefined)}
|
||||
>
|
||||
{localeService.t<LocaleKey>('sheets-filter-ui.panel.clear-filter')}
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</MobileDrawer>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,272 @@
|
||||
/**
|
||||
* 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 { IDisposable, IRange, Workbook } from '@univerjs/core';
|
||||
import type { IRenderContext, IRenderModule, SpreadsheetSkeleton } from '@univerjs/engine-render';
|
||||
import type { ISheetCommandSharedParams } from '@univerjs/sheets';
|
||||
import type { FilterModel } from '@univerjs/sheets-filter';
|
||||
import type { ISheetsFilterButtonShapeProps } from '../widgets/filter-button.shape';
|
||||
import {
|
||||
CommandType,
|
||||
fromCallback,
|
||||
ICommandService,
|
||||
Inject,
|
||||
Injector,
|
||||
InterceptorEffectEnum,
|
||||
RxDisposable,
|
||||
ThemeService,
|
||||
VerticalAlign,
|
||||
} from '@univerjs/core';
|
||||
import {
|
||||
attachSelectionWithCoord,
|
||||
INTERCEPTOR_POINT,
|
||||
SetRangeValuesMutation,
|
||||
SheetInterceptorService,
|
||||
} from '@univerjs/sheets';
|
||||
import { FILTER_MUTATIONS, SheetsFilterService } from '@univerjs/sheets-filter';
|
||||
import {
|
||||
getCoordByCell,
|
||||
ISheetSelectionRenderService,
|
||||
SelectionControl,
|
||||
SheetSkeletonManagerService,
|
||||
} from '@univerjs/sheets-ui';
|
||||
import { filter, map, of, startWith, switchMap, takeUntil, throttleTime } from 'rxjs';
|
||||
import { FILTER_ICON_PADDING, FILTER_ICON_SIZE, SheetsFilterButtonShape } from '../widgets/filter-button.shape';
|
||||
|
||||
const DEFAULT_Z_INDEX = 1000;
|
||||
|
||||
const SHEETS_FILTER_BUTTON_Z_INDEX = 5000;
|
||||
|
||||
interface ISheetsFilterRenderParams {
|
||||
unitId: string;
|
||||
worksheetId: string;
|
||||
filterModel?: FilterModel;
|
||||
range?: IRange;
|
||||
skeleton: SpreadsheetSkeleton;
|
||||
}
|
||||
|
||||
function computeIconTop(
|
||||
startY: number,
|
||||
endY: number,
|
||||
cellHeight: number,
|
||||
verticalAlign?: VerticalAlign
|
||||
) {
|
||||
switch (verticalAlign) {
|
||||
case VerticalAlign.TOP:
|
||||
return startY + FILTER_ICON_PADDING;
|
||||
case VerticalAlign.MIDDLE:
|
||||
return startY + Math.max(0, (cellHeight - FILTER_ICON_SIZE) / 2);
|
||||
case VerticalAlign.BOTTOM:
|
||||
default:
|
||||
return endY - FILTER_ICON_SIZE - FILTER_ICON_PADDING;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Show selected range in filter.
|
||||
*/
|
||||
export class SheetsFilterRenderController extends RxDisposable implements IRenderModule {
|
||||
private _currentRenderParams: ISheetsFilterRenderParams | null = null;
|
||||
private _filterRangeShape: SelectionControl | null = null;
|
||||
private _buttonRenderDisposable: IDisposable | null = null;
|
||||
private _filterButtonShapes: SheetsFilterButtonShape[] = [];
|
||||
|
||||
constructor(
|
||||
private readonly _context: IRenderContext<Workbook>,
|
||||
@Inject(Injector) private readonly _injector: Injector,
|
||||
@Inject(SheetSkeletonManagerService) private readonly _sheetSkeletonManagerService: SheetSkeletonManagerService,
|
||||
@Inject(SheetsFilterService) private readonly _sheetsFilterService: SheetsFilterService,
|
||||
@Inject(ThemeService) private readonly _themeService: ThemeService,
|
||||
@Inject(SheetInterceptorService) private readonly _sheetInterceptorService: SheetInterceptorService,
|
||||
@ICommandService private readonly _commandService: ICommandService,
|
||||
@ISheetSelectionRenderService private readonly _selectionRenderService: ISheetSelectionRenderService
|
||||
) {
|
||||
super();
|
||||
|
||||
this._initRenderer();
|
||||
}
|
||||
|
||||
override dispose(): void {
|
||||
super.dispose();
|
||||
this._disposeRendering();
|
||||
}
|
||||
|
||||
private _initRenderer(): void {
|
||||
this.disposeWithMe(this._themeService.currentTheme$.subscribe(() => {
|
||||
this._refreshRendering(this._currentRenderParams);
|
||||
}));
|
||||
|
||||
// Subscribe to skeleton change and filter model change.
|
||||
this._sheetSkeletonManagerService.currentSkeleton$.pipe(
|
||||
switchMap((skeletonParams) => {
|
||||
if (!skeletonParams) return of(null);
|
||||
|
||||
const { unit: workbook, unitId } = this._context;
|
||||
const worksheetId = workbook.getActiveSheet()?.getSheetId() || '';
|
||||
const filterModel = this._sheetsFilterService.getFilterModel(unitId, worksheetId) ?? undefined;
|
||||
const getParams = (): ISheetsFilterRenderParams => ({
|
||||
unitId,
|
||||
worksheetId,
|
||||
filterModel,
|
||||
range: filterModel?.getRange(),
|
||||
skeleton: skeletonParams.skeleton,
|
||||
});
|
||||
|
||||
return fromCallback(this._commandService.onCommandExecuted.bind(this._commandService)).pipe(
|
||||
filter(([command]) =>
|
||||
command.type === CommandType.MUTATION
|
||||
&& (command.params as Partial<ISheetCommandSharedParams>)?.unitId === workbook.getUnitId()
|
||||
&& (FILTER_MUTATIONS.has(command.id) || command.id === SetRangeValuesMutation.id)
|
||||
),
|
||||
throttleTime(20, undefined, { leading: false, trailing: true }),
|
||||
map(getParams),
|
||||
startWith(getParams()) // must trigger once
|
||||
);
|
||||
}),
|
||||
takeUntil(this.dispose$)
|
||||
).subscribe((renderParams) => {
|
||||
this._currentRenderParams = renderParams;
|
||||
this._refreshRendering(renderParams);
|
||||
});
|
||||
}
|
||||
|
||||
private _refreshRendering(renderParams: ISheetsFilterRenderParams | null): void {
|
||||
this._disposeRendering();
|
||||
if (!renderParams || !renderParams.range) {
|
||||
return;
|
||||
}
|
||||
|
||||
this._renderRange(renderParams.range, renderParams.skeleton);
|
||||
this._renderButtons(renderParams as Required<ISheetsFilterRenderParams>);
|
||||
}
|
||||
|
||||
private _renderRange(range: IRange, skeleton: SpreadsheetSkeleton): void {
|
||||
const { scene } = this._context;
|
||||
const { rowHeaderWidth, columnHeaderHeight } = skeleton;
|
||||
const filterRangeShape = this._filterRangeShape = new SelectionControl(
|
||||
scene,
|
||||
DEFAULT_Z_INDEX,
|
||||
this._themeService,
|
||||
{
|
||||
rowHeaderWidth,
|
||||
columnHeaderHeight,
|
||||
enableAutoFill: false,
|
||||
highlightHeader: false,
|
||||
}
|
||||
);
|
||||
const selectionWithStyle = {
|
||||
range,
|
||||
primary: null,
|
||||
style: { fill: 'rgba(0, 0, 0, 0.0)' },
|
||||
};
|
||||
const selectionWithCoord = attachSelectionWithCoord(selectionWithStyle, skeleton);
|
||||
filterRangeShape.updateRangeBySelectionWithCoord(selectionWithCoord);
|
||||
filterRangeShape.setEvent(false);
|
||||
|
||||
scene.makeDirty(true);
|
||||
}
|
||||
|
||||
private _renderButtons(params: Required<ISheetsFilterRenderParams>): void {
|
||||
const { range, filterModel, unitId, skeleton, worksheetId } = params;
|
||||
const { unit: workbook, scene } = this._context;
|
||||
const worksheet = workbook.getSheetBySheetId(worksheetId);
|
||||
if (!worksheet) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Push cell contents to leave space for the filter buttons.
|
||||
this._interceptCellContent(unitId, worksheetId, params.range);
|
||||
|
||||
// Create filter button shapes.
|
||||
const { startColumn, endColumn, startRow } = range;
|
||||
for (let col = startColumn; col <= endColumn; col++) {
|
||||
const key = `sheets-filter-button-${col}`;
|
||||
const startPosition = getCoordByCell(startRow, col, scene, skeleton);
|
||||
const cellStyle = worksheet.getComposedCellStyle(startRow, col);
|
||||
const verticalAlign = cellStyle?.vt || VerticalAlign.BOTTOM;
|
||||
const { startX, startY, endX, endY } = startPosition;
|
||||
|
||||
// Too little space to draw the button, just ignore it.
|
||||
const cellWidth = endX - startX;
|
||||
const cellHeight = endY - startY;
|
||||
if (cellHeight <= FILTER_ICON_PADDING || cellWidth <= FILTER_ICON_PADDING) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// In other cases we need to draw the button, and we need to take care of the position and clipping.
|
||||
const hasCriteria = !!filterModel.getFilterColumn(col);
|
||||
const iconStartX = endX - FILTER_ICON_SIZE - FILTER_ICON_PADDING;
|
||||
const iconStartY = computeIconTop(startY, endY, cellHeight, verticalAlign);
|
||||
const props: ISheetsFilterButtonShapeProps = {
|
||||
left: iconStartX,
|
||||
top: iconStartY,
|
||||
height: FILTER_ICON_SIZE,
|
||||
width: FILTER_ICON_SIZE,
|
||||
zIndex: SHEETS_FILTER_BUTTON_Z_INDEX,
|
||||
cellHeight,
|
||||
cellWidth,
|
||||
filterParams: { unitId, subUnitId: worksheetId, col, hasCriteria },
|
||||
};
|
||||
|
||||
const buttonShape = this._injector.createInstance(SheetsFilterButtonShape, key, props);
|
||||
this._filterButtonShapes.push(buttonShape);
|
||||
}
|
||||
|
||||
scene.addObjects(this._filterButtonShapes);
|
||||
scene.makeDirty();
|
||||
}
|
||||
|
||||
private _interceptCellContent(workbookId: string, worksheetId: string, range: IRange): void {
|
||||
const { startRow, startColumn, endColumn } = range;
|
||||
this._buttonRenderDisposable = this._sheetInterceptorService.intercept(INTERCEPTOR_POINT.CELL_CONTENT, {
|
||||
effect: InterceptorEffectEnum.Style,
|
||||
handler: (cell, pos, next) => {
|
||||
const { row, col, unitId, subUnitId } = pos;
|
||||
if (
|
||||
unitId !== workbookId ||
|
||||
subUnitId !== worksheetId ||
|
||||
row !== startRow ||
|
||||
col < startColumn ||
|
||||
col > endColumn
|
||||
) {
|
||||
return next(cell);
|
||||
}
|
||||
|
||||
if (!cell || cell === pos.rawData) {
|
||||
cell = { ...pos.rawData };
|
||||
}
|
||||
|
||||
cell.fontRenderExtension = {
|
||||
...cell?.fontRenderExtension,
|
||||
rightOffset: FILTER_ICON_SIZE,
|
||||
};
|
||||
|
||||
return next(cell);
|
||||
},
|
||||
priority: 10,
|
||||
});
|
||||
}
|
||||
|
||||
private _disposeRendering(): void {
|
||||
this._filterRangeShape?.dispose();
|
||||
this._filterButtonShapes.forEach((s) => s.dispose());
|
||||
this._buttonRenderDisposable?.dispose();
|
||||
|
||||
this._filterRangeShape = null;
|
||||
this._buttonRenderDisposable = null;
|
||||
this._filterButtonShapes = [];
|
||||
}
|
||||
}
|
||||
68
packages/sheets-filter-ui/src/views/widgets/drawings.ts
Normal file
68
packages/sheets-filter-ui/src/views/widgets/drawings.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 { UniverRenderingContext2D } from '@univerjs/engine-render';
|
||||
import { Rect } from '@univerjs/engine-render';
|
||||
|
||||
const BUTTON_VIEWPORT = 16;
|
||||
|
||||
// This path is deprecated. We need to get rounded edge of the stroked line.
|
||||
// export const FILTER_BUTTON_HAS_CRITERIA = new Path2D('M3 4H13 M4.5 8H11.5 M6 12H10');
|
||||
export const FILTER_BUTTON_EMPTY = new Path2D('M3.30363 3C2.79117 3 2.51457 3.60097 2.84788 3.99024L6.8 8.60593V12.5662C6.8 12.7184 6.8864 12.8575 7.02289 12.9249L8.76717 13.7863C8.96655 13.8847 9.2 13.7396 9.2 13.5173V8.60593L13.1521 3.99024C13.4854 3.60097 13.2088 3 12.6964 3H3.30363Z');
|
||||
|
||||
export class FilterButton {
|
||||
static drawNoCriteria(ctx: UniverRenderingContext2D, size: number, fgColor: string, bgColor: string): void {
|
||||
ctx.save();
|
||||
|
||||
Rect.drawWith(ctx, {
|
||||
radius: 2,
|
||||
width: BUTTON_VIEWPORT,
|
||||
height: BUTTON_VIEWPORT,
|
||||
fill: bgColor,
|
||||
});
|
||||
|
||||
ctx.lineCap = 'square';
|
||||
ctx.strokeStyle = fgColor;
|
||||
ctx.scale(size / BUTTON_VIEWPORT, size / BUTTON_VIEWPORT);
|
||||
ctx.beginPath();
|
||||
ctx.lineWidth = 1;
|
||||
ctx.lineCap = 'round';
|
||||
ctx.moveTo(3, 4);
|
||||
ctx.lineTo(13, 4);
|
||||
ctx.moveTo(4.5, 8);
|
||||
ctx.lineTo(11.5, 8);
|
||||
ctx.moveTo(6, 12);
|
||||
ctx.lineTo(10, 12);
|
||||
ctx.stroke();
|
||||
ctx.restore();
|
||||
}
|
||||
|
||||
static drawHasCriteria(ctx: UniverRenderingContext2D, size: number, fgColor: string, bgColor: string): void {
|
||||
ctx.save();
|
||||
|
||||
Rect.drawWith(ctx, {
|
||||
radius: 2,
|
||||
width: BUTTON_VIEWPORT,
|
||||
height: BUTTON_VIEWPORT,
|
||||
fill: bgColor,
|
||||
});
|
||||
|
||||
ctx.scale(size / BUTTON_VIEWPORT, size / BUTTON_VIEWPORT);
|
||||
ctx.fillStyle = fgColor;
|
||||
ctx.fill(FILTER_BUTTON_EMPTY);
|
||||
ctx.restore();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,139 @@
|
||||
/**
|
||||
* Copyright 2023-present DreamNum Co., Ltd.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import type { IMouseEvent, IPointerEvent, IShapeProps, UniverRenderingContext2D } from '@univerjs/engine-render';
|
||||
import type { IOpenFilterPanelOperationParams } from '../../commands/operations/sheets-filter.operation';
|
||||
import { ICommandService, IContextService, Inject, ThemeService } from '@univerjs/core';
|
||||
import { Shape } from '@univerjs/engine-render';
|
||||
import { FILTER_PANEL_OPENED_KEY, OpenFilterPanelOperation } from '../../commands/operations/sheets-filter.operation';
|
||||
import { FilterButton } from './drawings';
|
||||
|
||||
export const FILTER_ICON_SIZE = 16;
|
||||
export const FILTER_ICON_PADDING = 1;
|
||||
|
||||
export interface ISheetsFilterButtonShapeProps extends IShapeProps {
|
||||
cellWidth: number;
|
||||
cellHeight: number;
|
||||
filterParams: { col: number; unitId: string; subUnitId: string; hasCriteria: boolean };
|
||||
}
|
||||
|
||||
/**
|
||||
* The widget to render a filter button on canvas.
|
||||
*/
|
||||
export class SheetsFilterButtonShape extends Shape<ISheetsFilterButtonShapeProps> {
|
||||
private _cellWidth: number = 0;
|
||||
private _cellHeight: number = 0;
|
||||
|
||||
private _filterParams?: { col: number; unitId: string; subUnitId: string; hasCriteria: boolean };
|
||||
|
||||
private _hovered = false;
|
||||
|
||||
constructor(
|
||||
key: string,
|
||||
props: ISheetsFilterButtonShapeProps,
|
||||
@IContextService private readonly _contextService: IContextService,
|
||||
@ICommandService private readonly _commandService: ICommandService,
|
||||
@Inject(ThemeService) private readonly _themeService: ThemeService
|
||||
) {
|
||||
super(key, props);
|
||||
|
||||
this.setShapeProps(props);
|
||||
|
||||
// Here we need to make sure that the event is on the rectangle range.
|
||||
this.onPointerDown$.subscribeEvent((evt) => this.onPointerDown(evt));
|
||||
this.onPointerEnter$.subscribeEvent(() => this.onPointerEnter());
|
||||
this.onPointerLeave$.subscribeEvent(() => this.onPointerLeave());
|
||||
}
|
||||
|
||||
setShapeProps(props: Partial<ISheetsFilterButtonShapeProps>): void {
|
||||
if (typeof props.cellHeight !== 'undefined') {
|
||||
this._cellHeight = props.cellHeight;
|
||||
}
|
||||
|
||||
if (typeof props.cellWidth !== 'undefined') {
|
||||
this._cellWidth = props.cellWidth;
|
||||
}
|
||||
|
||||
if (typeof props.filterParams !== 'undefined') {
|
||||
this._filterParams = props.filterParams;
|
||||
}
|
||||
|
||||
this.transformByState({
|
||||
width: props.width!,
|
||||
height: props.height!,
|
||||
});
|
||||
}
|
||||
|
||||
protected override _draw(ctx: UniverRenderingContext2D): void {
|
||||
const cellHeight = this._cellHeight;
|
||||
const cellWidth = this._cellWidth;
|
||||
|
||||
const left = FILTER_ICON_SIZE - cellWidth;
|
||||
const top = FILTER_ICON_SIZE - cellHeight;
|
||||
|
||||
ctx.save();
|
||||
|
||||
const cellRegion = new Path2D();
|
||||
cellRegion.rect(left, top, cellWidth, cellHeight);
|
||||
ctx.clip(cellRegion);
|
||||
|
||||
const { hasCriteria } = this._filterParams!;
|
||||
|
||||
const fgColor = this._themeService.getColorFromTheme('primary.600');
|
||||
const bgColor = this._hovered
|
||||
? this._themeService.getColorFromTheme('gray.50')
|
||||
: 'rgba(255, 255, 255, 1.0)';
|
||||
|
||||
if (hasCriteria) {
|
||||
FilterButton.drawHasCriteria(ctx, FILTER_ICON_SIZE, fgColor, bgColor);
|
||||
} else {
|
||||
FilterButton.drawNoCriteria(ctx, FILTER_ICON_SIZE, fgColor, bgColor);
|
||||
}
|
||||
|
||||
ctx.restore();
|
||||
}
|
||||
|
||||
onPointerDown(evt: IPointerEvent | IMouseEvent): void {
|
||||
// Right click not trigger this event.
|
||||
if (evt.button === 2) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { col, unitId, subUnitId } = this._filterParams!;
|
||||
const opened = this._contextService.getContextValue(FILTER_PANEL_OPENED_KEY);
|
||||
if (opened || !this._commandService.hasCommand(OpenFilterPanelOperation.id)) {
|
||||
return;
|
||||
}
|
||||
|
||||
setTimeout(() => {
|
||||
this._commandService.executeCommand(OpenFilterPanelOperation.id, {
|
||||
unitId,
|
||||
subUnitId,
|
||||
col,
|
||||
} as IOpenFilterPanelOperationParams);
|
||||
}, 200);
|
||||
}
|
||||
|
||||
onPointerEnter(): void {
|
||||
this._hovered = true;
|
||||
this.makeDirty(true);
|
||||
}
|
||||
|
||||
onPointerLeave(): void {
|
||||
this._hovered = false;
|
||||
this.makeDirty(true);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
/**
|
||||
* 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 { DesktopLogService, ILogService, Injector, IUniverInstanceService, LocaleService } from '@univerjs/core';
|
||||
import { beforeEach, describe, expect, it } from 'vitest';
|
||||
import { SheetsGenerateFilterValuesService } from '../generate-filter-values.service';
|
||||
|
||||
describe('SheetsGenerateFilterValuesService', () => {
|
||||
let service: SheetsGenerateFilterValuesService;
|
||||
|
||||
beforeEach(() => {
|
||||
const injector = new Injector();
|
||||
injector.add([LocaleService]);
|
||||
injector.add([ILogService, { useClass: DesktopLogService }]);
|
||||
injector.add([IUniverInstanceService, { useValue: { getUnit: () => null } as unknown as IUniverInstanceService }]);
|
||||
injector.add([SheetsGenerateFilterValuesService]);
|
||||
service = injector.get(SheetsGenerateFilterValuesService);
|
||||
});
|
||||
|
||||
it('returns no filter value tree when the workbook or worksheet is unavailable', async () => {
|
||||
await expect(service.getFilterValues({
|
||||
unitId: 'missing-book',
|
||||
subUnitId: 'sheet-1',
|
||||
filteredOutRowsByOtherColumns: [],
|
||||
filterColumn: null,
|
||||
filters: false,
|
||||
blankChecked: true,
|
||||
iterateRange: { startRow: 0, endRow: 3, startColumn: 0, endColumn: 0 },
|
||||
alreadyChecked: [],
|
||||
})).resolves.toEqual([]);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,362 @@
|
||||
/**
|
||||
* 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, Nullable, Styles, Workbook, Worksheet } from '@univerjs/core';
|
||||
import type { FilterColumn } from '@univerjs/sheets-filter';
|
||||
import type { LocaleKey } from '../locale/types';
|
||||
import type { IFilterByValueItem, IFilterByValueWithTreeItem } from '../services/sheets-filter-panel.service';
|
||||
import {
|
||||
createIdentifier,
|
||||
Disposable,
|
||||
extractPureTextFromCell,
|
||||
ILogService,
|
||||
Inject,
|
||||
isNumeric,
|
||||
IUniverInstanceService,
|
||||
LocaleService,
|
||||
numfmt,
|
||||
} from '@univerjs/core';
|
||||
import { FilterBy } from '@univerjs/sheets-filter';
|
||||
|
||||
export interface ISheetsGenerateFilterValuesService {
|
||||
getFilterValues(params: {
|
||||
unitId: string;
|
||||
subUnitId: string;
|
||||
filteredOutRowsByOtherColumns: number[];
|
||||
filterColumn: Nullable<FilterColumn>;
|
||||
filters: boolean;
|
||||
blankChecked: boolean;
|
||||
iterateRange: IRange;
|
||||
alreadyChecked: string[];
|
||||
}): Promise<{
|
||||
filterTreeItems: IFilterByValueWithTreeItem[];
|
||||
filterTreeMapCache: Map<string, string[]>;
|
||||
}>;
|
||||
}
|
||||
export const SHEETS_GENERATE_FILTER_VALUES_SERVICE_NAME = 'sheets-filter.generate-filter-values.service';
|
||||
export const ISheetsGenerateFilterValuesService = createIdentifier<ISheetsGenerateFilterValuesService>(SHEETS_GENERATE_FILTER_VALUES_SERVICE_NAME);
|
||||
|
||||
export class SheetsGenerateFilterValuesService extends Disposable {
|
||||
constructor(
|
||||
@Inject(LocaleService) private readonly _localeService: LocaleService,
|
||||
@IUniverInstanceService private readonly _univerInstanceService: IUniverInstanceService,
|
||||
@ILogService private readonly _logService: ILogService
|
||||
) {
|
||||
super();
|
||||
}
|
||||
|
||||
async getFilterValues(params: {
|
||||
unitId: string;
|
||||
subUnitId: string;
|
||||
filteredOutRowsByOtherColumns: number[];
|
||||
filterColumn: Nullable<FilterColumn>;
|
||||
filters: boolean;
|
||||
blankChecked: boolean;
|
||||
iterateRange: IRange;
|
||||
alreadyChecked: string[];
|
||||
}) {
|
||||
const { unitId, subUnitId, filteredOutRowsByOtherColumns, filterColumn, filters, blankChecked, iterateRange, alreadyChecked } = params;
|
||||
const workbook = this._univerInstanceService.getUnit<Workbook>(unitId);
|
||||
const worksheet = this._univerInstanceService.getUnit<Workbook>(unitId)?.getSheetBySheetId(subUnitId);
|
||||
|
||||
if (!workbook || !worksheet) return [];
|
||||
|
||||
this._logService.debug('[SheetsGenerateFilterValuesService]', 'getFilterValues for', { unitId, subUnitId });
|
||||
|
||||
return getFilterTreeByValueItems(
|
||||
filters,
|
||||
this._localeService,
|
||||
iterateRange,
|
||||
worksheet,
|
||||
new Set(filteredOutRowsByOtherColumns),
|
||||
filterColumn,
|
||||
new Set(alreadyChecked.map(String)),
|
||||
blankChecked,
|
||||
workbook.getStyles()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export function getFilterByValueItems(
|
||||
filters: boolean,
|
||||
blankChecked: boolean,
|
||||
localeService: LocaleService,
|
||||
iterateRange: IRange,
|
||||
worksheet: Worksheet,
|
||||
alreadyChecked: Set<string>,
|
||||
filteredOutRowsByOtherColumns: Set<number>
|
||||
): IFilterByValueItem[] {
|
||||
const items: IFilterByValueItem[] = [];
|
||||
const itemsByKey: Record<string, IFilterByValueItem> = {};
|
||||
|
||||
let index = 0;
|
||||
let emptyCount = 0;
|
||||
for (const cell of worksheet.iterateByColumn(iterateRange, false, false)) { // iterate and do not skip empty cells
|
||||
const { row, rowSpan = 1 } = cell;
|
||||
|
||||
let rowIndex = 0;
|
||||
while (rowIndex < rowSpan) {
|
||||
const targetRow = row + rowIndex;
|
||||
|
||||
if (filteredOutRowsByOtherColumns.has(targetRow)) {
|
||||
rowIndex++;
|
||||
continue;
|
||||
}
|
||||
|
||||
const value = cell?.value ? extractPureTextFromCell(cell.value) : '';
|
||||
if (!value) {
|
||||
emptyCount += 1;
|
||||
rowIndex += rowSpan;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!itemsByKey[value]) {
|
||||
const item: IFilterByValueItem = {
|
||||
value,
|
||||
checked: alreadyChecked.size ? alreadyChecked.has(value) : !blankChecked,
|
||||
count: 1,
|
||||
index,
|
||||
isEmpty: false,
|
||||
};
|
||||
|
||||
itemsByKey[value] = item;
|
||||
items.push(item);
|
||||
} else {
|
||||
itemsByKey[value].count++;
|
||||
}
|
||||
rowIndex++;
|
||||
}
|
||||
|
||||
index++;
|
||||
}
|
||||
|
||||
const initialBlankChecked = filters ? blankChecked : true;
|
||||
if (emptyCount > 0) {
|
||||
const item: IFilterByValueItem = {
|
||||
value: localeService.t<LocaleKey>('sheets-filter-ui.panel.empty'),
|
||||
checked: initialBlankChecked,
|
||||
count: emptyCount,
|
||||
index,
|
||||
isEmpty: true,
|
||||
};
|
||||
|
||||
items.push(item);
|
||||
}
|
||||
|
||||
return items;
|
||||
}
|
||||
|
||||
// eslint-disable-next-line max-lines-per-function, complexity
|
||||
export function getFilterTreeByValueItems(
|
||||
filters: boolean,
|
||||
localeService: LocaleService,
|
||||
iterateRange: IRange,
|
||||
worksheet: Worksheet,
|
||||
filteredOutRowsByOtherColumns: Set<number>,
|
||||
filterColumn: Nullable<FilterColumn>,
|
||||
alreadyChecked: Set<string>,
|
||||
blankChecked: boolean,
|
||||
styles: Styles
|
||||
): {
|
||||
filterTreeItems: IFilterByValueWithTreeItem[];
|
||||
filterTreeMapCache: Map<string, string[]>;
|
||||
} {
|
||||
const items: Map<string, IFilterByValueWithTreeItem> = new Map();
|
||||
const treeMap: Map<string, string[]> = new Map();
|
||||
|
||||
const DefaultPattern = 'yyyy-mm-dd';
|
||||
const EmptyKey = 'empty';
|
||||
// filter column is the column that is currently being filtered.
|
||||
// If the filter column is filter by colors or conditions, it means that the tab is switched from filter by colors or conditions to filter by values,
|
||||
// so if the filter column has filtered out rows, we need clear all values checked status, refer to excel and wps.
|
||||
const isNeedClearCheckedStatus = !filters
|
||||
&& (filterColumn?.filterBy === FilterBy.COLORS || filterColumn?.filterBy === FilterBy.CONDITIONS)
|
||||
&& filterColumn.filteredOutRows?.size;
|
||||
|
||||
let emptyCount = 0;
|
||||
for (const cell of worksheet.iterateByColumn(iterateRange, false, false)) { // iterate and do not skip empty cells
|
||||
const { row, rowSpan = 1 } = cell;
|
||||
|
||||
let rowIndex = 0;
|
||||
while (rowIndex < rowSpan) {
|
||||
const targetRow = row + rowIndex;
|
||||
|
||||
if (filteredOutRowsByOtherColumns.has(targetRow)) {
|
||||
rowIndex++;
|
||||
continue;
|
||||
}
|
||||
|
||||
const value = cell?.value ? extractPureTextFromCell(cell.value) : '';
|
||||
if (!value) {
|
||||
emptyCount += 1;
|
||||
rowIndex += rowSpan;
|
||||
continue;
|
||||
}
|
||||
|
||||
const fmtStr = (cell.value?.v && !cell.value.p) ? styles.get(cell.value?.s)?.n?.pattern : '';
|
||||
const isDateValue = fmtStr && numfmt.getFormatInfo(fmtStr).isDate;
|
||||
|
||||
// The date formatted value must include date part (year/month/day) to be treated as date grouping tree item.
|
||||
let isIncludeDatePart = false;
|
||||
if (isDateValue) {
|
||||
const { year, month, day } = numfmt.getFormatDateInfo(fmtStr);
|
||||
isIncludeDatePart = year || month || day;
|
||||
}
|
||||
|
||||
if (fmtStr && isDateValue && isIncludeDatePart) {
|
||||
const originValue = worksheet.getCellRaw(cell.row, cell.col)?.v;
|
||||
if (!originValue) {
|
||||
rowIndex++;
|
||||
continue;
|
||||
}
|
||||
const valueParsedByDefaultPattern = numfmt.format(DefaultPattern, Number(originValue));
|
||||
const [year, month, day] = valueParsedByDefaultPattern.split('-').map(Number);
|
||||
let yearItem = items.get(`${year}`);
|
||||
if (!yearItem) {
|
||||
yearItem = {
|
||||
title: `${year}`,
|
||||
key: `${year}`,
|
||||
children: [],
|
||||
count: 0,
|
||||
leaf: false,
|
||||
checked: false,
|
||||
};
|
||||
items.set(`${year}`, yearItem);
|
||||
treeMap.set(`${year}`, [`${year}`]);
|
||||
}
|
||||
let monthItem = yearItem.children?.find((item) => item.key === `${year}-${month}`);
|
||||
if (!monthItem) {
|
||||
monthItem = {
|
||||
title: localeService.t(`sheets-filter-ui.date.${month}`),
|
||||
key: `${year}-${month}`,
|
||||
children: [],
|
||||
count: 0,
|
||||
leaf: false,
|
||||
checked: false,
|
||||
};
|
||||
yearItem.children?.push(monthItem);
|
||||
treeMap.set(`${year}-${month}`, [`${year}`, `${year}-${month}`]);
|
||||
}
|
||||
const dayItem = monthItem?.children?.find((item) => item.key === `${year}-${month}-${day}`);
|
||||
if (!dayItem) {
|
||||
monthItem.children?.push({
|
||||
title: `${day}`,
|
||||
key: `${year}-${month}-${day}`,
|
||||
count: 1,
|
||||
originValues: new Set([value]),
|
||||
leaf: true,
|
||||
checked: isNeedClearCheckedStatus
|
||||
? false
|
||||
: alreadyChecked.size
|
||||
? alreadyChecked.has(value)
|
||||
: !blankChecked,
|
||||
});
|
||||
monthItem.count++;
|
||||
yearItem.count++;
|
||||
treeMap.set(`${year}-${month}-${day}`, [`${year}`, `${year}-${month}`, `${year}-${month}-${day}`]);
|
||||
} else {
|
||||
dayItem.originValues!.add(value);
|
||||
dayItem.count++;
|
||||
monthItem.count++;
|
||||
yearItem.count++;
|
||||
}
|
||||
} else {
|
||||
const key = value;
|
||||
let item = items.get(key);
|
||||
if (!item) {
|
||||
item = {
|
||||
title: value,
|
||||
leaf: true,
|
||||
checked: isNeedClearCheckedStatus
|
||||
? false
|
||||
: alreadyChecked.size
|
||||
? alreadyChecked.has(value)
|
||||
: !blankChecked,
|
||||
key,
|
||||
count: 1,
|
||||
};
|
||||
items.set(key, item);
|
||||
treeMap.set(key, [key]);
|
||||
} else {
|
||||
item.count++;
|
||||
}
|
||||
}
|
||||
rowIndex++;
|
||||
}
|
||||
}
|
||||
|
||||
const initialBlankChecked = isNeedClearCheckedStatus
|
||||
? false
|
||||
: filters
|
||||
? blankChecked
|
||||
: true;
|
||||
if (emptyCount > 0) {
|
||||
const item: IFilterByValueWithTreeItem = {
|
||||
title: localeService.t<LocaleKey>('sheets-filter-ui.panel.empty'),
|
||||
count: emptyCount,
|
||||
leaf: true,
|
||||
checked: initialBlankChecked,
|
||||
key: EmptyKey,
|
||||
};
|
||||
items.set('empty', item);
|
||||
treeMap.set('empty', [EmptyKey]);
|
||||
}
|
||||
|
||||
return {
|
||||
filterTreeItems: generateFilterTreeBySort(Array.from(items.values())),
|
||||
filterTreeMapCache: treeMap,
|
||||
};
|
||||
}
|
||||
|
||||
function generateFilterTreeBySort(tree: IFilterByValueWithTreeItem[]) {
|
||||
return Array.from(tree).sort((a, b) => {
|
||||
if (a.children && !b.children) return -1;
|
||||
if (!a.children && b.children) return 1;
|
||||
return compare(a.title, b.title);
|
||||
}).map((yearItem) => {
|
||||
if (yearItem.children) {
|
||||
yearItem.children.sort((a, b) => {
|
||||
const monthA = Number.parseInt(a.key.split('-')[1], 10);
|
||||
const monthB = Number.parseInt(b.key.split('-')[1], 10);
|
||||
return monthA - monthB;
|
||||
}).forEach((monthItem) => {
|
||||
if (monthItem.children) {
|
||||
monthItem.children.sort((a, b) => {
|
||||
const dayA = Number.parseInt(a.key.split('-')[2], 10);
|
||||
const dayB = Number.parseInt(b.key.split('-')[2], 10);
|
||||
return dayA - dayB;
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
return yearItem;
|
||||
});
|
||||
}
|
||||
|
||||
function compare(strA: string, strB: string) {
|
||||
const aIsNumeric = isNumeric(strA);
|
||||
const bIsNumeric = isNumeric(strB);
|
||||
|
||||
if (aIsNumeric && bIsNumeric) {
|
||||
return Number.parseFloat(strA) - Number.parseFloat(strB);
|
||||
} else if (aIsNumeric && !bIsNumeric) {
|
||||
return -1;
|
||||
} else if (!aIsNumeric && bIsNumeric) {
|
||||
return 1;
|
||||
} else {
|
||||
return strA.localeCompare(strB);
|
||||
}
|
||||
}
|
||||
63
packages/sheets-filter-ui/src/worker/plugin.ts
Normal file
63
packages/sheets-filter-ui/src/worker/plugin.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 { Dependency } from '@univerjs/core';
|
||||
import type { IUniverSheetsFilterUIConfig } from '../config/config';
|
||||
import { IConfigService, Inject, Injector, merge, Plugin, UniverInstanceType } from '@univerjs/core';
|
||||
import { fromModule, IRPCChannelService } from '@univerjs/rpc';
|
||||
import pkg from '../../package.json';
|
||||
import { defaultPluginConfig, SHEETS_FILTER_UI_PLUGIN_CONFIG_KEY } from '../config/config';
|
||||
import {
|
||||
ISheetsGenerateFilterValuesService,
|
||||
SHEETS_GENERATE_FILTER_VALUES_SERVICE_NAME,
|
||||
SheetsGenerateFilterValuesService,
|
||||
} from './generate-filter-values.service';
|
||||
|
||||
export class UniverSheetsFilterUIWorkerPlugin extends Plugin {
|
||||
static override type = UniverInstanceType.UNIVER_SHEET;
|
||||
static override pluginName: string = 'SHEET_FILTER_UI_WORKER_PLUGIN';
|
||||
static override packageName = pkg.name;
|
||||
static override version = pkg.version;
|
||||
|
||||
constructor(
|
||||
private readonly _config: Partial<IUniverSheetsFilterUIConfig> = defaultPluginConfig,
|
||||
@Inject(Injector) protected readonly _injector: Injector,
|
||||
@IConfigService private readonly _configService: IConfigService,
|
||||
@IRPCChannelService private readonly _rpcChannelService: IRPCChannelService
|
||||
) {
|
||||
super();
|
||||
|
||||
const { ...rest } = merge(
|
||||
{},
|
||||
defaultPluginConfig,
|
||||
this._config
|
||||
);
|
||||
this._configService.setConfig(SHEETS_FILTER_UI_PLUGIN_CONFIG_KEY, rest);
|
||||
}
|
||||
|
||||
override onStarting() {
|
||||
([
|
||||
[ISheetsGenerateFilterValuesService, { useClass: SheetsGenerateFilterValuesService }],
|
||||
] as Dependency[]).forEach((d) => this._injector.add(d));
|
||||
}
|
||||
|
||||
override onReady(): void {
|
||||
this._rpcChannelService.registerChannel(
|
||||
SHEETS_GENERATE_FILTER_VALUES_SERVICE_NAME,
|
||||
fromModule(this._injector.get(ISheetsGenerateFilterValuesService))
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user