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
This commit is contained in:
Bruce Martin
2020-02-24 18:51:26 -07:00
committed by GitHub
parent 5c70cc5bcd
commit 7bc58bba2b
10 changed files with 75 additions and 60 deletions
@@ -70,7 +70,7 @@ class AnnoDialog extends React.Component {
onClick={handleSecondaryButtonSubmit} onClick={handleSecondaryButtonSubmit}
disabled={!text || validationError} disabled={!text || validationError}
intent="none" intent="none"
type="submit" type="button"
> >
{secondaryButtonText} {secondaryButtonText}
</Button> </Button>
@@ -32,43 +32,43 @@ class Category extends React.Component {
}); });
}; };
handleAddNewLabelToCategory = () => { handleAddNewLabelToCategory = e => {
const { dispatch, metadataField } = this.props; const { dispatch, metadataField } = this.props;
const { newLabelText } = this.state; const { newLabelText } = this.state;
this.disableAddNewLabelMode();
dispatch({ dispatch({
type: "annotation: add new label to category", type: "annotation: add new label to category",
metadataField, metadataField,
newLabelText, newLabelText,
assignSelectedCells: false assignSelectedCells: false
}); });
this.setState({ newLabelText: "" }); e.preventDefault();
}; };
addLabelAndAssignCells = () => { addLabelAndAssignCells = () => {
const { dispatch, metadataField } = this.props; const { dispatch, metadataField } = this.props;
const { newLabelText } = this.state; const { newLabelText } = this.state;
this.disableAddNewLabelMode();
dispatch({ dispatch({
type: "annotation: add new label to category", type: "annotation: add new label to category",
metadataField, metadataField,
newLabelText, newLabelText,
assignSelectedCells: true assignSelectedCells: true
}); });
this.setState({ newLabelText: "" });
}; };
handleCreateArbitraryLabel = newLabelTextNotInOntology => { handleCreateArbitraryLabel = newLabelTextNotInOntology => {
const { dispatch, metadataField } = this.props; const { dispatch, metadataField } = this.props;
this.disableAddNewLabelMode();
dispatch({ dispatch({
type: "annotation: add new label to category", type: "annotation: add new label to category",
metadataField, metadataField,
newLabelText: newLabelTextNotInOntology, newLabelText: newLabelTextNotInOntology,
assignSelectedCells: false assignSelectedCells: false
}); });
this.setState({ newLabelText: "" });
}; };
labelNameError = name => { labelNameError = name => {
@@ -102,7 +102,9 @@ class Category extends React.Component {
annotations.categoryAddingNewLabel === metadataField annotations.categoryAddingNewLabel === metadataField
} }
inputProps={{ "data-testid": `${metadataField}:create-label-dialog` }} inputProps={{ "data-testid": `${metadataField}:create-label-dialog` }}
primaryButtonProps={{ "data-testid": `${metadataField}:submit-label` }} primaryButtonProps={{
"data-testid": `${metadataField}:submit-label`
}}
title="Add new label to category" title="Add new label to category"
instruction="New, unique label name:" instruction="New, unique label name:"
cancelTooltipContent="Close this dialog without adding a label." cancelTooltipContent="Close this dialog without adding a label."
@@ -32,43 +32,43 @@ class Category extends React.Component {
}); });
}; };
handleAddNewLabelToCategory = () => { handleAddNewLabelToCategory = e => {
const { dispatch, metadataField } = this.props; const { dispatch, metadataField } = this.props;
const { newLabelText } = this.state; const { newLabelText } = this.state;
this.disableAddNewLabelFromOntologyMode();
dispatch({ dispatch({
type: "annotation: add new label to category", type: "annotation: add new label to category",
metadataField, metadataField,
newLabelText, newLabelText,
assignSelectedCells: false assignSelectedCells: false
}); });
this.setState({ newLabelText: "" }); e.preventDefault();
}; };
addLabelAndAssignCells = () => { addLabelAndAssignCells = () => {
const { dispatch, metadataField } = this.props; const { dispatch, metadataField } = this.props;
const { newLabelText } = this.state; const { newLabelText } = this.state;
this.disableAddNewLabelFromOntologyMode();
dispatch({ dispatch({
type: "annotation: add new label to category", type: "annotation: add new label to category",
metadataField, metadataField,
newLabelText, newLabelText,
assignSelectedCells: true assignSelectedCells: true
}); });
this.setState({ newLabelText: "" });
}; };
handleCreateArbitraryLabel = newLabelTextNotInOntology => { handleCreateArbitraryLabel = newLabelTextNotInOntology => {
const { dispatch, metadataField } = this.props; const { dispatch, metadataField } = this.props;
this.disableAddNewLabelFromOntologyMode();
dispatch({ dispatch({
type: "annotation: add new label to category", type: "annotation: add new label to category",
metadataField, metadataField,
newLabelText: newLabelTextNotInOntology, newLabelText: newLabelTextNotInOntology,
assignSelectedCells: false assignSelectedCells: false
}); });
this.setState({ newLabelText: "" });
}; };
labelNameError = name => { labelNameError = name => {
@@ -30,13 +30,14 @@ class AnnoDialogEditCategoryName extends React.Component {
}; };
disableEditCategoryMode = () => { disableEditCategoryMode = () => {
const { dispatch } = this.props; const { dispatch, metadataField } = this.props;
dispatch({ dispatch({
type: "annotation: disable category edit mode" type: "annotation: disable category edit mode"
}); });
this.setState({ newCategoryText: metadataField });
}; };
handleEditCategory = () => { handleEditCategory = e => {
const { dispatch, metadataField, categoricalSelection } = this.props; const { dispatch, metadataField, categoricalSelection } = this.props;
const { newCategoryText } = this.state; const { newCategoryText } = this.state;
@@ -50,12 +51,14 @@ class AnnoDialogEditCategoryName extends React.Component {
return; return;
} }
this.disableEditCategoryMode();
dispatch({ dispatch({
type: "annotation: category edited", type: "annotation: category edited",
metadataField, metadataField,
newCategoryText, newCategoryText,
data: newCategoryText data: newCategoryText
}); });
e.preventDefault();
}; };
categoryNameErrorMessage = () => { categoryNameErrorMessage = () => {
@@ -121,8 +124,12 @@ class AnnoDialogEditCategoryName extends React.Component {
annotations.isEditingCategoryName && annotations.isEditingCategoryName &&
annotations.categoryBeingEdited === metadataField annotations.categoryBeingEdited === metadataField
} }
inputProps={{ "data-testid": `${metadataField}:edit-category-name-dialog` }} inputProps={{
primaryButtonProps={{ "data-testid": `${metadataField}:submit-category-edit` }} "data-testid": `${metadataField}:edit-category-name-dialog`
}}
primaryButtonProps={{
"data-testid": `${metadataField}:submit-category-edit`
}}
title="Edit category name" title="Edit category name"
instruction="New, unique category name:" instruction="New, unique category name:"
cancelTooltipContent="Close this dialog without editing this category." cancelTooltipContent="Close this dialog without editing this category."
@@ -134,7 +141,9 @@ class AnnoDialogEditCategoryName extends React.Component {
handleCancel={this.disableEditCategoryMode} handleCancel={this.disableEditCategoryMode}
annoInput={ annoInput={
<AnnoInputs <AnnoInputs
inputProps={{ "data-testid": `${metadataField}:edit-category-name-text`}} inputProps={{
"data-testid": `${metadataField}:edit-category-name-text`
}}
useSuggest={false} useSuggest={false}
text={newCategoryText} text={newCategoryText}
handleTextChange={this.handleCategoryEditTextChange} handleTextChange={this.handleCategoryEditTextChange}
@@ -24,7 +24,7 @@ class Categories extends React.Component {
}; };
} }
handleCreateUserAnno = () => { handleCreateUserAnno = e => {
const { dispatch } = this.props; const { dispatch } = this.props;
const { newCategoryText, categoryToDuplicate } = this.state; const { newCategoryText, categoryToDuplicate } = this.state;
dispatch({ dispatch({
@@ -37,6 +37,7 @@ class Categories extends React.Component {
categoryToDuplicate: null, categoryToDuplicate: null,
newCategoryText: "" newCategoryText: ""
}); });
e.preventDefault();
}; };
handleEnableAnnoMode = () => { handleEnableAnnoMode = () => {
+15 -7
View File
@@ -83,10 +83,11 @@ class CategoryValue extends React.Component {
}); });
}; };
handleEditValue = () => { handleEditValue = e => {
const { dispatch, metadataField, categoryIndex } = this.props; const { dispatch, metadataField, categoryIndex } = this.props;
const { editedLabelText } = this.state; const { editedLabelText } = this.state;
const label = this.getLabel(); const label = this.getLabel();
this.cancelEditMode();
dispatch({ dispatch({
type: "annotation: label edited", type: "annotation: label edited",
editedLabel: editedLabelText, editedLabel: editedLabelText,
@@ -94,12 +95,13 @@ class CategoryValue extends React.Component {
categoryIndex, categoryIndex,
label label
}); });
e.preventDefault();
}; };
handleCreateArbitraryLabel = editedLabelTextNotInOntology => { handleCreateArbitraryLabel = editedLabelTextNotInOntology => {
const { dispatch, metadataField, categoryIndex } = this.props; const { dispatch, metadataField, categoryIndex } = this.props;
const label = this.getLabel(); const label = this.getLabel();
this.cancelEditMode();
dispatch({ dispatch({
type: "annotation: label edited", type: "annotation: label edited",
metadataField, metadataField,
@@ -150,7 +152,7 @@ class CategoryValue extends React.Component {
}); });
}; };
cancelEdit = () => { cancelEditMode = () => {
const { dispatch, metadataField, categoryIndex } = this.props; const { dispatch, metadataField, categoryIndex } = this.props;
dispatch({ dispatch({
type: "annotation: cancel edit label mode", type: "annotation: cancel edit label mode",
@@ -433,8 +435,12 @@ class CategoryValue extends React.Component {
<div> <div>
<AnnoDialog <AnnoDialog
isActive={editModeActive} isActive={editModeActive}
inputProps={{ "data-testid": `${metadataField}:edit-label-name-dialog` }} inputProps={{
primaryButtonProps={{ "data-testid": `${metadataField}:${displayString}:submit-label-edit` }} "data-testid": `${metadataField}:edit-label-name-dialog`
}}
primaryButtonProps={{
"data-testid": `${metadataField}:${displayString}:submit-label-edit`
}}
title="Edit label" title="Edit label"
instruction={`New label text must be unique within category ${metadataField}:`} instruction={`New label text must be unique within category ${metadataField}:`}
cancelTooltipContent="Close this dialog without editing label text." cancelTooltipContent="Close this dialog without editing label text."
@@ -444,12 +450,14 @@ class CategoryValue extends React.Component {
validationError={this.labelNameError(editedLabelText)} validationError={this.labelNameError(editedLabelText)}
errorMessage={this.labelNameErrorMessage(editedLabelText)} errorMessage={this.labelNameErrorMessage(editedLabelText)}
handleSubmit={this.handleEditValue} handleSubmit={this.handleEditValue}
handleCancel={this.cancelEdit} handleCancel={this.cancelEditMode}
annoInput={ annoInput={
<AnnoInputs <AnnoInputs
useSuggest={ontologyEnabled} useSuggest={ontologyEnabled}
text={editedLabelText} text={editedLabelText}
inputProps={{ "data-testid": `${metadataField}:${displayString}:edit-label-name` }} inputProps={{
"data-testid": `${metadataField}:${displayString}:edit-label-name`
}}
handleCreateArbitraryLabel={ handleCreateArbitraryLabel={
this.handleCreateArbitraryLabel this.handleCreateArbitraryLabel
} }
-21
View File
@@ -80,15 +80,6 @@ const Annotations = (
isAddingNewLabel: false, isAddingNewLabel: false,
categoryAddingNewLabel: null categoryAddingNewLabel: null
}; };
/* this handles both cases, since at this point it's just a string */
case "annotation: add new label to category":
return {
...state,
isAddingNewLabel: false,
categoryAddingNewLabel: null,
isAddingNewLabelFromOntology: false,
categoryAddingNewLabelFromOntology: null
};
case "annotation: activate category edit mode": case "annotation: activate category edit mode":
return { return {
...state, ...state,
@@ -101,12 +92,6 @@ const Annotations = (
isEditingCategoryName: false, isEditingCategoryName: false,
categoryBeingEdited: null categoryBeingEdited: null
}; };
case "annotation: category edited":
return {
...state,
isEditingCategoryName: false,
categoryBeingEdited: null
};
/* LABEL */ /* LABEL */
case "annotation: activate edit label mode": case "annotation: activate edit label mode":
@@ -124,12 +109,6 @@ const Annotations = (
isEditingLabelName: false, isEditingLabelName: false,
labelEditable: { category: null, label: null } labelEditable: { category: null, label: null }
}; };
case "annotation: label edited":
return {
...state,
isEditingLabelName: false,
labelEditable: { category: null, label: null }
};
default: default:
return state; return state;
} }
+3 -3
View File
@@ -85,7 +85,6 @@ const Undoable = (reducer, undoableKeys, options = {}) => {
); );
const newPast = [...past]; const newPast = [...past];
const newState = newPast.pop(); const newState = newPast.pop();
const newStateFilterState = newState[filterStateKey];
const newFuture = push(future, currentUndoableState); const newFuture = push(future, currentUndoableState);
const nextState = { const nextState = {
...currentState, ...currentState,
@@ -94,7 +93,6 @@ const Undoable = (reducer, undoableKeys, options = {}) => {
[futureKey]: newFuture, [futureKey]: newFuture,
[pendingKey]: null [pendingKey]: null
}; };
nextState[filterStateKey] = newStateFilterState;
return nextState; return nextState;
} }
@@ -139,12 +137,13 @@ const Undoable = (reducer, undoableKeys, options = {}) => {
*/ */
function skip(currentState, action, filterState) { function skip(currentState, action, filterState) {
const past = currentState[pastKey] || []; const past = currentState[pastKey] || [];
const future = currentState[futureKey] || [];
const pending = currentState[pendingKey]; const pending = currentState[pendingKey];
const res = reducer(currentState, action); const res = reducer(currentState, action);
return { return {
...res, ...res,
[pastKey]: past, [pastKey]: past,
[futureKey]: [], [futureKey]: future,
[filterStateKey]: filterState, [filterStateKey]: filterState,
[pendingKey]: pending [pendingKey]: pending
}; };
@@ -218,6 +217,7 @@ const Undoable = (reducer, undoableKeys, options = {}) => {
}, },
action action
) => { ) => {
if (debug > 1) console.log("---- ACTION", action.type);
const aType = action.type; const aType = action.type;
switch (aType) { switch (aType) {
case "@@undoable/undo": { case "@@undoable/undo": {
+26 -10
View File
@@ -35,18 +35,30 @@ const skipOnActions = new Set([
"get single gene expression for coloring error", "get single gene expression for coloring error",
"category value mouse hover start", "category value mouse hover start",
"category value mouse hover end" "category value mouse hover end",
/* autosave annotations */
"writable obs annotations - save complete",
"writable obs annotations - save started",
"writable obs annotations - save error",
/* annotation component action */
"annotation: activate add new label mode",
"annotation: activate add new ontology label mode",
"annotation: disable add new ontology label mode",
"annotation: disable add new label mode",
"annotation: activate category edit mode",
"annotation: disable category edit mode",
"annotation: activate edit label mode",
"annotation: cancel edit label mode",
"set annotations collection name"
]); ]);
/* /*
identical, repeated occurances of these action types will be debounced. identical, repeated occurances of these action types will be debounced.
Entire action must be identical (all keys). Entire action must be identical (all keys).
*/ */
const debounceOnActions = new Set([ const debounceOnActions = new Set([]);
"color by categorical metadata",
"color by continuous metadata",
"color by expression"
]);
/* /*
history will be cleared when these actions occur history will be cleared when these actions occur
@@ -91,7 +103,8 @@ const saveOnActions = new Set([
"annotation: delete category", "annotation: delete category",
"annotation: label edited", "annotation: label edited",
"annotation: label current cell selection", "annotation: label current cell selection",
"annotation: delete label" "annotation: delete label",
"annotation: category edited"
]); ]);
/** /**
@@ -172,8 +185,6 @@ const actionFilter = debug => (state, action, prevFilterState) => {
} }
if ( if (
debounceOnActions.has(actionType) && debounceOnActions.has(actionType) &&
prevFilterState !== undefined &&
prevFilterState.prevAction !== undefined &&
shallowObjectEq(action, prevFilterState.prevAction) shallowObjectEq(action, prevFilterState.prevAction)
) { ) {
return { [actionKey]: "skip", [stateKey]: filterState }; return { [actionKey]: "skip", [stateKey]: filterState };
@@ -229,7 +240,12 @@ function shallowArrayEq(arrA, arrB) {
} }
/* configuration for the undoable meta reducer */ /* configuration for the undoable meta reducer */
const debug = false; // set truish for undoble debugging /*
debug: set to any falsish value to disable logging of helpful debugging information.
Set to true or 1 for base logging, high number for more verbosity (currently only 1/true
or 2).
*/
const debug = false;
const undoableConfig = { const undoableConfig = {
debug, debug,
historyLimit: 50, // maximum history size historyLimit: 50, // maximum history size
+1 -1
View File
@@ -64,7 +64,7 @@ def annotations_put_fbs_helper(data_adaptor, annotations, fbs):
new_label_df = decode_matrix_fbs(fbs) new_label_df = decode_matrix_fbs(fbs)
if not new_label_df.empty: if not new_label_df.empty:
data_adaptor.check_new_labels(new_label_df) data_adaptor.check_new_labels(new_label_df)
annotations.write_labels(new_label_df, data_adaptor) annotations.write_labels(new_label_df, data_adaptor)
def annotations_obs_put(request, data_adaptor, annotations): def annotations_obs_put(request, data_adaptor, annotations):