mirror of
https://github.com/chanzuckerberg/cellxgene.git
synced 2026-10-06 13:38:12 +08:00
improve category and label name validation (#1034)
This commit is contained in:
@@ -14,6 +14,7 @@ import { Select } from "@blueprintjs/select";
|
|||||||
import { connect } from "react-redux";
|
import { connect } from "react-redux";
|
||||||
import * as globals from "../../globals";
|
import * as globals from "../../globals";
|
||||||
import Category from "./category";
|
import Category from "./category";
|
||||||
|
import { AnnotationsHelpers } from "../../util/stateManager";
|
||||||
|
|
||||||
@connect(state => ({
|
@connect(state => ({
|
||||||
categoricalSelection: state.categoricalSelection,
|
categoricalSelection: state.categoricalSelection,
|
||||||
@@ -50,13 +51,57 @@ class Categories extends React.Component {
|
|||||||
};
|
};
|
||||||
|
|
||||||
handleDisableAnnoMode = () => {
|
handleDisableAnnoMode = () => {
|
||||||
this.setState({ createAnnoModeActive: false });
|
this.setState({
|
||||||
|
createAnnoModeActive: false,
|
||||||
|
categoryToDuplicate: null,
|
||||||
|
newCategoryText: ""
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
handleModalDuplicateCategorySelection = d => {
|
handleModalDuplicateCategorySelection = d => {
|
||||||
this.setState({ categoryToDuplicate: d });
|
this.setState({ categoryToDuplicate: d });
|
||||||
};
|
};
|
||||||
|
|
||||||
|
categoryNameError = name => {
|
||||||
|
/*
|
||||||
|
return false if this is a LEGAL/acceptable category name,
|
||||||
|
or return an error type.
|
||||||
|
*/
|
||||||
|
const { categoricalSelection } = this.props;
|
||||||
|
const allCategoryNames = Object.keys(categoricalSelection);
|
||||||
|
|
||||||
|
if (allCategoryNames.indexOf(name) !== -1) {
|
||||||
|
return "duplicate";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!AnnotationsHelpers.isLegalAnnotationName(name)) {
|
||||||
|
return "characters";
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
|
||||||
|
categoryNameErrorMessage = name => {
|
||||||
|
const err = this.categoryNameError(name);
|
||||||
|
if (err == "duplicate") {
|
||||||
|
return (
|
||||||
|
<span>
|
||||||
|
<span style={{ fontStyle: "italic" }}>{name}</span> already exists -
|
||||||
|
no duplicates allowed
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (err == "characters") {
|
||||||
|
return (
|
||||||
|
<span>
|
||||||
|
<span style={{ fontStyle: "italic" }}>{name}</span> contains illegal
|
||||||
|
characters. Hint: use alpha-numeric and underscore
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return err;
|
||||||
|
};
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const {
|
const {
|
||||||
createAnnoModeActive,
|
createAnnoModeActive,
|
||||||
@@ -81,30 +126,26 @@ class Categories extends React.Component {
|
|||||||
>
|
>
|
||||||
{/* READ ONLY CATEGORICAL FIELDS */}
|
{/* READ ONLY CATEGORICAL FIELDS */}
|
||||||
{/* this is duplicative but flat, could be abstracted */}
|
{/* this is duplicative but flat, could be abstracted */}
|
||||||
{_.map(
|
{_.map(allCategoryNames, catName =>
|
||||||
allCategoryNames,
|
!schema.annotations.obsByName[catName].writable ? (
|
||||||
catName =>
|
<Category
|
||||||
!schema.annotations.obsByName[catName].writable ? (
|
key={catName}
|
||||||
<Category
|
metadataField={catName}
|
||||||
key={catName}
|
createAnnoModeActive={createAnnoModeActive}
|
||||||
metadataField={catName}
|
isUserAnno={false}
|
||||||
createAnnoModeActive={createAnnoModeActive}
|
/>
|
||||||
isUserAnno={false}
|
) : null
|
||||||
/>
|
|
||||||
) : null
|
|
||||||
)}
|
)}
|
||||||
{/* WRITEABLE FIELDS */}
|
{/* WRITEABLE FIELDS */}
|
||||||
{_.map(
|
{_.map(allCategoryNames, catName =>
|
||||||
allCategoryNames,
|
schema.annotations.obsByName[catName].writable ? (
|
||||||
catName =>
|
<Category
|
||||||
schema.annotations.obsByName[catName].writable ? (
|
key={catName}
|
||||||
<Category
|
metadataField={catName}
|
||||||
key={catName}
|
createAnnoModeActive={createAnnoModeActive}
|
||||||
metadataField={catName}
|
isUserAnno
|
||||||
createAnnoModeActive={createAnnoModeActive}
|
/>
|
||||||
isUserAnno
|
) : null
|
||||||
/>
|
|
||||||
) : null
|
|
||||||
)}
|
)}
|
||||||
{writableCategoriesEnabled ? (
|
{writableCategoriesEnabled ? (
|
||||||
<div>
|
<div>
|
||||||
@@ -124,8 +165,10 @@ class Categories extends React.Component {
|
|||||||
<p>New, unique category name:</p>
|
<p>New, unique category name:</p>
|
||||||
<InputGroup
|
<InputGroup
|
||||||
autoFocus
|
autoFocus
|
||||||
|
value={newCategoryText}
|
||||||
intent={
|
intent={
|
||||||
allCategoryNames.indexOf(newCategoryText) !== -1
|
newCategoryText &&
|
||||||
|
this.categoryNameError(newCategoryText)
|
||||||
? "warning"
|
? "warning"
|
||||||
: "none"
|
: "none"
|
||||||
}
|
}
|
||||||
@@ -138,16 +181,14 @@ class Categories extends React.Component {
|
|||||||
style={{
|
style={{
|
||||||
marginTop: 7,
|
marginTop: 7,
|
||||||
visibility:
|
visibility:
|
||||||
allCategoryNames.indexOf(newCategoryText) !== -1
|
newCategoryText &&
|
||||||
|
this.categoryNameError(newCategoryText)
|
||||||
? "visible"
|
? "visible"
|
||||||
: "hidden",
|
: "hidden",
|
||||||
color: Colors.ORANGE3
|
color: Colors.ORANGE3
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<span style={{ fontStyle: "italic" }}>
|
{this.categoryNameErrorMessage(newCategoryText)}
|
||||||
{newCategoryText}
|
|
||||||
</span>{" "}
|
|
||||||
already exists
|
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -186,9 +227,7 @@ class Categories extends React.Component {
|
|||||||
</Tooltip>
|
</Tooltip>
|
||||||
<Button
|
<Button
|
||||||
onClick={this.handleCreateUserAnno}
|
onClick={this.handleCreateUserAnno}
|
||||||
disabled={
|
disabled={this.categoryNameError(newCategoryText)}
|
||||||
allCategoryNames.indexOf(newCategoryText) !== -1
|
|
||||||
}
|
|
||||||
intent="primary"
|
intent="primary"
|
||||||
type="submit"
|
type="submit"
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ import {
|
|||||||
import * as globals from "../../globals";
|
import * as globals from "../../globals";
|
||||||
import Value from "./value";
|
import Value from "./value";
|
||||||
import sortedCategoryValues from "./util";
|
import sortedCategoryValues from "./util";
|
||||||
|
import { AnnotationsHelpers } from "../../util/stateManager";
|
||||||
|
|
||||||
@connect(state => ({
|
@connect(state => ({
|
||||||
colorAccessor: state.colors.colorAccessor,
|
colorAccessor: state.colors.colorAccessor,
|
||||||
@@ -82,18 +83,14 @@ class Category extends React.Component {
|
|||||||
dispatch({
|
dispatch({
|
||||||
type: "annotation: disable add new label mode"
|
type: "annotation: disable add new label mode"
|
||||||
});
|
});
|
||||||
|
this.setState({
|
||||||
|
newLabelText: ""
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
handleAddNewLabelToCategory = () => {
|
handleAddNewLabelToCategory = () => {
|
||||||
const { dispatch, metadataField } = this.props;
|
const { dispatch, metadataField } = this.props;
|
||||||
const { newLabelText } = this.state;
|
const { newLabelText } = this.state;
|
||||||
/*
|
|
||||||
XXX TODO - temporary code generates random label string. Remove
|
|
||||||
when the label creation UI is implemented.
|
|
||||||
|
|
||||||
const { newLabelText } = this.state;
|
|
||||||
*/
|
|
||||||
// const newLabelText = `label${Math.random()}`;
|
|
||||||
dispatch({
|
dispatch({
|
||||||
type: "annotation: add new label to category",
|
type: "annotation: add new label to category",
|
||||||
metadataField,
|
metadataField,
|
||||||
@@ -146,6 +143,48 @@ class Category extends React.Component {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
labelNameError = name => {
|
||||||
|
/*
|
||||||
|
return false if this is a LEGAL/acceptable category name,
|
||||||
|
or return an error type.
|
||||||
|
*/
|
||||||
|
const { metadataField, universe } = this.props;
|
||||||
|
const obsByName = universe.schema.annotations.obsByName;
|
||||||
|
|
||||||
|
if (obsByName[metadataField].categories.indexOf(name) !== -1) {
|
||||||
|
return "duplicate";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!AnnotationsHelpers.isLegalAnnotationName(name)) {
|
||||||
|
return "characters";
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
|
||||||
|
labelNameErrorMessage = name => {
|
||||||
|
const { metadataField } = this.props;
|
||||||
|
const err = this.labelNameError(name);
|
||||||
|
if (err == "duplicate") {
|
||||||
|
return (
|
||||||
|
<span>
|
||||||
|
<span style={{ fontStyle: "italic" }}>{name}</span> already exists
|
||||||
|
already exists within{" "}
|
||||||
|
<span style={{ fontStyle: "italic" }}>{metadataField}</span>{" "}
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (err == "characters") {
|
||||||
|
return (
|
||||||
|
<span>
|
||||||
|
<span style={{ fontStyle: "italic" }}>{name}</span> contains illegal
|
||||||
|
characters. Hint: use alpha-numeric and underscore
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return err;
|
||||||
|
};
|
||||||
|
|
||||||
toggleAll() {
|
toggleAll() {
|
||||||
const { dispatch, metadataField } = this.props;
|
const { dispatch, metadataField } = this.props;
|
||||||
dispatch({
|
dispatch({
|
||||||
@@ -346,10 +385,12 @@ class Category extends React.Component {
|
|||||||
<p>New, unique label name:</p>
|
<p>New, unique label name:</p>
|
||||||
<InputGroup
|
<InputGroup
|
||||||
autoFocus
|
autoFocus
|
||||||
|
value={newLabelText}
|
||||||
intent={
|
intent={
|
||||||
universe.schema.annotations.obsByName[
|
// universe.schema.annotations.obsByName[
|
||||||
metadataField
|
// metadataField
|
||||||
].categories.indexOf(newLabelText) !== -1
|
// ].categories.indexOf(newLabelText) !== -1
|
||||||
|
newLabelText && this.labelNameError(newLabelText)
|
||||||
? "warning"
|
? "warning"
|
||||||
: "none"
|
: "none"
|
||||||
}
|
}
|
||||||
@@ -362,21 +403,16 @@ class Category extends React.Component {
|
|||||||
style={{
|
style={{
|
||||||
marginTop: 7,
|
marginTop: 7,
|
||||||
visibility:
|
visibility:
|
||||||
universe.schema.annotations.obsByName[
|
// universe.schema.annotations.obsByName[
|
||||||
metadataField
|
// metadataField
|
||||||
].categories.indexOf(newLabelText) !== -1
|
// ].categories.indexOf(newLabelText) !== -1
|
||||||
|
newLabelText && this.labelNameError(newLabelText)
|
||||||
? "visible"
|
? "visible"
|
||||||
: "hidden",
|
: "hidden",
|
||||||
color: Colors.ORANGE3
|
color: Colors.ORANGE3
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<span style={{ fontStyle: "italic" }}>
|
{this.labelNameErrorMessage(newLabelText)}
|
||||||
{newLabelText}
|
|
||||||
</span>{" "}
|
|
||||||
already exists within{" "}
|
|
||||||
<span style={{ fontStyle: "italic" }}>
|
|
||||||
{metadataField}
|
|
||||||
</span>
|
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -390,9 +426,10 @@ class Category extends React.Component {
|
|||||||
<Button
|
<Button
|
||||||
disabled={
|
disabled={
|
||||||
newLabelText.length === 0 ||
|
newLabelText.length === 0 ||
|
||||||
universe.schema.annotations.obsByName[
|
// universe.schema.annotations.obsByName[
|
||||||
metadataField
|
// metadataField
|
||||||
].categories.indexOf(newLabelText) !== -1
|
// ].categories.indexOf(newLabelText) !== -1
|
||||||
|
this.labelNameError(newLabelText)
|
||||||
}
|
}
|
||||||
onClick={this.handleAddNewLabelToCategory}
|
onClick={this.handleAddNewLabelToCategory}
|
||||||
intent="primary"
|
intent="primary"
|
||||||
|
|||||||
@@ -182,3 +182,8 @@ export function createWritableAnnotationDimensions(world, crossfilter) {
|
|||||||
}, crossfilter);
|
}, crossfilter);
|
||||||
return crossfilter;
|
return crossfilter;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const legalNames = /^\w+$/;
|
||||||
|
export function isLegalAnnotationName(name) {
|
||||||
|
return legalNames.test(name);
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user