From d48647a6551daf59784681135fdaa2b386bce2bd Mon Sep 17 00:00:00 2001 From: Colin Megill Date: Thu, 23 Jan 2020 17:04:17 -0500 Subject: [PATCH] Ontologies (#1110) * add sample ontologies file * add ontologies reducer * Move select category to own component * Dialog and Input factored out * refactoring categorical, partway * validationn * anno * suggest populates input * frontend for ontology working * initial implementation of back-end support for ontologies * edit is now dialog again * autosuggest working on edit * part way through create arbitrary label * handle choice in function * pass duplicate cat prop * editing works * update test to match new CLI params * fix occupancy alignment * edit category as dialogue * secondary button * remove stubbed out ontologies * add label setting upon new label creation * Update legal characters for labels (#1119) * Allow any term in the ontology (bypass legal name check) * Add hyphens and parens to legal characters in names * improve performance for large ontologies * correctly handle case where ontologies are disabled * fix logic error in CLI Co-authored-by: Bruce Martin * PR cleanup 1 * lint * validate user generated labels * finish hooking up connected suggest component * protect against undefined callbacks * Fix illegal characters error message * break out npm run commands * fix error detection on label edit Co-authored-by: Bruce Martin Co-authored-by: Sidney Bell --- client/package.json | 3 +- .../src/components/categorical/annoDialog.js | 91 ++++ .../src/components/categorical/annoInputs.js | 159 +++++++ .../src/components/categorical/annoSelect.js | 57 +++ .../src/components/categorical/categorical.js | 153 ++---- client/src/components/categorical/category.js | 271 +++++------ .../src/components/categorical/labelUtil.js | 57 +++ client/src/components/categorical/value.js | 443 +++++++++--------- client/src/reducers/annotations.js | 1 - client/src/reducers/crossfilter.js | 7 + client/src/reducers/index.js | 8 +- client/src/reducers/ontology.js | 29 ++ client/src/reducers/universe.js | 73 ++- client/src/reducers/world.js | 55 ++- .../util/stateManager/annotationsHelpers.js | 5 +- server/app/scanpy_engine/scanpy_engine.py | 5 + server/app/util/ontology.py | 37 ++ server/cli/launch.py | 38 ++ server/requirements.txt | 1 + server/test/test_scanpy_engine_data_load.py | 3 + 20 files changed, 964 insertions(+), 532 deletions(-) create mode 100644 client/src/components/categorical/annoDialog.js create mode 100644 client/src/components/categorical/annoInputs.js create mode 100644 client/src/components/categorical/annoSelect.js create mode 100644 client/src/components/categorical/labelUtil.js create mode 100644 client/src/reducers/ontology.js create mode 100644 server/app/util/ontology.py diff --git a/client/package.json b/client/package.json index b442740c..21b472bf 100644 --- a/client/package.json +++ b/client/package.json @@ -6,7 +6,8 @@ "repository": "https://github.com/chanzuckerberg/cellxgene", "scripts": { "backend-dev": "python3.6 -m venv cellxgene && source cellxgene/bin/activate && yes | pip uninstall cellxgene || true && pip install -e .. && cellxgene launch ", - "backend-dev-anno": "python3.6 -m venv cellxgene && source cellxgene/bin/activate && yes | pip uninstall cellxgene || true && pip install -e .. && cellxgene launch --experimental-annotations ", + "backend-dev-anno-ontology": "python3.6 -m venv cellxgene && source cellxgene/bin/activate && yes | pip uninstall cellxgene || true && pip install -e .. && cellxgene launch --experimental-annotations --experimental-annotations-ontology", + "backend-dev-anno": "python3.6 -m venv cellxgene && source cellxgene/bin/activate && yes | pip uninstall cellxgene || true && pip install -e .. && cellxgene launch --experimental-annotations", "build": "npm run clean && webpack --config configuration/webpack/webpack.config.prod.js", "clean": "rimraf build", "dev": "npm run clean && webpack --config configuration/webpack/webpack.config.dev.js", diff --git a/client/src/components/categorical/annoDialog.js b/client/src/components/categorical/annoDialog.js new file mode 100644 index 00000000..2e66dc21 --- /dev/null +++ b/client/src/components/categorical/annoDialog.js @@ -0,0 +1,91 @@ +import React from "react"; +import { connect } from "react-redux"; +import { Button, Tooltip, Dialog, Classes, Colors } from "@blueprintjs/core"; + +@connect(state => ({ + colorAccessor: state.colors.colorAccessor, + categoricalSelection: state.categoricalSelection, + annotations: state.annotations, + universe: state.universe, + ontology: state.ontology, + ontologyLoading: state.ontology?.loading +})) +class AnnoDialog extends React.Component { + constructor(props) { + super(props); + this.state = {}; + } + + render() { + const { + isActive, + text, + title, + instruction, + cancelTooltipContent, + errorMessage, + validationError, + annoSelect, + annoInput, + handleCancel, + handleSubmit, + primaryButtonText, + secondaryButtonText, + handleSecondaryButtonSubmit + } = this.props; + + return ( + +
{ + e.preventDefault(); + }} + > +
+
+

