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

Commit 52ec54d

Browse files
juliosgarbihlomzik
andauthored
fix: LEAP-218: Improve performance of search (#1601)
* fix: LEAP-218: Improve performance of search * add new tests to see how it works between the original ui and our own search * working with external input search * implements new search and fix UI * split taxonomy and taxonomy search * add spacing line in the end of the file * add tests * change the expandedKeys method from its own method to filterDataTree * fix tests * add ff and fix some UX problems * remove useless function * expand just the mached value * fix small bugs and add comments * stop digging if it doesnt have chindrens * remove preset ff * check if is leaf * Update src/components/NewTaxonomy/NewTaxonomy.tsx Co-authored-by: hlomzik <hlomzik@gmail.com> * Update src/components/NewTaxonomy/TaxonomySearch.tsx Co-authored-by: hlomzik <hlomzik@gmail.com> * Update src/components/NewTaxonomy/TaxonomySearch.tsx Co-authored-by: hlomzik <hlomzik@gmail.com> * Update src/components/NewTaxonomy/TaxonomySearch.tsx Co-authored-by: hlomzik <hlomzik@gmail.com> * Update src/components/NewTaxonomy/TaxonomySearch.tsx Co-authored-by: hlomzik <hlomzik@gmail.com> * Update src/components/NewTaxonomy/NewTaxonomy.tsx Co-authored-by: hlomzik <hlomzik@gmail.com> * change changeValue method name to resetValue * Update src/components/NewTaxonomy/TaxonomySearch.tsx Co-authored-by: hlomzik <hlomzik@gmail.com> --------- Co-authored-by: juliosgarbi <juliosgarbi@users.noreply.github.com> Co-authored-by: hlomzik <hlomzik@gmail.com>
1 parent cb560e3 commit 52ec54d

File tree

6 files changed

+223
-3
lines changed

6 files changed

+223
-3
lines changed

src/components/NewTaxonomy/NewTaxonomy.tsx

Lines changed: 49 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
11
import { TreeSelect } from 'antd';
2-
import React, { useCallback, useEffect, useState } from 'react';
2+
import React, { ReactNode, useCallback, useEffect, useRef, useState } from 'react';
33

44
import { Tooltip } from '../../common/Tooltip/Tooltip';
55

66
import './NewTaxonomy.styl';
7+
import { TaxonomySearch, TaxonomySearchRef } from './TaxonomySearch';
78

89
type TaxonomyPath = string[];
910
type onAddLabelCallback = (path: string[]) => any;
@@ -20,7 +21,7 @@ type TaxonomyItem = {
2021
color?: string,
2122
};
2223

23-
type AntTaxonomyItem = {
24+
export type AntTaxonomyItem = {
2425
title: string | JSX.Element,
2526
value: string,
2627
key: string,
@@ -54,6 +55,7 @@ type TaxonomyProps = {
5455
onDeleteLabel?: onDeleteLabelCallback,
5556
options: TaxonomyOptions,
5657
isEditable?: boolean,
58+
defaultSearch?: boolean,
5759
};
5860

5961
type TaxonomyExtendedOptions = TaxonomyOptions & {
@@ -107,14 +109,18 @@ const NewTaxonomy = ({
107109
selected,
108110
onChange,
109111
onLoadData,
112+
defaultSearch = true,
110113
// @todo implement user labels
111114
// onAddLabel,
112115
// onDeleteLabel,
113116
options,
114117
// @todo implement readonly mode
115118
// isEditable = true,
116119
}: TaxonomyProps) => {
120+
const refInput = useRef<TaxonomySearchRef>(null);
117121
const [treeData, setTreeData] = useState<AntTaxonomyItem[]>([]);
122+
const [filteredTreeData, setFilteredTreeData] = useState<AntTaxonomyItem[]>([]);
123+
const [expandedKeys, setExpandedKeys] = useState<React.Key[]>([]);
118124
const separator = options.pathSeparator;
119125
const style = { minWidth: options.minWidth ?? 200, maxWidth: options.maxWidth };
120126
const dropdownWidth = options.dropdownWidth === undefined ? true : +options.dropdownWidth;
@@ -133,14 +139,54 @@ const NewTaxonomy = ({
133139
return onLoadData?.(node.value.split(separator));
134140
}, []);
135141

142+
const handleSearch = useCallback((list: AntTaxonomyItem[], expandedKeys: React.Key[] | null) => {
143+
setFilteredTreeData(list);
144+
if (expandedKeys) setExpandedKeys(expandedKeys);
145+
}, []);
146+
147+
const renderDropdown = useCallback((origin: ReactNode) => {
148+
return (
149+
<>
150+
{!defaultSearch && (
151+
<TaxonomySearch
152+
ref={refInput}
153+
treeData={treeData}
154+
onChange={handleSearch}
155+
/>
156+
)}
157+
{origin}
158+
</>
159+
);
160+
}, [treeData]);
161+
162+
const handleDropdownChange = useCallback((open: boolean) => {
163+
if (open) {
164+
// handleDropdownChange is being called before the dropdown is rendered,
165+
// 100ms is the time that we have to wait to dropdown be rendered
166+
setTimeout(() => {
167+
refInput.current?.focus();
168+
}, 100);
169+
} else {
170+
refInput.current?.resetValue();
171+
}
172+
}, [refInput]);
173+
136174
return (
137175
<TreeSelect
138-
treeData={treeData}
176+
treeData={defaultSearch ? treeData : filteredTreeData}
139177
value={displayed}
140178
labelInValue={true}
141179
onChange={items => onChange(null, items.map(item => item.value.split(separator)))}
142180
loadData={loadData}
143181
treeCheckable
182+
showSearch={defaultSearch}
183+
showArrow={!defaultSearch}
184+
dropdownRender={renderDropdown}
185+
onDropdownVisibleChange={handleDropdownChange}
186+
treeExpandedKeys={!defaultSearch ? expandedKeys : undefined}
187+
onTreeExpand={(expandedKeys: React.Key[]) => {
188+
setExpandedKeys(expandedKeys);
189+
}}
144190
treeCheckStrictly
145191
showCheckedStrategy={TreeSelect.SHOW_ALL}
146192
treeExpandAction="click"
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
.taxonomy-search-input
2+
width calc(100% - 8px)
3+
height 40px
4+
border-radius 4px
5+
border 1px solid rgba(137, 128, 152, 0.16)
6+
background url("data:image/svg+xml;base64, PHN2ZyB3aWR0aD0iMjQiIGhlaWdodD0iMjQiIHZpZXdCb3g9IjAgMCAyNCAyNCIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHBhdGggZD0iTTE1Ljc1NSAxNC4yNTVIMTQuOTY1TDE0LjY4NSAxMy45ODVDMTUuNjY1IDEyLjg0NSAxNi4yNTUgMTEuMzY1IDE2LjI1NSA5Ljc1NUMxNi4yNTUgNi4xNjUgMTMuMzQ1IDMuMjU1IDkuNzU1IDMuMjU1QzYuMTY1IDMuMjU1IDMuMjU1IDYuMTY1IDMuMjU1IDkuNzU1QzMuMjU1IDEzLjM0NSA2LjE2NSAxNi4yNTUgOS43NTUgMTYuMjU1QzExLjM2NSAxNi4yNTUgMTIuODQ1IDE1LjY2NSAxMy45ODUgMTQuNjg1TDE0LjI1NSAxNC45NjVWMTUuNzU1TDE5LjI1NSAyMC43NDVMMjAuNzQ1IDE5LjI1NUwxNS43NTUgMTQuMjU1Wk05Ljc1NSAxNC4yNTVDNy4yNjUwMSAxNC4yNTUgNS4yNTUgMTIuMjQ1IDUuMjU1IDkuNzU1QzUuMjU1IDcuMjY1MDEgNy4yNjUwMSA1LjI1NSA5Ljc1NSA1LjI1NUMxMi4yNDUgNS4yNTUgMTQuMjU1IDcuMjY1MDEgMTQuMjU1IDkuNzU1QzE0LjI1NSAxMi4yNDUgMTIuMjQ1IDE0LjI1NSA5Ljc1NSAxNC4yNTVaIiBmaWxsPSIjMDk2REQ5Ii8+Cjwvc3ZnPgo=") center left 4px no-repeat #FAFAFA;
7+
padding 4px 4px 4px 32px
8+
margin 0 4px 14px 4px
Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,132 @@
1+
import React, { ChangeEvent, KeyboardEvent, useCallback, useEffect, useImperativeHandle, useRef, useState } from 'react';
2+
3+
import './TaxonomySearch.styl';
4+
import { Block } from '../../utils/bem';
5+
import { AntTaxonomyItem } from './NewTaxonomy';
6+
import { debounce } from 'lodash';
7+
8+
type TaxonomySearchProps = {
9+
treeData: AntTaxonomyItem[],
10+
onChange: (list: AntTaxonomyItem[], expandedKeys: React.Key[] | null) => void,
11+
}
12+
13+
export type TaxonomySearchRef = {
14+
resetValue: () => void,
15+
focus: () => void,
16+
}
17+
18+
const TaxonomySearch = React.forwardRef<TaxonomySearchRef, TaxonomySearchProps>(({
19+
treeData,
20+
onChange,
21+
}, ref) => {
22+
useImperativeHandle(ref, (): TaxonomySearchRef => {
23+
return {
24+
resetValue() {
25+
setInputValue('');
26+
onChange(treeData, []);
27+
},
28+
focus() {
29+
return inputRef.current?.focus();
30+
},
31+
};
32+
});
33+
34+
const inputRef = useRef<HTMLInputElement>();
35+
const [inputValue, setInputValue] = useState('');
36+
37+
useEffect(() => {
38+
const _filteredData = filterTreeData(treeData, inputValue);
39+
40+
onChange(_filteredData.filteredDataTree, null);
41+
}, [treeData]);
42+
43+
// When the treeNode has additional formatting because of `hint` or `color` props,
44+
// the `treeNode.title` is not a string but a react component,
45+
// so we have to look for the title in children (1 or 2 levels deep)
46+
const getTitle = useCallback((treeNodeTitle: any): string => {
47+
if (typeof treeNodeTitle === 'string') return treeNodeTitle;
48+
49+
if (typeof treeNodeTitle.props.children === 'object')
50+
return getTitle(treeNodeTitle.props.children);
51+
52+
return treeNodeTitle.props.children;
53+
}, []);
54+
55+
// To filter the treeData items that match with the searchValue
56+
const filterTreeNode = useCallback((searchValue: string, treeNode: AntTaxonomyItem) => {
57+
const lowerSearchValue = String(searchValue).toLowerCase();
58+
const lowerResultValue = getTitle(treeNode.title);
59+
60+
if (!lowerSearchValue) {
61+
return false;
62+
}
63+
64+
return String(lowerResultValue).toLowerCase().includes(lowerSearchValue);
65+
}, []);
66+
67+
// It's running recursively through treeData and its children filtering the content that match with the search value
68+
const filterTreeData = useCallback((treeData: AntTaxonomyItem[], searchValue: string) => {
69+
const _expandedKeys: React.Key[] = [];
70+
71+
if (!searchValue) {
72+
return {
73+
filteredDataTree: treeData,
74+
expandedKeys: _expandedKeys,
75+
};
76+
}
77+
78+
const dig = (list: AntTaxonomyItem[], keepAll = false) => {
79+
return list.reduce<AntTaxonomyItem[]>((total, dataNode) => {
80+
const children = dataNode['children'];
81+
82+
const match = keepAll || filterTreeNode(searchValue, dataNode);
83+
const childList = children?.length ? dig(children, match) : undefined;
84+
85+
if (match || childList?.length) {
86+
if (!keepAll)
87+
_expandedKeys.push(dataNode.key);
88+
89+
total.push({
90+
...dataNode,
91+
isLeaf: !childList?.length,
92+
children: childList,
93+
});
94+
}
95+
96+
return total;
97+
}, []);
98+
};
99+
100+
return {
101+
filteredDataTree: dig(treeData),
102+
expandedKeys: _expandedKeys,
103+
};
104+
}, []);
105+
106+
const handleSearch = useCallback(debounce(async (e: ChangeEvent<HTMLInputElement>) => {
107+
const _filteredData = filterTreeData(treeData, e.target.value);
108+
109+
onChange(_filteredData.filteredDataTree, _filteredData.expandedKeys);
110+
}, 300), [treeData]);
111+
112+
return (
113+
<Block
114+
ref={inputRef}
115+
value={inputValue}
116+
tag={'input'}
117+
onChange={(e: ChangeEvent<HTMLInputElement>) => {
118+
setInputValue(e.target.value);
119+
handleSearch(e);
120+
}}
121+
onKeyDown={(e: KeyboardEvent<HTMLInputElement>) => {
122+
// to prevent selected items from being deleted
123+
if (e.key === 'Backspace' || e.key === 'Delete') e.stopPropagation();
124+
}}
125+
placeholder={'Search'}
126+
data-testid={'taxonomy-search'}
127+
name={'taxonomy-search-input'}
128+
/>
129+
);
130+
});
131+
132+
export { TaxonomySearch };

src/tags/control/Taxonomy/Taxonomy.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import VisibilityMixin from '../../../mixins/Visibility';
2222
import { parseValue } from '../../../utils/data';
2323
import {
2424
FF_DEV_3617,
25+
FF_LEAP_218,
2526
FF_LSDV_4583,
2627
FF_TAXONOMY_ASYNC,
2728
FF_TAXONOMY_LABELING,
@@ -631,6 +632,7 @@ const HtxTaxonomy = observer(({ item }) => {
631632
onAddLabel={item.userLabels && item.onAddLabel}
632633
onDeleteLabel={item.userLabels && item.onDeleteLabel}
633634
options={options}
635+
defaultSearch={!isFF(FF_LEAP_218)}
634636
isEditable={!item.isReadOnly()}
635637
/>
636638
) : (

src/utils/feature-flags.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -295,6 +295,8 @@ export const FF_DBLCLICK_DELAY = 'fflag_fix_front_lsdv_5248_double_click_delay_2
295295
*/
296296
export const FF_TAXONOMY_ASYNC = 'fflag_feat_front_lsdv_5451_async_taxonomy_110823_short';
297297

298+
export const FF_LEAP_218 = 'fflag_fix_front_leap_218_improve_performance_of_taxonomy_search_short';
299+
298300
/**
299301
* Allow to label NER directly with Taxonomy instead of Labels
300302
* @link https://app.launchdarkly.com/default/production/features/fflag_feat_front_lsdv_5452_taxonomy_labeling_110823_short

tests/functional/specs/control_tags/taxonomy.cy.ts

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import {
1414
} from '../../data/control_tags/taxonomy';
1515
import {
1616
FF_DEV_3617,
17+
FF_LEAP_218,
1718
FF_TAXONOMY_ASYNC,
1819
FF_TAXONOMY_LABELING
1920
} from '../../../../src/utils/feature-flags';
@@ -161,4 +162,33 @@ Object.entries(taxonomies).forEach(([title, Taxonomy]) => {
161162
Taxonomy.hasNoSelected('Book 1 / Chapter 2 / Section 2.1');
162163
});
163164
});
165+
166+
describe('Control Tags - Taxonomy - search', () => {
167+
beforeEach(() => {
168+
if (Taxonomy.isNew) {
169+
LabelStudio.addFeatureFlagsOnPageLoad({
170+
[FF_TAXONOMY_ASYNC]: true,
171+
[FF_LEAP_218]: true,
172+
});
173+
}
174+
});
175+
176+
it('should input search and filter treeData', () => {
177+
if (!Taxonomy.isNew) return;
178+
179+
LabelStudio.params()
180+
.config(buildDynamicTaxonomyConfig({ showFullPath: true }))
181+
.data(taxonomyDataWithSimilarAliases)
182+
.withResult([taxonomyResultWithSimilarAliases])
183+
.init();
184+
185+
Taxonomy.open();
186+
cy.get('[data-testid="taxonomy-search"]').type('Section 3.3');
187+
Taxonomy.dropdown.contains('.ant-select-tree-treenode', 'Section 3.3').should('be.visible');
188+
cy.get('[data-testid="taxonomy-search"]').clear();
189+
cy.get('[data-testid="taxonomy-search"]').type('Section 7.0');
190+
Taxonomy.dropdown.contains('No Data').should('be.visible');
191+
Taxonomy.hasSelected('Book 1 / Chapter 2 / Section 2.1');
192+
});
193+
});
164194
});

0 commit comments

Comments
 (0)