mirror of
https://github.com/chanzuckerberg/cellxgene.git
synced 2026-10-02 08:28:12 +08:00
Gene sets UI, right sidebar refactor (#2097)
* prototyping
* render histos on open gene set
* prototyping
* render histos on open gene set
* factor out add genes to own component
* remove unused import
* mock reducer
* color by geneset stub
* menus and buttons
* geneset dialogue stub
* remove heatmap mock
* componetize histogram
* reenable add genes
* re-add isuserdefined
* test data
* remove have fetched
* add isExpanded state to gene, and pass to histogram
* expand button
* toggleable
* mini
* bump number of genes to 50
* don't clear diffexp on subset
* move create category to top
* render diffexp as geneset
* geneset show mean expression
* gene set reducer
* add geneset UI reducer
* wire e2e gene set loading prototype
* fix sniffing bug
* fix typo
* add gene modals
* client/src/actions/
* add autosave
* rename data-dir cli param
* add geneset, add gene, delete set
* prototype: remove csv upload placeholder
* handle delete gene from set
* prepopulate geneset with genes from modal
* add geneset: rename action
* icons, language consistency
* chevron after
* handle empty string case on genes for create geneset
* edit geneset
* fix language on create
* copy correction
* add popper2
upgrade react popper
upgrade react popper
adding popover2 package
* truncate uses tooltip2
* gene set button text typo
* remove logging
* moving server over
* remove test imports
* don't try to destructure map, use array.from
* fix add gene map datastructure error
* Revert "fix add gene map datastructure error"
This reverts commit b0eed45952.
* name --> genesetName, genes --> geneSymbols
* add gene to geneset, temporary format
* handle empty case, clear form input
* lint -- genesets wasn't passed via props
* userinfo
* move genes string to object conversion to action
* remove tmp gene description
* emptystring default for description
* remove empty string
* remove top level package json
* remove package lock as well
* remove flag for feature toggle
* remove comments in geneset
* comment cleanup
* remove comment
* revert diffexp genes to 10
* color by gene set
* disable color by gene set
* Gene menus are now inline, remove dead prototype code
* remove todo, magic number to variable
* remove jshint in rightsidebar
Co-authored-by: Severiano Badajoz <sbadajoz@chanzuckerberg.com>
* remove unused geneset validation code
* tmp format pending geneset description
* move magic number into variable
* reorganize genesetsUI reducer pending tests
* rewire edit given new action name
* add basic validation and feedback for geneset name uniqueness
* mv annoDialog
* mv label, repair paths
* Update client/src/components/brushableHistogram/header.js
Co-authored-by: Severiano Badajoz <sbadajoz@chanzuckerberg.com>
* add imports for icon in histo
* update jest snapshots given blueprint/tooltip2 usage of index -1
* ensure no empty paragraph
* intent from blueprint
* remove remainder of jshint references
* do not push undo when autosave fires
* fix autosave bugs
* remove todos
* clamp to util
* scient to util
* revert clearing diffexp
* rename value to be more specific stacked bar
* clean up logging and commetns
* remove gene entry tests pending rewrite
* tab index -1
* update jest snapshot, blueprint tooltip 2
* caret margin
* snapshot update
* ensure histogram is centered
* add geneset actions to config
* comment maybeScientific
* comment clamp
* comment ui reducer
* remove prototype code
* remove error log
* remove references to bl.ocks
* componetize parseBulkGeneString
* catch case where geneset rename same name
* genesetui reducer tests
* add geneset ui to index reducer config
Co-authored-by: bkmartinjr <bruce@chanzuckerberg.com>
Co-authored-by: Severiano Badajoz <sbadajoz@chanzuckerberg.com>
This commit is contained in:
co-authored by
Severiano Badajoz
bkmartinjr
parent
5335c39184
commit
e6e358ddc8
@@ -1,84 +0,0 @@
|
||||
import React from "react";
|
||||
import { Button, Tooltip, Dialog, Classes, Colors } from "@blueprintjs/core";
|
||||
|
||||
class AnnoDialog extends React.PureComponent {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {};
|
||||
}
|
||||
|
||||
render() {
|
||||
const {
|
||||
isActive,
|
||||
text,
|
||||
title,
|
||||
instruction,
|
||||
cancelTooltipContent,
|
||||
errorMessage,
|
||||
validationError,
|
||||
annoSelect,
|
||||
annoInput,
|
||||
handleCancel,
|
||||
handleSubmit,
|
||||
primaryButtonText,
|
||||
secondaryButtonText,
|
||||
handleSecondaryButtonSubmit,
|
||||
primaryButtonProps,
|
||||
} = this.props;
|
||||
|
||||
return (
|
||||
<Dialog icon="tag" title={title} isOpen={isActive} onClose={handleCancel}>
|
||||
<form
|
||||
onSubmit={(e) => {
|
||||
e.preventDefault();
|
||||
}}
|
||||
>
|
||||
<div className={Classes.DIALOG_BODY}>
|
||||
<div style={{ marginBottom: 20 }}>
|
||||
<p>{instruction}</p>
|
||||
{annoInput || null}
|
||||
<p
|
||||
style={{
|
||||
marginTop: 7,
|
||||
visibility: validationError ? "visible" : "hidden",
|
||||
color: Colors.ORANGE3,
|
||||
}}
|
||||
>
|
||||
{errorMessage}
|
||||
</p>
|
||||
</div>
|
||||
{annoSelect || null}
|
||||
</div>
|
||||
<div className={Classes.DIALOG_FOOTER}>
|
||||
<div className={Classes.DIALOG_FOOTER_ACTIONS}>
|
||||
<Tooltip content={cancelTooltipContent}>
|
||||
<Button onClick={handleCancel}>Cancel</Button>
|
||||
</Tooltip>
|
||||
{handleSecondaryButtonSubmit && secondaryButtonText ? (
|
||||
<Button
|
||||
onClick={handleSecondaryButtonSubmit}
|
||||
disabled={!text || validationError}
|
||||
intent="none"
|
||||
type="button"
|
||||
>
|
||||
{secondaryButtonText}
|
||||
</Button>
|
||||
) : null}
|
||||
<Button
|
||||
{...primaryButtonProps} // eslint-disable-line react/jsx-props-no-spreading -- Spreading props allows for modularity
|
||||
onClick={handleSubmit}
|
||||
disabled={!text || validationError}
|
||||
intent="primary"
|
||||
type="submit"
|
||||
>
|
||||
{primaryButtonText}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default AnnoDialog;
|
||||
@@ -1,7 +1,7 @@
|
||||
import React from "react";
|
||||
import { connect } from "react-redux";
|
||||
import AnnoDialog from "../annoDialog";
|
||||
import LabelInput from "../labelInput";
|
||||
import AnnoDialog from "../../annoDialog";
|
||||
import LabelInput from "../../labelInput";
|
||||
import { labelPrompt, isLabelErroneous } from "../labelUtil";
|
||||
import actions from "../../../actions";
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import React from "react";
|
||||
import { connect } from "react-redux";
|
||||
import AnnoDialog from "../annoDialog";
|
||||
import LabelInput from "../labelInput";
|
||||
import AnnoDialog from "../../annoDialog";
|
||||
import LabelInput from "../../labelInput";
|
||||
import { labelPrompt } from "../labelUtil";
|
||||
|
||||
import { AnnotationsHelpers } from "../../../util/stateManager";
|
||||
|
||||
@@ -9,7 +9,9 @@ import {
|
||||
Tooltip,
|
||||
Icon,
|
||||
PopoverInteractionKind,
|
||||
Intent,
|
||||
} from "@blueprintjs/core";
|
||||
import { IconNames } from "@blueprintjs/icons";
|
||||
|
||||
import * as globals from "../../../globals";
|
||||
import actions from "../../../actions";
|
||||
@@ -89,8 +91,8 @@ class AnnoMenuCategory extends React.PureComponent {
|
||||
text={editText}
|
||||
/>
|
||||
<MenuItem
|
||||
icon="delete"
|
||||
intent="danger"
|
||||
icon={IconNames.TRASH}
|
||||
intent={Intent.DANGER}
|
||||
data-testclass="handleDeleteCategory"
|
||||
data-testid={`${metadataField}:delete-category`}
|
||||
onClick={this.handleDeleteCategory}
|
||||
|
||||
@@ -1,13 +1,12 @@
|
||||
// jshint esversion: 6
|
||||
import React from "react";
|
||||
import { AnchorButton, Tooltip, Position } from "@blueprintjs/core";
|
||||
import { connect } from "react-redux";
|
||||
import * as globals from "../../globals";
|
||||
import Category from "./category";
|
||||
import { AnnotationsHelpers, ControlsHelpers } from "../../util/stateManager";
|
||||
import AnnoDialog from "./annoDialog";
|
||||
import AnnoDialog from "../annoDialog";
|
||||
import AnnoSelect from "./annoSelect";
|
||||
import LabelInput from "./labelInput";
|
||||
import LabelInput from "../labelInput";
|
||||
import { labelPrompt } from "./labelUtil";
|
||||
import actions from "../../actions";
|
||||
|
||||
@@ -182,6 +181,35 @@ class Categories extends React.Component {
|
||||
}
|
||||
/>
|
||||
|
||||
{writableCategoriesEnabled ? (
|
||||
<div style={{ marginBottom: 10 }}>
|
||||
<Tooltip
|
||||
content={
|
||||
userInfo.is_authenticated
|
||||
? "Create a new category"
|
||||
: "You must be logged in to create new categorical fields"
|
||||
}
|
||||
position={Position.RIGHT}
|
||||
boundary="viewport"
|
||||
hoverOpenDelay={globals.tooltipHoverOpenDelay}
|
||||
modifiers={{
|
||||
preventOverflow: { enabled: false },
|
||||
hide: { enabled: false },
|
||||
}}
|
||||
>
|
||||
<AnchorButton
|
||||
type="button"
|
||||
data-testid="open-annotation-dialog"
|
||||
onClick={this.handleEnableAnnoMode}
|
||||
intent="primary"
|
||||
disabled={!userInfo.is_authenticated}
|
||||
>
|
||||
Create new <strong>category</strong>
|
||||
</AnchorButton>
|
||||
</Tooltip>
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{/* READ ONLY CATEGORICAL FIELDS */}
|
||||
{/* this is duplicative but flat, could be abstracted */}
|
||||
{allCategoryNames.map((catName) =>
|
||||
@@ -209,33 +237,6 @@ class Categories extends React.Component {
|
||||
/>
|
||||
) : null
|
||||
)}
|
||||
|
||||
{writableCategoriesEnabled ? (
|
||||
<Tooltip
|
||||
content={
|
||||
userInfo.is_authenticated
|
||||
? "Create a new category"
|
||||
: "You must be logged in to create new categorical fields"
|
||||
}
|
||||
position={Position.RIGHT}
|
||||
boundary="viewport"
|
||||
hoverOpenDelay={globals.tooltipHoverOpenDelay}
|
||||
modifiers={{
|
||||
preventOverflow: { enabled: false },
|
||||
hide: { enabled: false },
|
||||
}}
|
||||
>
|
||||
<AnchorButton
|
||||
type="button"
|
||||
data-testid="open-annotation-dialog"
|
||||
onClick={this.handleEnableAnnoMode}
|
||||
intent="primary"
|
||||
disabled={!userInfo.is_authenticated}
|
||||
>
|
||||
Create new category
|
||||
</AnchorButton>
|
||||
</Tooltip>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,178 +0,0 @@
|
||||
import React from "react";
|
||||
import { InputGroup, MenuItem, Keys } from "@blueprintjs/core";
|
||||
import { Suggest } from "@blueprintjs/select";
|
||||
import fuzzysort from "fuzzysort";
|
||||
|
||||
export default class LabelInput extends React.PureComponent {
|
||||
/*
|
||||
Input widget for text labels, which acts like an InputGroup, but will also
|
||||
accept a suggestion list (of labels), with sublime-like suggest search.
|
||||
|
||||
Properties:
|
||||
|
||||
* labelSuggestions -- array of suggested lables. May be array of string or
|
||||
other objects. If array of objects, specify `labelKey`. If null, suggestion
|
||||
mode is disabled.
|
||||
* onSelect -- optional, callback upon selection of item from labelSuggestions.
|
||||
(label) => void
|
||||
* onChange -- optional, callback upon change in text input. (label) => void
|
||||
* label -- component value, for controlled use
|
||||
* newLabelMessage -- text to display when user enters a label not in labelSuggestions
|
||||
(not used if suggestion mode disabled)
|
||||
* inputProps -- will be passed to InputGroup
|
||||
* popoverProps -- will be passed to <Suggest>
|
||||
*/
|
||||
|
||||
/* maxinum number of suggestions */
|
||||
static QueryResultLimit = 100;
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
const { label } = props;
|
||||
const query = label || "";
|
||||
const queryResults = this.filterLabels(query);
|
||||
this.state = {
|
||||
query,
|
||||
queryResults,
|
||||
};
|
||||
}
|
||||
|
||||
handleQueryChange = (query, event) => {
|
||||
// https://github.com/palantir/blueprint/issues/2983
|
||||
if (!event) return;
|
||||
|
||||
const queryResults = this.filterLabels(query);
|
||||
this.setState({
|
||||
query,
|
||||
queryResults,
|
||||
});
|
||||
|
||||
const { onChange } = this.props;
|
||||
if (onChange) onChange(query, event);
|
||||
};
|
||||
|
||||
handleItemSelect = (item, event) => {
|
||||
/* only report the select if not already reported via onChange() */
|
||||
const { target } = item;
|
||||
const { query } = this.state;
|
||||
const { onSelect } = this.props;
|
||||
if (target !== query && onSelect) onSelect(target, event);
|
||||
};
|
||||
|
||||
handleKeyDown = (e) => {
|
||||
/*
|
||||
prevent these events from propagating to containing form/dialog
|
||||
and causing further side effects (eg, closing dialog, submitting
|
||||
form, etc).
|
||||
*/
|
||||
const { keyCode } = e;
|
||||
if (keyCode === Keys.ENTER || keyCode === Keys.ESCAPE) {
|
||||
e.preventDefault();
|
||||
}
|
||||
if (keyCode === Keys.ESCAPE) {
|
||||
e.stopPropagation();
|
||||
}
|
||||
};
|
||||
|
||||
handleChange = (e) => {
|
||||
const { onChange } = this.props;
|
||||
if (onChange) onChange(e.target.value);
|
||||
};
|
||||
|
||||
renderLabelSuggestion = (queryResult, { handleClick, modifiers }) => {
|
||||
if (queryResult.newLabel) {
|
||||
const { newLabelMessage } = this.props;
|
||||
return (
|
||||
<MenuItem
|
||||
icon="flag"
|
||||
active={modifiers.active}
|
||||
disabled={modifiers.disabled}
|
||||
key={queryResult.target}
|
||||
onClick={handleClick}
|
||||
text={<em>{queryResult.target}</em>}
|
||||
label={newLabelMessage || "New label"}
|
||||
/>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<MenuItem
|
||||
active={modifiers.active}
|
||||
disabled={modifiers.disabled}
|
||||
key={queryResult.target}
|
||||
onClick={handleClick}
|
||||
text={queryResult.target}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
filterLabels(query) {
|
||||
const { labelSuggestions } = this.props;
|
||||
if (!labelSuggestions) return [];
|
||||
|
||||
/* empty query is wildcard */
|
||||
if (query === "") {
|
||||
return labelSuggestions
|
||||
.slice(0, LabelInput.QueryResultLimit)
|
||||
.map((l) => ({
|
||||
target: l,
|
||||
score: -10000,
|
||||
}));
|
||||
}
|
||||
|
||||
/* else, do a fuzzy query */
|
||||
const options = {
|
||||
limit: LabelInput.QueryResultLimit,
|
||||
threshold: -10000, // don't return bad results
|
||||
};
|
||||
let queryResults = fuzzysort.go(query, labelSuggestions, options);
|
||||
/* exact match will always be first in list */
|
||||
if (query !== "" && queryResults[0]?.target !== query)
|
||||
queryResults = [{ target: query, newLabel: true }, ...queryResults];
|
||||
|
||||
return queryResults;
|
||||
}
|
||||
|
||||
render() {
|
||||
const { props } = this;
|
||||
const { labelSuggestions, label, autoFocus = true } = props;
|
||||
const suggestEnabled = !!labelSuggestions && labelSuggestions.length > 0;
|
||||
|
||||
if (!suggestEnabled) {
|
||||
return (
|
||||
<InputGroup
|
||||
autoFocus={autoFocus}
|
||||
{...props.inputProps} // eslint-disable-line react/jsx-props-no-spreading --- Allows for modularity
|
||||
value={label}
|
||||
onChange={this.handleChange}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
const popoverProps = {
|
||||
minimal: true,
|
||||
...props.popoverProps,
|
||||
};
|
||||
const inputProps = {
|
||||
...props.inputProps,
|
||||
autoFocus: false,
|
||||
};
|
||||
const { queryResults } = this.state;
|
||||
return (
|
||||
<>
|
||||
<Suggest
|
||||
fill
|
||||
inputValueRenderer={(i) => i.target}
|
||||
items={queryResults}
|
||||
itemRenderer={this.renderLabelSuggestion}
|
||||
onItemSelect={this.handleItemSelect}
|
||||
query={label}
|
||||
onQueryChange={this.handleQueryChange}
|
||||
popoverProps={popoverProps}
|
||||
inputProps={inputProps}
|
||||
onKeyDown={this.handleKeyDown}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -14,8 +14,8 @@ import {
|
||||
} from "@blueprintjs/core";
|
||||
import * as globals from "../../../globals";
|
||||
import styles from "../categorical.css";
|
||||
import AnnoDialog from "../annoDialog";
|
||||
import LabelInput from "../labelInput";
|
||||
import AnnoDialog from "../../annoDialog";
|
||||
import LabelInput from "../../labelInput";
|
||||
import Truncate from "../../util/truncate";
|
||||
|
||||
import { AnnotationsHelpers } from "../../../util/stateManager";
|
||||
@@ -25,8 +25,8 @@ import MiniHistogram from "../../miniHistogram";
|
||||
import MiniStackedBar from "../../miniStackedBar";
|
||||
import { CategoryCrossfilterContext } from "../categoryContext";
|
||||
|
||||
const VALUE_HEIGHT = 11;
|
||||
const CHART_WIDTH = 100;
|
||||
const STACKED_BAR_HEIGHT = 11;
|
||||
const STACKED_BAR_WIDTH = 100;
|
||||
|
||||
/* this is defined outside of the class so we can use it in connect() */
|
||||
function _currentLabelAsString(ownProps) {
|
||||
@@ -410,7 +410,7 @@ class CategoryValue extends React.Component {
|
||||
label,
|
||||
colorTable,
|
||||
schema,
|
||||
CHART_WIDTH
|
||||
STACKED_BAR_WIDTH
|
||||
) ?? {};
|
||||
|
||||
if (!domainValues || !scale || !domain || !occupancy) {
|
||||
@@ -426,8 +426,8 @@ class CategoryValue extends React.Component {
|
||||
domain,
|
||||
occupancy,
|
||||
}}
|
||||
height={VALUE_HEIGHT}
|
||||
width={CHART_WIDTH}
|
||||
height={STACKED_BAR_HEIGHT}
|
||||
width={STACKED_BAR_WIDTH}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@@ -460,8 +460,8 @@ class CategoryValue extends React.Component {
|
||||
colorAccessor,
|
||||
colorData,
|
||||
label,
|
||||
CHART_WIDTH,
|
||||
VALUE_HEIGHT
|
||||
STACKED_BAR_WIDTH,
|
||||
STACKED_BAR_HEIGHT
|
||||
) ?? {}; // if createHistogramBins returns empty object assign null to deconstructed
|
||||
|
||||
if (!xScale || !yScale || !bins) return null;
|
||||
@@ -476,8 +476,8 @@ class CategoryValue extends React.Component {
|
||||
}}
|
||||
obsOrVarContinuousFieldDisplayName={colorAccessor}
|
||||
domainLabel={label}
|
||||
height={VALUE_HEIGHT}
|
||||
width={CHART_WIDTH}
|
||||
height={STACKED_BAR_HEIGHT}
|
||||
width={STACKED_BAR_WIDTH}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@@ -534,7 +534,7 @@ class CategoryValue extends React.Component {
|
||||
colorAccessor && !isColorBy
|
||||
? globals.leftSidebarWidth -
|
||||
otherElementsWidth -
|
||||
CHART_WIDTH -
|
||||
STACKED_BAR_WIDTH -
|
||||
CHART_MARGIN
|
||||
: globals.leftSidebarWidth - otherElementsWidth;
|
||||
|
||||
@@ -677,8 +677,8 @@ class CategoryValue extends React.Component {
|
||||
display={isColorBy && categoryValueIndices ? "auto" : "none"}
|
||||
style={{
|
||||
marginLeft: 5,
|
||||
width: VALUE_HEIGHT,
|
||||
height: VALUE_HEIGHT,
|
||||
width: 15,
|
||||
height: 15,
|
||||
backgroundColor:
|
||||
isColorBy && categoryValueIndices
|
||||
? colorScale(categoryValueIndices.get(label))
|
||||
@@ -739,7 +739,7 @@ class CategoryValue extends React.Component {
|
||||
) : null}
|
||||
{displayString !== globals.unassignedCategoryLabel ? (
|
||||
<MenuItem
|
||||
icon="delete"
|
||||
icon="trash"
|
||||
intent="danger"
|
||||
data-testclass="handleDeleteValue"
|
||||
data-testid={`${metadataField}:${displayString}:delete-label`}
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
// jshint esversion: 6
|
||||
import React from "react";
|
||||
import { connect } from "react-redux";
|
||||
import * as d3 from "d3";
|
||||
|
||||
Reference in New Issue
Block a user