{instruction}

+ {annoInput || null} +

+ {errorMessage} +

+
+ {annoSelect || null} +
+
+
+ + + + {handleSecondaryButtonSubmit && secondaryButtonText ? ( + + ) : null} + +
+
+
+
+ ); + } +} + +export default AnnoDialog; diff --git a/client/src/components/categorical/annoInputs.js b/client/src/components/categorical/annoInputs.js new file mode 100644 index 00000000..17d02fc8 --- /dev/null +++ b/client/src/components/categorical/annoInputs.js @@ -0,0 +1,159 @@ +import React from "react"; +import { connect } from "react-redux"; +import { InputGroup, MenuItem } from "@blueprintjs/core"; +import { Suggest } from "@blueprintjs/select"; +import fuzzysort from "fuzzysort"; + +const filterOntology = (query, ontology) => + /* fires on load, once, and then for each character typed into the input */ + fuzzysort.go(query, ontology, { + limit: 100, + threshold: -10000 // don't return bad results + }); + +const renderListItem = (fuzzySortResult, { handleClick, modifiers }) => { + if (!modifiers.matchesPredicate) { + return null; + } + /* the fuzzysort wraps the object with other properties, like a score */ + const geneName = fuzzySortResult.target; + + return ( + + /* this fires when user clicks a menu item */ + handleClick(g) + } + text={geneName} + /> + ); +}; + +const AnnoSuggest = props => { + const { + ontologyLoading, + handleItemChange, + handleChoice, + ontology, + handleCreateArbitraryLabel, + handleTextChange, + isTextInvalid, + isTextInvalidErrorMessage + } = props; + return ( + {}} + createNewItemRenderer={userInputStr => { + return isTextInvalid?.(userInputStr) ? ( + + ) : ( + { + handleCreateArbitraryLabel(userInputStr); + }} + shouldDismissPopover={false} + /> + ); + }} + itemDisabled={ontologyLoading ? () => true : () => false} + noResults={} + onItemSelect={handleChoice} + initialContent={ + + } + inputProps={{ "data-testid": "gene-search" }} + inputValueRenderer={t => t.target} + itemListPredicate={filterOntology} + onActiveItemChange={handleItemChange} + itemRenderer={renderListItem.bind(this)} + items={!ontologyLoading && ontology ? ontology : ["No ontology loaded"]} + popoverProps={{ minimal: true }} + onQueryChange={(s, e) => { + // undefined event means resetOnSelect + if (e !== undefined) handleTextChange?.(s); + }} + /> + ); +}; + +const VanillaInput = props => { + const { text, handleTextChange } = props; + return ( + handleTextChange?.(e.target.value)} + leftIcon="tag" + /> + ); +}; + +@connect(state => ({ + colorAccessor: state.colors.colorAccessor, + categoricalSelection: state.categoricalSelection, + annotations: state.annotations, + universe: state.universe, + world: state.world, + ontology: state.ontology?.terms, + ontologyLoading: state.ontology?.loading +})) +class AnnoInputs extends React.Component { + constructor(props) { + super(props); + this.state = {}; + } + + render() { + const { + useSuggest, + handleTextChange, + text, + ontologyLoading, + world, + handleChoice, + handleItemChange, + handleCreateArbitraryLabel, + ontology, + isTextInvalid, + isTextInvalidErrorMessage + } = this.props; + return ( +
+ {/* Le sigh https://github.com/palantir/blueprint/issues/2864 */} + {useSuggest ? ( + + ) : ( + + )} +
+ ); + } +} + +export default AnnoInputs; diff --git a/client/src/components/categorical/annoSelect.js b/client/src/components/categorical/annoSelect.js new file mode 100644 index 00000000..fe121c36 --- /dev/null +++ b/client/src/components/categorical/annoSelect.js @@ -0,0 +1,57 @@ +import React from "react"; +import { connect } from "react-redux"; +import { Button, MenuItem } from "@blueprintjs/core"; +import { Select } from "@blueprintjs/select"; + +@connect(state => ({ + colorAccessor: state.colors.colorAccessor, + categoricalSelection: state.categoricalSelection, + annotations: state.annotations, + universe: state.universe, + ontology: state.ontology, + ontologyLoading: state.ontology?.loading +})) +class DuplicateCategorySelect extends React.Component { + constructor(props) { + super(props); + this.state = {}; + } + + render() { + const { + allCategoryNames, + categoryToDuplicate, + handleModalDuplicateCategorySelection + } = this.props; + return ( +
+

+ Optionally duplicate all labels & cell assignments from existing + category into new category: +

+ +
+ ); + } +} + +export default DuplicateCategorySelect; diff --git a/client/src/components/categorical/categorical.js b/client/src/components/categorical/categorical.js index 31f3d370..5e80057a 100644 --- a/client/src/components/categorical/categorical.js +++ b/client/src/components/categorical/categorical.js @@ -1,20 +1,13 @@ // jshint esversion: 6 import React from "react"; -import _ from "lodash"; -import { - Button, - Tooltip, - InputGroup, - Dialog, - Classes, - MenuItem, - Colors -} from "@blueprintjs/core"; -import { Select } from "@blueprintjs/select"; +import { Button } from "@blueprintjs/core"; import { connect } from "react-redux"; import * as globals from "../../globals"; import Category from "./category"; import { AnnotationsHelpers } from "../../util/stateManager"; +import AnnoDialog from "./annoDialog"; +import AnnoInputs from "./annoInputs"; +import AnnoSelect from "./annoSelect"; @connect(state => ({ categoricalSelection: state.categoricalSelection, @@ -71,7 +64,7 @@ class Categories extends React.Component { /* allow empty string */ if (name === "") return false; - /* + /* test for uniqueness against *all* annotation names, not just the subset we render as categorical. */ @@ -102,13 +95,25 @@ class Categories extends React.Component { "empty-string": "Blank names not allowed", duplicate: "Name must be unique", "trim-spaces": "Leading and trailing spaces not allowed", - "illegal-characters": "Only alphanumeric, underscore and period allowed", + "illegal-characters": + "Only alphanumeric and special characters (-_.) allowed", "multi-space-run": "Multiple consecutive spaces not allowed" }; const errorMessage = errorMessageMap[err] ?? "error"; return {errorMessage}; }; + handleNewCategoryText = txt => { + this.setState({ newCategoryText: txt }); + }; + + handleChoice = e => { + /* Blueprint Suggest format */ + this.setState({ newCategoryText: e.target }); + }; + + handleSuggestActiveItemChange = () => {}; + render() { const { createAnnoModeActive, @@ -131,9 +136,39 @@ class Categories extends React.Component { padding: globals.leftSidebarSectionPadding }} > + + } + annoSelect={ + + } + /> + {/* READ ONLY CATEGORICAL FIELDS */} {/* this is duplicative but flat, could be abstracted */} - {_.map(allCategoryNames, catName => + {allCategoryNames.map(catName => !schema.annotations.obsByName[catName].writable ? ( + {allCategoryNames.map(catName => schema.annotations.obsByName[catName].writable ? ( - -
{ - e.preventDefault(); - }} - > -
-
-

