Skip to content

Commit 34148c6

Browse files
committed
Adds filter when there is no native search
1 parent ba5c3f8 commit 34148c6

File tree

7 files changed

+275
-24
lines changed

7 files changed

+275
-24
lines changed
Lines changed: 26 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,36 @@
1+
import { useState } from 'react';
2+
3+
import Card from '@material-ui/core/Card';
14
import CardContent from '@material-ui/core/CardContent';
25

3-
import SquareCard from '../../ui/SquareCard';
6+
import FilterTextField from '../../ui/FilterTextField';
47

58
import PropertiesTable from './PropertiesTable';
69

7-
export default function PropertiesCard({ ...props }) {
10+
export default function PropertiesCard({ configurationPropertyListDocument, ...props }) {
11+
const options = configurationPropertyListDocument?.property || [];
12+
const [filter, setFilter] = useState(options);
13+
const filterConfigurationPropertyListDocument = {
14+
...configurationPropertyListDocument,
15+
property: filter,
16+
};
817
return (
9-
<SquareCard>
18+
<Card>
1019
<CardContent>
11-
<PropertiesTable {...props} />
20+
<FilterTextField
21+
variant="outlined"
22+
fullWidth
23+
options={options}
24+
onChange={setFilter}
25+
optionsKey="key"
26+
minScore={0.5}
27+
/>
28+
29+
<PropertiesTable
30+
configurationPropertyListDocument={filterConfigurationPropertyListDocument}
31+
{...props}
32+
/>
1233
</CardContent>
13-
</SquareCard>
34+
</Card>
1435
);
1536
}
Lines changed: 22 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,32 @@
1+
import { useState } from 'react';
2+
3+
import Card from '@material-ui/core/Card';
14
import CardContent from '@material-ui/core/CardContent';
25

3-
import SquareCard from '../ui/SquareCard';
6+
import FilterTextField from '../ui/FilterTextField';
47

58
import FieldGroupListTable from './FieldGroupListTable';
69

7-
export default function FieldGroupListCard({ ...props }) {
10+
export default function FieldGroupListCard({ metadataFieldGroupListDocument, ...props }) {
11+
const options = metadataFieldGroupListDocument?.group || [];
12+
const [filter, setFilter] = useState(options);
13+
const filterMetadataFieldGroupListDocument = { ...metadataFieldGroupListDocument, group: filter };
814
return (
9-
<SquareCard>
15+
<Card>
1016
<CardContent>
11-
<FieldGroupListTable {...props} />
17+
<FilterTextField
18+
variant="outlined"
19+
fullWidth
20+
options={options}
21+
onChange={setFilter}
22+
optionsKey="name"
23+
minScore={0.5}
24+
/>
25+
<FieldGroupListTable
26+
metadataFieldGroupListDocument={filterMetadataFieldGroupListDocument}
27+
{...props}
28+
/>
1229
</CardContent>
13-
</SquareCard>
30+
</Card>
1431
);
1532
}
Lines changed: 23 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,33 @@
1+
import { useState } from 'react';
2+
3+
import Card from '@material-ui/core/Card';
14
import CardContent from '@material-ui/core/CardContent';
25

3-
import SquareCard from '../ui/SquareCard';
6+
import FilterTextField from '../ui/FilterTextField';
47

58
import MetadataDatasetListTable from './MetadataDatasetListTable';
69

7-
export default function MetadataDatasetListCard({ ...props }) {
10+
export default function MetadataDatasetListCard({ metadataDatasetListDocument, ...props }) {
11+
const options = metadataDatasetListDocument?.dataset || [];
12+
const [filter, setFilter] = useState(options);
13+
const filterMetadataDatasetListDocument = { ...metadataDatasetListDocument, dataset: filter };
814
return (
9-
<SquareCard>
15+
<Card>
1016
<CardContent>
11-
<MetadataDatasetListTable {...props} />
17+
<FilterTextField
18+
variant="outlined"
19+
fullWidth
20+
options={options}
21+
onChange={setFilter}
22+
optionsKey="name"
23+
minScore={0.5}
24+
/>
25+
26+
<MetadataDatasetListTable
27+
metadataDatasetListDocument={filterMetadataDatasetListDocument}
28+
{...props}
29+
/>
1230
</CardContent>
13-
</SquareCard>
31+
</Card>
1432
);
1533
}
Lines changed: 22 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,32 @@
1+
import { useState } from 'react';
2+
3+
import Card from '@material-ui/core/Card';
14
import CardContent from '@material-ui/core/CardContent';
25

3-
import SquareCard from '../ui/SquareCard';
6+
import FilterTextField from '../ui/FilterTextField';
47

58
import MetadataFieldListTable from './MetadataFieldListTable';
69

7-
export default function MetadataFieldListCard({ ...props }) {
10+
export default function MetadataFieldListCard({ metadataFieldListDocument, ...props }) {
11+
const options = metadataFieldListDocument?.field || [];
12+
const [filter, setFilter] = useState(options);
13+
const filterMetadataFieldListDocument = { ...metadataFieldListDocument, field: filter };
814
return (
9-
<SquareCard>
15+
<Card>
1016
<CardContent>
11-
<MetadataFieldListTable {...props} />
17+
<FilterTextField
18+
variant="outlined"
19+
fullWidth
20+
options={options}
21+
onChange={setFilter}
22+
optionsKey={['name', 'type']}
23+
minScore={0.5}
24+
/>
25+
<MetadataFieldListTable
26+
metadataFieldListDocument={filterMetadataFieldListDocument}
27+
{...props}
28+
/>
1229
</CardContent>
13-
</SquareCard>
30+
</Card>
1431
);
1532
}
Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,29 @@
1+
import { useState } from 'react';
2+
3+
import Card from '@material-ui/core/Card';
14
import CardContent from '@material-ui/core/CardContent';
25

3-
import SquareCard from '../ui/SquareCard';
6+
import FilterTextField from '../ui/FilterTextField';
47
import UriListTable from '../ui/UriListTable';
58

69
export default function ShapeTagListCard({ uriListDocument }) {
710
const linkTo = (uri) => `/shape-tag/${uri}/`;
11+
const options = uriListDocument?.uri || [];
12+
const [filter, setFilter] = useState(options);
13+
const filterUriListDocument = { ...uriListDocument, uri: filter };
814
return (
9-
<SquareCard>
15+
<Card>
1016
<CardContent>
11-
<UriListTable uriListDocument={uriListDocument} linkTo={linkTo} />
17+
<FilterTextField
18+
variant="outlined"
19+
fullWidth
20+
options={options}
21+
onChange={setFilter}
22+
minScore={0.5}
23+
/>
24+
25+
<UriListTable uriListDocument={filterUriListDocument} linkTo={linkTo} />
1226
</CardContent>
13-
</SquareCard>
27+
</Card>
1428
);
1529
}
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
import { useState } from 'react';
2+
3+
import IconButton from '@material-ui/core/IconButton';
4+
import InputAdornment from '@material-ui/core/InputAdornment';
5+
import TextField from '@material-ui/core/TextField';
6+
import ClearIcon from '@material-ui/icons/Clear';
7+
import SearchIcon from '@material-ui/icons/Search';
8+
import debounce from 'lodash.debounce';
9+
10+
import fuzzySearch from '../../utils/fuzzySearch';
11+
12+
const DEBOUNCE_WAIT = 150;
13+
14+
/**
15+
* FilterTextField is a controlled text input component for filtering a list of options using fuzzy search.
16+
*
17+
* @param {Object[]} options - The array of options to filter.
18+
* @param {string|string[]} optionsKey - The key in each option object to use for searching.
19+
* @param {number} [minScore=0.5] - The minimum fuzzy search score required for an option to be included.
20+
* @param {Function} onChange - Callback invoked with the filtered options whenever the input value changes.
21+
* @param {...Object} props - Additional props passed to the underlying TextField component.
22+
*
23+
* @returns {JSX.Element} A Material-UI TextField with search and clear functionality.
24+
*/
25+
function FilterTextField({
26+
options,
27+
optionsKey,
28+
minScore = 0.5,
29+
onChange: onChangeOptions,
30+
...props
31+
}) {
32+
const [value, setValue] = useState('');
33+
const onFilter = debounce((newValue) => {
34+
if (newValue === '' || newValue === undefined) {
35+
onChangeOptions(options);
36+
return;
37+
}
38+
const filterOptions = fuzzySearch(newValue, options, minScore, optionsKey);
39+
onChangeOptions(filterOptions);
40+
}, DEBOUNCE_WAIT);
41+
const onChange = (event) => {
42+
setValue(event.target.value);
43+
const newValue = event?.target?.value;
44+
onFilter(newValue);
45+
};
46+
const onReset = () => {
47+
setValue('');
48+
onFilter('');
49+
};
50+
51+
return (
52+
<TextField
53+
placeholder="Search"
54+
InputProps={{
55+
startAdornment: (
56+
<InputAdornment position="start">
57+
<SearchIcon />
58+
</InputAdornment>
59+
),
60+
endAdornment: value ? (
61+
<InputAdornment position="end">
62+
<IconButton onClick={onReset} onMouseDown={onReset}>
63+
<ClearIcon />
64+
</IconButton>
65+
</InputAdornment>
66+
) : undefined,
67+
}}
68+
{...props}
69+
onChange={onChange}
70+
value={value}
71+
/>
72+
);
73+
}
74+
75+
export default FilterTextField;

src/utils/fuzzySearch.js

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
/**
2+
*
3+
* @param {string} pattern - Search term
4+
* @param {string} str - Target string
5+
* @returns {number} - Score (higher = better match, 0 = no match)
6+
*/
7+
function fuzzyMatchScore(pattern, str) {
8+
if (!pattern) return 0;
9+
10+
const lowerPattern = pattern.toLowerCase();
11+
const lowerStr = str.toLowerCase();
12+
13+
let score = 0;
14+
let patternIndex = 0;
15+
let consecutiveBonus = 0;
16+
17+
for (let i = 0; i < str.length; i += 1) {
18+
if (patternIndex >= pattern.length) break;
19+
20+
if (lowerStr[i] === lowerPattern[patternIndex]) {
21+
// Base points
22+
score += 10;
23+
24+
// Bonus for consecutive characters
25+
score += consecutiveBonus * 5;
26+
consecutiveBonus += 1;
27+
28+
// Bonus for word boundary (start of string or after space/underscore/dash)
29+
if (i === 0 || [' ', '_', '-'].includes(str[i - 1])) {
30+
score += 15;
31+
}
32+
33+
// Bonus for exact case match
34+
if (str[i] === pattern[patternIndex]) {
35+
score += 2;
36+
}
37+
38+
patternIndex += 1;
39+
} else {
40+
// Reset consecutive bonus if break
41+
consecutiveBonus = 0;
42+
43+
// Small penalty for skipping characters
44+
score -= 1;
45+
}
46+
}
47+
48+
// Pattern must be fully matched
49+
if (patternIndex < pattern.length) return 0;
50+
51+
// Slight penalty for matches later in the string
52+
score -= str.length - patternIndex;
53+
54+
return Math.max(score, 0);
55+
}
56+
57+
/**
58+
* Performs a fuzzy search on a list of items and returns the items that match the pattern above a minimum score.
59+
*
60+
* @param {string} pattern - The search pattern to match against the items.
61+
* @param {Array} items - The array of items to search.
62+
* @param {number} [minScore=0.5] - The minimum score threshold for matches to be included in the results.
63+
* @param {string|string[]} [key] - The property name(s) of each item to match against. If an array is provided, the highest score among the keys is used. If omitted, items themselves are matched.
64+
* @returns {Array} The filtered and sorted array of items that match the pattern above the minimum score.
65+
*/
66+
function fuzzySearch(pattern, items, minScore = 0.5, key = undefined) {
67+
return items
68+
.map((item) => {
69+
if (Array.isArray(key)) {
70+
const highScore = Math.max(
71+
...key.map((thisKey) => fuzzyMatchScore(pattern, item[thisKey])),
72+
);
73+
return {
74+
item,
75+
score: highScore,
76+
};
77+
}
78+
const str = key ? item[key] : item;
79+
return {
80+
item,
81+
score: fuzzyMatchScore(pattern, str),
82+
};
83+
})
84+
.filter((entry) => entry.score > minScore)
85+
.sort((a, b) => b.score - a.score)
86+
.map((entry) => entry.item);
87+
}
88+
89+
export default fuzzySearch;

0 commit comments

Comments
 (0)