Files
cellxgene/client/src/components/categorical/categorical.js
T
Bruce Martin 7bc58bba2b undo/redo cleanup (#1165)
* fix refactoring error which disabled annotation file clearing

* fix undo behavior on add category label

* fix various undo/redo bugs

* remove logging

* further refinement of annotation undo/redo and actions

* address PR comment
2020-02-24 18:51:26 -07:00

215 lines
6.1 KiB
JavaScript

// jshint esversion: 6
import React from "react";
import { Button } 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 AnnoInputs from "./annoInputs";
import AnnoSelect from "./annoSelect";
@connect(state => ({
writableCategoriesEnabled: state.config?.parameters?.["annotations"] ?? false,
schema: state.world?.schema,
config: state.config
}))
class Categories extends React.Component {
constructor(props) {
super(props);
this.state = {
createAnnoModeActive: false,
newCategoryText: "",
categoryToDuplicate: null
};
}
handleCreateUserAnno = e => {
const { dispatch } = this.props;
const { newCategoryText, categoryToDuplicate } = this.state;
dispatch({
type: "annotation: create category",
data: newCategoryText,
categoryToDuplicate
});
this.setState({
createAnnoModeActive: false,
categoryToDuplicate: null,
newCategoryText: ""
});
e.preventDefault();
};
handleEnableAnnoMode = () => {
this.setState({ createAnnoModeActive: true });
};
handleDisableAnnoMode = () => {
this.setState({
createAnnoModeActive: false,
categoryToDuplicate: null,
newCategoryText: ""
});
};
handleModalDuplicateCategorySelection = d => {
this.setState({ categoryToDuplicate: d });
};
categoryNameError = 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;
/*
test for uniqueness against *all* annotation names, not just the subset
we render as categorical.
*/
const { schema } = this.props;
const allCategoryNames = schema.annotations.obs.columns.map(c => c.name);
/* check category name syntax */
const error = AnnotationsHelpers.annotationNameIsErroneous(name);
if (error) {
return error;
}
/* disallow duplicates */
if (allCategoryNames.indexOf(name) !== -1) {
return "duplicate";
}
/* otherwise, no error */
return false;
};
categoryNameErrorMessage = name => {
const err = this.categoryNameError(name);
if (err === false) return null;
const errorMessageMap = {
/* map error code to human readable error message */
"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 <span>{errorMessage}</span>;
};
handleNewCategoryText = txt => {
this.setState({ newCategoryText: txt });
};
handleChoice = e => {
/* Blueprint Suggest format */
this.setState({ newCategoryText: e.target });
};
handleSuggestActiveItemChange = () => {};
render() {
const {
createAnnoModeActive,
categoryToDuplicate,
newCategoryText
} = this.state;
const {
categoricalSelection,
writableCategoriesEnabled,
schema,
config
} = this.props;
/* all names, sorted in display order. Will be rendered in this order */
const allCategoryNames = ControlsHelpers.selectableCategoryNames(
schema,
ControlsHelpers.maxCategoryItems(config)
).sort();
return (
<div
style={{
padding: globals.leftSidebarSectionPadding
}}
>
<AnnoDialog
isActive={createAnnoModeActive}
title="Create new category"
instruction="New, unique category name:"
cancelTooltipContent="Close this dialog without creating a category."
primaryButtonText="Create new category"
primaryButtonProps={{ "data-testid": "submit-category" }}
text={newCategoryText}
validationError={this.categoryNameError(newCategoryText)}
errorMessage={this.categoryNameErrorMessage(newCategoryText)}
handleSubmit={this.handleCreateUserAnno}
handleCancel={this.handleDisableAnnoMode}
annoInput={
<AnnoInputs
text={newCategoryText}
inputProps={{ "data-testid": "new-category-name" }}
handleItemChange={this.handleSuggestActiveItemChange}
handleChoice={this.handleChoice}
handleTextChange={this.handleNewCategoryText}
/>
}
annoSelect={
<AnnoSelect
handleModalDuplicateCategorySelection={
this.handleModalDuplicateCategorySelection
}
categoryToDuplicate={categoryToDuplicate}
allCategoryNames={allCategoryNames}
/>
}
/>
{/* READ ONLY CATEGORICAL FIELDS */}
{/* this is duplicative but flat, could be abstracted */}
{allCategoryNames.map(catName =>
!schema.annotations.obsByName[catName].writable ? (
<Category
key={catName}
metadataField={catName}
createAnnoModeActive={createAnnoModeActive}
isUserAnno={false}
/>
) : null
)}
{/* WRITEABLE FIELDS */}
{allCategoryNames.map(catName =>
schema.annotations.obsByName[catName].writable ? (
<Category
key={catName}
metadataField={catName}
createAnnoModeActive={createAnnoModeActive}
isUserAnno
/>
) : null
)}
{writableCategoriesEnabled ? (
<div>
<Button
data-testid="open-annotation-dialog"
onClick={this.handleEnableAnnoMode}
intent="primary"
>
Create new category
</Button>
</div>
) : null}
</div>
);
}
}
export default Categories;