-
Notifications
You must be signed in to change notification settings - Fork 0
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Orthogroup tree table protein filter #1252
Changes from 6 commits
355d5a4
f5ee686
fccec4c
ca400d5
ab88a2a
2f60de7
72e4740
c22b905
f6f9153
337348e
2520735
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change | ||
---|---|---|---|---|
@@ -1,4 +1,10 @@ | ||||
import React, { CSSProperties, useCallback, useMemo, useState } from 'react'; | ||||
import React, { | ||||
CSSProperties, | ||||
useCallback, | ||||
useDeferredValue, | ||||
useMemo, | ||||
useState, | ||||
} from 'react'; | ||||
import TreeTable from '@veupathdb/components/lib/components/tidytree/TreeTable'; | ||||
import { RecordTableProps, WrappedComponentProps } from './Types'; | ||||
import { useOrthoService } from 'ortho-client/hooks/orthoService'; | ||||
|
@@ -17,9 +23,12 @@ import { PfamDomainArchitecture } from 'ortho-client/components/pfam-domains/Pfa | |||
import { extractPfamDomain } from 'ortho-client/records/utils'; | ||||
import Banner from '@veupathdb/coreui/lib/components/banners/Banner'; | ||||
import { RowCounter } from '@veupathdb/coreui/lib/components/Mesa'; | ||||
import PopoverButton from '@veupathdb/coreui/lib/components/buttons/PopoverButton/PopoverButton'; | ||||
import { PfamDomain } from 'ortho-client/components/pfam-domains/PfamDomain'; | ||||
import { | ||||
FilledButton, | ||||
FloatingButton, | ||||
OutlinedButton, | ||||
SelectList, | ||||
Undo, | ||||
useDeferredState, | ||||
|
@@ -45,13 +54,15 @@ export function RecordTable_Sequences( | |||
props: WrappedComponentProps<RecordTableProps> | ||||
) { | ||||
const [searchQuery, setSearchQuery] = useState(''); | ||||
const safeSearchRegexp = useMemo( | ||||
() => createSafeSearchRegExp(searchQuery), | ||||
[searchQuery] | ||||
const safeSearchRegexp = useDeferredValue( | ||||
useMemo(() => createSafeSearchRegExp(searchQuery), [searchQuery]) | ||||
); | ||||
|
||||
const [resetCounter, setResetCounter] = useState(0); // used for forcing re-render of filter buttons | ||||
|
||||
const [proteinFilterIds, setProteinFilterIds, volatileProteinFilterIds] = | ||||
useDeferredState<string[]>([]); | ||||
|
||||
const [selectedSpecies, setSelectedSpecies, volatileSelectedSpecies] = | ||||
useDeferredState<string[]>([]); | ||||
|
||||
|
@@ -185,13 +196,15 @@ export function RecordTable_Sequences( | |||
// 2. core-peripheral radio button | ||||
// 3. checked boxes in the Pfam legend | ||||
|
||||
const [selectedColumnFilters, setSelectedColumnFilters] = useState<string[]>( | ||||
[] | ||||
); | ||||
const [ | ||||
selectedColumnFilters, | ||||
setSelectedColumnFilters, | ||||
volatileSelectedColumnFilters, | ||||
] = useDeferredState<string[]>([]); | ||||
|
||||
const filteredRows = useMemo(() => { | ||||
if ( | ||||
searchQuery !== '' || | ||||
safeSearchRegexp != null || | ||||
corePeripheralFilterValue != null || | ||||
pfamFilterIds.length > 0 || | ||||
selectedSpecies.length > 0 | ||||
|
@@ -204,7 +217,7 @@ export function RecordTable_Sequences( | |||
const rowPfamIdsSet = accessionToPfamIds.get(rowFullId); | ||||
|
||||
const searchMatch = | ||||
searchQuery === '' || | ||||
safeSearchRegexp == null || | ||||
rowMatch(row, safeSearchRegexp, selectedColumnFilters); | ||||
const corePeripheralMatch = | ||||
corePeripheralFilterValue.length === 0 || | ||||
|
@@ -217,22 +230,29 @@ export function RecordTable_Sequences( | |||
const speciesMatch = | ||||
selectedSpecies.length === 0 || | ||||
selectedSpecies.some((specie) => row.taxon_abbrev === specie); | ||||
const proteinMatch = | ||||
proteinFilterIds.length === 0 || | ||||
proteinFilterIds.some((proteinId) => rowFullId === proteinId); | ||||
|
||||
return ( | ||||
searchMatch && corePeripheralMatch && pfamIdMatch && speciesMatch | ||||
searchMatch && | ||||
corePeripheralMatch && | ||||
pfamIdMatch && | ||||
speciesMatch && | ||||
proteinMatch | ||||
); | ||||
}); | ||||
} | ||||
return undefined; | ||||
}, [ | ||||
searchQuery, | ||||
selectedColumnFilters, | ||||
safeSearchRegexp, | ||||
sortedRows, | ||||
corePeripheralFilterValue, | ||||
accessionToPfamIds, | ||||
pfamFilterIds, | ||||
selectedSpecies, | ||||
proteinFilterIds, | ||||
]); | ||||
|
||||
// now filter the tree if needed - takes a couple of seconds for large trees | ||||
|
@@ -303,12 +323,7 @@ export function RecordTable_Sequences( | |||
numSequences <= MAX_SEQUENCES_FOR_TREE && | ||||
(tree == null || treeResponse == null)) | ||||
) { | ||||
return ( | ||||
<> | ||||
<div>Loading...</div> | ||||
<Loading /> | ||||
</> | ||||
); // The loading spinner does not show :-( | ||||
return <Loading />; | ||||
} | ||||
|
||||
if ( | ||||
|
@@ -457,6 +472,90 @@ export function RecordTable_Sequences( | |||
/> | ||||
) : null; | ||||
|
||||
const resetProteinFilterButton = ( | ||||
<OutlinedButton | ||||
text="Reset protein filter" | ||||
onPress={() => { | ||||
setProteinFilterIds([]); | ||||
}} | ||||
/> | ||||
); | ||||
|
||||
const updateProteinFilterIds = () => { | ||||
setProteinFilterIds(highlightedNodes); | ||||
setHighlightedNodes([]); | ||||
}; | ||||
|
||||
const proteinFilter = ( | ||||
<PopoverButton | ||||
buttonDisplayContent={`Proteins${ | ||||
volatileProteinFilterIds.length > 0 | ||||
? ` (${volatileProteinFilterIds.length})` | ||||
: '' | ||||
}`} | ||||
key={volatileProteinFilterIds.join(':')} | ||||
> | ||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This isn't needed
Suggested change
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I see. This could use a comment. I'm not a big fan of using keys like this, but it does solve this problem. There are two ways we can avoid using key, here:
Both have their pros and cons. The first option means introducing more state for the consumer, and more state management for the There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I didn't expose an |
||||
<div | ||||
style={{ | ||||
margin: '1em', | ||||
display: 'flex', | ||||
flexDirection: 'column', | ||||
gap: '1em', | ||||
maxWidth: '300px', | ||||
}} | ||||
> | ||||
{highlightedNodes.length === 0 ? ( | ||||
volatileProteinFilterIds.length === 0 ? ( | ||||
<div> | ||||
Select some proteins using the checkboxes in the table below. | ||||
</div> | ||||
) : ( | ||||
<> | ||||
<div> | ||||
You are filtering on{' '} | ||||
{volatileProteinFilterIds.length.toLocaleString()} proteins. | ||||
</div> | ||||
{resetProteinFilterButton} | ||||
</> | ||||
) | ||||
) : volatileProteinFilterIds.length === 0 ? ( | ||||
<> | ||||
<div> | ||||
You have checked {highlightedNodes.length.toLocaleString()}{' '} | ||||
proteins in the table. | ||||
</div> | ||||
<FilledButton | ||||
text="Filter to keep only these proteins" | ||||
onPress={updateProteinFilterIds} | ||||
/> | ||||
</> | ||||
) : highlightedNodes.length < volatileProteinFilterIds.length ? ( | ||||
<> | ||||
<div> | ||||
You have checked {highlightedNodes.length.toLocaleString()}{' '} | ||||
proteins in the table and are already filtering on{' '} | ||||
{volatileProteinFilterIds.length.toLocaleString()} proteins. | ||||
</div> | ||||
<FilledButton | ||||
text="Refine filter to keep only checked proteins" | ||||
onPress={updateProteinFilterIds} | ||||
/> | ||||
{resetProteinFilterButton} | ||||
</> | ||||
) : ( | ||||
<> | ||||
<div> | ||||
You have checked all the proteins that are currently being | ||||
filtered on. Either uncheck one or more proteins or reset the | ||||
filter entirely using the button below. | ||||
</div> | ||||
{resetProteinFilterButton} | ||||
</> | ||||
)} | ||||
</div> | ||||
</PopoverButton> | ||||
); | ||||
|
||||
const resetButton = ( | ||||
<FloatingButton | ||||
text={''} | ||||
|
@@ -465,13 +564,15 @@ export function RecordTable_Sequences( | |||
disabled={ | ||||
pfamFilterIds.length + | ||||
corePeripheralFilterValue.length + | ||||
selectedSpecies.length === | ||||
selectedSpecies.length + | ||||
proteinFilterIds.length === | ||||
0 | ||||
} | ||||
icon={Undo} | ||||
size={'medium'} | ||||
themeRole={'primary'} | ||||
onPress={() => { | ||||
setProteinFilterIds([]); | ||||
setPfamFilterIds([]); | ||||
setCorePeripheralFilterValue([]); | ||||
setSelectedSpecies([]); | ||||
|
@@ -527,7 +628,7 @@ export function RecordTable_Sequences( | |||
onSearchTermChange={setSearchQuery} | ||||
recordDisplayName="Proteins" | ||||
filterAttributes={filterAttributes} | ||||
selectedColumnFilters={selectedColumnFilters} | ||||
selectedColumnFilters={volatileSelectedColumnFilters} | ||||
onColumnFilterChange={(keys) => setSelectedColumnFilters(keys)} | ||||
/> | ||||
<div className="MesaComponent" style={{ marginRight: 'auto' }}> | ||||
|
@@ -551,6 +652,7 @@ export function RecordTable_Sequences( | |||
}} | ||||
> | ||||
<strong>Filters: </strong> | ||||
{proteinFilter} | ||||
{pfamFilter} | ||||
{corePeripheralFilter} | ||||
{taxonFilter} | ||||
|
@@ -639,7 +741,8 @@ function rowMatch(row: RowType, query: RegExp, keys?: string[]): boolean { | |||
); | ||||
} | ||||
|
||||
function createSafeSearchRegExp(input: string): RegExp { | ||||
function createSafeSearchRegExp(input: string): RegExp | undefined { | ||||
if (input === '') return undefined; | ||||
try { | ||||
// Attempt to create a RegExp from the user input directly | ||||
return new RegExp(input, 'i'); | ||||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should we change the label when rows are selected, to indicate that the underlying filter is operable?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I've added an asterisk just as a placeholder (though the asterisk is also used inside the popover, so maybe it's good)