category and label name validation updates (#1056)

* update label validation with new schema

* add new error messages for new label errors; general cleanup
This commit is contained in:
Bruce Martin
2019-11-22 10:32:00 -08:00
committed by GitHub
parent 9aaaad6709
commit 5713aa8792
4 changed files with 176 additions and 160 deletions
@@ -67,42 +67,42 @@ class Categories extends React.Component {
return false if this is a LEGAL/acceptable category name or NULL/empty string, return false if this is a LEGAL/acceptable category name or NULL/empty string,
or return an error type. or return an error type.
*/ */
if (!name) return false;
/* allow empty string */
if (name === "") return false;
const { categoricalSelection } = this.props; const { categoricalSelection } = this.props;
const allCategoryNames = Object.keys(categoricalSelection); const allCategoryNames = Object.keys(categoricalSelection);
/* check category name syntax */
const error = AnnotationsHelpers.annotationNameIsErroneous(name);
if (error) {
return error;
}
/* disallow duplicates */
if (allCategoryNames.indexOf(name) !== -1) { if (allCategoryNames.indexOf(name) !== -1) {
return "duplicate"; return "duplicate";
} }
if (!AnnotationsHelpers.isLegalAnnotationName(name)) { /* otherwise, no error */
return "characters";
}
return false; return false;
}; };
categoryNameErrorMessage = name => { categoryNameErrorMessage = name => {
const err = this.categoryNameError(name); const err = this.categoryNameError(name);
if (err === false) return null; if (err === false) return null;
if (err === "duplicate") {
return ( const errorMessageMap = {
<span> /* map error code to human readable error message */
<span style={{ fontStyle: "italic" }}>{name}</span> already exists - "empty-string": "Blank names not allowed",
no duplicates allowed duplicate: "Name must be unique",
</span> "trim-spaces": "Leading and trailing spaces not allowed",
); "illegal-characters": "Only alphanumeric, underscore and period allowed",
} "multi-space-run": "Multiple consecutive spaces not allowed"
if (err === "characters") { };
return ( const errorMessage = errorMessageMap[err] ?? "error";
<span> return <span>{errorMessage}</span>;
<span style={{ fontStyle: "italic" }}>{name}</span> contains illegal
characters. Hint: use alpha-numeric and underscore
</span>
);
}
return err;
}; };
render() { render() {
+67 -79
View File
@@ -158,25 +158,30 @@ class Category extends React.Component {
return false if this is a LEGAL/acceptable category name or NULL/empty string, return false if this is a LEGAL/acceptable category name or NULL/empty string,
or return an error type. or return an error type.
*/ */
let error = false;
if (name) {
const { metadataField, universe } = this.props;
const { obsByName } = universe.schema.annotations;
if (obsByName[metadataField].categories.indexOf(name) !== -1) { /* allow empty string */
error = "duplicate"; if (name === "") return false;
} else if (!AnnotationsHelpers.isLegalAnnotationName(name)) {
error = "characters"; /* check for label syntax errors */
} const error = AnnotationsHelpers.annotationNameIsErroneous(name);
} if (error) return 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;
}; };
labelNameErrorMessage = name => { labelNameErrorMessage = name => {
const { metadataField } = this.props; const { metadataField } = this.props;
const err = this.labelNameError(name); const err = this.labelNameError(name);
if (err === false) return null;
if (err === "duplicate") { if (err === "duplicate") {
/* duplicate error is special cased because it has special formatting */
return ( return (
<span> <span>
<span style={{ fontStyle: "italic" }}>{name}</span> already exists <span style={{ fontStyle: "italic" }}>{name}</span> already exists
@@ -185,15 +190,23 @@ class Category extends React.Component {
</span> </span>
); );
} }
if (err === "characters") {
return ( if (err) {
<span> /* all other errors - map code to human error message */
<span style={{ fontStyle: "italic" }}>{name}</span> contains illegal const errorMessageMap = {
characters. Hint: use alpha-numeric and underscore "empty-string": "Blank names not allowed",
</span> 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 <span>{errorMessage}</span>;
} }
return err;
/* no error, no message generated */
return null;
}; };
categoryNameErrorMessage = () => { categoryNameErrorMessage = () => {
@@ -201,76 +214,51 @@ class Category extends React.Component {
const err = this.editedCategoryNameError(); const err = this.editedCategoryNameError();
if (err === false) return null; if (err === false) return null;
let markup = null; const errorMessageMap = {
/* map error code to human readable error message */
if (err === "empty_string") { "empty-string": "Blank names not allowed",
markup = ( duplicate: "Category name must be unique",
<span "trim-spaces": "Leading and trailing spaces not allowed",
style={{ "illegal-characters": "Only alphanumeric, underscore and period allowed",
display: "block", "multi-space-run": "Multiple consecutive spaces not allowed"
fontStyle: "italic", };
fontSize: 12, const errorMessage = errorMessageMap[err] ?? "error";
marginTop: 5, return (
color: Colors.ORANGE3 <span
}} style={{
> display: "block",
{"Category name cannot be blank"} fontStyle: "italic",
</span> fontSize: 12,
); marginTop: 5,
} else if (err === "already_exists") { color: Colors.ORANGE3
markup = ( }}
<span >
style={{ {errorMessage}
display: "block", </span>
fontStyle: "italic", );
fontSize: 12,
marginTop: 5,
color: Colors.ORANGE3
}}
>
{"Category name must be unique"}
</span>
);
} else if (err === "characters") {
markup = (
<span
style={{
display: "block",
fontStyle: "italic",
fontSize: 12,
marginTop: 5,
color: Colors.ORANGE3
}}
>
{"Only alphanumeric and underscore allowed"}
</span>
);
}
return markup;
}; };
editedCategoryNameError = () => { editedCategoryNameError = () => {
const { metadataField, categoricalSelection } = this.props; const { metadataField, categoricalSelection } = this.props;
const { newCategoryText } = this.state; const { newCategoryText } = this.state;
const allCategoryNames = _.keys(categoricalSelection);
const isEmptyString = newCategoryText === ""; /* check for syntax errors in category name */
const error = AnnotationsHelpers.annotationNameIsErroneous(newCategoryText);
if (error) {
return error;
}
/* check for duplicative categories */
const allCategoryNames = _.keys(categoricalSelection);
const categoryNameAlreadyExists = const categoryNameAlreadyExists =
allCategoryNames.indexOf(newCategoryText) > -1; allCategoryNames.indexOf(newCategoryText) > -1;
const sameName = newCategoryText === metadataField; const sameName = newCategoryText === metadataField;
if (categoryNameAlreadyExists && !sameName) {
let error = false; return "duplicate";
if (isEmptyString) {
error = "empty_string";
} else if (categoryNameAlreadyExists && !sameName) {
error = "already_exists";
} else if (!AnnotationsHelpers.isLegalAnnotationName(newCategoryText)) {
error = "characters";
} }
return error; /* otherwise, no error */
return false;
}; };
toggleAll() { toggleAll() {
+56 -56
View File
@@ -10,7 +10,8 @@ import {
Popover, Popover,
Position, Position,
PopoverInteractionKind, PopoverInteractionKind,
Tooltip Tooltip,
Colors
} from "@blueprintjs/core"; } from "@blueprintjs/core";
import Occupancy from "./occupancy"; import Occupancy from "./occupancy";
import * as globals from "../../globals"; import * as globals from "../../globals";
@@ -85,75 +86,58 @@ class CategoryValue extends React.Component {
valueNameErrorMessage = () => { valueNameErrorMessage = () => {
const { editedLabelText } = this.state; const { editedLabelText } = this.state;
const err = this.valueNameError(); const err = this.valueNameError();
if (!err) return null; if (err === false) return null;
let markup = null; const errorMessageMap = {
/* map error code to human readable error message */
if (err === "empty_string") { "empty-string": "Blank names not allowed",
markup = ( duplicate: "Label must be unique",
<span "trim-spaces": "Leading and trailing spaces not allowed",
style={{ "illegal-characters": "Only alphanumeric, underscore and period allowed",
fontStyle: "italic", "multi-space-run": "Multiple consecutive spaces not allowed"
fontSize: 12, };
marginTop: 5, const errorMessage = errorMessageMap[err] ?? "error";
color: Colors.ORANGE3 return (
}} <span
> style={{
{"Label cannot be blank"} fontStyle: "italic",
</span> fontSize: 12,
); marginTop: 5,
} else if (err === "duplicate") { color: Colors.ORANGE3
markup = ( }}
<span >
style={{ {errorMessage}
fontStyle: "italic", </span>
fontSize: 12, );
marginTop: 5,
color: Colors.ORANGE3
}}
>
{"Label must be unique"}
</span>
);
} else if (err === "characters") {
markup = (
<span
style={{
fontStyle: "italic",
fontSize: 12,
marginTop: 5,
color: Colors.ORANGE3
}}
>
{"Only alphanumeric and underscore allowed"}
</span>
);
}
return markup;
}; };
valueNameError = () => { valueNameError = () => {
const { editedLabelText } = this.state; const { editedLabelText } = this.state;
const { categoricalSelection, metadataField, categoryIndex } = this.props; const { categoricalSelection, metadataField, categoryIndex } = this.props;
let err = null; /*
check label syntax
*/
const err = AnnotationsHelpers.annotationNameIsErroneous(editedLabelText);
if (err) return err;
/*
disallow duplicates
*/
const category = categoricalSelection[metadataField]; const category = categoricalSelection[metadataField];
const displayString = String( const displayString = String(
category.categoryValues[categoryIndex] category.categoryValues[categoryIndex]
).valueOf(); ).valueOf();
if (
if (editedLabelText === "") {
err = "empty_string";
} else if (
category.categoryValues.indexOf(editedLabelText) > -1 && category.categoryValues.indexOf(editedLabelText) > -1 &&
editedLabelText !== displayString editedLabelText !== displayString
) { )
err = "duplicate"; return "duplicate";
} else if (!AnnotationsHelpers.isLegalAnnotationName(editedLabelText)) {
err = "characters"; /*
} otherwise, all good!
return err; */
return false;
}; };
activateEditLabelMode = () => { activateEditLabelMode = () => {
@@ -221,6 +205,21 @@ 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 = () => { toggleOn = () => {
const { dispatch, metadataField, categoryIndex } = this.props; const { dispatch, metadataField, categoryIndex } = this.props;
dispatch({ dispatch({
@@ -546,6 +545,7 @@ class CategoryValue extends React.Component {
data-testclass="handleEditValue" data-testclass="handleEditValue"
data-testid={`handleEditValue-${metadataField}`} data-testid={`handleEditValue-${metadataField}`}
onClick={this.activateEditLabelMode} onClick={this.activateEditLabelMode}
disabled={annotations.isEditingLabelName}
/> />
) : null} ) : null}
{displayString !== globals.unassignedCategoryLabel ? ( {displayString !== globals.unassignedCategoryLabel ? (
@@ -183,7 +183,35 @@ export function createWritableAnnotationDimensions(world, crossfilter) {
return crossfilter; return crossfilter;
} }
const legalNames = /^\w+$/; const legalCharacters = /^(\w|[ .])+$/;
export function isLegalAnnotationName(name) { export function annotationNameIsErroneous(name) {
return legalNames.test(name); /*
Validate the name - return:
* false - a valid name
* string - a named error, indicating why it was invalid.
Tests:
0. must be string, non-null
1. no leading or trailing spaces
2. only accept alpha, numeric, underscore, period and space
3. no runs of multiple spaces
*/
if (name === "") {
return "empty-string";
}
if (name[0] === " " || name[name.length - 1] === " ") {
return "trim-spaces";
}
if (!legalCharacters.test(name)) {
return "illegal-characters";
}
for (let i = 1, l = name.length; i < l; i += 1) {
if (name[i] === " " && name[i - 1] === " ") {
return "multi-space-run";
}
}
/* all is well! Indicte not erroneous with a false */
return false;
} }