Anno editing label/category error states (#1048)

* anno error states

* label validation should not treat empty string as error

* disable Submit if no label or category entered

* edit category name error states

* flex start

* pr cleanup

* label name validation fixes

* PR comment responses
This commit is contained in:
Colin Megill
2019-11-21 19:08:29 -05:00
committed by GitHub
parent ee62dd355f
commit c2ededfc62
3 changed files with 227 additions and 67 deletions
@@ -64,9 +64,11 @@ class Categories extends React.Component {
categoryNameError = name => { categoryNameError = name => {
/* /*
return false if this is a LEGAL/acceptable category name, 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;
const { categoricalSelection } = this.props; const { categoricalSelection } = this.props;
const allCategoryNames = Object.keys(categoricalSelection); const allCategoryNames = Object.keys(categoricalSelection);
@@ -83,6 +85,7 @@ class Categories extends React.Component {
categoryNameErrorMessage = name => { categoryNameErrorMessage = name => {
const err = this.categoryNameError(name); const err = this.categoryNameError(name);
if (err === false) return null;
if (err === "duplicate") { if (err === "duplicate") {
return ( return (
<span> <span>
@@ -167,7 +170,6 @@ class Categories extends React.Component {
autoFocus autoFocus
value={newCategoryText} value={newCategoryText}
intent={ intent={
newCategoryText &&
this.categoryNameError(newCategoryText) this.categoryNameError(newCategoryText)
? "warning" ? "warning"
: "none" : "none"
@@ -180,11 +182,9 @@ class Categories extends React.Component {
<p <p
style={{ style={{
marginTop: 7, marginTop: 7,
visibility: visibility: this.categoryNameError(newCategoryText)
newCategoryText && ? "visible"
this.categoryNameError(newCategoryText) : "hidden",
? "visible"
: "hidden",
color: Colors.ORANGE3 color: Colors.ORANGE3
}} }}
> >
@@ -227,7 +227,10 @@ class Categories extends React.Component {
</Tooltip> </Tooltip>
<Button <Button
onClick={this.handleCreateUserAnno} onClick={this.handleCreateUserAnno}
disabled={this.categoryNameError(newCategoryText)} disabled={
!newCategoryText ||
this.categoryNameError(newCategoryText)
}
intent="primary" intent="primary"
type="submit" type="submit"
> >
+110 -31
View File
@@ -35,7 +35,7 @@ class Category extends React.Component {
this.state = { this.state = {
isChecked: true, isChecked: true,
isExpanded: false, isExpanded: false,
newCategoryText: "", newCategoryText: props.metadataField,
newLabelText: "" newLabelText: ""
}; };
} }
@@ -116,9 +116,19 @@ class Category extends React.Component {
}; };
handleEditCategory = () => { handleEditCategory = () => {
const { dispatch, metadataField } = this.props; const { dispatch, metadataField, categoricalSelection } = this.props;
const { newCategoryText } = this.state; const { newCategoryText } = this.state;
const allCategoryNames = _.keys(categoricalSelection);
if (
(allCategoryNames.indexOf(newCategoryText) > -1 &&
newCategoryText !== metadataField) ||
newCategoryText === ""
) {
return;
}
dispatch({ dispatch({
type: "annotation: category edited", type: "annotation: category edited",
metadataField, metadataField,
@@ -145,26 +155,27 @@ class Category extends React.Component {
labelNameError = name => { labelNameError = name => {
/* /*
return false if this is a LEGAL/acceptable category name, return false if this is a LEGAL/acceptable category name or NULL/empty string,
or return an error type. or return an error type.
*/ */
const { metadataField, universe } = this.props; let error = false;
const { obsByName } = universe.schema.annotations; if (name) {
const { metadataField, universe } = this.props;
const { obsByName } = universe.schema.annotations;
if (obsByName[metadataField].categories.indexOf(name) !== -1) { if (obsByName[metadataField].categories.indexOf(name) !== -1) {
return "duplicate"; error = "duplicate";
} else if (!AnnotationsHelpers.isLegalAnnotationName(name)) {
error = "characters";
}
} }
return error;
if (!AnnotationsHelpers.isLegalAnnotationName(name)) {
return "characters";
}
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") {
return ( return (
<span> <span>
@@ -185,6 +196,83 @@ class Category extends React.Component {
return err; return err;
}; };
categoryNameErrorMessage = () => {
const { newCategoryText } = this.state;
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;
};
editedCategoryNameError = () => {
const { metadataField, categoricalSelection } = this.props;
const { newCategoryText } = this.state;
const allCategoryNames = _.keys(categoricalSelection);
const isEmptyString = newCategoryText === "";
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";
}
return error;
};
toggleAll() { toggleAll() {
const { dispatch, metadataField } = this.props; const { dispatch, metadataField } = this.props;
dispatch({ dispatch({
@@ -251,6 +339,7 @@ class Category extends React.Component {
...cat.categoryValueIndices ...cat.categoryValueIndices
]); ]);
const optTuplesAsKey = _.map(optTuples, t => t[0]).join(""); // animation const optTuplesAsKey = _.map(optTuples, t => t[0]).join(""); // animation
const allCategoryNames = _.keys(categoricalSelection);
return ( return (
<div <div
@@ -271,7 +360,7 @@ class Category extends React.Component {
style={{ style={{
display: "flex", display: "flex",
justifyContent: "flex-start", justifyContent: "flex-start",
alignItems: "baseline" alignItems: "flex-start"
}} }}
> >
<label className="bp3-control bp3-checkbox"> <label className="bp3-control bp3-checkbox">
@@ -332,7 +421,7 @@ class Category extends React.Component {
rightElement={ rightElement={
<Button <Button
minimal minimal
disabled={newCategoryText.length === 0} disabled={this.editedCategoryNameError()}
style={{ position: "relative", top: -1 }} style={{ position: "relative", top: -1 }}
type="button" type="button"
icon="small-tick" icon="small-tick"
@@ -342,6 +431,7 @@ class Category extends React.Component {
/> />
} }
/> />
{this.categoryNameErrorMessage()}
</form> </form>
) : ( ) : (
metadataField metadataField
@@ -385,10 +475,7 @@ class Category extends React.Component {
autoFocus autoFocus
value={newLabelText} value={newLabelText}
intent={ intent={
// universe.schema.annotations.obsByName[ this.labelNameError(newLabelText)
// metadataField
// ].categories.indexOf(newLabelText) !== -1
newLabelText && this.labelNameError(newLabelText)
? "warning" ? "warning"
: "none" : "none"
} }
@@ -400,13 +487,9 @@ class Category extends React.Component {
<p <p
style={{ style={{
marginTop: 7, marginTop: 7,
visibility: visibility: this.labelNameError(newLabelText)
// universe.schema.annotations.obsByName[ ? "visible"
// metadataField : "hidden",
// ].categories.indexOf(newLabelText) !== -1
newLabelText && this.labelNameError(newLabelText)
? "visible"
: "hidden",
color: Colors.ORANGE3 color: Colors.ORANGE3
}} }}
> >
@@ -423,11 +506,7 @@ class Category extends React.Component {
</Tooltip> </Tooltip>
<Button <Button
disabled={ disabled={
newLabelText.length === 0 || !newLabelText || this.labelNameError(newLabelText)
// universe.schema.annotations.obsByName[
// metadataField
// ].categories.indexOf(newLabelText) !== -1
this.labelNameError(newLabelText)
} }
onClick={this.handleAddNewLabelToCategory} onClick={this.handleAddNewLabelToCategory}
intent="primary" intent="primary"
+106 -28
View File
@@ -31,19 +31,26 @@ class CategoryValue extends React.Component {
constructor(props) { constructor(props) {
super(props); super(props);
this.state = { this.state = {
editedLabelText: "" editedLabelText: String(
props.categoricalSelection[props.metadataField].categoryValues[
props.categoryIndex
]
).valueOf()
}; };
} }
handleDeleteValue = () => { getLabel = () => {
const { const { metadataField, categoryIndex, categoricalSelection } = this.props;
dispatch,
metadataField,
categoryIndex,
categoricalSelection
} = this.props;
const category = categoricalSelection[metadataField]; const category = categoricalSelection[metadataField];
const label = category.categoryValues[categoryIndex]; const label = category.categoryValues[categoryIndex];
return label;
};
handleDeleteValue = () => {
const { dispatch, metadataField } = this.props;
const label = this.getLabel();
dispatch({ dispatch({
type: "annotation: delete label", type: "annotation: delete label",
metadataField, metadataField,
@@ -52,14 +59,8 @@ class CategoryValue extends React.Component {
}; };
handleAddCurrentSelectionToThisLabel = () => { handleAddCurrentSelectionToThisLabel = () => {
const { const { dispatch, metadataField, categoryIndex } = this.props;
dispatch, const label = this.getLabel();
metadataField,
categoryIndex,
categoricalSelection
} = this.props;
const category = categoricalSelection[metadataField];
const label = category.categoryValues[categoryIndex];
dispatch({ dispatch({
type: "annotation: label current cell selection", type: "annotation: label current cell selection",
metadataField, metadataField,
@@ -69,15 +70,9 @@ class CategoryValue extends React.Component {
}; };
handleEditValue = () => { handleEditValue = () => {
const { const { dispatch, metadataField, categoryIndex } = this.props;
dispatch,
metadataField,
categoryIndex,
categoricalSelection
} = this.props;
const { editedLabelText } = this.state; const { editedLabelText } = this.state;
const category = categoricalSelection[metadataField]; const label = this.getLabel();
const label = category.categoryValues[categoryIndex];
dispatch({ dispatch({
type: "annotation: label edited", type: "annotation: label edited",
editedLabel: editedLabelText, editedLabel: editedLabelText,
@@ -85,7 +80,80 @@ class CategoryValue extends React.Component {
categoryIndex, categoryIndex,
label label
}); });
this.setState({ editedLabelText: "" }); };
valueNameErrorMessage = () => {
const { editedLabelText } = this.state;
const err = this.valueNameError();
if (!err) 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;
};
valueNameError = () => {
const { editedLabelText } = this.state;
const { categoricalSelection, metadataField, categoryIndex } = this.props;
let err = null;
const category = categoricalSelection[metadataField];
const displayString = String(
category.categoryValues[categoryIndex]
).valueOf();
if (editedLabelText === "") {
err = "empty_string";
} else if (
category.categoryValues.indexOf(editedLabelText) > -1 &&
editedLabelText !== displayString
) {
err = "duplicate";
} else if (!AnnotationsHelpers.isLegalAnnotationName(editedLabelText)) {
err = "characters";
}
return err;
}; };
activateEditLabelMode = () => { activateEditLabelMode = () => {
@@ -115,7 +183,7 @@ class CategoryValue extends React.Component {
}); });
}; };
shouldComponentUpdate = nextProps => { shouldComponentUpdate = (nextProps, nextState) => {
/* /*
Checks to see if at least one of the following changed: Checks to see if at least one of the following changed:
* world state * world state
@@ -125,7 +193,7 @@ class CategoryValue extends React.Component {
If and only if true, update the component If and only if true, update the component
*/ */
const { props } = this; const { props, state } = this;
const { metadataField, categoryIndex, categoricalSelection } = props; const { metadataField, categoryIndex, categoricalSelection } = props;
const { categoricalSelection: newCategoricalSelection } = nextProps; const { categoricalSelection: newCategoricalSelection } = nextProps;
@@ -141,13 +209,15 @@ class CategoryValue extends React.Component {
const colorAccessorChange = props.colorAccessor !== nextProps.colorAccessor; const colorAccessorChange = props.colorAccessor !== nextProps.colorAccessor;
const annotationsChange = props.annotations !== nextProps.annotations; const annotationsChange = props.annotations !== nextProps.annotations;
const crossfilterChange = props.crossfilter !== nextProps.crossfilter; const crossfilterChange = props.crossfilter !== nextProps.crossfilter;
const editingLabel = state.editedLabelText !== nextState.editedLabelText;
return ( return (
valueSelectionChange || valueSelectionChange ||
worldChange || worldChange ||
colorAccessorChange || colorAccessorChange ||
annotationsChange || annotationsChange ||
crossfilterChange crossfilterChange ||
editingLabel
); );
}; };
@@ -222,6 +292,8 @@ class CategoryValue extends React.Component {
flippedProps flippedProps
} = this.props; } = this.props;
const { editedLabelText } = this.state;
if (!categoricalSelection) return null; if (!categoricalSelection) return null;
const category = categoricalSelection[metadataField]; const category = categoricalSelection[metadataField];
@@ -347,6 +419,9 @@ class CategoryValue extends React.Component {
<form <form
onSubmit={e => { onSubmit={e => {
e.preventDefault(); e.preventDefault();
if (this.valueNameError()) {
return;
}
this.handleEditValue(); this.handleEditValue();
}} }}
> >
@@ -357,6 +432,7 @@ class CategoryValue extends React.Component {
}} }}
small small
autoFocus autoFocus
intent={this.valueNameError() ? "warning" : "none"}
onChange={e => { onChange={e => {
this.setState({ editedLabelText: e.target.value }); this.setState({ editedLabelText: e.target.value });
}} }}
@@ -365,6 +441,7 @@ class CategoryValue extends React.Component {
<Button <Button
minimal minimal
style={{ position: "relative", top: -1 }} style={{ position: "relative", top: -1 }}
disabled={this.valueNameError()}
type="button" type="button"
icon="small-tick" icon="small-tick"
data-testclass="submitEdit" data-testclass="submitEdit"
@@ -373,6 +450,7 @@ class CategoryValue extends React.Component {
/> />
} }
/> />
{this.valueNameErrorMessage()}
</form> </form>
) : null} ) : null}
{/* {/*