From 1fecffcc38aee15d15f93c94cef549749abc25aa Mon Sep 17 00:00:00 2001 From: Cody O'Donnell Date: Tue, 14 May 2024 12:41:41 -0700 Subject: [PATCH 01/14] Add bool filter type and controls --- src/common/api/collectionsApi.ts | 2 +- src/features/collections/CollectionDetail.tsx | 60 ++++++++++++++++++- src/features/collections/Filters.tsx | 32 +++++----- src/features/collections/collectionsSlice.ts | 4 ++ 4 files changed, 82 insertions(+), 16 deletions(-) diff --git a/src/common/api/collectionsApi.ts b/src/common/api/collectionsApi.ts index bc5129f8..bc0c3699 100644 --- a/src/common/api/collectionsApi.ts +++ b/src/common/api/collectionsApi.ts @@ -177,7 +177,7 @@ export type ColumnMeta = { display_name?: string; } & ( | { - type: 'date' | 'int' | 'float'; + type: 'date' | 'int' | 'float' | 'bool'; filter_strategy: undefined; min_value: number; max_value: number; diff --git a/src/features/collections/CollectionDetail.tsx b/src/features/collections/CollectionDetail.tsx index 2dde7f64..4bd46e9b 100644 --- a/src/features/collections/CollectionDetail.tsx +++ b/src/features/collections/CollectionDetail.tsx @@ -41,6 +41,10 @@ import { Accordion, AccordionSummary, AccordionDetails, + FormControl, + Select, + MenuItem, + SelectChangeEvent, } from '@mui/material'; import { useForm } from 'react-hook-form'; import { CollectionOverview } from './CollectionOverview'; @@ -178,7 +182,8 @@ export const CollectionDetail = () => { !filter || filter.type === 'int' || filter.type === 'float' || - filter.type === 'date' + filter.type === 'date' || + filter.type === 'bool' ) return; if (e.target.value === filter.value) return; @@ -596,6 +601,15 @@ const FilterControls = ({ collectionId={collectionId} /> ); + } else if (filter.type === 'bool') { + return ( + + ); } return null; }; @@ -977,3 +991,47 @@ const TextFilterControls = ({ /> ); }; + +const BooleanFilterControls = ({ + column, + filter, + collectionId, + context, +}: FilterControlProps & { + filter: { type: 'bool' }; +}) => { + const dispatch = useAppDispatch(); + const [selectValue, setSelectValue] = useState(() => { + if (filter.value === true || filter.value === 1) { + return 'true'; + } else if (filter.value === false || filter.value === 0) { + return 'false'; + } else { + return; + } + }); + + const handleChange = (event: SelectChangeEvent) => { + setSelectValue(event.target.value as string); + }; + + useEffect(() => { + let value; + if (selectValue === 'true') { + value = 1; + } else if (selectValue === 'false') { + value = 0; + } + dispatch(setFilter([collectionId, context, column, { ...filter, value }])); + }, [selectValue, collectionId, context, column, filter, dispatch]); + + return ( + + + + ); +}; diff --git a/src/features/collections/Filters.tsx b/src/features/collections/Filters.tsx index a22c555f..552ec11c 100644 --- a/src/features/collections/Filters.tsx +++ b/src/features/collections/Filters.tsx @@ -1,3 +1,4 @@ +/* eslint-disable @typescript-eslint/no-unused-vars */ import { Chip, Stack, Tab, Tabs } from '@mui/material'; import { useCallback, useEffect, useRef } from 'react'; import { toast } from 'react-hot-toast'; @@ -240,20 +241,23 @@ export const useContextFilterQueryManagement = ( dispatch( setColumnMeta([collectionId, context, column.col_id, filterMeta]) ); - dispatch( - setFilter([ - collectionId, - context, - column.col_id, - { - type: filterMeta.type, - min_value: filterMeta.min_value, - max_value: filterMeta.max_value, - value: - current?.type === column.type ? current.value : undefined, - }, - ]) - ); + /** + * Commenting out for restbecause this is throwing a typescript error + */ + // dispatch( + // setFilter([ + // collectionId, + // context, + // column.col_id, + // { + // type: filterMeta.type, + // min_value: filterMeta.min_value, + // max_value: filterMeta.max_value, + // value: + // current?.type === column.type ? current.value : undefined, + // }, + // ]) + // ); }); }); }, diff --git a/src/features/collections/collectionsSlice.ts b/src/features/collections/collectionsSlice.ts index 5de28867..23d8f130 100644 --- a/src/features/collections/collectionsSlice.ts +++ b/src/features/collections/collectionsSlice.ts @@ -40,6 +40,10 @@ export type FilterState = value?: FilterRange; min_value: number; max_value: number; + } + | { + type: 'bool'; + value?: boolean | number; }; interface ClnState { From 66d78475204d499eada551ef0a0f1fd5abc30c36 Mon Sep 17 00:00:00 2001 From: David Lyon Date: Tue, 14 May 2024 13:47:22 -0700 Subject: [PATCH 02/14] Fix ts errors caused by hardcoding the heatmap column type --- src/common/api/collectionsApi.ts | 9 ++- src/features/collections/Filters.tsx | 92 ++++++++++++++++++---------- 2 files changed, 69 insertions(+), 32 deletions(-) diff --git a/src/common/api/collectionsApi.ts b/src/common/api/collectionsApi.ts index bc0c3699..b0ade21e 100644 --- a/src/common/api/collectionsApi.ts +++ b/src/common/api/collectionsApi.ts @@ -177,7 +177,7 @@ export type ColumnMeta = { display_name?: string; } & ( | { - type: 'date' | 'int' | 'float' | 'bool'; + type: 'date' | 'int' | 'float'; filter_strategy: undefined; min_value: number; max_value: number; @@ -197,6 +197,13 @@ export type ColumnMeta = { max_value: undefined; enum_values: string[]; } + | { + type: 'bool'; + filter_strategy: undefined; + min_value: undefined; + max_value: undefined; + enum_values: undefined; + } ); interface CollectionsResults { diff --git a/src/features/collections/Filters.tsx b/src/features/collections/Filters.tsx index 552ec11c..0abb88aa 100644 --- a/src/features/collections/Filters.tsx +++ b/src/features/collections/Filters.tsx @@ -227,37 +227,67 @@ export const useContextFilterQueryManagement = ( category.columns.forEach((column) => { const current = filtersRef.current && filtersRef.current[column.col_id]; - const filterMeta: ColumnMeta = { - type: 'int', - key: column.col_id, - max_value: filterData.max_value, - min_value: filterData.min_value, - category: category.category, - description: column.description, - display_name: column.name, - filter_strategy: undefined, - enum_values: undefined, - }; - dispatch( - setColumnMeta([collectionId, context, column.col_id, filterMeta]) - ); - /** - * Commenting out for restbecause this is throwing a typescript error - */ - // dispatch( - // setFilter([ - // collectionId, - // context, - // column.col_id, - // { - // type: filterMeta.type, - // min_value: filterMeta.min_value, - // max_value: filterMeta.max_value, - // value: - // current?.type === column.type ? current.value : undefined, - // }, - // ]) - // ); + if (current?.type === 'bool') { + setColumnMeta([ + collectionId, + context, + column.col_id, + { + type: 'bool', + key: column.col_id, + max_value: undefined, + min_value: undefined, + category: category.category, + description: column.description, + display_name: column.name, + filter_strategy: undefined, + enum_values: undefined, + }, + ]); + dispatch( + setFilter([ + collectionId, + context, + column.col_id, + { + type: 'bool', + value: + current?.type === column.type + ? Boolean(current.value) + : undefined, + }, + ]) + ); + } else { + const filterMeta: ColumnMeta = { + type: 'int', + key: column.col_id, + max_value: filterData.max_value, + min_value: filterData.min_value, + category: category.category, + description: column.description, + display_name: column.name, + filter_strategy: undefined, + enum_values: undefined, + }; + dispatch( + setColumnMeta([collectionId, context, column.col_id, filterMeta]) + ); + dispatch( + setFilter([ + collectionId, + context, + column.col_id, + { + type: filterMeta.type, + min_value: filterMeta.min_value, + max_value: filterMeta.max_value, + value: + current?.type === column.type ? current.value : undefined, + }, + ]) + ); + } }); }); }, From f5d61116c09af16367f7d77b2590310eb719a4f2 Mon Sep 17 00:00:00 2001 From: David Lyon Date: Tue, 14 May 2024 14:08:19 -0700 Subject: [PATCH 03/14] fix column type and add filter to pageConfig for heatmaps --- src/features/collections/CollectionDetail.tsx | 4 ++-- src/features/collections/Filters.tsx | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/features/collections/CollectionDetail.tsx b/src/features/collections/CollectionDetail.tsx index 4bd46e9b..6a25d908 100644 --- a/src/features/collections/CollectionDetail.tsx +++ b/src/features/collections/CollectionDetail.tsx @@ -67,8 +67,8 @@ const pageConfig: Record< } > = { samples: { features: ['filter'] }, - biolog: { features: [] }, - microtrait: { features: [] }, + biolog: { features: ['filter'] }, + microtrait: { features: ['filter'] }, genome_attribs: { features: ['filter', 'match', 'search'], }, diff --git a/src/features/collections/Filters.tsx b/src/features/collections/Filters.tsx index 0abb88aa..931e7135 100644 --- a/src/features/collections/Filters.tsx +++ b/src/features/collections/Filters.tsx @@ -227,7 +227,7 @@ export const useContextFilterQueryManagement = ( category.columns.forEach((column) => { const current = filtersRef.current && filtersRef.current[column.col_id]; - if (current?.type === 'bool') { + if (column.type === 'bool') { setColumnMeta([ collectionId, context, From 70277e7ed0e5f26f153a5f16a1b38c2166fdeaae Mon Sep 17 00:00:00 2001 From: David Lyon Date: Tue, 14 May 2024 15:04:52 -0700 Subject: [PATCH 04/14] fix requestId race condition and actually dispatch bool columnMeta --- src/features/collections/Filters.tsx | 55 +++++++++++++--------------- 1 file changed, 26 insertions(+), 29 deletions(-) diff --git a/src/features/collections/Filters.tsx b/src/features/collections/Filters.tsx index 931e7135..9987e0aa 100644 --- a/src/features/collections/Filters.tsx +++ b/src/features/collections/Filters.tsx @@ -148,25 +148,22 @@ export const useContextFilterQueryManagement = ( ( result: T, collectionId: string | undefined - ): { filterData?: T['data']; context?: FilterContext } => { + ): { filterData?: T['data'] } => { if (result.isError) { const err = parseError(result.error); if (err.name !== 'AbortError') toast('Filter Loading failed: ' + parseError(result.error).message); } - const context = result.requestId - ? requestContext.current[result.requestId] - : undefined; - if (!collectionId || !result?.data || !context || result.isError) - return {}; - return { filterData: result.data, context }; + + if (!collectionId || !result?.data || result.isError) return {}; + return { filterData: result.data }; }, [] ); const handleTableFilters = useCallback( (result: T) => { - const { filterData, context } = handleResult(result, collectionId); + const { filterData } = handleResult(result, collectionId); if (!filterData || !collectionId || !context) return; dispatch(clearFiltersAndColumnMeta([collectionId, context])); filterData.columns.forEach((column) => { @@ -215,12 +212,12 @@ export const useContextFilterQueryManagement = ( } }); }, - [collectionId, dispatch, handleResult] + [collectionId, dispatch, handleResult, context] ); const handleHeatmapFilters = useCallback( (result: T) => { - const { filterData, context } = handleResult(result, collectionId); + const { filterData } = handleResult(result, collectionId); if (!filterData || !collectionId || !context) return; dispatch(clearFiltersAndColumnMeta([collectionId, context])); filterData.categories.forEach((category) => { @@ -228,22 +225,24 @@ export const useContextFilterQueryManagement = ( const current = filtersRef.current && filtersRef.current[column.col_id]; if (column.type === 'bool') { - setColumnMeta([ - collectionId, - context, - column.col_id, - { - type: 'bool', - key: column.col_id, - max_value: undefined, - min_value: undefined, - category: category.category, - description: column.description, - display_name: column.name, - filter_strategy: undefined, - enum_values: undefined, - }, - ]); + dispatch( + setColumnMeta([ + collectionId, + context, + column.col_id, + { + type: 'bool', + key: column.col_id, + max_value: undefined, + min_value: undefined, + category: category.category, + description: column.description, + display_name: column.name, + filter_strategy: undefined, + enum_values: undefined, + }, + ]) + ); dispatch( setFilter([ collectionId, @@ -291,7 +290,7 @@ export const useContextFilterQueryManagement = ( }); }); }, - [collectionId, dispatch, handleResult] + [collectionId, dispatch, handleResult, context] ); // When the context (or collection) changes, set the filter context and trigger appropriate query @@ -311,8 +310,6 @@ export const useContextFilterQueryManagement = ( } else { throw new Error(`No filter query matches filter context "${context}"`); } - requestContext.current[request.requestId] = context; - return () => { // Abort request if context changes while running (prevents race conditions) if (request) { From 2cd82f78b890e67f09182c4c5db7d9e90268baa8 Mon Sep 17 00:00:00 2001 From: David Lyon Date: Tue, 14 May 2024 15:15:36 -0700 Subject: [PATCH 05/14] fix bug setting all bool filters to false --- src/features/collections/Filters.tsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/features/collections/Filters.tsx b/src/features/collections/Filters.tsx index 9987e0aa..49f16083 100644 --- a/src/features/collections/Filters.tsx +++ b/src/features/collections/Filters.tsx @@ -252,7 +252,9 @@ export const useContextFilterQueryManagement = ( type: 'bool', value: current?.type === column.type - ? Boolean(current.value) + ? typeof current.value !== 'undefined' + ? Boolean(current.value) + : undefined : undefined, }, ]) From e36b611a21506f3075213a573500c9a80711e0e4 Mon Sep 17 00:00:00 2001 From: David Lyon Date: Tue, 14 May 2024 15:22:48 -0700 Subject: [PATCH 06/14] Fix infinite loop in effect --- src/features/collections/CollectionDetail.tsx | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/src/features/collections/CollectionDetail.tsx b/src/features/collections/CollectionDetail.tsx index 6a25d908..2d781a6c 100644 --- a/src/features/collections/CollectionDetail.tsx +++ b/src/features/collections/CollectionDetail.tsx @@ -1015,6 +1015,11 @@ const BooleanFilterControls = ({ setSelectValue(event.target.value as string); }; + // In order not to create a dependency loop when filter changes within the below effect, + // use a filter ref + const filterRef = useRef(filter); + filterRef.current = filter; + useEffect(() => { let value; if (selectValue === 'true') { @@ -1022,8 +1027,15 @@ const BooleanFilterControls = ({ } else if (selectValue === 'false') { value = 0; } - dispatch(setFilter([collectionId, context, column, { ...filter, value }])); - }, [selectValue, collectionId, context, column, filter, dispatch]); + dispatch( + setFilter([ + collectionId, + context, + column, + { ...filterRef.current, value }, + ]) + ); + }, [selectValue, collectionId, context, column, dispatch]); return ( From 0f57c19cf709888a86c9cc5156b85df5829d2239 Mon Sep 17 00:00:00 2001 From: David Lyon Date: Fri, 26 Apr 2024 13:28:55 -0700 Subject: [PATCH 07/14] Add bool support (as range value, dedicated state will be implemented when we have a bool filter controller component) --- src/common/api/collectionsApi.ts | 2 +- src/common/components/FilterChip.tsx | 2 ++ src/features/collections/CollectionDetail.tsx | 8 ++++++-- src/features/collections/collectionsSlice.ts | 11 ++++++++++- 4 files changed, 19 insertions(+), 4 deletions(-) diff --git a/src/common/api/collectionsApi.ts b/src/common/api/collectionsApi.ts index b0ade21e..f7162c94 100644 --- a/src/common/api/collectionsApi.ts +++ b/src/common/api/collectionsApi.ts @@ -177,7 +177,7 @@ export type ColumnMeta = { display_name?: string; } & ( | { - type: 'date' | 'int' | 'float'; + type: 'date' | 'int' | 'float' | 'bool'; filter_strategy: undefined; min_value: number; max_value: number; diff --git a/src/common/components/FilterChip.tsx b/src/common/components/FilterChip.tsx index 521017e0..44b50c62 100644 --- a/src/common/components/FilterChip.tsx +++ b/src/common/components/FilterChip.tsx @@ -30,6 +30,8 @@ export const FilterChip = ({ }) .map((val) => val.toLocaleString()); filterString = `${minString} to ${maxString}`; + } else if (filter.type === 'bool') { + filterString = filter.value.range[1] === 1 ? 'true' : 'false'; } else { const val = filter.value; if (typeof val === 'string') { diff --git a/src/features/collections/CollectionDetail.tsx b/src/features/collections/CollectionDetail.tsx index 2d781a6c..6bd8ed32 100644 --- a/src/features/collections/CollectionDetail.tsx +++ b/src/features/collections/CollectionDetail.tsx @@ -583,7 +583,11 @@ const FilterControls = ({ collectionId={collectionId} /> ); - } else if (filter.type === 'int' || filter.type === 'float') { + } else if ( + filter.type === 'int' || + filter.type === 'float' || + filter.type === 'bool' + ) { return ( { // initial defaults const [defMin, defMax] = filter.value?.range ?? [ diff --git a/src/features/collections/collectionsSlice.ts b/src/features/collections/collectionsSlice.ts index 23d8f130..d97a1582 100644 --- a/src/features/collections/collectionsSlice.ts +++ b/src/features/collections/collectionsSlice.ts @@ -30,7 +30,7 @@ interface FilterRange { export type FilterState = | { type: 'fulltext' | 'prefix' | 'identity' | 'ngram'; value?: string } | { - type: 'int' | 'float'; + type: 'int' | 'float' | 'bool'; value?: FilterRange; min_value: number; max_value: number; @@ -435,6 +435,15 @@ export const useFilters = ( filterState.type === 'ngram' ) { if (filterState.value !== undefined) filterValue = filterState.value; + } else if ( + filterState.type === 'bool' && + filterState.value !== undefined + ) { + const fStart = filterState.value.range[0]; + const fEnd = filterState.value.range[1]; + if (fStart === fEnd) { + filterValue = fStart === 1 ? 'true' : 'false'; + } } else if ( (filterState.type === 'date' || filterState.type === 'int' || From 2af716332f87a4aeb2885cdf35d1e2d51499f0bb Mon Sep 17 00:00:00 2001 From: David Lyon Date: Fri, 26 Apr 2024 13:45:31 -0700 Subject: [PATCH 08/14] Use columnMeta for filter chip labels, use filter context for filter menu header --- src/features/collections/CollectionDetail.tsx | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/src/features/collections/CollectionDetail.tsx b/src/features/collections/CollectionDetail.tsx index 6bd8ed32..4cd93295 100644 --- a/src/features/collections/CollectionDetail.tsx +++ b/src/features/collections/CollectionDetail.tsx @@ -49,7 +49,7 @@ import { import { useForm } from 'react-hook-form'; import { CollectionOverview } from './CollectionOverview'; import { FilterChip } from '../../common/components/FilterChip'; -import { FilterContextTabs } from './Filters'; +import { filterContextScope, FilterContextTabs } from './Filters'; export const detailPath = ':id'; export const detailDataProductPath = ':id/:data_product'; @@ -406,6 +406,7 @@ const useFilterEntries = (collectionId: string) => { }; const FilterChips = ({ collectionId }: { collectionId: string }) => { + const { columnMeta } = useFilters(collectionId); const { filterEntries, clearFilterState, context } = useFilterEntries(collectionId); if (filterEntries.length === 0) return <>; @@ -416,7 +417,7 @@ const FilterChips = ({ collectionId }: { collectionId: string }) => { clearFilterState(column)} /> ); @@ -456,11 +457,19 @@ const FilterMenu = ({ setExpandedCategory(expanded ? category : ''); }; + const menuLabel = { + DEFAULT: 'Filters', + genomes: 'Genome Filters', + samples: 'Sample Filters', + biolog: 'Biolog Filters', + microtrait: 'Microtrait Filters', + }[filterContextScope(context) || 'DEFAULT']; + if (open) { return (
-

Genome Filters

+

{menuLabel}