-
Notifications
You must be signed in to change notification settings - Fork 3
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: add quick gene lookup functionality (#2250)
* add/remove gene functionality back with geneset style gene * styling and expansion * memo gene list to prevent re render
- Loading branch information
Showing
3 changed files
with
251 additions
and
21 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,176 @@ | ||
import { H4, Icon, MenuItem } from "@blueprintjs/core"; | ||
import { IconNames } from "@blueprintjs/icons"; | ||
import React, { useState, useEffect, useRef, useMemo } from "react"; | ||
import fuzzysort from "fuzzysort"; | ||
import { Suggest } from "@blueprintjs/select"; | ||
import { useSelector, useDispatch } from "react-redux"; | ||
|
||
import Gene from "./gene"; | ||
|
||
import { postUserErrorToast } from "../framework/toasters"; | ||
import actions from "../../actions"; | ||
|
||
const usePrevious = (value) => { | ||
const ref = useRef(); | ||
useEffect(() => { | ||
ref.current = value; | ||
}); | ||
return ref.current; | ||
}; | ||
|
||
function QuickGene() { | ||
const dispatch = useDispatch(); | ||
|
||
const [isExpanded, setIsExpanded] = useState(true); | ||
const [geneNames, setGeneNames] = useState([]); | ||
const [, setStatus] = useState("pending"); | ||
|
||
const { annoMatrix, userDefinedGenes, userDefinedGenesLoading } = useSelector( | ||
(state) => { | ||
return { | ||
annoMatrix: state.annoMatrix, | ||
userDefinedGenes: state.controls.userDefinedGenes, | ||
userDefinedGenesLoading: state.controls.userDefinedGenesLoading, | ||
}; | ||
} | ||
); | ||
|
||
const prevProps = usePrevious({ annoMatrix }); | ||
|
||
useEffect(async () => { | ||
if (!annoMatrix) return; | ||
if (annoMatrix !== prevProps?.annoMatrix) { | ||
const { schema } = annoMatrix; | ||
const varIndex = schema.annotations.var.index; | ||
|
||
setStatus("pending"); | ||
try { | ||
const df = await annoMatrix.fetch("var", varIndex); | ||
setStatus("success"); | ||
setGeneNames(df.col(varIndex).asArray()); | ||
} catch (error) { | ||
setStatus("error"); | ||
throw error; | ||
} | ||
} | ||
}, [annoMatrix, prevProps]); | ||
|
||
const handleExpand = () => setIsExpanded(!isExpanded); | ||
|
||
const renderGene = (fuzzySortResult, { handleClick, modifiers }) => { | ||
if (!modifiers.matchesPredicate) { | ||
return null; | ||
} | ||
/* the fuzzysort wraps the object with other properties, like a score */ | ||
const geneName = fuzzySortResult.target; | ||
|
||
return ( | ||
<MenuItem | ||
active={modifiers.active} | ||
disabled={modifiers.disabled} | ||
data-testid={`suggest-menu-item-${geneName}`} | ||
key={geneName} | ||
onClick={(g) => | ||
/* this fires when user clicks a menu item */ | ||
handleClick(g) | ||
} | ||
text={geneName} | ||
/> | ||
); | ||
}; | ||
|
||
const handleClick = (g) => { | ||
if (!g) return; | ||
const gene = g.target; | ||
if (userDefinedGenes.indexOf(gene) !== -1) { | ||
postUserErrorToast("That gene already exists"); | ||
} else if (geneNames.indexOf(gene) === undefined) { | ||
postUserErrorToast("That doesn't appear to be a valid gene name."); | ||
} else { | ||
dispatch({ type: "single user defined gene start" }); | ||
dispatch(actions.requestUserDefinedGene(gene)); | ||
dispatch({ type: "single user defined gene complete" }); | ||
} | ||
}; | ||
|
||
const filterGenes = (query, genes) => | ||
/* fires on load, once, and then for each character typed into the input */ | ||
fuzzysort.go(query, genes, { | ||
limit: 5, | ||
threshold: -10000, // don't return bad results | ||
}); | ||
|
||
const removeGene = (gene) => () => { | ||
dispatch({ type: "clear user defined gene", data: gene }); | ||
}; | ||
|
||
const QuickGenes = useMemo(() => { | ||
return userDefinedGenes.map((gene) => { | ||
return ( | ||
<Gene | ||
key={`quick=${gene}`} | ||
gene={gene} | ||
removeGene={removeGene} | ||
quickGene | ||
/> | ||
); | ||
}); | ||
}, [userDefinedGenes]); | ||
|
||
return ( | ||
<div style={{ width: "100%", marginBottom: "16px" }}> | ||
<H4 | ||
role="menuitem" | ||
tabIndex="0" | ||
data-testclass="category-expand" | ||
onKeyPress={handleExpand} | ||
style={{ | ||
cursor: "pointer", | ||
}} | ||
onClick={handleExpand} | ||
> | ||
Genes{" "} | ||
{isExpanded ? ( | ||
<Icon icon={IconNames.CHEVRON_DOWN} /> | ||
) : ( | ||
<Icon icon={IconNames.CHEVRON_RIGHT} /> | ||
)} | ||
</H4> | ||
{isExpanded && ( | ||
<> | ||
<div style={{ marginBottom: "8px" }}> | ||
<Suggest | ||
resetOnSelect | ||
closeOnSelect | ||
resetOnClose | ||
itemDisabled={userDefinedGenesLoading ? () => true : () => false} | ||
noResults={<MenuItem disabled text="No matching genes." />} | ||
onItemSelect={(g) => { | ||
/* this happens on 'enter' */ | ||
handleClick(g); | ||
}} | ||
initialContent={<MenuItem disabled text="Enter a gene…" />} | ||
inputProps={{ | ||
"data-testid": "gene-search", | ||
placeholder: "Quick Gene Search", | ||
leftIcon: IconNames.SEARCH, | ||
fill: true, | ||
}} | ||
inputValueRenderer={() => { | ||
return ""; | ||
}} | ||
itemListPredicate={filterGenes} | ||
itemRenderer={renderGene} | ||
items={geneNames || ["No genes"]} | ||
popoverProps={{ minimal: true }} | ||
fill | ||
/> | ||
</div> | ||
{QuickGenes} | ||
</> | ||
)} | ||
</div> | ||
); | ||
} | ||
|
||
export default QuickGene; |