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,
or return an error type.
*/
if (!name) return false;
/* allow empty string */
if (name === "") return false;
const { categoricalSelection } = this.props;
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) {
return "duplicate";
}
if (!AnnotationsHelpers.isLegalAnnotationName(name)) {
return "characters";
}
/* otherwise, no error */
return false;
};
categoryNameErrorMessage = name => {
const err = this.categoryNameError(name);
if (err === false) return null;
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;
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, underscore and period allowed",
"multi-space-run": "Multiple consecutive spaces not allowed"
};
const errorMessage = errorMessageMap[err] ?? "error";
return <span>{errorMessage}</span>;
};
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,
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) {
error = "duplicate";
} else if (!AnnotationsHelpers.isLegalAnnotationName(name)) {
error = "characters";
}
}
return error;
/* 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;
};
labelNameErrorMessage = name => {
const { metadataField } = this.props;
const err = this.labelNameError(name);
if (err === false) return null;
if (err === "duplicate") {
/* duplicate error is special cased because it has special formatting */
return (
<span>
<span style={{ fontStyle: "italic" }}>{name}</span> already exists
@@ -185,15 +190,23 @@ class Category extends React.Component {
</span>
);
}
if (err === "characters") {
return (
<span>
<span style={{ fontStyle: "italic" }}>{name}</span> contains illegal
characters. Hint: use alpha-numeric and underscore
</span>
);
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 <span>{errorMessage}</span>;
}
return err;
/* no error, no message generated */
return null;
};
categoryNameErrorMessage = () => {
@@ -201,76 +214,51 @@ class Category extends React.Component {
const err = this.editedCategoryNameError();
if (err === false) return null;
let markup = null;
if (err === "empty_string") {
markup = (
<span
style={{
display: "block",
fontStyle: "italic",
fontSize: 12,
marginTop: 5,
color: Colors.ORANGE3
}}
>
{"Category name cannot be blank"}
</span>
);
} else if (err === "already_exists") {
markup = (
<span
style={{
display: "block",
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;
const errorMessageMap = {
/* map error code to human readable error message */
"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",
"multi-space-run": "Multiple consecutive spaces not allowed"
};
const errorMessage = errorMessageMap[err] ?? "error";
return (
<span
style={{
display: "block",
fontStyle: "italic",
fontSize: 12,
marginTop: 5,
color: Colors.ORANGE3
}}
>
{errorMessage}
</span>
);
};
editedCategoryNameError = () => {
const { metadataField, categoricalSelection } = this.props;
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 =
allCategoryNames.indexOf(newCategoryText) > -1;
const sameName = newCategoryText === metadataField;
let error = false;
if (isEmptyString) {
error = "empty_string";
} else if (categoryNameAlreadyExists && !sameName) {
error = "already_exists";
} else if (!AnnotationsHelpers.isLegalAnnotationName(newCategoryText)) {
error = "characters";
if (categoryNameAlreadyExists && !sameName) {
return "duplicate";
}
return error;
/* otherwise, no error */
return false;
};
toggleAll() {
+56 -56
View File
@@ -10,7 +10,8 @@ import {
Popover,
Position,
PopoverInteractionKind,
Tooltip
Tooltip,
Colors
} from "@blueprintjs/core";
import Occupancy from "./occupancy";
import * as globals from "../../globals";
@@ -85,75 +86,58 @@ class CategoryValue extends React.Component {
valueNameErrorMessage = () => {
const { editedLabelText } = this.state;
const err = this.valueNameError();
if (!err) return null;
if (err === false) return null;
let markup = null;
if (err === "empty_string") {
markup = (
<span
style={{
fontStyle: "italic",
fontSize: 12,
marginTop: 5,
color: Colors.ORANGE3
}}
>
{"Label cannot be blank"}
</span>
);
} else if (err === "duplicate") {
markup = (
<span
style={{
fontStyle: "italic",
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;
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 (
<span
style={{
fontStyle: "italic",
fontSize: 12,
marginTop: 5,
color: Colors.ORANGE3
}}
>
{errorMessage}
</span>
);
};
valueNameError = () => {
const { editedLabelText } = this.state;
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 displayString = String(
category.categoryValues[categoryIndex]
).valueOf();
if (editedLabelText === "") {
err = "empty_string";
} else if (
if (
category.categoryValues.indexOf(editedLabelText) > -1 &&
editedLabelText !== displayString
) {
err = "duplicate";
} else if (!AnnotationsHelpers.isLegalAnnotationName(editedLabelText)) {
err = "characters";
}
return err;
)
return "duplicate";
/*
otherwise, all good!
*/
return false;
};
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 = () => {
const { dispatch, metadataField, categoryIndex } = this.props;
dispatch({
@@ -546,6 +545,7 @@ class CategoryValue extends React.Component {
data-testclass="handleEditValue"
data-testid={`handleEditValue-${metadataField}`}
onClick={this.activateEditLabelMode}
disabled={annotations.isEditingLabelName}
/>
) : null}
{displayString !== globals.unassignedCategoryLabel ? (
@@ -183,7 +183,35 @@ export function createWritableAnnotationDimensions(world, crossfilter) {
return crossfilter;
}
const legalNames = /^\w+$/;
export function isLegalAnnotationName(name) {
return legalNames.test(name);
const legalCharacters = /^(\w|[ .])+$/;
export function annotationNameIsErroneous(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;
}