Skip to content
This repository was archived by the owner on Apr 18, 2024. It is now read-only.

Commit 40b9311

Browse files
authored
feat: LSDV-3025-11: Add functionality to filter with multiple rules (#1324)
* feat: LSDV-3025-11: Add functionality to filter with multiple rules * add new tests * set the real content to Filter * create new tests * set basic test * change the logic to enum and convert to array * remove unusable ref * remove filter test * add new unit tests
1 parent 235ae39 commit 40b9311

File tree

14 files changed

+453
-41
lines changed

14 files changed

+453
-41
lines changed

src/common/Select/Select.tsx

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ interface SelectProps {
2020
renderMultipleSelected?: (value: string[]) => ReactNode;
2121
tabIndex?: number;
2222
onChange?: (newValue?: string | string[]) => void;
23+
dataTestid?: string;
2324
}
2425

2526
interface SelectComponent<T> extends FC<T> {
@@ -74,6 +75,7 @@ export const Select: SelectComponent<SelectProps> = ({
7475
onChange,
7576
variant,
7677
surface,
78+
dataTestid,
7779
tabIndex = 0,
7880
placeholder = 'Select value',
7981
}) => {
@@ -196,7 +198,7 @@ export const Select: SelectComponent<SelectProps> = ({
196198
if (!visible) setFocused(null);
197199
}}
198200
>
199-
<Elem name="selected">
201+
<Elem name="selected" data-testid={dataTestid}>
200202
<Elem name="value">{selected ?? placeholder}</Elem>
201203
<Elem name="icon" />
202204
</Elem>

src/components/Filter/Filter.tsx

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ export const Filter: FC<FilterInterface> = ({
2828
...filterList,
2929
{
3030
field: availableFilters[0]?.label ?? '',
31+
logic: 'and',
3132
operation: '',
3233
value: '',
3334
path: '',
@@ -36,19 +37,20 @@ export const Filter: FC<FilterInterface> = ({
3637
}, [setFilterList, availableFilters]);
3738

3839
const onChangeRow = useCallback(
39-
(index: number, { field, operation, value, path }: Partial<FilterListInterface>) => {
40+
(index: number, { field, operation, value, path, logic }: Partial<FilterListInterface>) => {
4041
setFilterList((oldList) => {
4142
const newList = [...oldList];
4243

4344
newList[index] = {
4445
...newList[index],
4546
field: field ?? newList[index].field,
4647
operation: operation ?? newList[index].operation,
48+
logic: logic ?? newList[index].logic,
4749
value: value ?? newList[index].value,
4850
path: path ?? newList[index].path,
4951
};
5052

51-
onChange(FilterItems(filterData, newList[index]));
53+
onChange(FilterItems(filterData, newList));
5254

5355
return newList;
5456
});
@@ -66,12 +68,13 @@ export const Filter: FC<FilterInterface> = ({
6668
}, [setFilterList]);
6769

6870
const renderFilterList = useMemo(() => {
69-
return filterList.map(({ field, operation, value }, index) => (
71+
return filterList.map(({ field, operation, logic, value }, index) => (
7072
<Block key={index} name="filter-item">
7173
<FilterRow
7274
index={index}
7375
availableFilters={availableFilters}
7476
field={field}
77+
logic={logic}
7578
operation={operation}
7679
value={value}
7780
onDelete={onDeleteRow}
@@ -101,7 +104,7 @@ export const Filter: FC<FilterInterface> = ({
101104
<Dropdown.Trigger
102105
content={renderFilter}
103106
>
104-
<Button type="text" style={{ padding: 0, whiteSpace: 'nowrap' }}>
107+
<Button data-cy={'filter-button'} type="text" style={{ padding: 0, whiteSpace: 'nowrap' }}>
105108
<Elem name={'icon'}>
106109
<IconFilter />
107110
</Elem>

src/components/Filter/FilterDropdown.tsx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ interface FilterDropdownInterface {
88
placeholder?: string;
99
defaultValue?: string | string[] | undefined;
1010
optionRender?:any;
11+
dataTestid?: string;
1112
style?:any;
1213
}
1314

@@ -32,13 +33,15 @@ export const FilterDropdown: FC<FilterDropdownInterface> = ({
3233
defaultValue,
3334
items,
3435
style,
36+
dataTestid,
3537
value,
3638
onChange }) => {
3739

3840
return (
3941
<Select
4042
placeholder={placeholder}
4143
defaultValue={defaultValue}
44+
dataTestid={dataTestid}
4245
value={value}
4346
style={{
4447
fontSize: 12,

src/components/Filter/FilterInput.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ export const FilterInput: FC<FilterInputInterface> = ({
3434
value={value ?? ''}
3535
ref={inputRef}
3636
placeholder={placeholder}
37+
data-testid={'filter-input'}
3738
onChange={onChangeHandler}
3839
style={style}
3940
{...(schema ?? {})}

src/components/Filter/FilterInterfaces.tsx

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,8 @@
1+
export enum Logic {
2+
and = 'And',
3+
or = 'Or',
4+
}
5+
16
export interface FilterInterface {
27
availableFilters: AvailableFiltersInterface[];
38
onChange: (filter: any) => void;
@@ -9,10 +14,11 @@ export interface FilterListInterface {
914
operation?: string | string[] | undefined;
1015
value?: any;
1116
path?: string;
17+
logic?: Logic;
1218
}
1319

1420
export interface AvailableFiltersInterface {
1521
label: string;
1622
path: string;
1723
type: 'Boolean' | 'Common' | 'Number' | 'String' | string;
18-
}
24+
}

src/components/Filter/FilterRow.styl

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,11 @@
22
display flex
33
flex-direction row
44
align-items center
5-
margin-bottom 18px
5+
margin-bottom 8px
6+
7+
&__title-row
8+
width 60px
9+
text-align right
610

711
&__delete
812
cursor pointer

src/components/Filter/FilterRow.tsx

Lines changed: 22 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import { Block, Elem } from '../../utils/bem';
88
import { FilterDropdown } from './FilterDropdown';
99

1010
import './FilterRow.styl';
11-
import { FilterListInterface } from './FilterInterfaces';
11+
import { FilterListInterface, Logic } from './FilterInterfaces';
1212
import { isDefined } from '../../utils/utilities';
1313
import { IconDelete } from '../../assets/icons';
1414

@@ -19,10 +19,13 @@ interface FilterRowInterface extends FilterListInterface {
1919
onDelete: (index: number) => void;
2020
}
2121

22+
const logicItems = Object.entries(Logic).map(([key, label]) => ({ key, label }));
23+
2224
export const FilterRow: FC<FilterRowInterface> = ({
2325
field,
2426
operation,
2527
value,
28+
logic,
2629
availableFilters,
2730
index,
2831
onChange,
@@ -33,7 +36,7 @@ export const FilterRow: FC<FilterRowInterface> = ({
3336
const [_inputComponent, setInputComponent] = useState(null);
3437

3538
useEffect(() => {
36-
onChange(index, { field:availableFilters[_selectedField].label, path: availableFilters[_selectedField].path, operation:'', value:'' });
39+
onChange(index, { field:availableFilters[_selectedField].label, path: availableFilters[_selectedField].path });
3740
}, [_selectedField]);
3841

3942
useEffect(() => {
@@ -45,24 +48,38 @@ export const FilterRow: FC<FilterRowInterface> = ({
4548
}, [_selectedOperation, _selectedField]);
4649

4750
return (
48-
<Block name={'filter-row'}>
51+
<Block name={'filter-row'} data-testid={'filter-row'}>
4952
<Elem name={'column'}>
50-
Where
53+
{index === 0 ? <Elem name={'title-row'}>Where</Elem>: (
54+
<FilterDropdown
55+
value={logic}
56+
items={logicItems}
57+
dataTestid={'logic-dropdown'}
58+
style={{ width: '60px' }}
59+
onChange={(value: any) => {
60+
onChange(index, { logic:logicItems[value].key });
61+
}}
62+
/>
63+
)}
5164
</Elem>
5265
<Elem name={'column'}>
5366
<FilterDropdown
5467
value={field}
5568
items={availableFilters}
69+
dataTestid={'field-dropdown'}
5670
style={{ width: '140px' }}
5771
onChange={(value: any) => {
5872
setSelectedField(value);
73+
74+
onChange(index, { value:'' });
5975
}}
6076
/>
6177
</Elem>
6278
<Elem name={'column'}>
6379
<FilterDropdown
6480
value={operation}
6581
items={FilterInputs?.[availableFilters[_selectedField].type]}
82+
dataTestid={'operation-dropdown'}
6683
style={{ width: '110px' }}
6784
onChange={(value: any) => {
6885
setSelectedOperation(value);
@@ -85,6 +102,7 @@ export const FilterRow: FC<FilterRowInterface> = ({
85102
onClick={() => {
86103
onDelete(index);
87104
}}
105+
data-testid={`delete-row-${index}`}
88106
name={'delete'}>
89107
<IconDelete />
90108
</Elem>
Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
1+
import React from 'react';
2+
import { fireEvent, render, screen } from '@testing-library/react';
3+
import { Filter } from '../Filter';
4+
5+
describe('Filter', () => {
6+
const mockOnChange = jest.fn();
7+
const filterData = [
8+
{
9+
'labelName': 'AirPlane',
10+
},
11+
{
12+
'labelName': 'Car',
13+
},
14+
{
15+
'labelName': 'AirCar',
16+
},
17+
];
18+
19+
test('Validate if filter is rendering', () => {
20+
const filter = render(<Filter
21+
onChange={mockOnChange}
22+
filterData={filterData}
23+
availableFilters={[{
24+
label: 'Annotation results',
25+
path: 'labelName',
26+
type: 'String',
27+
},
28+
{
29+
label: 'Confidence score',
30+
path: 'score',
31+
type: 'Number',
32+
}]}
33+
/>);
34+
35+
const whereText = filter.getByText('Filter');
36+
37+
expect(whereText).toBeDefined();
38+
});
39+
40+
test('Should delete row when delete button is clicked', () => {
41+
const filter = render(<Filter
42+
onChange={mockOnChange}
43+
filterData={filterData}
44+
availableFilters={[{
45+
label: 'Annotation results',
46+
path: 'labelName',
47+
type: 'String',
48+
},
49+
{
50+
label: 'Confidence score',
51+
path: 'score',
52+
type: 'Number',
53+
}]}
54+
/>);
55+
56+
const FilterButton = filter.getByText('Filter');
57+
58+
fireEvent.click(FilterButton);
59+
60+
const AddButton = filter.getByText('Add Filter');
61+
62+
fireEvent.click(AddButton);
63+
fireEvent.click(AddButton);
64+
65+
const selectBox = filter.getByTestId('logic-dropdown');
66+
67+
expect(selectBox.textContent).toBe('And');
68+
69+
fireEvent.click(selectBox);
70+
fireEvent.click(screen.getByText('Or'));
71+
72+
expect(selectBox.textContent).toBe('Or');
73+
74+
fireEvent.click(screen.getByTestId('delete-row-1'));
75+
76+
expect(filter.getAllByTestId('filter-row')).toHaveLength(1);
77+
});
78+
79+
test('Should filter the content', () => {
80+
let filteredContent: any;
81+
82+
const filter = render(<Filter
83+
onChange={value => {
84+
filteredContent = value;
85+
}}
86+
filterData={filterData}
87+
availableFilters={[{
88+
label: 'Annotation results',
89+
path: 'labelName',
90+
type: 'String',
91+
},
92+
{
93+
label: 'Confidence score',
94+
path: 'score',
95+
type: 'Number',
96+
}]}
97+
/>);
98+
99+
const FilterButton = filter.getByText('Filter');
100+
101+
fireEvent.click(FilterButton);
102+
103+
expect(screen.getByText('No filters applied')).toBeDefined();
104+
105+
const AddButton = filter.getByText('Add Filter');
106+
107+
fireEvent.click(AddButton);
108+
109+
const fieldDropdown = filter.getByTestId('field-dropdown');
110+
const operationDropdown = filter.getByTestId('operation-dropdown');
111+
112+
fireEvent.click(operationDropdown);
113+
fireEvent.click(screen.getByText('not contains'));
114+
115+
const filterInput = filter.getByTestId('filter-input');
116+
117+
expect(filterInput).toBeDefined();
118+
119+
expect(fieldDropdown.textContent).toBe('Annotation results');
120+
expect(operationDropdown.textContent).toBe('not contains');
121+
122+
fireEvent.change(filterInput, { target: { value: 'Plane' } });
123+
124+
125+
expect(filteredContent).toStrictEqual([{ labelName: 'Car' }, { labelName: 'AirCar' }]) ;
126+
});
127+
});

0 commit comments

Comments
 (0)