Skip to content

Commit 8d09762

Browse files
authored
Improvements to the filter panel in JBrowse free-text search (#214)
* Improvements to the filter panel in JBrowse free-text search
1 parent 25a6015 commit 8d09762

File tree

8 files changed

+248
-93
lines changed

8 files changed

+248
-93
lines changed

jbrowse/api-src/org/labkey/api/jbrowse/GroupsProvider.java

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,5 +9,8 @@
99
public interface GroupsProvider
1010
{
1111
@Nullable List<String> getGroupMembers(String trackId, String groupName, Container c, User u);
12+
13+
@Nullable List<String> getGroupNames(Container c, User u);
14+
1215
boolean isAvailable(Container c, User u);
1316
}

jbrowse/api-src/org/labkey/api/jbrowse/JBrowseFieldCustomizer.java

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,13 @@
33
import org.labkey.api.data.Container;
44
import org.labkey.api.security.User;
55

6+
import java.util.Collection;
7+
import java.util.List;
8+
69
public interface JBrowseFieldCustomizer {
710
public void customizeField(JBrowseFieldDescriptor field, Container c, User u);
811

12+
public List<String> getPromotedFilters(Collection<String> indexedFields, Container c, User u);
13+
914
public boolean isAvailable(Container c, User u);
1015
}

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

Lines changed: 56 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,9 @@ import InputLabel from '@material-ui/core/InputLabel';
99
import CardActions from '@material-ui/core/CardActions';
1010
import Card from '@material-ui/core/Card';
1111
import CardContent from '@material-ui/core/CardContent';
12-
import { FieldModel, searchStringToInitialFilters } from '../../utils';
12+
import { FieldModel, Filter, getOperatorsForField, searchStringToInitialFilters } from '../../utils';
13+
import KeyboardArrowDownIcon from '@material-ui/icons/KeyboardArrowDown';
14+
import { Box, Menu } from '@material-ui/core';
1315

1416
const useStyles = makeStyles((theme) => ({
1517
formControl: {
@@ -86,29 +88,31 @@ const useStyles = makeStyles((theme) => ({
8688
}));
8789

8890
export declare type FilterFormProps = {
89-
availableOperators: any,
90-
handleQuery: (filters: string[]) => void,
91-
setFilters: (filters: string[]) => void,
91+
handleQuery: (filters: Filter[]) => void,
92+
setFilters: (filters: Filter[]) => void,
9293
handleClose?: any,
93-
fieldTypeInfo: FieldModel[]
94+
fieldTypeInfo: FieldModel[],
95+
allowedGroupNames?: string[],
96+
promotedFilters?: Map<string, Filter[]>
9497
}
9598

9699
const FilterForm = (props: FilterFormProps ) => {
97-
const { availableOperators, handleQuery, setFilters, handleClose, fieldTypeInfo } = props
98-
// TODO: this should use a typed class for Filter. see utils.ts
99-
const [filters, localSetFilters] = useState(searchStringToInitialFilters(availableOperators));
100+
const { handleQuery, setFilters, handleClose, fieldTypeInfo, allowedGroupNames, promotedFilters } = props
101+
const [filters, localSetFilters] = useState<Filter[]>(searchStringToInitialFilters(fieldTypeInfo.map((x) => x.name)));
100102
const [highlightedInputs, setHighlightedInputs] = useState<{ [index: number]: { field: boolean, operator: boolean, value: boolean } }>({});
103+
const [commonFilterMenuOpen, setCommonFilterMenuOpen] = useState<boolean>(false)
104+
const buttonRef = React.useRef(null);
101105

102106
const classes = useStyles();
103107

104108
const handleAddFilter = () => {
105-
localSetFilters([...filters, { field: "", operator: "", value: "" }]);
109+
localSetFilters([...filters, new Filter()]);
106110
};
107111

108112
const handleRemoveFilter = (index) => {
109113
// If it's the last filter, just reset its values to default empty values
110114
if (filters.length === 1) {
111-
localSetFilters([{ field: "", operator: "", value: "" }]);
115+
localSetFilters([new Filter()]);
112116
} else {
113117
// Otherwise, remove the filter normally
114118
localSetFilters(
@@ -119,9 +123,9 @@ const FilterForm = (props: FilterFormProps ) => {
119123
}};
120124

121125
const handleFilterChange = (index, key, value) => {
122-
const newFilters = filters.map((filter, i) => {
126+
const newFilters = filters.map((filter, i) => {
123127
if (i === index) {
124-
return { ...filter, [key]: value };
128+
return Object.assign(new Filter(), { ...filter, [key]: value });
125129
}
126130
return filter;
127131
});
@@ -159,22 +163,51 @@ const handleSubmit = (event) => {
159163
}
160164
};
161165

166+
const handleMenuClose = () => {
167+
setCommonFilterMenuOpen(false)
168+
}
169+
170+
const handleMenuClick = (filterLabel: string) => {
171+
handleMenuClose()
172+
const f = promotedFilters.get(filterLabel)
173+
let toAdd = [...filters].filter((f) => f.isEmpty())
174+
toAdd = Filter.deduplicate(toAdd.concat(f))
175+
176+
localSetFilters(toAdd)
177+
}
178+
162179
return (
163180
<Card className={classes.card} elevation={0}>
164181
<form>
165182
<CardContent className={classes.centeredContent}>
166183
<div className={classes.addFilterExternalWrapper}>
184+
<Box padding={'5px'}>
167185
<Button
168186
variant="contained"
169187
color="primary"
170188
onClick={handleAddFilter}
171189
>
172190
Add Search Filter
173191
</Button>
192+
<Button
193+
ref={buttonRef}
194+
style={{marginLeft: '5px'}}
195+
variant="contained"
196+
color="primary"
197+
hidden={!!promotedFilters}
198+
onClick={() => setCommonFilterMenuOpen(!commonFilterMenuOpen)}
199+
endIcon={<KeyboardArrowDownIcon />}
200+
>
201+
Common Filters
202+
</Button>
203+
<Menu open={commonFilterMenuOpen} onClose={handleMenuClose} anchorEl={buttonRef.current}>
204+
{Array.from(promotedFilters?.keys()).map((label) => (
205+
<MenuItem key={label} onClick={(e) => handleMenuClick(label)}>{label}</MenuItem>
206+
))}
207+
</Menu>
208+
</Box>
174209
</div>
175210

176-
{/* TODO: this should read the FieldModel and interpret allowableValues, perhaps isMultiValued, etc. */}
177-
{/* TODO: consider also using something like FieldModel.supportsFilter */}
178211
<div className={classes.formScroll}>
179212
{filters.map((filter, index) => (
180213
<div key={index} className={`${classes.filterRow}`}>
@@ -190,9 +223,9 @@ const handleSubmit = (event) => {
190223
<MenuItem value="">
191224
<em>None</em>
192225
</MenuItem>
193-
{Object.keys(availableOperators).map((field) => (
194-
<MenuItem key={field} value={field}>
195-
{fieldTypeInfo.find(obj => obj.name === field).label ?? field}
226+
{fieldTypeInfo.map((field) => (
227+
<MenuItem key={field.name} value={field.name}>
228+
{field.label ?? field.name}
196229
</MenuItem>
197230
))}
198231
</Select>
@@ -211,8 +244,8 @@ const handleSubmit = (event) => {
211244
<em>None</em>
212245
</MenuItem>
213246

214-
{availableOperators[filter.field] && availableOperators[filter.field].type ? (
215-
availableOperators[filter.field].type.map((operator) => (
247+
{getOperatorsForField(fieldTypeInfo.find(obj => obj.name === filter.field)) ? (
248+
getOperatorsForField(fieldTypeInfo.find(obj => obj.name === filter.field)).map((operator) => (
216249
<MenuItem key={operator} value={operator}>
217250
{operator}
218251
</MenuItem>
@@ -234,8 +267,10 @@ const handleSubmit = (event) => {
234267
handleFilterChange(index, "value", event.target.value)
235268
}
236269
>
237-
{/* TODO: remove this. Maybe make a free-text field?*/}
238-
<MenuItem value="ONPRC">ONPRC</MenuItem>
270+
{allowedGroupNames?.map((gn) => (
271+
<MenuItem value="{gn}">{gn}</MenuItem>
272+
))}
273+
239274
</Select>
240275
</FormControl>
241276
) : fieldTypeInfo.find(obj => obj.name === filter.field)?.allowableValues?.length > 0 ? (

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

Lines changed: 14 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -25,8 +25,7 @@ import {
2525
createEncodedFilterString,
2626
fetchFieldTypeInfo,
2727
fetchLuceneQuery,
28-
FieldModel,
29-
fieldTypeInfoToOperators,
28+
FieldModel, Filter,
3029
getBrowserUrlNoFilters,
3130
getGenotypeURL,
3231
searchStringToInitialFilters,
@@ -216,17 +215,15 @@ const VariantTableWidget = observer(props => {
216215
const [filterModalOpen, setFilterModalOpen] = useState(false);
217216
const [filters, setFilters] = useState([]);
218217
const [fieldTypeInfo, setFieldTypeInfo] = useState<FieldModel[]>([]);
219-
const [hiddenColumns, setHiddenColumns] = useState<String[]>([]);
218+
const [allowedGroupNames, setAllowedGroupNames] = useState<string[]>([]);
219+
const [promotedFilters, setPromotedFilters] = useState<Map<string, Filter[]>>(null);
220+
const [hiddenColumns, setHiddenColumns] = useState<string[]>([]);
220221

221222
const [adapter, setAdapter] = useState<EVAdapterClass | undefined>(undefined)
222223

223-
const [availableOperators, setAvailableOperators] = useState([]);
224-
225224
// Active widget ID list to force rerender when a JBrowseUIButton is clicked
226225
const [activeWidgetList, setActiveWidgetList] = useState<string[]>([])
227226

228-
const [isValidLocString, setIsValidLocString] = useState(true)
229-
230227
// False until initial data load or an error:
231228
const [dataLoaded, setDataLoaded] = useState(!parsedLocString)
232229

@@ -235,24 +232,23 @@ const VariantTableWidget = observer(props => {
235232
// API call to retrieve the requested features.
236233
useEffect(() => {
237234
async function fetch() {
238-
const queryParam = new URLSearchParams(window.location.search)
239-
240235
await fetchFieldTypeInfo(sessionId, trackGUID,
241-
(res: FieldModel[]) => {
242-
res.sort((a, b) => a.orderKey - b.orderKey);
236+
(fields: FieldModel[], groups: string[], promotedFilters: Map<string, Filter[]>) => {
237+
fields.sort((a, b) => a.orderKey - b.orderKey);
243238

244-
let columns: GridColDef[] = res.map((x) => {
239+
let columns: GridColDef[] = fields.filter((x) => !x.isHidden).map((x) => {
245240
return {...x.toGridColDef(),
246241
renderCell: (params: any) => { return <TableCellWithPopover value={params.value} /> }
247242
}
248243
})
249244

250245
setColumns(columns)
251246
setHiddenColumns(columns.filter((x) => x.hide).map((x) => x.field))
252-
const operators = fieldTypeInfoToOperators(res)
253-
setAvailableOperators(operators)
254-
setFieldTypeInfo(res)
255-
handleQuery(searchStringToInitialFilters(operators))
247+
setFieldTypeInfo(fields)
248+
setAllowedGroupNames(groups)
249+
setPromotedFilters(promotedFilters)
250+
251+
handleQuery(searchStringToInitialFilters(fields.map((x) => x.name)))
256252
},
257253
(error) => {
258254
setError(error)
@@ -398,8 +394,9 @@ const VariantTableWidget = observer(props => {
398394
handleClose={() => setFilterModalOpen(false)}
399395
filterProps={{
400396
setFilters: setFilters,
401-
availableOperators: availableOperators,
402397
fieldTypeInfo: fieldTypeInfo,
398+
allowedGroupNames: allowedGroupNames,
399+
promotedFilters: promotedFilters,
403400
handleQuery: (filters) => handleQuery(filters)
404401
}}
405402
/>

0 commit comments

Comments
 (0)