Create / edit geneset description (#2139)

* geneset description add

* edit geneset description

* default state for desc

* remove log

* naming, todo

* check for both dup name and desc

* fixes

* do not store gene set modal state in history stack

* Update createGenesetDialogue.js

* Update editGenesetNameDialogue.js

Co-authored-by: bkmartinjr <bruce@chanzuckerberg.com>
This commit is contained in:
Colin Megill
2021-04-21 19:50:29 -04:00
committed by GitHub
co-authored by bkmartinjr
parent 99a795a688
commit 876ceb4d8b
9 changed files with 170 additions and 69 deletions
+1 -1
View File
@@ -276,7 +276,7 @@ describe("geneset: update", () => {
}, },
} }
); );
}).toThrow("update specified existing name"); }).toThrow("update specified existing name and description");
}); });
}); });
+2
View File
@@ -48,6 +48,7 @@ class AnnoDialog extends React.PureComponent {
> >
{errorMessage} {errorMessage}
</p> </p>
{/* we might rename, secondary button and secondary input are not related */}
{secondaryInstructions && ( {secondaryInstructions && (
<p style={{ marginTop: secondaryInstructions ? 20 : 0 }}> <p style={{ marginTop: secondaryInstructions ? 20 : 0 }}>
{secondaryInstructions} {secondaryInstructions}
@@ -62,6 +63,7 @@ class AnnoDialog extends React.PureComponent {
<Tooltip content={cancelTooltipContent}> <Tooltip content={cancelTooltipContent}>
<Button onClick={handleCancel}>Cancel</Button> <Button onClick={handleCancel}>Cancel</Button>
</Tooltip> </Tooltip>
{/* we might rename, secondary button and secondary input are not related */}
{handleSecondaryButtonSubmit && secondaryButtonText ? ( {handleSecondaryButtonSubmit && secondaryButtonText ? (
<Button <Button
onClick={handleSecondaryButtonSubmit} onClick={handleSecondaryButtonSubmit}
@@ -1,6 +1,7 @@
import React from "react"; import React from "react";
import { connect } from "react-redux"; import { connect } from "react-redux";
import { Tooltip, Position, Switch } from "@blueprintjs/core"; import { Position, Switch } from "@blueprintjs/core";
import { Tooltip2 } from "@blueprintjs/popover2";
import { FaChevronRight, FaChevronDown } from "react-icons/fa"; import { FaChevronRight, FaChevronDown } from "react-icons/fa";
import actions from "../../actions"; import actions from "../../actions";
import Gene from "./gene"; import Gene from "./gene";
@@ -128,7 +129,7 @@ class GeneSet extends React.Component {
} }
render() { render() {
const { setName, setGenes } = this.props; const { setName, setGenes, genesetDescription } = this.props;
const { isOpen, toggleSummaryHisto } = this.state; const { isOpen, toggleSummaryHisto } = this.state;
const genesetNameLengthVisible = 120; /* this magic number determines how much of a long geneset name we see */ const genesetNameLengthVisible = 120; /* this magic number determines how much of a long geneset name we see */
const genesetIsEmpty = setGenes.length === 0; const genesetIsEmpty = setGenes.length === 0;
@@ -156,7 +157,11 @@ class GeneSet extends React.Component {
}} }}
onClick={this.onGenesetMenuClick} onClick={this.onGenesetMenuClick}
> >
<Truncate> <Truncate
tooltipAddendum={
genesetDescription ? `: ${genesetDescription}` : ""
}
>
<span <span
style={{ style={{
maxWidth: globals.leftSidebarWidth - genesetNameLengthVisible, maxWidth: globals.leftSidebarWidth - genesetNameLengthVisible,
@@ -185,7 +190,7 @@ class GeneSet extends React.Component {
<div style={{ marginLeft: 15, marginTop: 5, marginRight: 0 }}> <div style={{ marginLeft: 15, marginTop: 5, marginRight: 0 }}>
{isOpen && !genesetIsEmpty && ( {isOpen && !genesetIsEmpty && (
<Tooltip <Tooltip2
content="Aggregate all genes in this geneset, and allow coloring by and selecting on the entire set." content="Aggregate all genes in this geneset, and allow coloring by and selecting on the entire set."
position={Position.BOTTOM_RIGHT} position={Position.BOTTOM_RIGHT}
usePortal usePortal
@@ -204,7 +209,7 @@ class GeneSet extends React.Component {
innerLabelChecked="on" innerLabelChecked="on"
onChange={this.toggleSummaryHisto} onChange={this.toggleSummaryHisto}
/> />
</Tooltip> </Tooltip2>
)} )}
{isOpen && genesetIsEmpty && ( {isOpen && genesetIsEmpty && (
@@ -223,7 +228,10 @@ class GeneSet extends React.Component {
setGenes={setGenes} setGenes={setGenes}
/> />
))} ))}
<EditGenesetNameDialogue parentGeneset={setName} /> <EditGenesetNameDialogue
parentGeneset={setName}
parentGenesetDescription={genesetDescription}
/>
</div> </div>
); );
} }
@@ -23,6 +23,7 @@ class GeneExpression extends React.Component {
setGenes={Array.from(geneset.genes.keys())} setGenes={Array.from(geneset.genes.keys())}
setGenesWithDescriptions={geneset.genes} setGenesWithDescriptions={geneset.genes}
setName={name} setName={name}
genesetDescription={geneset.genesetDescription}
/> />
); );
} }
@@ -2,7 +2,8 @@ import pull from "lodash.pull";
import uniq from "lodash.uniq"; import uniq from "lodash.uniq";
import React from "react"; import React from "react";
import { connect } from "react-redux"; import { connect } from "react-redux";
import AnnoDialog from "../../annoDialog"; import { Button, Dialog, Classes, Colors } from "@blueprintjs/core";
import { Tooltip2 } from "@blueprintjs/popover2";
import LabelInput from "../../labelInput"; import LabelInput from "../../labelInput";
import actions from "../../../actions"; import actions from "../../../actions";
@@ -20,6 +21,7 @@ class CreateGenesetDialogue extends React.PureComponent {
this.state = { this.state = {
genesetName: "", genesetName: "",
genesToPopulateGeneset: "", genesToPopulateGeneset: "",
genesetDescription: "",
}; };
} }
@@ -28,6 +30,7 @@ class CreateGenesetDialogue extends React.PureComponent {
this.setState({ this.setState({
genesetName: "", genesetName: "",
genesToPopulateGeneset: "", genesToPopulateGeneset: "",
genesetDescription: "",
}); });
dispatch({ dispatch({
type: "geneset: disable create geneset mode", type: "geneset: disable create geneset mode",
@@ -37,12 +40,16 @@ class CreateGenesetDialogue extends React.PureComponent {
createGeneset = (e) => { createGeneset = (e) => {
const { dispatch } = this.props; const { dispatch } = this.props;
const { genesetName, genesToPopulateGeneset } = this.state; const {
genesetName,
genesToPopulateGeneset,
genesetDescription,
} = this.state;
dispatch({ dispatch({
type: "geneset: create", type: "geneset: create",
genesetName, genesetName,
genesetDescription: "", genesetDescription,
}); });
if (genesToPopulateGeneset) { if (genesToPopulateGeneset) {
const genesTmpHardcodedFormat = []; const genesTmpHardcodedFormat = [];
@@ -82,10 +89,14 @@ class CreateGenesetDialogue extends React.PureComponent {
this.setState({ genesToPopulateGeneset: e }); this.setState({ genesToPopulateGeneset: e });
}; };
handleDescriptionInputChange = (e) => {
this.setState({ genesetDescription: e });
};
instruction = (genesetName, genesets) => { instruction = (genesetName, genesets) => {
return genesets.has(genesetName) return genesets.has(genesetName)
? "Geneset name must be unique." ? "Gene set name must be unique."
: "New, unique geneset name"; : "New, unique gene set name";
}; };
validate = (genesetName, genesets) => { validate = (genesetName, genesets) => {
@@ -98,47 +109,94 @@ class CreateGenesetDialogue extends React.PureComponent {
return ( return (
<> <>
<AnnoDialog <Dialog
isActive={genesetsUI.createGenesetModeActive} icon="tag"
inputProps={{
"data-testid": `${metadataField}:create-geneset-dialog`,
}}
primaryButtonProps={{
"data-testid": `${metadataField}:submit-geneset`,
}}
title="Create gene set" title="Create gene set"
instruction={this.instruction(genesetName, genesets)} isOpen={genesetsUI.createGenesetModeActive}
cancelTooltipContent="Close this dialog without creating a new gene set." onClose={this.disableCreateGenesetMode}
primaryButtonText="Create gene set" >
text={genesetName} <form
validationError={this.validate(genesetName, genesets)} onSubmit={(e) => {
annoInput={ e.preventDefault();
<LabelInput }}
onChange={this.handleChange} >
inputProps={{ <div className={Classes.DIALOG_BODY}>
"data-testid": "create-geneset-modal", <div style={{ marginBottom: 20 }}>
leftIcon: "manually-entered-data", <p>{this.instruction(genesetName, genesets)}</p>
intent: "none", <LabelInput
autoFocus: true, onChange={this.handleChange}
}} inputProps={{
newLabelMessage="Create gene set" "data-testid": "create-geneset-modal",
/> leftIcon: "manually-entered-data",
} intent: "none",
secondaryInstructions="Optionally add a list of comma separated genes to populate the gene set" autoFocus: true,
secondaryInput={ }}
<LabelInput newLabelMessage="Create gene set"
onChange={this.handleGenesetInputChange} />
inputProps={{ <p
"data-testid": "add-genes", style={{
intent: "none", marginTop: 7,
autoFocus: false, visibility: this.validate(genesetName, genesets)
}} ? "visible"
newLabelMessage="populate geneset with genes" : "hidden",
/> color: Colors.ORANGE3,
} }}
handleSubmit={this.createGeneset} >
handleCancel={this.disableCreateGenesetMode} {this.genesetNameError()}
/> </p>
<p style={{ marginTop: 20 }}>
Optionally add a{" "}
<span style={{ fontWeight: 700 }}>description</span> for this
gene set
</p>
<LabelInput
onChange={this.handleDescriptionInputChange}
inputProps={{
"data-testid": "add-geneset-description",
intent: "none",
autoFocus: false,
}}
newLabelMessage="Add geneset description"
/>
<p style={{ marginTop: 20 }}>
Optionally add a list of comma separated{" "}
<span style={{ fontWeight: 700 }}>genes</span> to populate the
gene set
</p>
<LabelInput
onChange={this.handleGenesetInputChange}
inputProps={{
"data-testid": "add-genes",
intent: "none",
autoFocus: false,
}}
newLabelMessage="populate geneset with genes"
/>
</div>
</div>
<div className={Classes.DIALOG_FOOTER}>
<div className={Classes.DIALOG_FOOTER_ACTIONS}>
<Tooltip2 content="Close this dialog without creating a new gene set.">
<Button onClick={this.disableCreateGenesetMode}>
Cancel
</Button>
</Tooltip2>
<Button
data-testid={`${metadataField}:submit-geneset`}
onClick={this.createGeneset}
disabled={
!genesetName || this.validate(genesetName, genesets)
}
intent="primary"
type="submit"
>
Create gene set
</Button>
</div>
</div>
</form>
</Dialog>
</> </>
); );
} }
@@ -14,14 +14,16 @@ class RenameGeneset extends React.PureComponent {
constructor(props) { constructor(props) {
super(props); super(props);
this.state = { this.state = {
genesetName: props.parentGeneset, newGenesetName: props.parentGeneset,
newGenesetDescription: props.parentGenesetDescription,
}; };
} }
disableEditGenesetNameMode = (e) => { disableEditGenesetNameMode = (e) => {
const { dispatch } = this.props; const { dispatch } = this.props;
this.setState({ this.setState({
genesetName: "", newGenesetName: "",
newGenesetDescription: "",
}); });
dispatch({ dispatch({
type: "geneset: disable rename geneset mode", type: "geneset: disable rename geneset mode",
@@ -31,12 +33,15 @@ class RenameGeneset extends React.PureComponent {
renameGeneset = (e) => { renameGeneset = (e) => {
const { dispatch, genesetsUI } = this.props; const { dispatch, genesetsUI } = this.props;
const { genesetName } = this.state; const { newGenesetName, newGenesetDescription } = this.state;
dispatch({ dispatch({
type: "geneset: update", type: "geneset: update",
genesetName: genesetsUI.isEditingGenesetName, genesetName: genesetsUI.isEditingGenesetName,
update: { genesetName }, update: {
genesetName: newGenesetName,
genesetDescription: newGenesetDescription,
},
}); });
dispatch({ dispatch({
type: "geneset: disable rename geneset mode", type: "geneset: disable rename geneset mode",
@@ -49,12 +54,16 @@ class RenameGeneset extends React.PureComponent {
}; };
handleChange = (e) => { handleChange = (e) => {
this.setState({ genesetName: e }); this.setState({ newGenesetName: e });
};
handleChangeDescription = (e) => {
this.setState({ newGenesetDescription: e });
}; };
render() { render() {
const { genesetName } = this.state; const { newGenesetName, newGenesetDescription } = this.state;
const { genesetsUI, parentGeneset } = this.props; const { genesetsUI, parentGeneset, parentGenesetDescription } = this.props;
return ( return (
<> <>
@@ -66,15 +75,19 @@ class RenameGeneset extends React.PureComponent {
primaryButtonProps={{ primaryButtonProps={{
"data-testid": `${genesetsUI.isEditingGenesetName}:submit-geneset`, "data-testid": `${genesetsUI.isEditingGenesetName}:submit-geneset`,
}} }}
title="Rename gene set" title="Edit gene set name and description"
instruction={`Rename ${genesetsUI.isEditingGenesetName}`} instruction={`Rename ${genesetsUI.isEditingGenesetName}`}
cancelTooltipContent="Close this dialog without renaming the gene set." cancelTooltipContent="Close this dialog without renaming the gene set."
primaryButtonText="Rename gene set" primaryButtonText="Edit gene set name and description"
text={genesetName} text={newGenesetName}
validationError={genesetsUI.isEditingGenesetName === genesetName} secondaryText={newGenesetDescription}
validationError={
genesetsUI.isEditingGenesetName === newGenesetName &&
parentGenesetDescription === newGenesetDescription
}
annoInput={ annoInput={
<LabelInput <LabelInput
label={genesetName} label={newGenesetName}
onChange={this.handleChange} onChange={this.handleChange}
inputProps={{ inputProps={{
"data-testid": "rename-geneset-modal", "data-testid": "rename-geneset-modal",
@@ -82,7 +95,16 @@ class RenameGeneset extends React.PureComponent {
intent: "none", intent: "none",
autoFocus: true, autoFocus: true,
}} }}
newLabelMessage="Rename gene set" />
}
secondaryInstructions="Edit description"
secondaryInput={
<LabelInput
label={newGenesetDescription}
onChange={this.handleChangeDescription}
inputProps={{ "data-testid": "change geneset description" }}
intent="none"
autoFocus={false}
/> />
} }
handleSubmit={this.renameGeneset} handleSubmit={this.renameGeneset}
@@ -82,7 +82,7 @@ class GenesetMenus extends React.PureComponent {
data-testclass="activateEditGenesetNameMode" data-testclass="activateEditGenesetNameMode"
data-testid={`${geneset}:edit-genesetName-mode`} data-testid={`${geneset}:edit-genesetName-mode`}
onClick={this.activateEditGenesetNameMode} onClick={this.activateEditGenesetNameMode}
text="Edit gene set name" text="Edit gene set name and description"
/> />
<MenuItem <MenuItem
icon="trash" icon="trash"
+13 -2
View File
@@ -149,6 +149,7 @@ const GeneSets = (
*/ */
case "geneset: update": { case "geneset: update": {
const { genesetName, update } = action; const { genesetName, update } = action;
if ( if (
typeof genesetName !== "string" || typeof genesetName !== "string" ||
!genesetName || !genesetName ||
@@ -157,8 +158,18 @@ const GeneSets = (
throw new Error( throw new Error(
"geneset: update -- geneset name unspecified or does not exist." "geneset: update -- geneset name unspecified or does not exist."
); );
if (state.genesets.has(update.genesetName))
throw new Error("geneset: update -- update specified existing name."); /* now that we've confirmed the gene set exists, check for duplicates */
const genesetNameIsDuplicate = state.genesets.has(update.genesetName);
const descriptionIsDuplicate =
state.genesets.get(update.genesetName) &&
state.genesets.get(update.genesetName).genesetDescription ===
update.genesetDescription;
if (genesetNameIsDuplicate && descriptionIsDuplicate)
throw new Error(
"geneset: update -- update specified existing name and description."
);
const prevGs = state.genesets.get(genesetName); const prevGs = state.genesets.get(genesetName);
const newGs = { const newGs = {
-1
View File
@@ -60,7 +60,6 @@ const Reducer = undoable(
"layoutChoice", "layoutChoice",
"centroidLabels", "centroidLabels",
"genesets", "genesets",
"genesetsUI",
"annotations", "annotations",
], ],
undoableConfig undoableConfig