New, unique category name:

- - this.setState({ newCategoryText: e.target.value }) - } - leftIcon="tag" - /> -

- {this.categoryNameErrorMessage(newCategoryText)} -

-
- -

- Optionally duplicate all labels & cell assignments from - existing category into new category: -

- -
-
-
- - - - -
-
-
-
diff --git a/client/src/components/categorical/category.js b/client/src/components/categorical/category.js index 4522af5f..dffbf95e 100644 --- a/client/src/components/categorical/category.js +++ b/client/src/components/categorical/category.js @@ -6,28 +6,30 @@ import { Flipper, Flipped } from "react-flip-toolkit"; import { Button, Tooltip, - InputGroup, Menu, - Dialog, MenuItem, Popover, - Classes, Icon, Position, PopoverInteractionKind, Colors } from "@blueprintjs/core"; +import AnnoDialog from "./annoDialog"; +import AnnoInputs from "./annoInputs"; import * as globals from "../../globals"; import Value from "./value"; -import sortedCategoryLabels from "../../util/catLabelSort"; import { AnnotationsHelpers } from "../../util/stateManager"; +import { labelErrorMessage, isLabelErroneous } from "./labelUtil"; @connect(state => ({ colorAccessor: state.colors.colorAccessor, categoricalSelection: state.categoricalSelection, annotations: state.annotations, - universe: state.universe + universe: state.universe, + ontology: state.ontology, + ontologyLoading: state.ontology?.loading, + ontologyEnabled: state.ontology?.enabled })) class Category extends React.Component { constructor(props) { @@ -91,14 +93,48 @@ class Category extends React.Component { handleAddNewLabelToCategory = () => { const { dispatch, metadataField } = this.props; const { newLabelText } = this.state; + dispatch({ type: "annotation: add new label to category", metadataField, - newLabelText + newLabelText, + assignSelectedCells: false }); this.setState({ newLabelText: "" }); }; + addLabelAndAssignCells = () => { + const { dispatch, metadataField } = this.props; + const { newLabelText } = this.state; + + dispatch({ + type: "annotation: add new label to category", + metadataField, + newLabelText, + assignSelectedCells: true + }); + + this.setState({ newLabelText: "" }); + }; + + handleCreateArbitraryLabel = newLabelTextNotInOntology => { + const { dispatch, metadataField } = this.props; + + dispatch({ + type: "annotation: add new label to category", + metadataField, + newLabelText: newLabelTextNotInOntology, + assignSelectedCells: false + }); + this.setState({ newLabelText: "" }); + }; + + handleCategoryEditTextChange = txt => { + this.setState({ + newCategoryText: txt + }); + }; + activateEditCategoryMode = () => { const { dispatch, metadataField } = this.props; @@ -154,63 +190,16 @@ class Category extends React.Component { }; labelNameError = name => { - /* - return false if this is a LEGAL/acceptable category name or NULL/empty string, - or return an error type. - */ - - /* allow empty string */ - if (name === "") return false; - - /* check for label syntax errors */ - const error = AnnotationsHelpers.annotationNameIsErroneous(name); - if (error) return error; - - /* disallow duplicates */ - const { metadataField, universe } = this.props; - const { obsByName } = universe.schema.annotations; - if (obsByName[metadataField].categories.indexOf(name) !== -1) - return "duplicate"; - - /* otherwise, no error */ - return false; + const { metadataField, ontology, universe } = this.props; + return isLabelErroneous(name, metadataField, ontology, universe.schema); }; labelNameErrorMessage = name => { - const { metadataField } = this.props; - const err = this.labelNameError(name); - - if (err === "duplicate") { - /* duplicate error is special cased because it has special formatting */ - return ( - - {name} already exists - already exists within{" "} - {metadataField}{" "} - - ); - } - - if (err) { - /* all other errors - map code to human error message */ - const errorMessageMap = { - "empty-string": "Blank names not allowed", - duplicate: "Name must be unique", - "trim-spaces": "Leading and trailing spaces not allowed", - "illegal-characters": - "Only alphanumeric, underscore and period allowed", - "multi-space-run": "Multiple consecutive spaces not allowed" - }; - const errorMessage = errorMessageMap[err] ?? "error"; - return {errorMessage}; - } - - /* no error, no message generated */ - return null; + const { metadataField, ontology, universe } = this.props; + return labelErrorMessage(name, metadataField, ontology, universe.schema); }; categoryNameErrorMessage = () => { - const { newCategoryText } = this.state; const err = this.editedCategoryNameError(); if (err === false) return null; @@ -219,7 +208,8 @@ class Category extends React.Component { "empty-string": "Blank names not allowed", duplicate: "Category name must be unique", "trim-spaces": "Leading and trailing spaces not allowed", - "illegal-characters": "Only alphanumeric, underscore and period allowed", + "illegal-characters": + "Only alphanumeric and special characters (-_.) allowed", "multi-space-run": "Multiple consecutive spaces not allowed" }; const errorMessage = errorMessageMap[err] ?? "error"; @@ -261,6 +251,15 @@ class Category extends React.Component { return false; }; + /* leaky to have both of these in multiple components */ + handleChoice = e => { + this.setState({ newLabelText: e.target }); + }; + + handleTextChange = text => { + this.setState({ newLabelText: text }); + }; + toggleAll() { const { dispatch, metadataField } = this.props; dispatch({ @@ -318,13 +317,13 @@ class Category extends React.Component { colorAccessor, categoricalSelection, isUserAnno, - annotations + annotations, + ontologyEnabled } = this.props; const { isTruncated } = categoricalSelection[metadataField]; const cat = categoricalSelection[metadataField]; const optTuples = [...cat.categoryValueIndices]; const optTuplesAsKey = _.map(optTuples, t => t[0]).join(""); // animation - const allCategoryNames = _.keys(categoricalSelection); return (
) : null} - {annotations.isEditingCategoryName && - annotations.categoryBeingEdited === metadataField ? ( -
{ - e.preventDefault(); - this.handleEditCategory(); - }} - > - { - this.editableCategoryInput = input; - }} - small - autoFocus - onChange={e => { - this.setState({ - newCategoryText: e.target.value - }); - }} - defaultValue={metadataField} - rightElement={ - - - -
- - - + title="Add new label to category" + instruction="New, unique label name:" + cancelTooltipContent="Close this dialog without adding a label." + primaryButtonText="Add" + secondaryButtonText="Add label & assign currently selected cells" + handleSecondaryButtonSubmit={this.addLabelAndAssignCells} + text={newLabelText} + validationError={this.labelNameError(newLabelText)} + errorMessage={this.labelNameErrorMessage(newLabelText)} + handleSubmit={this.handleAddNewLabelToCategory} + handleCancel={this.disableAddNewLabelMode} + annoInput={ + + } + /> + {label} already + exists already exists within{" "} + {metadataField}{" "} + + ); + } + + if (err) { + /* all other errors - map code to human error message */ + const errorMessageMap = { + "empty-string": "Blank names not allowed", + duplicate: "Name must be unique", + "trim-spaces": "Leading and trailing spaces not allowed", + "illegal-characters": + "Only alphanumeric and special characters (-_.) allowed", + "multi-space-run": "Multiple consecutive spaces not allowed" + }; + const errorMessage = errorMessageMap[err] ?? "error"; + return {errorMessage}; + } + + /* no error, no message generated */ + return null; +} diff --git a/client/src/components/categorical/value.js b/client/src/components/categorical/value.js index fe9a9a17..60633d78 100644 --- a/client/src/components/categorical/value.js +++ b/client/src/components/categorical/value.js @@ -4,7 +4,6 @@ import React from "react"; import { Button, - InputGroup, Menu, MenuItem, Popover, @@ -16,8 +15,11 @@ import { import Occupancy from "./occupancy"; import * as globals from "../../globals"; import styles from "./categorical.css"; +import AnnoDialog from "./annoDialog"; +import AnnoInputs from "./annoInputs"; import { AnnotationsHelpers } from "../../util/stateManager"; +import { labelErrorMessage, isLabelErroneous } from "./labelUtil"; @connect(state => ({ categoricalSelection: state.categoricalSelection, @@ -27,7 +29,10 @@ import { AnnotationsHelpers } from "../../util/stateManager"; pointDilation: state.pointDilation, schema: state.world?.schema, world: state.world, - crossfilter: state.crossfilter + crossfilter: state.crossfilter, + ontology: state.ontology, + ontologyLoading: state.ontology?.loading, + ontologyEnabled: state.ontology?.enabled })) class CategoryValue extends React.Component { constructor(props) { @@ -41,13 +46,20 @@ class CategoryValue extends React.Component { }; } - getLabel = () => { - const { metadataField, categoryIndex, categoricalSelection } = this.props; - const category = categoricalSelection[metadataField]; - const label = category.categoryValues[categoryIndex]; - - return label; - }; + componentDidUpdate(prevProps) { + const { categoricalSelection, metadataField, categoryIndex } = this.props; + if ( + prevProps.categoricalSelection !== categoricalSelection || + prevProps.metadataField !== metadataField || + prevProps.categoryIndex !== categoryIndex + ) { + this.setState({ + editedLabelText: String( + categoricalSelection[metadataField].categoryValues[categoryIndex] + ).valueOf() + }); + } + } handleDeleteValue = () => { const { dispatch, metadataField } = this.props; @@ -84,60 +96,49 @@ class CategoryValue extends React.Component { }); }; - valueNameErrorMessage = () => { - const err = this.valueNameError(); - if (err === false) return null; + handleCreateArbitraryLabel = editedLabelTextNotInOntology => { + const { dispatch, metadataField, categoryIndex } = this.props; + const label = this.getLabel(); - const errorMessageMap = { - /* map error code to human readable error message */ - "empty-string": "Blank names not allowed", - duplicate: "Label must be unique", - "trim-spaces": "Leading and trailing spaces not allowed", - "illegal-characters": "Only alphanumeric, underscore and period allowed", - "multi-space-run": "Multiple consecutive spaces not allowed" - }; - const errorMessage = errorMessageMap[err] ?? "error"; - return ( - - {errorMessage} - - ); + dispatch({ + type: "annotation: label edited", + metadataField, + editedLabel: editedLabelTextNotInOntology, + categoryIndex, + label + }); }; - valueNameError = () => { - const { editedLabelText } = this.state; - const { categoricalSelection, metadataField, categoryIndex } = this.props; - - /* - check label syntax - */ - const err = AnnotationsHelpers.annotationNameIsErroneous(editedLabelText); - if (err) return err; - - /* - disallow duplicates - */ + labelNameError = name => { + const { + metadataField, + ontology, + schema, + categoricalSelection, + categoryIndex + } = this.props; const category = categoricalSelection[metadataField]; const displayString = String( category.categoryValues[categoryIndex] ).valueOf(); - if ( - category.categoryValues.indexOf(editedLabelText) > -1 && - editedLabelText !== displayString - ) - return "duplicate"; + if (name === displayString) return false; + return isLabelErroneous(name, metadataField, ontology, schema); + }; - /* - otherwise, all good! - */ - return false; + labelNameErrorMessage = name => { + const { + metadataField, + ontology, + schema, + categoricalSelection, + categoryIndex + } = this.props; + const category = categoricalSelection[metadataField]; + const displayString = String( + category.categoryValues[categoryIndex] + ).valueOf(); + if (name === displayString) return null; + return labelErrorMessage(name, metadataField, ontology, schema); }; activateEditLabelMode = () => { @@ -208,21 +209,6 @@ class CategoryValue extends React.Component { ); }; - componentDidUpdate(prevProps) { - const { categoricalSelection, metadataField, categoryIndex } = this.props; - if ( - prevProps.categoricalSelection !== categoricalSelection || - prevProps.metadataField !== metadataField || - prevProps.categoryIndex !== categoryIndex - ) { - this.setState({ - editedLabelText: String( - categoricalSelection[metadataField].categoryValues[categoryIndex] - ).valueOf() - }); - } - } - toggleOn = () => { const { dispatch, metadataField, categoryIndex } = this.props; dispatch({ @@ -250,6 +236,23 @@ class CategoryValue extends React.Component { }); }; + handleTextChange = text => { + this.setState({ editedLabelText: text }); + }; + + handleChoice = e => { + /* Blueprint Suggest format */ + this.setState({ editedLabelText: e.target }); + }; + + getLabel = () => { + const { metadataField, categoryIndex, categoricalSelection } = this.props; + const category = categoricalSelection[metadataField]; + const label = category.categoryValues[categoryIndex]; + + return label; + }; + isAddCurrentSelectionDisabled(category, value) { /* disable "add current selection to label", if one of the following is true: @@ -289,6 +292,7 @@ class CategoryValue extends React.Component { schema, isUserAnno, annotations, + ontologyEnabled, // flippedProps is potentially brittle, their docs want {...flippedProps} on our div, // our lint doesn't like jsx spread, we are version pinned to prevent api change on their part flippedProps, @@ -339,6 +343,12 @@ class CategoryValue extends React.Component { )}`; } + const editModeActive = + isUserAnno && + annotations.labelEditable.category === metadataField && + annotations.isEditingLabelName && + annotations.labelEditable.label === categoryIndex; + return (
- {annotations.isEditingLabelName && - annotations.labelEditable.category === metadataField && - annotations.labelEditable.label === categoryIndex - ? null - : truncatedString || displayString} + {truncatedString || displayString} - {isUserAnno && - annotations.labelEditable.category === metadataField && - annotations.isEditingLabelName && - annotations.labelEditable.label === categoryIndex ? ( -
{ - e.preventDefault(); - if (this.valueNameError()) { - return; - } - this.handleEditValue(); - }} - > - { - this.editableInput = input; - }} - small - autoFocus - intent={this.valueNameError() ? "warning" : "none"} - onChange={e => { - this.setState({ editedLabelText: e.target.value }); - }} - defaultValue={displayString} - rightElement={ -
) : null} - {/* - CANCEL IT, WITH BUTTON, ESCAPE KEY, CLICK OUT, UNDO? - -