Skip to content

Commit 9adf47b

Browse files
author
Sebastian Benjamin
committed
Filter refactor
1 parent 3da91fa commit 9adf47b

File tree

4 files changed

+352
-257
lines changed

4 files changed

+352
-257
lines changed

jbrowse/src/client/JBrowse/VariantSearch/components/FilterForm.tsx

Lines changed: 84 additions & 66 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,8 @@ import FormControl from '@mui/material/FormControl';
99
import InputLabel from '@mui/material/InputLabel';
1010
import CardActions from '@mui/material/CardActions';
1111
import Card from '@mui/material/Card';
12-
import { FieldModel, Filter, getOperatorsForField, searchStringToInitialFilters } from '../../utils';
12+
import { FieldModel, Filter, searchStringToInitialFilters } from '../../utils';
13+
import { OperatorKey, OperatorRegistry } from '../operators';
1314
import KeyboardArrowDownIcon from '@mui/icons-material/KeyboardArrowDown';
1415
import { Box, Menu } from '@mui/material';
1516
import { styled } from '@mui/material/styles';
@@ -77,19 +78,22 @@ const SubmitAndExternal = styled('div')(({ theme }) => ({
7778

7879
const FilterForm = (props: FilterFormProps ) => {
7980
const { handleQuery, setFilters, handleClose, fieldTypeInfo, allowedGroupNames, promotedFilters } = props
80-
const [filters, localSetFilters] = useState<Filter[]>(searchStringToInitialFilters(fieldTypeInfo.map((x) => x.name)));
81+
const initial = searchStringToInitialFilters(fieldTypeInfo.map(x => x.name))
82+
const [filters, localSetFilters] = useState<Filter[]>(
83+
initial.length ? initial : [ new Filter('', OperatorKey.None, '') ]
84+
)
8185
const [highlightedInputs, setHighlightedInputs] = useState<{ [index: number]: { field: boolean, operator: boolean, value: boolean } }>({});
8286
const [commonFilterMenuOpen, setCommonFilterMenuOpen] = useState<boolean>(false)
8387
const buttonRef = React.useRef(null);
8488

8589
const handleAddFilter = () => {
86-
localSetFilters([...filters, new Filter()]);
90+
localSetFilters([...filters, new Filter('', OperatorKey.None, '')]);
8791
};
8892

8993
const handleRemoveFilter = (index) => {
9094
// If it's the last filter, just reset its values to default empty values
9195
if (filters.length === 1) {
92-
localSetFilters([new Filter()]);
96+
localSetFilters([new Filter('', OperatorKey.None, '')]);
9397
} else {
9498
// Otherwise, remove the filter normally
9599
localSetFilters(
@@ -103,14 +107,16 @@ const FilterForm = (props: FilterFormProps ) => {
103107
const handleFilterChange = (index, key, value) => {
104108
const newFilters = filters.map((filter, i) => {
105109
if (i === index) {
106-
const updatedFilter = Object.assign(new Filter(), { ...filter, [key]: value });
110+
const updatedFilter = Object.assign(new Filter('', OperatorKey.None, ''), { ...filter, [key]: value });
107111

108112
if (key === "operator") {
109-
if (value === "is empty" || value === "is not empty") {
113+
updatedFilter.operator = OperatorRegistry[value as OperatorKey];
114+
115+
if (value === OperatorKey.IsEmpty || value === OperatorKey.IsNotEmpty) {
110116
updatedFilter.value = '';
111117
}
112118

113-
if (value === "equals one of" || filter.operator === "equals one of") {
119+
if (value === OperatorKey.EqualsOneOf || updatedFilter.operator.key === OperatorKey.EqualsOneOf) {
114120
updatedFilter.value = '';
115121
}
116122
}
@@ -124,40 +130,49 @@ const FilterForm = (props: FilterFormProps ) => {
124130
};
125131

126132
const handleSubmit = (event) => {
127-
event.preventDefault();
128-
const highlightedInputs = {};
129-
130-
filters.forEach((filter, index) => {
131-
highlightedInputs[index] = { field: false, operator: false, value: false };
133+
event.preventDefault()
134+
const highlighted: Record<number, { field: boolean; operator: boolean; value: boolean }> = {}
132135

133-
filter.field = filter.field ?? '';
134-
filter.operator = filter.operator ?? '';
135-
filter.value = filter.value ?? '';
136-
137-
if (filter.field === '') {
138-
highlightedInputs[index].field = true;
139-
}
136+
filters.forEach((filter, i) => {
137+
highlighted[i] = { field: false, operator: false, value: false }
140138

141-
if (filter.operator === '') {
142-
highlightedInputs[index].operator = true;
143-
}
139+
filter.field = filter.field ?? ''
140+
filter.value = filter.value ?? ''
144141

145-
if (filter.operator === 'is empty' || filter.operator === 'is not empty') {
146-
filter.value = '';
147-
} else if (filter.value === '') {
148-
highlightedInputs[index].value = true;
149-
}
150-
});
142+
if (!filter.field) {
143+
highlighted[i].field = true
144+
}
151145

152-
const isSingleEmptyFilter = filters.length === 1 && !filters[0].field && !filters[0].operator && !filters[0].value;
146+
if (!filter.operator.key) {
147+
highlighted[i].operator = true
148+
}
153149

154-
setHighlightedInputs(highlightedInputs);
155-
if (isSingleEmptyFilter || !Object.values(highlightedInputs).some(v => (v as any).field || (v as any).operator || (v as any).value)) {
156-
handleQuery(filters);
157-
setFilters(filters);
158-
handleClose();
150+
if (
151+
filter.operator.key === OperatorKey.IsEmpty ||
152+
filter.operator.key === OperatorKey.IsNotEmpty
153+
) {
154+
filter.value = ''
155+
} else if (!filter.value) {
156+
highlighted[i].value = true
159157
}
160-
};
158+
})
159+
160+
const isSingleEmpty =
161+
filters.length === 1 &&
162+
!filters[0].field &&
163+
!filters[0].operator.key &&
164+
!filters[0].value
165+
166+
setHighlightedInputs(highlighted)
167+
if (
168+
isSingleEmpty ||
169+
!Object.values(highlighted).some(v => v.field || v.operator || v.value)
170+
) {
171+
handleQuery(filters)
172+
setFilters(filters)
173+
handleClose?.()
174+
}
175+
}
161176

162177
const handleMenuClose = () => {
163178
setCommonFilterMenuOpen(false)
@@ -241,34 +256,33 @@ const FilterForm = (props: FilterFormProps ) => {
241256
/>
242257
</FormControlMinWidth>
243258

244-
<FormControlMinWidth sx={ highlightedInputs[index]?.operator ? highlightedSx : null } >
245-
<InputLabel id="operator-label">Operator</InputLabel>
246-
<Select
247-
labelId="operator-label"
248-
label="Operator"
249-
value={filter.operator}
250-
onChange={(event) =>
251-
handleFilterChange(index, "operator", event.target.value)
252-
}
253-
>
254-
<MenuItem value="None" style={{ display: 'none' }}>
255-
<em>None</em>
256-
</MenuItem>
257-
258-
{getOperatorsForField(fieldTypeInfo.find(obj => obj.name === filter.field)) ? (
259-
getOperatorsForField(fieldTypeInfo.find(obj => obj.name === filter.field)).map((operator) => (
260-
<MenuItem key={operator} value={operator}>
261-
{operator}
262-
</MenuItem>
263-
))
264-
) : (
265-
<MenuItem></MenuItem>
266-
)}
267-
268-
</Select>
259+
<FormControlMinWidth sx={highlightedInputs[index]?.operator ? highlightedSx : null}>
260+
<InputLabel id="operator-label">Operator</InputLabel>
261+
<Select
262+
labelId="operator-label"
263+
label="Operator"
264+
value={filter.operator.key}
265+
onChange={event =>
266+
handleFilterChange(index, "operator", event.target.value as OperatorKey)
267+
}
268+
>
269+
<MenuItem value={OperatorKey.None}>
270+
<em>None</em>
271+
</MenuItem>
272+
{(() => {
273+
const ops = fieldTypeInfo.find(f => f.name === filter.field)?.getOperators() ?? [];
274+
return ops.length > 0
275+
? ops.map(op => (
276+
<MenuItem key={op.key} value={op.key}>
277+
{op.label}
278+
</MenuItem>
279+
))
280+
: <MenuItem />;
281+
})()}
282+
</Select>
269283
</FormControlMinWidth>
270284

271-
{filter.operator === "equals one of" ? (
285+
{filter.operator.key === OperatorKey.EqualsOneOf ? (
272286
<FormControlMinWidth sx={ highlightedInputs[index]?.value ? highlightedSx : null } >
273287
<InputLabel id="value-select-label">Value</InputLabel>
274288
<Select
@@ -294,7 +308,7 @@ const FilterForm = (props: FilterFormProps ) => {
294308
noOptionsMessage={() => 'Type to search...'}
295309
menuPortalTarget={document.body}
296310
menuPosition={'fixed'}
297-
isDisabled={filter.operator === "is empty" || filter.operator === "is not empty"}
311+
isDisabled={filter.operator.key === OperatorKey.IsEmpty || filter.operator.key === OperatorKey.IsNotEmpty}
298312
menuShouldBlockScroll={true}
299313
// See here: https://stackoverflow.com/questions/77625507/my-react-project-with-react-18-2-0-version-and-react-select-5-4-0-v
300314
styles={{ menuPortal: (base: any) => ({ ...base, zIndex: 9999 }) }}
@@ -309,8 +323,12 @@ const FilterForm = (props: FilterFormProps ) => {
309323
.map(value => ({label: value, value}))
310324
);
311325
}}
312-
onChange={(selected) => handleFilterChange(index, "value", selected?.length > 0 ? selected.map(s => s.value).join(',') : undefined)}
313-
value={filter.value ? filter.value.split(',').map(value => ({label: value, value})) : undefined}
326+
onChange={(selected) => {
327+
const arr = Array.isArray(selected) ? selected : [selected].filter(Boolean)
328+
const val = arr.map(s => s.value).join(',')
329+
handleFilterChange(index, 'value', val)
330+
}}
331+
value={filter.value ? (filter.value as string).split(',').map(value => ({label: value, value})) : undefined}
314332
/>
315333
</FormControlMinWidth>
316334
) : fieldTypeInfo.find(obj => obj.name === filter.field)?.allowableValues?.length > 0 ? (
@@ -321,7 +339,7 @@ const FilterForm = (props: FilterFormProps ) => {
321339
label="Value"
322340
id={`value-select-${index}`}
323341
value={filter.value}
324-
disabled={filter.operator === "is empty" || filter.operator === "is not empty"}
342+
disabled={filter.operator.key === OperatorKey.IsEmpty || filter.operator.key === OperatorKey.IsNotEmpty}
325343
onChange={(event) =>
326344
handleFilterChange(index, "value", event.target.value)
327345
}
@@ -340,7 +358,7 @@ const FilterForm = (props: FilterFormProps ) => {
340358
sx={ highlightedInputs[index]?.value ? highlightedSx : null }
341359
variant="outlined"
342360
value={filter.value}
343-
disabled={filter.operator === "is empty" || filter.operator === "is not empty"}
361+
disabled={filter.operator.key === OperatorKey.IsEmpty || filter.operator.key === OperatorKey.IsNotEmpty}
344362
onChange={(event) =>
345363
handleFilterChange(index, 'value', event.target.value)
346364
}

jbrowse/src/client/JBrowse/VariantSearch/components/VariantTableWidget.tsx

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ import { NoAssemblyRegion } from '@jbrowse/core/util/types';
2525
import { toArray } from 'rxjs/operators';
2626
import {
2727
createEncodedFilterString,
28+
buildLuceneQuery,
2829
fetchFieldTypeInfo,
2930
fetchLuceneQuery,
3031
FieldModel,
@@ -78,7 +79,7 @@ const VariantTableWidget = observer(props => {
7879
const { page = pageSizeModel.page, pageSize = pageSizeModel.pageSize } = pageQueryModel;
7980
const { field = "genomicPosition", sort = false } = sortQueryModel[0] ?? {};
8081

81-
const encodedSearchString = createEncodedFilterString(passedFilters, false);
82+
const encodedSearchString = createEncodedFilterString(passedFilters);
8283
const currentUrl = new URL(window.location.href);
8384
currentUrl.searchParams.set("searchString", encodedSearchString);
8485
currentUrl.searchParams.set("page", page.toString());
@@ -98,7 +99,7 @@ const VariantTableWidget = observer(props => {
9899
const handleExport = () => {
99100
const currentUrl = new URL(window.location.href);
100101

101-
const searchString = createEncodedFilterString(filters, true);
102+
const searchString = buildLuceneQuery(filters);
102103
const sortField = sortModel[0]?.field ?? 'genomicPosition';
103104
const sortDirection = sortModel[0]?.sort ?? false;
104105

0 commit comments

Comments
 (0)