mirror of
https://github.com/chanzuckerberg/cellxgene.git
synced 2026-09-19 02:48:30 +08:00
* add/remove gene functionality back with geneset style gene * styling and expansion * memo gene list to prevent re render
177 lines
4.9 KiB
JavaScript
177 lines
4.9 KiB
JavaScript
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;
|