React UI: Undo/redo (#1135)

main
Boris Sekachev 6 years ago committed by GitHub
parent fccccabe2b
commit 7e8fc2366a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -32,6 +32,7 @@
} = require('./exceptions'); } = require('./exceptions');
const { const {
HistoryActions,
ObjectShape, ObjectShape,
ObjectType, ObjectType,
colors, colors,
@ -109,6 +110,7 @@
return labelAccumulator; return labelAccumulator;
}, {}); }, {});
this.history = data.history;
this.shapes = {}; // key is a frame this.shapes = {}; // key is a frame
this.tags = {}; // key is a frame this.tags = {}; // key is a frame
this.tracks = []; this.tracks = [];
@ -124,16 +126,25 @@
collectionZ: this.collectionZ, collectionZ: this.collectionZ,
groups: this.groups, groups: this.groups,
frameMeta: this.frameMeta, frameMeta: this.frameMeta,
history: this.history,
}; };
} }
import(data) { import(data) {
const result = {
tags: [],
shapes: [],
tracks: [],
};
for (const tag of data.tags) { for (const tag of data.tags) {
const clientID = ++this.count; const clientID = ++this.count;
const tagModel = new Tag(tag, clientID, this.injection); const tagModel = new Tag(tag, clientID, this.injection);
this.tags[tagModel.frame] = this.tags[tagModel.frame] || []; this.tags[tagModel.frame] = this.tags[tagModel.frame] || [];
this.tags[tagModel.frame].push(tagModel); this.tags[tagModel.frame].push(tagModel);
this.objects[clientID] = tagModel; this.objects[clientID] = tagModel;
result.tags.push(tagModel);
} }
for (const shape of data.shapes) { for (const shape of data.shapes) {
@ -142,6 +153,8 @@
this.shapes[shapeModel.frame] = this.shapes[shapeModel.frame] || []; this.shapes[shapeModel.frame] = this.shapes[shapeModel.frame] || [];
this.shapes[shapeModel.frame].push(shapeModel); this.shapes[shapeModel.frame].push(shapeModel);
this.objects[clientID] = shapeModel; this.objects[clientID] = shapeModel;
result.shapes.push(shapeModel);
} }
for (const track of data.tracks) { for (const track of data.tracks) {
@ -152,10 +165,12 @@
if (trackModel) { if (trackModel) {
this.tracks.push(trackModel); this.tracks.push(trackModel);
this.objects[clientID] = trackModel; this.objects[clientID] = trackModel;
result.tracks.push(trackModel);
} }
} }
return this; return result;
} }
export() { export() {
@ -378,6 +393,18 @@
for (const object of objectsForMerge) { for (const object of objectsForMerge) {
object.removed = true; object.removed = true;
} }
this.history.do(HistoryActions.MERGED_OBJECTS, () => {
trackModel.removed = true;
for (const object of objectsForMerge) {
object.removed = false;
}
}, () => {
trackModel.removed = false;
for (const object of objectsForMerge) {
object.removed = true;
}
}, [...objectsForMerge.map((object) => object.clientID), trackModel.clientID]);
} }
split(objectState, frame) { split(objectState, frame) {
@ -463,6 +490,16 @@
// Remove source object // Remove source object
object.removed = true; object.removed = true;
this.history.do(HistoryActions.SPLITTED_TRACK, () => {
object.removed = false;
prevTrack.removed = true;
nextTrack.removed = true;
}, () => {
object.removed = true;
prevTrack.removed = false;
nextTrack.removed = false;
}, [object.clientID, prevTrack.clientID, nextTrack.clientID]);
} }
group(objectStates, reset) { group(objectStates, reset) {
@ -480,9 +517,21 @@
}); });
const groupIdx = reset ? 0 : ++this.groups.max; const groupIdx = reset ? 0 : ++this.groups.max;
const undoGroups = objectsForGroup.map((object) => object.group);
for (const object of objectsForGroup) { for (const object of objectsForGroup) {
object.group = groupIdx; object.group = groupIdx;
} }
const redoGroups = objectsForGroup.map((object) => object.group);
this.history.do(HistoryActions.GROUPED_OBJECTS, () => {
objectsForGroup.forEach((object, idx) => {
object.group = undoGroups[idx];
});
}, () => {
objectsForGroup.forEach((object, idx) => {
object.group = redoGroups[idx];
});
}, objectsForGroup.map((object) => object.clientID));
return groupIdx; return groupIdx;
} }
@ -704,7 +753,20 @@
} }
// Add constructed objects to a collection // Add constructed objects to a collection
this.import(constructed); const imported = this.import(constructed);
const importedArray = imported.tags
.concat(imported.tracks)
.concat(imported.shapes);
this.history.do(HistoryActions.CREATED_OBJECTS, () => {
importedArray.forEach((object) => {
object.removed = true;
});
}, () => {
importedArray.forEach((object) => {
object.removed = false;
});
}, importedArray.map((object) => object.clientID));
} }
select(objectStates, x, y) { select(objectStates, x, y) {

@ -0,0 +1,71 @@
/*
* Copyright (C) 2019-2020 Intel Corporation
* SPDX-License-Identifier: MIT
*/
const MAX_HISTORY_LENGTH = 128;
class AnnotationHistory {
constructor() {
this.clear();
}
get() {
return {
undo: this._undo.map((undo) => undo.action),
redo: this._redo.map((redo) => redo.action),
};
}
do(action, undo, redo, clientIDs) {
const actionItem = {
clientIDs,
action,
undo,
redo,
};
this._undo = this._undo.slice(-MAX_HISTORY_LENGTH + 1);
this._undo.push(actionItem);
this._redo = [];
}
undo(count) {
const affectedObjects = [];
for (let i = 0; i < count; i++) {
const action = this._undo.pop();
if (action) {
action.undo();
this._redo.push(action);
affectedObjects.push(...action.clientIDs);
} else {
break;
}
}
return affectedObjects;
}
redo(count) {
const affectedObjects = [];
for (let i = 0; i < count; i++) {
const action = this._redo.pop();
if (action) {
action.redo();
this._undo.push(action);
affectedObjects.push(...action.clientIDs);
} else {
break;
}
}
return affectedObjects;
}
clear() {
this._undo = [];
this._redo = [];
}
}
module.exports = AnnotationHistory;

@ -1,5 +1,5 @@
/* /*
* Copyright (C) 2019 Intel Corporation * Copyright (C) 2019-2020 Intel Corporation
* SPDX-License-Identifier: MIT * SPDX-License-Identifier: MIT
*/ */
@ -17,6 +17,7 @@
ObjectShape, ObjectShape,
ObjectType, ObjectType,
AttributeType, AttributeType,
HistoryActions,
} = require('./enums'); } = require('./enums');
const { const {
@ -139,6 +140,7 @@
class Annotation { class Annotation {
constructor(data, clientID, injection) { constructor(data, clientID, injection) {
this.taskLabels = injection.labels; this.taskLabels = injection.labels;
this.history = injection.history;
this.clientID = clientID; this.clientID = clientID;
this.serverID = data.id; this.serverID = data.id;
this.group = data.group; this.group = data.group;
@ -156,6 +158,79 @@
injection.groups.max = Math.max(injection.groups.max, this.group); injection.groups.max = Math.max(injection.groups.max, this.group);
} }
_saveLock(lock) {
const undoLock = this.lock;
const redoLock = lock;
this.history.do(HistoryActions.CHANGED_LOCK, () => {
this.lock = undoLock;
}, () => {
this.lock = redoLock;
}, [this.clientID]);
this.lock = lock;
}
_saveColor(color) {
const undoColor = this.color;
const redoColor = color;
this.history.do(HistoryActions.CHANGED_COLOR, () => {
this.color = undoColor;
}, () => {
this.color = redoColor;
}, [this.clientID]);
this.color = color;
}
_saveHidden(hidden) {
const undoHidden = this.hidden;
const redoHidden = hidden;
this.history.do(HistoryActions.CHANGED_HIDDEN, () => {
this.hidden = undoHidden;
}, () => {
this.hidden = redoHidden;
}, [this.clientID]);
this.hidden = hidden;
}
_saveLabel(label) {
const undoLabel = this.label;
const redoLabel = label;
const undoAttributes = { ...this.attributes };
this.label = label;
this.attributes = {};
this.appendDefaultAttributes(label);
const redoAttributes = { ...this.attributes };
this.history.do(HistoryActions.CHANGED_LABEL, () => {
this.label = undoLabel;
this.attributes = undoAttributes;
}, () => {
this.label = redoLabel;
this.attributes = redoAttributes;
}, [this.clientID]);
}
_saveAttributes(attributes) {
const undoAttributes = { ...this.attributes };
for (const attrID of Object.keys(attributes)) {
this.attributes[attrID] = attributes[attrID];
}
const redoAttributes = { ...this.attributes };
this.history.do(HistoryActions.CHANGED_ATTRIBUTES, () => {
this.attributes = undoAttributes;
}, () => {
this.attributes = redoAttributes;
}, [this.clientID]);
}
appendDefaultAttributes(label) { appendDefaultAttributes(label) {
const labelAttributes = label.attributes; const labelAttributes = label.attributes;
for (const attribute of labelAttributes) { for (const attribute of labelAttributes) {
@ -178,9 +253,15 @@
delete(force) { delete(force) {
if (!this.lock || force) { if (!this.lock || force) {
this.removed = true; this.removed = true;
this.history.do(HistoryActions.REMOVED_OBJECT, () => {
this.removed = false;
}, () => {
this.removed = true;
}, [this.clientID]);
} }
return true; return this.removed;
} }
} }
@ -205,9 +286,8 @@
return this.collectionZ[frame]; return this.collectionZ[frame];
} }
validateStateBeforeSave(frame, data) { _validateStateBeforeSave(frame, data, updated) {
let fittedPoints = []; let fittedPoints = [];
const updated = data.updateFlags;
if (updated.label) { if (updated.label) {
checkObjectType('label', data.label, null, Label); checkObjectType('label', data.label, null, Label);
@ -255,7 +335,7 @@
} }
if (!checkShapeArea(this.shapeType, fittedPoints)) { if (!checkShapeArea(this.shapeType, fittedPoints)) {
fittedPoints = false; fittedPoints = [];
} }
} }
@ -393,6 +473,45 @@
}; };
} }
_savePoints(points) {
const undoPoints = this.points;
const redoPoints = points;
this.history.do(HistoryActions.CHANGED_POINTS, () => {
this.points = undoPoints;
}, () => {
this.points = redoPoints;
}, [this.clientID]);
this.points = points;
}
_saveOccluded(occluded) {
const undoOccluded = this.occluded;
const redoOccluded = occluded;
this.history.do(HistoryActions.CHANGED_OCCLUDED, () => {
this.occluded = undoOccluded;
}, () => {
this.occluded = redoOccluded;
}, [this.clientID]);
this.occluded = occluded;
}
_saveZOrder(zOrder) {
const undoZOrder = this.zOrder;
const redoZOrder = zOrder;
this.history.do(HistoryActions.CHANGED_ZORDER, () => {
this.zOrder = undoZOrder;
}, () => {
this.zOrder = redoZOrder;
}, [this.clientID]);
this.zOrder = zOrder;
}
save(frame, data) { save(frame, data) {
if (frame !== this.frame) { if (frame !== this.frame) {
throw new ScriptingError( throw new ScriptingError(
@ -404,44 +523,40 @@
return objectStateFactory.call(this, frame, this.get(frame)); return objectStateFactory.call(this, frame, this.get(frame));
} }
const fittedPoints = this.validateStateBeforeSave(frame, data);
const updated = data.updateFlags; const updated = data.updateFlags;
const fittedPoints = this._validateStateBeforeSave(frame, data, updated);
// Now when all fields are validated, we can apply them // Now when all fields are validated, we can apply them
if (updated.label) { if (updated.label) {
this.label = data.label; this._saveLabel(data.label);
this.attributes = {};
this.appendDefaultAttributes(data.label);
} }
if (updated.attributes) { if (updated.attributes) {
for (const attrID of Object.keys(data.attributes)) { this._saveAttributes(data.attributes);
this.attributes[attrID] = data.attributes[attrID];
}
} }
if (updated.points && fittedPoints.length) { if (updated.points && fittedPoints.length) {
this.points = [...fittedPoints]; this._savePoints(fittedPoints);
} }
if (updated.occluded) { if (updated.occluded) {
this.occluded = data.occluded; this._saveOccluded(data.occluded);
} }
if (updated.zOrder) { if (updated.zOrder) {
this.zOrder = data.zOrder; this._saveZOrder(data.zOrder);
} }
if (updated.lock) { if (updated.lock) {
this.lock = data.lock; this._saveLock(data.lock);
} }
if (updated.color) { if (updated.color) {
this.color = data.color; this._saveColor(data.color);
} }
if (updated.hidden) { if (updated.hidden) {
this.hidden = data.hidden; this._saveHidden(data.hidden);
} }
this.updateTimestamp(updated); this.updateTimestamp(updated);
@ -622,87 +737,304 @@
return result; return result;
} }
save(frame, data) { _saveLabel(label) {
if (this.lock && data.lock) { const undoLabel = this.label;
return objectStateFactory.call(this, frame, this.get(frame)); const redoLabel = label;
const undoAttributes = {
unmutable: { ...this.attributes },
mutable: Object.keys(this.shapes).map((key) => ({
frame: +key,
attributes: { ...this.shapes[key].attributes },
})),
};
this.label = label;
this.attributes = {};
for (const shape of Object.values(this.shapes)) {
shape.attributes = {};
} }
this.appendDefaultAttributes(label);
const fittedPoints = this.validateStateBeforeSave(frame, data); const redoAttributes = {
const updated = data.updateFlags; unmutable: { ...this.attributes },
mutable: Object.keys(this.shapes).map((key) => ({
frame: +key,
attributes: { ...this.shapes[key].attributes },
})),
};
this.history.do(HistoryActions.CHANGED_LABEL, () => {
this.label = undoLabel;
this.attributes = undoAttributes.unmutable;
for (const mutable of undoAttributes.mutable) {
this.shapes[mutable.frame].attributes = mutable.attributes;
}
}, () => {
this.label = redoLabel;
this.attributes = redoAttributes.unmutable;
for (const mutable of redoAttributes.mutable) {
this.shapes[mutable.frame].attributes = mutable.attributes;
}
}, [this.clientID]);
}
_saveAttributes(frame, attributes) {
const current = this.get(frame); const current = this.get(frame);
const labelAttributes = data.label.attributes const labelAttributes = this.label.attributes
.reduce((accumulator, value) => { .reduce((accumulator, value) => {
accumulator[value.id] = value; accumulator[value.id] = value;
return accumulator; return accumulator;
}, {}); }, {});
if (updated.label) { const wasKeyframe = frame in this.shapes;
this.label = data.label; const undoAttributes = this.attributes;
this.attributes = {}; const undoShape = wasKeyframe ? this.shapes[frame] : undefined;
for (const shape of Object.values(this.shapes)) {
shape.attributes = {}; let mutableAttributesUpdated = false;
const redoAttributes = { ...this.attributes };
for (const attrID of Object.keys(attributes)) {
if (!labelAttributes[attrID].mutable) {
redoAttributes[attrID] = attributes[attrID];
} else if (attributes[attrID] !== current.attributes[attrID]) {
mutableAttributesUpdated = mutableAttributesUpdated
// not keyframe yet
|| !(frame in this.shapes)
// keyframe, but without this attrID
|| !(attrID in this.shapes[frame].attributes)
// keyframe with attrID, but with another value
|| (this.shapes[frame].attributes[attrID] !== attributes[attrID]);
}
}
let redoShape;
if (mutableAttributesUpdated) {
if (wasKeyframe) {
redoShape = {
...this.shapes[frame],
attributes: {
...this.shapes[frame].attributes,
},
};
} else {
redoShape = {
frame,
zOrder: current.zOrder,
points: current.points,
outside: current.outside,
occluded: current.occluded,
attributes: {},
};
} }
this.appendDefaultAttributes(data.label);
} }
let mutableAttributesUpdated = false; for (const attrID of Object.keys(attributes)) {
if (updated.attributes) { if (labelAttributes[attrID].mutable
for (const attrID of Object.keys(data.attributes)) { && attributes[attrID] !== current.attributes[attrID]) {
if (!labelAttributes[attrID].mutable) { redoShape.attributes[attrID] = attributes[attrID];
this.attributes[attrID] = data.attributes[attrID]; }
this.attributes[attrID] = data.attributes[attrID]; }
} else if (data.attributes[attrID] !== current.attributes[attrID]) {
mutableAttributesUpdated = mutableAttributesUpdated this.attributes = redoAttributes;
// not keyframe yet if (redoShape) {
|| !(frame in this.shapes) this.shapes[frame] = redoShape;
// keyframe, but without this attrID }
|| !(attrID in this.shapes[frame])
// keyframe with attrID, but with another value this.history.do(HistoryActions.CHANGED_ATTRIBUTES, () => {
|| (this.shapes[frame][attrID] !== data.attributes[attrID]); this.attributes = undoAttributes;
} if (undoShape) {
this.shapes[frame] = undoShape;
} else if (redoShape) {
delete this.shapes[frame];
}
}, () => {
this.attributes = redoAttributes;
if (redoShape) {
this.shapes[frame] = redoShape;
}
}, [this.clientID]);
}
_appendShapeActionToHistory(actionType, frame, undoShape, redoShape) {
this.history.do(actionType, () => {
if (!undoShape) {
delete this.shapes[frame];
} else {
this.shapes[frame] = undoShape;
}
}, () => {
if (!redoShape) {
delete this.shapes[frame];
} else {
this.shapes[frame] = redoShape;
} }
}, [this.clientID]);
}
_savePoints(frame, points) {
const current = this.get(frame);
const wasKeyframe = frame in this.shapes;
const undoShape = wasKeyframe ? this.shapes[frame] : undefined;
const redoShape = wasKeyframe ? { ...this.shapes[frame], points } : {
frame,
points,
zOrder: current.zOrder,
outside: current.outside,
occluded: current.occluded,
attributes: {},
};
this.shapes[frame] = redoShape;
this._appendShapeActionToHistory(
HistoryActions.CHANGED_POINTS,
frame,
undoShape,
redoShape,
);
}
_saveOutside(frame, outside) {
const current = this.get(frame);
const wasKeyframe = frame in this.shapes;
const undoShape = wasKeyframe ? this.shapes[frame] : undefined;
const redoShape = wasKeyframe ? { ...this.shapes[frame], outside } : {
frame,
outside,
zOrder: current.zOrder,
points: current.points,
occluded: current.occluded,
attributes: {},
};
this.shapes[frame] = redoShape;
this._appendShapeActionToHistory(
HistoryActions.CHANGED_OUTSIDE,
frame,
undoShape,
redoShape,
);
}
_saveOccluded(frame, occluded) {
const current = this.get(frame);
const wasKeyframe = frame in this.shapes;
const undoShape = wasKeyframe ? this.shapes[frame] : undefined;
const redoShape = wasKeyframe ? { ...this.shapes[frame], occluded } : {
frame,
occluded,
zOrder: current.zOrder,
points: current.points,
outside: current.outside,
attributes: {},
};
this.shapes[frame] = redoShape;
this._appendShapeActionToHistory(
HistoryActions.CHANGED_OCCLUDED,
frame,
undoShape,
redoShape,
);
}
_saveZOrder(frame, zOrder) {
const current = this.get(frame);
const wasKeyframe = frame in this.shapes;
const undoShape = wasKeyframe ? this.shapes[frame] : undefined;
const redoShape = wasKeyframe ? { ...this.shapes[frame], zOrder } : {
frame,
zOrder,
occluded: current.occluded,
points: current.points,
outside: current.outside,
attributes: {},
};
this.shapes[frame] = redoShape;
this._appendShapeActionToHistory(
HistoryActions.CHANGED_ZORDER,
frame,
undoShape,
redoShape,
);
}
_saveKeyframe(frame, keyframe) {
const current = this.get(frame);
const wasKeyframe = frame in this.shapes;
if ((keyframe && wasKeyframe) || (!keyframe && !wasKeyframe)) {
return;
}
const undoShape = wasKeyframe ? this.shapes[frame] : undefined;
const redoShape = keyframe ? {
frame,
zOrder: current.zOrder,
points: current.points,
outside: current.outside,
occluded: current.occluded,
attributes: {},
} : undefined;
if (redoShape) {
this.shapes[frame] = redoShape;
} else {
delete this.shapes[frame];
}
this._appendShapeActionToHistory(
HistoryActions.CHANGED_KEYFRAME,
frame,
undoShape,
redoShape,
);
}
save(frame, data) {
if (this.lock && data.lock) {
return objectStateFactory.call(this, frame, this.get(frame));
}
const updated = data.updateFlags;
const fittedPoints = this._validateStateBeforeSave(frame, data, updated);
if (updated.label) {
this._saveLabel(data.label);
} }
if (updated.lock) { if (updated.lock) {
this.lock = data.lock; this._saveLock(data.lock);
} }
if (updated.color) { if (updated.color) {
this.color = data.color; this._saveColor(data.color);
} }
if (updated.hidden) { if (updated.hidden) {
this.hidden = data.hidden; this._saveHidden(data.hidden);
} }
if (updated.points || updated.keyframe || updated.outside
|| updated.occluded || updated.zOrder || mutableAttributesUpdated) {
const mutableAttributes = frame in this.shapes ? this.shapes[frame].attributes : {};
this.shapes[frame] = {
frame,
zOrder: data.zOrder,
points: updated.points && fittedPoints.length ? fittedPoints : current.points,
outside: data.outside,
occluded: data.occluded,
attributes: mutableAttributes,
};
for (const attrID of Object.keys(data.attributes)) { if (updated.points && fittedPoints.length) {
if (labelAttributes[attrID].mutable this._savePoints(frame, fittedPoints);
&& data.attributes[attrID] !== current.attributes[attrID]) { }
this.shapes[frame].attributes[attrID] = data.attributes[attrID];
this.shapes[frame].attributes[attrID] = data.attributes[attrID];
}
}
if (updated.keyframe && !data.keyframe) { if (updated.outside) {
if (Object.keys(this.shapes).length === 1) { this._saveOutside(frame, data.outside);
throw new DataError('You are not able to remove the latest keyframe for a track. ' }
+ 'Consider removing a track instead');
} else { if (updated.occluded) {
delete this.shapes[frame]; this._saveOccluded(frame, data.occluded);
} }
}
if (updated.zOrder) {
this._saveZOrder(frame, data.zOrder);
}
if (updated.attributes) {
this._saveAttributes(frame, data.attributes);
}
if (updated.keyframe) {
this._saveKeyframe(frame, data.keyframe);
} }
this.updateTimestamp(updated); this.updateTimestamp(updated);
@ -752,14 +1084,6 @@
+ `Interpolation impossible. Client ID: ${this.id}`, + `Interpolation impossible. Client ID: ${this.id}`,
); );
} }
delete(force) {
if (!this.lock || force) {
this.removed = true;
}
return this.removed;
}
} }
class Tag extends Annotation { class Tag extends Annotation {
@ -810,7 +1134,7 @@
save(frame, data) { save(frame, data) {
if (frame !== this.frame) { if (frame !== this.frame) {
throw new ScriptingError( throw new ScriptingError(
'Got frame is not equal to the frame of the shape', 'Got frame is not equal to the frame of the tag',
); );
} }
@ -818,59 +1142,20 @@
return objectStateFactory.call(this, frame, this.get(frame)); return objectStateFactory.call(this, frame, this.get(frame));
} }
if (this.lock && data.lock) {
return objectStateFactory.call(this, frame, this.get(frame));
}
const updated = data.updateFlags; const updated = data.updateFlags;
this._validateStateBeforeSave(frame, data, updated);
// First validate all the fields
if (updated.label) {
checkObjectType('label', data.label, null, Label);
}
if (updated.attributes) {
const labelAttributes = data.label.attributes
.reduce((accumulator, value) => {
accumulator[value.id] = value;
return accumulator;
}, {});
for (const attrID of Object.keys(data.attributes)) {
const value = data.attributes[attrID];
if (attrID in labelAttributes) {
if (!validateAttributeValue(value, labelAttributes[attrID])) {
throw new ArgumentError(
`Trying to save an attribute attribute with id ${attrID} and invalid value ${value}`,
);
}
} else {
throw new ArgumentError(
`Trying to save unknown attribute with id ${attrID} and value ${value}`,
);
}
}
}
if (updated.lock) {
checkObjectType('lock', data.lock, 'boolean', null);
}
// Now when all fields are validated, we can apply them // Now when all fields are validated, we can apply them
if (updated.label) { if (updated.label) {
this.label = data.label; this._saveLabel(data.label);
this.attributes = {};
this.appendDefaultAttributes(data.label);
} }
if (updated.attributes) { if (updated.attributes) {
for (const attrID of Object.keys(data.attributes)) { this._saveAttributes(data.attributes);
this.attributes[attrID] = data.attributes[attrID];
}
} }
if (updated.lock) { if (updated.lock) {
this.lock = data.lock; this._saveLock(data.lock);
} }
this.updateTimestamp(updated); this.updateTimestamp(updated);

@ -1,5 +1,5 @@
/* /*
* Copyright (C) 2019 Intel Corporation * Copyright (C) 2019-2020 Intel Corporation
* SPDX-License-Identifier: MIT * SPDX-License-Identifier: MIT
*/ */
@ -11,6 +11,7 @@
const serverProxy = require('./server-proxy'); const serverProxy = require('./server-proxy');
const Collection = require('./annotations-collection'); const Collection = require('./annotations-collection');
const AnnotationsSaver = require('./annotations-saver'); const AnnotationsSaver = require('./annotations-saver');
const AnnotationsHistory = require('./annotations-history');
const { checkObjectType } = require('./common'); const { checkObjectType } = require('./common');
const { Task } = require('./session'); const { Task } = require('./session');
const { const {
@ -56,27 +57,35 @@
frameMeta[i] = await session.frames.get(i); frameMeta[i] = await session.frames.get(i);
} }
const history = new AnnotationsHistory();
const collection = new Collection({ const collection = new Collection({
labels: session.labels || session.task.labels, labels: session.labels || session.task.labels,
history,
startFrame, startFrame,
stopFrame, stopFrame,
frameMeta, frameMeta,
}).import(rawAnnotations); });
collection.import(rawAnnotations);
const saver = new AnnotationsSaver(rawAnnotations.version, collection, session); const saver = new AnnotationsSaver(rawAnnotations.version, collection, session);
cache.set(session, { cache.set(session, {
collection, collection,
saver, saver,
history,
}); });
} }
} }
async function getAnnotations(session, frame, filter) { async function getAnnotations(session, frame, filter) {
await getAnnotationsFromServer(session);
const sessionType = session instanceof Task ? 'task' : 'job'; const sessionType = session instanceof Task ? 'task' : 'job';
const cache = getCache(sessionType); const cache = getCache(sessionType);
if (cache.has(session)) {
return cache.get(session).collection.get(frame, filter);
}
await getAnnotationsFromServer(session);
return cache.get(session).collection.get(frame, filter); return cache.get(session).collection.get(frame, filter);
} }
@ -244,6 +253,58 @@
return result; return result;
} }
function undoActions(session, count) {
const sessionType = session instanceof Task ? 'task' : 'job';
const cache = getCache(sessionType);
if (cache.has(session)) {
return cache.get(session).history.undo(count);
}
throw new DataError(
'Collection has not been initialized yet. Call annotations.get() or annotations.clear(true) before',
);
}
function redoActions(session, count) {
const sessionType = session instanceof Task ? 'task' : 'job';
const cache = getCache(sessionType);
if (cache.has(session)) {
return cache.get(session).history.redo(count);
}
throw new DataError(
'Collection has not been initialized yet. Call annotations.get() or annotations.clear(true) before',
);
}
function clearActions(session) {
const sessionType = session instanceof Task ? 'task' : 'job';
const cache = getCache(sessionType);
if (cache.has(session)) {
return cache.get(session).history.clear();
}
throw new DataError(
'Collection has not been initialized yet. Call annotations.get() or annotations.clear(true) before',
);
}
function getActions(session) {
const sessionType = session instanceof Task ? 'task' : 'job';
const cache = getCache(sessionType);
if (cache.has(session)) {
return cache.get(session).history.get();
}
throw new DataError(
'Collection has not been initialized yet. Call annotations.get() or annotations.clear(true) before',
);
}
module.exports = { module.exports = {
getAnnotations, getAnnotations,
putAnnotations, putAnnotations,
@ -258,5 +319,9 @@
uploadAnnotations, uploadAnnotations,
dumpAnnotations, dumpAnnotations,
exportDataset, exportDataset,
undoActions,
redoActions,
clearActions,
getActions,
}; };
})(); })();

@ -1,5 +1,5 @@
/* /*
* Copyright (C) 2019 Intel Corporation * Copyright (C) 2019-2020 Intel Corporation
* SPDX-License-Identifier: MIT * SPDX-License-Identifier: MIT
*/ */
@ -28,6 +28,7 @@ function build() {
ObjectType, ObjectType,
ObjectShape, ObjectShape,
LogType, LogType,
HistoryActions,
colors, colors,
} = require('./enums'); } = require('./enums');
@ -498,6 +499,7 @@ function build() {
ObjectType, ObjectType,
ObjectShape, ObjectShape,
LogType, LogType,
HistoryActions,
colors, colors,
}, },
/** /**

@ -1,5 +1,5 @@
/* /*
* Copyright (C) 2019 Intel Corporation * Copyright (C) 2019-2020 Intel Corporation
* SPDX-License-Identifier: MIT * SPDX-License-Identifier: MIT
*/ */
@ -166,6 +166,45 @@
rotateImage: 26, rotateImage: 26,
}; };
/**
* Types of actions with annotations
* @enum {string}
* @name HistoryActions
* @memberof module:API.cvat.enums
* @property {string} CHANGED_LABEL Changed label
* @property {string} CHANGED_ATTRIBUTES Changed attributes
* @property {string} CHANGED_POINTS Changed points
* @property {string} CHANGED_OUTSIDE Changed outside
* @property {string} CHANGED_OCCLUDED Changed occluded
* @property {string} CHANGED_ZORDER Changed z-order
* @property {string} CHANGED_LOCK Changed lock
* @property {string} CHANGED_COLOR Changed color
* @property {string} CHANGED_HIDDEN Changed hidden
* @property {string} MERGED_OBJECTS Merged objects
* @property {string} SPLITTED_TRACK Splitted track
* @property {string} GROUPED_OBJECTS Grouped objects
* @property {string} CREATED_OBJECTS Created objects
* @property {string} REMOVED_OBJECT Removed object
* @readonly
*/
const HistoryActions = Object.freeze({
CHANGED_LABEL: 'Changed label',
CHANGED_ATTRIBUTES: 'Changed attributes',
CHANGED_POINTS: 'Changed points',
CHANGED_OUTSIDE: 'Changed outside',
CHANGED_OCCLUDED: 'Changed occluded',
CHANGED_ZORDER: 'Changed z-order',
CHANGED_KEYFRAME: 'Changed keyframe',
CHANGED_LOCK: 'Changed lock',
CHANGED_COLOR: 'Changed color',
CHANGED_HIDDEN: 'Changed hidden',
MERGED_OBJECTS: 'Merged objects',
SPLITTED_TRACK: 'Splitted track',
GROUPED_OBJECTS: 'Grouped objects',
CREATED_OBJECTS: 'Created objects',
REMOVED_OBJECT: 'Removed object',
});
/** /**
* Array of hex colors * Array of hex colors
* @type {module:API.cvat.classes.Loader[]} values * @type {module:API.cvat.classes.Loader[]} values
@ -189,6 +228,7 @@
ObjectType, ObjectType,
ObjectShape, ObjectShape,
LogType, LogType,
HistoryActions,
colors, colors,
}; };
})(); })();

@ -1,5 +1,5 @@
/* /*
* Copyright (C) 2019 Intel Corporation * Copyright (C) 2019-2020 Intel Corporation
* SPDX-License-Identifier: MIT * SPDX-License-Identifier: MIT
*/ */
@ -141,12 +141,12 @@
}), }),
actions: Object.freeze({ actions: Object.freeze({
value: { value: {
async undo(count) { async undo(count = 1) {
const result = await PluginRegistry const result = await PluginRegistry
.apiWrapper.call(this, prototype.actions.undo, count); .apiWrapper.call(this, prototype.actions.undo, count);
return result; return result;
}, },
async redo(count) { async redo(count = 1) {
const result = await PluginRegistry const result = await PluginRegistry
.apiWrapper.call(this, prototype.actions.redo, count); .apiWrapper.call(this, prototype.actions.redo, count);
return result; return result;
@ -156,6 +156,11 @@
.apiWrapper.call(this, prototype.actions.clear); .apiWrapper.call(this, prototype.actions.clear);
return result; return result;
}, },
async get() {
const result = await PluginRegistry
.apiWrapper.call(this, prototype.actions.get);
return result;
},
}, },
writable: true, writable: true,
}), }),
@ -454,28 +459,48 @@
*/ */
/** /**
* Is a dictionary of pairs "id:action" where "id" is an identifier of an object * @typedef {Object} HistoryActions
* which has been affected by undo/redo and "action" is what exactly has been * @property {string[]} [undo] - array of possible actions to undo
* done with the object. Action can be: "created", "deleted", "updated". * @property {string[]} [redo] - array of possible actions to redo
* Size of an output array equal the param "count".
* @typedef {Object} HistoryAction
* @global * @global
*/ */
/** /**
* Undo actions * Make undo
* @method undo * @method undo
* @memberof Session.actions * @memberof Session.actions
* @returns {HistoryAction} * @param {number} [count=1] number of actions to undo
* @returns {number[]} Array of affected objects
* @throws {module:API.cvat.exceptions.PluginError} * @throws {module:API.cvat.exceptions.PluginError}
* @throws {module:API.cvat.exceptions.ArgumentError}
* @instance * @instance
* @async * @async
*/ */
/** /**
* Redo actions * Make redo
* @method redo * @method redo
* @memberof Session.actions * @memberof Session.actions
* @returns {HistoryAction} * @param {number} [count=1] number of actions to redo
* @returns {number[]} Array of affected objects
* @throws {module:API.cvat.exceptions.PluginError}
* @throws {module:API.cvat.exceptions.ArgumentError}
* @instance
* @async
*/
/**
* Remove all actions from history
* @method clear
* @memberof Session.actions
* @throws {module:API.cvat.exceptions.PluginError}
* @instance
* @async
*/
/**
* Get actions
* @method get
* @memberof Session.actions
* @returns {HistoryActions}
* @throws {module:API.cvat.exceptions.PluginError} * @throws {module:API.cvat.exceptions.PluginError}
* @throws {module:API.cvat.exceptions.ArgumentError}
* @instance * @instance
* @async * @async
*/ */
@ -653,6 +678,13 @@
.annotations.hasUnsavedChanges.bind(this), .annotations.hasUnsavedChanges.bind(this),
}; };
this.actions = {
undo: Object.getPrototypeOf(this).actions.undo.bind(this),
redo: Object.getPrototypeOf(this).actions.redo.bind(this),
clear: Object.getPrototypeOf(this).actions.clear.bind(this),
get: Object.getPrototypeOf(this).actions.get.bind(this),
};
this.frames = { this.frames = {
get: Object.getPrototypeOf(this).frames.get.bind(this), get: Object.getPrototypeOf(this).frames.get.bind(this),
preview: Object.getPrototypeOf(this).frames.preview.bind(this), preview: Object.getPrototypeOf(this).frames.preview.bind(this),
@ -1155,6 +1187,13 @@
.annotations.exportDataset.bind(this), .annotations.exportDataset.bind(this),
}; };
this.actions = {
undo: Object.getPrototypeOf(this).actions.undo.bind(this),
redo: Object.getPrototypeOf(this).actions.redo.bind(this),
clear: Object.getPrototypeOf(this).actions.clear.bind(this),
get: Object.getPrototypeOf(this).actions.get.bind(this),
};
this.frames = { this.frames = {
get: Object.getPrototypeOf(this).frames.get.bind(this), get: Object.getPrototypeOf(this).frames.get.bind(this),
preview: Object.getPrototypeOf(this).frames.preview.bind(this), preview: Object.getPrototypeOf(this).frames.preview.bind(this),
@ -1217,6 +1256,10 @@
uploadAnnotations, uploadAnnotations,
dumpAnnotations, dumpAnnotations,
exportDataset, exportDataset,
undoActions,
redoActions,
clearActions,
getActions,
} = require('./annotations'); } = require('./annotations');
buildDublicatedAPI(Job.prototype); buildDublicatedAPI(Job.prototype);
@ -1328,6 +1371,31 @@
return result; return result;
}; };
Job.prototype.annotations.exportDataset.implementation = async function (format) {
const result = await exportDataset(this.task, format);
return result;
};
Job.prototype.actions.undo.implementation = function (count) {
const result = undoActions(this, count);
return result;
};
Job.prototype.actions.redo.implementation = function (count) {
const result = redoActions(this, count);
return result;
};
Job.prototype.actions.clear.implementation = function () {
const result = clearActions(this);
return result;
};
Job.prototype.actions.get.implementation = function () {
const result = getActions(this);
return result;
};
Task.prototype.save.implementation = async function saveTaskImplementation(onUpdate) { Task.prototype.save.implementation = async function saveTaskImplementation(onUpdate) {
// TODO: Add ability to change an owner and an assignee // TODO: Add ability to change an owner and an assignee
if (typeof (this.id) !== 'undefined') { if (typeof (this.id) !== 'undefined') {
@ -1484,4 +1552,24 @@
const result = await exportDataset(this, format); const result = await exportDataset(this, format);
return result; return result;
}; };
Task.prototype.actions.undo.implementation = function (count) {
const result = undoActions(this, count);
return result;
};
Task.prototype.actions.redo.implementation = function (count) {
const result = redoActions(this, count);
return result;
};
Task.prototype.actions.clear.implementation = function () {
const result = clearActions(this);
return result;
};
Task.prototype.actions.get.implementation = function () {
const result = getActions(this);
return result;
};
})(); })();

@ -266,7 +266,7 @@ describe('Feature: check unsaved changes', () => {
expect(await task.annotations.hasUnsavedChanges()).toBe(false); expect(await task.annotations.hasUnsavedChanges()).toBe(false);
const annotations = await task.annotations.get(0); const annotations = await task.annotations.get(0);
annotations[0].keyframe = true; annotations[0].keyframe = false;
await annotations[0].save(); await annotations[0].save();
expect(await task.annotations.hasUnsavedChanges()).toBe(true); expect(await task.annotations.hasUnsavedChanges()).toBe(true);

@ -74,6 +74,64 @@ export enum AnnotationActionTypes {
REMOVE_JOB_ANNOTATIONS_SUCCESS = 'REMOVE_JOB_ANNOTATIONS_SUCCESS', REMOVE_JOB_ANNOTATIONS_SUCCESS = 'REMOVE_JOB_ANNOTATIONS_SUCCESS',
REMOVE_JOB_ANNOTATIONS_FAILED = 'REMOVE_JOB_ANNOTATIONS_FAILED', REMOVE_JOB_ANNOTATIONS_FAILED = 'REMOVE_JOB_ANNOTATIONS_FAILED',
UPDATE_CANVAS_CONTEXT_MENU = 'UPDATE_CANVAS_CONTEXT_MENU', UPDATE_CANVAS_CONTEXT_MENU = 'UPDATE_CANVAS_CONTEXT_MENU',
UNDO_ACTION_SUCCESS = 'UNDO_ACTION_SUCCESS',
UNDO_ACTION_FAILED = 'UNDO_ACTION_FAILED',
REDO_ACTION_SUCCESS = 'REDO_ACTION_SUCCESS',
REDO_ACTION_FAILED = 'REDO_ACTION_FAILED',
}
export function undoActionAsync(sessionInstance: any, frame: number):
ThunkAction<Promise<void>, {}, {}, AnyAction> {
return async (dispatch: ActionCreator<Dispatch>): Promise<void> => {
try {
// TODO: use affected IDs as an optimization
await sessionInstance.actions.undo();
const history = await sessionInstance.actions.get();
const states = await sessionInstance.annotations.get(frame);
dispatch({
type: AnnotationActionTypes.UNDO_ACTION_SUCCESS,
payload: {
history,
states,
},
});
} catch (error) {
dispatch({
type: AnnotationActionTypes.UNDO_ACTION_FAILED,
payload: {
error,
},
});
}
};
}
export function redoActionAsync(sessionInstance: any, frame: number):
ThunkAction<Promise<void>, {}, {}, AnyAction> {
return async (dispatch: ActionCreator<Dispatch>): Promise<void> => {
try {
// TODO: use affected IDs as an optimization
await sessionInstance.actions.redo();
const history = await sessionInstance.actions.get();
const states = await sessionInstance.annotations.get(frame);
dispatch({
type: AnnotationActionTypes.REDO_ACTION_SUCCESS,
payload: {
history,
states,
},
});
} catch (error) {
dispatch({
type: AnnotationActionTypes.REDO_ACTION_FAILED,
payload: {
error,
},
});
}
};
} }
export function updateCanvasContextMenu(visible: boolean, left: number, top: number): AnyAction { export function updateCanvasContextMenu(visible: boolean, left: number, top: number): AnyAction {
@ -91,11 +149,14 @@ export function removeAnnotationsAsync(sessionInstance: any):
ThunkAction<Promise<void>, {}, {}, AnyAction> { ThunkAction<Promise<void>, {}, {}, AnyAction> {
return async (dispatch: ActionCreator<Dispatch>): Promise<void> => { return async (dispatch: ActionCreator<Dispatch>): Promise<void> => {
try { try {
sessionInstance.annotations.clear(); await sessionInstance.annotations.clear();
await sessionInstance.actions.clear();
const history = await sessionInstance.actions.get();
dispatch({ dispatch({
type: AnnotationActionTypes.REMOVE_JOB_ANNOTATIONS_SUCCESS, type: AnnotationActionTypes.REMOVE_JOB_ANNOTATIONS_SUCCESS,
payload: { payload: {
sessionInstance, history,
}, },
}); });
} catch (error) { } catch (error) {
@ -109,7 +170,6 @@ ThunkAction<Promise<void>, {}, {}, AnyAction> {
}; };
} }
export function uploadJobAnnotationsAsync(job: any, loader: any, file: File): export function uploadJobAnnotationsAsync(job: any, loader: any, file: File):
ThunkAction<Promise<void>, {}, {}, AnyAction> { ThunkAction<Promise<void>, {}, {}, AnyAction> {
return async (dispatch: ActionCreator<Dispatch>): Promise<void> => { return async (dispatch: ActionCreator<Dispatch>): Promise<void> => {
@ -146,12 +206,15 @@ ThunkAction<Promise<void>, {}, {}, AnyAction> {
}); });
await job.annotations.clear(true); await job.annotations.clear(true);
await job.actions.clear();
const history = await job.actions.get();
const states = await job.annotations.get(frame); const states = await job.annotations.get(frame);
setTimeout(() => { setTimeout(() => {
dispatch({ dispatch({
type: AnnotationActionTypes.UPLOAD_JOB_ANNOTATIONS_SUCCESS, type: AnnotationActionTypes.UPLOAD_JOB_ANNOTATIONS_SUCCESS,
payload: { payload: {
history,
job, job,
states, states,
}, },
@ -264,11 +327,13 @@ export function propagateObjectAsync(
} }
await sessionInstance.annotations.put(states); await sessionInstance.annotations.put(states);
const history = await sessionInstance.actions.get();
dispatch({ dispatch({
type: AnnotationActionTypes.PROPAGATE_OBJECT_SUCCESS, type: AnnotationActionTypes.PROPAGATE_OBJECT_SUCCESS,
payload: { payload: {
objectState, objectState,
history,
}, },
}); });
} catch (error) { } catch (error) {
@ -300,16 +365,19 @@ export function changePropagateFrames(frames: number): AnyAction {
}; };
} }
export function removeObjectAsync(objectState: any, force: boolean): export function removeObjectAsync(sessionInstance: any, objectState: any, force: boolean):
ThunkAction<Promise<void>, {}, {}, AnyAction> { ThunkAction<Promise<void>, {}, {}, AnyAction> {
return async (dispatch: ActionCreator<Dispatch>): Promise<void> => { return async (dispatch: ActionCreator<Dispatch>): Promise<void> => {
try { try {
const removed = await objectState.delete(force); const removed = await objectState.delete(force);
const history = await sessionInstance.actions.get();
if (removed) { if (removed) {
dispatch({ dispatch({
type: AnnotationActionTypes.REMOVE_OBJECT_SUCCESS, type: AnnotationActionTypes.REMOVE_OBJECT_SUCCESS,
payload: { payload: {
objectState, objectState,
history,
}, },
}); });
} else { } else {
@ -647,11 +715,13 @@ ThunkAction<Promise<void>, {}, {}, AnyAction> {
try { try {
const promises = statesToUpdate.map((state: any): Promise<any> => state.save()); const promises = statesToUpdate.map((state: any): Promise<any> => state.save());
const states = await Promise.all(promises); const states = await Promise.all(promises);
const history = await sessionInstance.actions.get();
dispatch({ dispatch({
type: AnnotationActionTypes.UPDATE_ANNOTATIONS_SUCCESS, type: AnnotationActionTypes.UPDATE_ANNOTATIONS_SUCCESS,
payload: { payload: {
states, states,
history,
}, },
}); });
} catch (error) { } catch (error) {
@ -673,11 +743,13 @@ ThunkAction<Promise<void>, {}, {}, AnyAction> {
try { try {
await sessionInstance.annotations.put(statesToCreate); await sessionInstance.annotations.put(statesToCreate);
const states = await sessionInstance.annotations.get(frame); const states = await sessionInstance.annotations.get(frame);
const history = await sessionInstance.actions.get();
dispatch({ dispatch({
type: AnnotationActionTypes.CREATE_ANNOTATIONS_SUCCESS, type: AnnotationActionTypes.CREATE_ANNOTATIONS_SUCCESS,
payload: { payload: {
states, states,
history,
}, },
}); });
} catch (error) { } catch (error) {
@ -697,11 +769,13 @@ ThunkAction<Promise<void>, {}, {}, AnyAction> {
try { try {
await sessionInstance.annotations.merge(statesToMerge); await sessionInstance.annotations.merge(statesToMerge);
const states = await sessionInstance.annotations.get(frame); const states = await sessionInstance.annotations.get(frame);
const history = await sessionInstance.actions.get();
dispatch({ dispatch({
type: AnnotationActionTypes.MERGE_ANNOTATIONS_SUCCESS, type: AnnotationActionTypes.MERGE_ANNOTATIONS_SUCCESS,
payload: { payload: {
states, states,
history,
}, },
}); });
} catch (error) { } catch (error) {
@ -721,11 +795,13 @@ ThunkAction<Promise<void>, {}, {}, AnyAction> {
try { try {
await sessionInstance.annotations.group(statesToGroup); await sessionInstance.annotations.group(statesToGroup);
const states = await sessionInstance.annotations.get(frame); const states = await sessionInstance.annotations.get(frame);
const history = await sessionInstance.actions.get();
dispatch({ dispatch({
type: AnnotationActionTypes.GROUP_ANNOTATIONS_SUCCESS, type: AnnotationActionTypes.GROUP_ANNOTATIONS_SUCCESS,
payload: { payload: {
states, states,
history,
}, },
}); });
} catch (error) { } catch (error) {
@ -745,11 +821,13 @@ ThunkAction<Promise<void>, {}, {}, AnyAction> {
try { try {
await sessionInstance.annotations.split(stateToSplit, frame); await sessionInstance.annotations.split(stateToSplit, frame);
const states = await sessionInstance.annotations.get(frame); const states = await sessionInstance.annotations.get(frame);
const history = await sessionInstance.actions.get();
dispatch({ dispatch({
type: AnnotationActionTypes.SPLIT_ANNOTATIONS_SUCCESS, type: AnnotationActionTypes.SPLIT_ANNOTATIONS_SUCCESS,
payload: { payload: {
states, states,
history,
}, },
}); });
} catch (error) { } catch (error) {
@ -774,11 +852,13 @@ export function changeLabelColorAsync(
const updatedLabel = label; const updatedLabel = label;
updatedLabel.color = color; updatedLabel.color = color;
const states = await sessionInstance.annotations.get(frameNumber); const states = await sessionInstance.annotations.get(frameNumber);
const history = await sessionInstance.actions.get();
dispatch({ dispatch({
type: AnnotationActionTypes.CHANGE_LABEL_COLOR_SUCCESS, type: AnnotationActionTypes.CHANGE_LABEL_COLOR_SUCCESS,
payload: { payload: {
label: updatedLabel, label: updatedLabel,
history,
states, states,
}, },
}); });

@ -86,7 +86,7 @@ export default function ControlsSideBarComponent(props: Props): JSX.Element {
isDrawing={activeControl === ActiveControl.DRAW_POINTS} isDrawing={activeControl === ActiveControl.DRAW_POINTS}
/> />
<Tooltip overlay='Setup a tag' placement='right'> <Tooltip title='Setup a tag' placement='right'>
<Icon component={TagIcon} style={{ pointerEvents: 'none', opacity: 0.5 }} /> <Icon component={TagIcon} style={{ pointerEvents: 'none', opacity: 0.5 }} />
</Tooltip> </Tooltip>

@ -29,7 +29,7 @@ function CursorControl(props: Props): JSX.Element {
} = props; } = props;
return ( return (
<Tooltip overlay='Cursor' placement='right'> <Tooltip title='Cursor' placement='right'>
<Icon <Icon
component={CursorIcon} component={CursorIcon}
className={activeControl === ActiveControl.CURSOR className={activeControl === ActiveControl.CURSOR

@ -23,7 +23,7 @@ function FitControl(props: Props): JSX.Element {
} = props; } = props;
return ( return (
<Tooltip overlay='Fit the image' placement='right'> <Tooltip title='Fit the image' placement='right'>
<Icon component={FitIcon} onClick={(): void => canvasInstance.fit()} /> <Icon component={FitIcon} onClick={(): void => canvasInstance.fit()} />
</Tooltip> </Tooltip>
); );

@ -42,7 +42,7 @@ function GroupControl(props: Props): JSX.Element {
}; };
return ( return (
<Tooltip overlay='Group shapes/tracks' placement='right'> <Tooltip title='Group shapes/tracks' placement='right'>
<Icon {...dynamicIconProps} component={GroupIcon} /> <Icon {...dynamicIconProps} component={GroupIcon} />
</Tooltip> </Tooltip>
); );

@ -42,7 +42,7 @@ function MergeControl(props: Props): JSX.Element {
}; };
return ( return (
<Tooltip overlay='Merge shapes/tracks' placement='right'> <Tooltip title='Merge shapes/tracks' placement='right'>
<Icon {...dynamicIconProps} component={MergeIcon} /> <Icon {...dynamicIconProps} component={MergeIcon} />
</Tooltip> </Tooltip>
); );

@ -29,7 +29,7 @@ function MoveControl(props: Props): JSX.Element {
} = props; } = props;
return ( return (
<Tooltip overlay='Move the image' placement='right'> <Tooltip title='Move the image' placement='right'>
<Icon <Icon
component={MoveIcon} component={MoveIcon}
className={activeControl === ActiveControl.DRAG_CANVAS className={activeControl === ActiveControl.DRAG_CANVAS

@ -29,7 +29,7 @@ function ResizeControl(props: Props): JSX.Element {
} = props; } = props;
return ( return (
<Tooltip overlay='Select a region of interest' placement='right'> <Tooltip title='Select a region of interest' placement='right'>
<Icon <Icon
component={ZoomIcon} component={ZoomIcon}
className={activeControl === ActiveControl.ZOOM_CANVAS className={activeControl === ActiveControl.ZOOM_CANVAS

@ -32,7 +32,7 @@ function RotateControl(props: Props): JSX.Element {
placement='right' placement='right'
content={( content={(
<> <>
<Tooltip overlay='Rotate the image anticlockwise' placement='topRight'> <Tooltip title='Rotate the image anticlockwise' placement='topRight'>
<Icon <Icon
className='cvat-rotate-canvas-controls-left' className='cvat-rotate-canvas-controls-left'
onClick={(): void => canvasInstance onClick={(): void => canvasInstance
@ -40,7 +40,7 @@ function RotateControl(props: Props): JSX.Element {
component={RotateIcon} component={RotateIcon}
/> />
</Tooltip> </Tooltip>
<Tooltip overlay='Rotate the image clockwise' placement='topRight'> <Tooltip title='Rotate the image clockwise' placement='topRight'>
<Icon <Icon
className='cvat-rotate-canvas-controls-right' className='cvat-rotate-canvas-controls-right'
onClick={(): void => canvasInstance onClick={(): void => canvasInstance

@ -42,7 +42,7 @@ function SplitControl(props: Props): JSX.Element {
}; };
return ( return (
<Tooltip overlay='Split a track' placement='right'> <Tooltip title='Split a track' placement='right'>
<Icon {...dynamicIconProps} component={SplitIcon} /> <Icon {...dynamicIconProps} component={SplitIcon} />
</Tooltip> </Tooltip>
); );

@ -21,14 +21,22 @@ import {
interface Props { interface Props {
saving: boolean; saving: boolean;
savingStatuses: string[]; savingStatuses: string[];
undoAction?: string;
redoAction?: string;
onSaveAnnotation(): void; onSaveAnnotation(): void;
onUndoClick(): void;
onRedoClick(): void;
} }
function LeftGroup(props: Props): JSX.Element { function LeftGroup(props: Props): JSX.Element {
const { const {
saving, saving,
savingStatuses, savingStatuses,
undoAction,
redoAction,
onSaveAnnotation, onSaveAnnotation,
onUndoClick,
onRedoClick,
} = props; } = props;
return ( return (
@ -67,11 +75,25 @@ function LeftGroup(props: Props): JSX.Element {
</Timeline> </Timeline>
</Modal> </Modal>
</Button> </Button>
<Button disabled type='link' className='cvat-annotation-header-button'> <Button
title={undoAction}
disabled={!undoAction}
style={{ pointerEvents: undoAction ? 'initial' : 'none', opacity: undoAction ? 1 : 0.5 }}
type='link'
className='cvat-annotation-header-button'
onClick={onUndoClick}
>
<Icon component={UndoIcon} /> <Icon component={UndoIcon} />
Undo <span>Undo</span>
</Button> </Button>
<Button disabled type='link' className='cvat-annotation-header-button'> <Button
title={redoAction}
disabled={!redoAction}
style={{ pointerEvents: redoAction ? 'initial' : 'none', opacity: redoAction ? 1 : 0.5 }}
type='link'
className='cvat-annotation-header-button'
onClick={onRedoClick}
>
<Icon component={RedoIcon} /> <Icon component={RedoIcon} />
Redo Redo
</Button> </Button>

@ -42,19 +42,19 @@ function PlayerButtons(props: Props): JSX.Element {
return ( return (
<Col className='cvat-player-buttons'> <Col className='cvat-player-buttons'>
<Tooltip overlay='Go to the first frame'> <Tooltip title='Go to the first frame'>
<Icon component={FirstIcon} onClick={onFirstFrame} /> <Icon component={FirstIcon} onClick={onFirstFrame} />
</Tooltip> </Tooltip>
<Tooltip overlay='Go back with a step'> <Tooltip title='Go back with a step'>
<Icon component={BackJumpIcon} onClick={onBackward} /> <Icon component={BackJumpIcon} onClick={onBackward} />
</Tooltip> </Tooltip>
<Tooltip overlay='Go back'> <Tooltip title='Go back'>
<Icon component={PreviousIcon} onClick={onPrevFrame} /> <Icon component={PreviousIcon} onClick={onPrevFrame} />
</Tooltip> </Tooltip>
{!playing {!playing
? ( ? (
<Tooltip overlay='Play'> <Tooltip title='Play'>
<Icon <Icon
component={PlayIcon} component={PlayIcon}
onClick={onSwitchPlay} onClick={onSwitchPlay}
@ -62,7 +62,7 @@ function PlayerButtons(props: Props): JSX.Element {
</Tooltip> </Tooltip>
) )
: ( : (
<Tooltip overlay='Pause'> <Tooltip title='Pause'>
<Icon <Icon
component={PauseIcon} component={PauseIcon}
onClick={onSwitchPlay} onClick={onSwitchPlay}
@ -71,13 +71,13 @@ function PlayerButtons(props: Props): JSX.Element {
) )
} }
<Tooltip overlay='Go next'> <Tooltip title='Go next'>
<Icon component={NextIcon} onClick={onNextFrame} /> <Icon component={NextIcon} onClick={onNextFrame} />
</Tooltip> </Tooltip>
<Tooltip overlay='Go next with a step'> <Tooltip title='Go next with a step'>
<Icon component={ForwardJumpIcon} onClick={onForward} /> <Icon component={ForwardJumpIcon} onClick={onForward} />
</Tooltip> </Tooltip>
<Tooltip overlay='Go to the last frame'> <Tooltip title='Go to the last frame'>
<Icon component={LastIcon} onClick={onLastFrame} /> <Icon component={LastIcon} onClick={onLastFrame} />
</Tooltip> </Tooltip>
</Col> </Col>

@ -44,7 +44,7 @@ function PlayerNavigation(props: Props): JSX.Element {
</Row> </Row>
<Row type='flex' justify='space-around'> <Row type='flex' justify='space-around'>
<Col className='cvat-player-filename-wrapper'> <Col className='cvat-player-filename-wrapper'>
<Tooltip overlay='filename.png'> <Tooltip title='filename.png'>
<Text type='secondary'>filename.png</Text> <Text type='secondary'>filename.png</Text>
</Tooltip> </Tooltip>
</Col> </Col>

@ -90,7 +90,7 @@ export default function StatisticsModalComponent(props: Props): JSX.Element {
}); });
const makeShapesTracksTitle = (title: string): JSX.Element => ( const makeShapesTracksTitle = (title: string): JSX.Element => (
<Tooltip overlay='Shapes / Tracks'> <Tooltip title='Shapes / Tracks'>
<Text strong style={{ marginRight: 5 }}>{title}</Text> <Text strong style={{ marginRight: 5 }}>{title}</Text>
<Icon className='cvat-info-circle-icon' type='question-circle' /> <Icon className='cvat-info-circle-icon' type='question-circle' />
</Tooltip> </Tooltip>

@ -20,6 +20,8 @@ interface Props {
frameNumber: number; frameNumber: number;
startFrame: number; startFrame: number;
stopFrame: number; stopFrame: number;
undoAction?: string;
redoAction?: string;
showStatistics(): void; showStatistics(): void;
onSwitchPlay(): void; onSwitchPlay(): void;
onSaveAnnotation(): void; onSaveAnnotation(): void;
@ -31,21 +33,16 @@ interface Props {
onLastFrame(): void; onLastFrame(): void;
onSliderChange(value: SliderValue): void; onSliderChange(value: SliderValue): void;
onInputChange(value: number | undefined): void; onInputChange(value: number | undefined): void;
} onUndoClick(): void;
onRedoClick(): void;
function propsAreEqual(curProps: Props, prevProps: Props): boolean {
return curProps.playing === prevProps.playing
&& curProps.saving === prevProps.saving
&& curProps.frameNumber === prevProps.frameNumber
&& curProps.startFrame === prevProps.startFrame
&& curProps.stopFrame === prevProps.stopFrame
&& curProps.savingStatuses.length === prevProps.savingStatuses.length;
} }
function AnnotationTopBarComponent(props: Props): JSX.Element { function AnnotationTopBarComponent(props: Props): JSX.Element {
const { const {
saving, saving,
savingStatuses, savingStatuses,
undoAction,
redoAction,
playing, playing,
frameNumber, frameNumber,
startFrame, startFrame,
@ -61,6 +58,8 @@ function AnnotationTopBarComponent(props: Props): JSX.Element {
onLastFrame, onLastFrame,
onSliderChange, onSliderChange,
onInputChange, onInputChange,
onUndoClick,
onRedoClick,
} = props; } = props;
return ( return (
@ -70,6 +69,10 @@ function AnnotationTopBarComponent(props: Props): JSX.Element {
saving={saving} saving={saving}
savingStatuses={savingStatuses} savingStatuses={savingStatuses}
onSaveAnnotation={onSaveAnnotation} onSaveAnnotation={onSaveAnnotation}
undoAction={undoAction}
redoAction={redoAction}
onUndoClick={onUndoClick}
onRedoClick={onRedoClick}
/> />
<Col className='cvat-annotation-header-player-group'> <Col className='cvat-annotation-header-player-group'>
<Row type='flex' align='middle'> <Row type='flex' align='middle'>
@ -98,4 +101,4 @@ function AnnotationTopBarComponent(props: Props): JSX.Element {
); );
} }
export default React.memo(AnnotationTopBarComponent, propsAreEqual); export default React.memo(AnnotationTopBarComponent);

@ -107,7 +107,7 @@ export default class CreateModelContent extends React.PureComponent<Props> {
return ( return (
<Row type='flex' justify='start' align='middle' className='cvat-create-model-content'> <Row type='flex' justify='start' align='middle' className='cvat-create-model-content'>
<Col span={24}> <Col span={24}>
<Tooltip overlay='Click to open guide'> <Tooltip title='Click to open guide'>
<Icon <Icon
onClick={(): void => { onClick={(): void => {
// false positive // false positive

@ -59,7 +59,7 @@ export class CreateModelForm extends React.PureComponent<Props> {
</Col> </Col>
<Col span={8} offset={2}> <Col span={8} offset={2}>
<Form.Item> <Form.Item>
<Tooltip overlay='Will this model be availabe for everyone?'> <Tooltip title='Will this model be availabe for everyone?'>
{ getFieldDecorator('global', { { getFieldDecorator('global', {
initialValue: false, initialValue: false,
valuePropName: 'checked', valuePropName: 'checked',

@ -85,7 +85,7 @@ class AdvancedConfigurationForm extends React.PureComponent<Props> {
return ( return (
<Form.Item label={<span>Image quality</span>}> <Form.Item label={<span>Image quality</span>}>
<Tooltip overlay='Defines image compression level'> <Tooltip title='Defines image compression level'>
{form.getFieldDecorator('imageQuality', { {form.getFieldDecorator('imageQuality', {
initialValue: 70, initialValue: 70,
rules: [{ rules: [{
@ -111,7 +111,7 @@ class AdvancedConfigurationForm extends React.PureComponent<Props> {
return ( return (
<Form.Item label={<span>Overlap size</span>}> <Form.Item label={<span>Overlap size</span>}>
<Tooltip overlay='Defines a number of intersected frames between different segments'> <Tooltip title='Defines a number of intersected frames between different segments'>
{form.getFieldDecorator('overlapSize')( {form.getFieldDecorator('overlapSize')(
<Input size='large' type='number' />, <Input size='large' type='number' />,
)} )}
@ -125,7 +125,7 @@ class AdvancedConfigurationForm extends React.PureComponent<Props> {
return ( return (
<Form.Item label={<span>Segment size</span>}> <Form.Item label={<span>Segment size</span>}>
<Tooltip overlay='Defines a number of frames in a segment'> <Tooltip title='Defines a number of frames in a segment'>
{form.getFieldDecorator('segmentSize')( {form.getFieldDecorator('segmentSize')(
<Input size='large' type='number' />, <Input size='large' type='number' />,
)} )}

@ -138,7 +138,7 @@ class LabelForm extends React.PureComponent<Props, {}> {
return ( return (
<Col span={4}> <Col span={4}>
<Form.Item> <Form.Item>
<Tooltip overlay='An HTML element representing the attribute'> <Tooltip title='An HTML element representing the attribute'>
{ form.getFieldDecorator(`type[${key}]`, { { form.getFieldDecorator(`type[${key}]`, {
initialValue: type, initialValue: type,
})( })(
@ -188,7 +188,7 @@ class LabelForm extends React.PureComponent<Props, {}> {
}; };
return ( return (
<Tooltip overlay='Press enter to add a new value'> <Tooltip title='Press enter to add a new value'>
<Form.Item> <Form.Item>
{ form.getFieldDecorator(`values[${key}]`, { { form.getFieldDecorator(`values[${key}]`, {
initialValue: existedValues, initialValue: existedValues,
@ -215,7 +215,7 @@ class LabelForm extends React.PureComponent<Props, {}> {
const { form } = this.props; const { form } = this.props;
return ( return (
<Tooltip overlay='Specify a default value'> <Tooltip title='Specify a default value'>
<Form.Item> <Form.Item>
{ form.getFieldDecorator(`values[${key}]`, { { form.getFieldDecorator(`values[${key}]`, {
initialValue: value, initialValue: value,
@ -299,7 +299,7 @@ class LabelForm extends React.PureComponent<Props, {}> {
return ( return (
<Form.Item> <Form.Item>
<Tooltip overlay='Can this attribute be changed frame to frame?'> <Tooltip title='Can this attribute be changed frame to frame?'>
{ form.getFieldDecorator(`mutable[${key}]`, { { form.getFieldDecorator(`mutable[${key}]`, {
initialValue: value, initialValue: value,
valuePropName: 'checked', valuePropName: 'checked',
@ -316,7 +316,7 @@ class LabelForm extends React.PureComponent<Props, {}> {
return ( return (
<Form.Item> <Form.Item>
<Tooltip overlay='Delete the attribute'> <Tooltip title='Delete the attribute'>
<Button <Button
type='link' type='link'
className='cvat-delete-attribute-button' className='cvat-delete-attribute-button'
@ -417,7 +417,7 @@ class LabelForm extends React.PureComponent<Props, {}> {
private renderDoneButton(): JSX.Element { private renderDoneButton(): JSX.Element {
return ( return (
<Col> <Col>
<Tooltip overlay='Save the label and return'> <Tooltip title='Save the label and return'>
<Button <Button
style={{ width: '150px' }} style={{ width: '150px' }}
type='primary' type='primary'
@ -440,7 +440,7 @@ class LabelForm extends React.PureComponent<Props, {}> {
label ? <div /> label ? <div />
: ( : (
<Col offset={1}> <Col offset={1}>
<Tooltip overlay='Save the label and create one more'> <Tooltip title='Save the label and create one more'>
<Button <Button
style={{ width: '150px' }} style={{ width: '150px' }}
type='primary' type='primary'
@ -462,7 +462,7 @@ class LabelForm extends React.PureComponent<Props, {}> {
return ( return (
<Col offset={1}> <Col offset={1}>
<Tooltip overlay='Do not save the label and return'> <Tooltip title='Do not save the label and return'>
<Button <Button
style={{ width: '150px' }} style={{ width: '150px' }}
type='danger' type='danger'

@ -236,7 +236,7 @@ export default class LabelsEditor
tabBarStyle={{ marginBottom: '0px' }} tabBarStyle={{ marginBottom: '0px' }}
tabBarExtraContent={( tabBarExtraContent={(
<> <>
<Tooltip overlay='Copied to clipboard!' trigger='click'> <Tooltip title='Copied to clipboard!' trigger='click'>
<Button <Button
type='link' type='link'
icon='copy' icon='copy'

@ -76,7 +76,7 @@ class RawViewer extends React.PureComponent<Props> {
</Form.Item> </Form.Item>
<Row type='flex' justify='start' align='middle'> <Row type='flex' justify='start' align='middle'>
<Col> <Col>
<Tooltip overlay='Save labels and return'> <Tooltip title='Save labels and return'>
<Button <Button
style={{ width: '150px' }} style={{ width: '150px' }}
type='primary' type='primary'
@ -87,7 +87,7 @@ class RawViewer extends React.PureComponent<Props> {
</Tooltip> </Tooltip>
</Col> </Col>
<Col offset={1}> <Col offset={1}>
<Tooltip overlay='Do not save the label and return'> <Tooltip title='Do not save the label and return'>
<Button <Button
style={{ width: '150px' }} style={{ width: '150px' }}
type='danger' type='danger'

@ -186,7 +186,7 @@ export default class ModelRunnerModalComponent extends React.PureComponent<Props
<Tag color={colors[modelLabel]}>{taskLabel}</Tag> <Tag color={colors[modelLabel]}>{taskLabel}</Tag>
</Col> </Col>
<Col span={1} offset={1}> <Col span={1} offset={1}>
<Tooltip overlay='Remove the mapped values'> <Tooltip title='Remove the mapped values'>
<Icon <Icon
className='cvat-run-model-dialog-remove-mapping-icon' className='cvat-run-model-dialog-remove-mapping-icon'
type='close-circle' type='close-circle'
@ -288,7 +288,7 @@ export default class ModelRunnerModalComponent extends React.PureComponent<Props
)} )}
</Col> </Col>
<Col span={1} offset={1}> <Col span={1} offset={1}>
<Tooltip overlay='Specify a label mapping between model labels and task labels'> <Tooltip title='Specify a label mapping between model labels and task labels'>
<Icon className='cvat-info-circle-icon' type='question-circle' /> <Icon className='cvat-info-circle-icon' type='question-circle' />
</Tooltip> </Tooltip>
</Col> </Col>

@ -39,7 +39,7 @@ interface DispatchToProps {
updateState(sessionInstance: any, frameNumber: number, objectState: any): void; updateState(sessionInstance: any, frameNumber: number, objectState: any): void;
collapseOrExpand(objectStates: any[], collapsed: boolean): void; collapseOrExpand(objectStates: any[], collapsed: boolean): void;
activateObject: (activatedStateID: number | null) => void; activateObject: (activatedStateID: number | null) => void;
removeObject: (objectState: any) => void; removeObject: (sessionInstance: any, objectState: any) => void;
copyShape: (objectState: any) => void; copyShape: (objectState: any) => void;
propagateObject: (objectState: any) => void; propagateObject: (objectState: any) => void;
} }
@ -109,8 +109,8 @@ function mapDispatchToProps(dispatch: any): DispatchToProps {
activateObject(activatedStateID: number | null): void { activateObject(activatedStateID: number | null): void {
dispatch(activateObjectAction(activatedStateID)); dispatch(activateObjectAction(activatedStateID));
}, },
removeObject(objectState: any): void { removeObject(sessionInstance: any, objectState: any): void {
dispatch(removeObjectAsync(objectState, true)); dispatch(removeObjectAsync(sessionInstance, objectState, true));
}, },
copyShape(objectState: any): void { copyShape(objectState: any): void {
dispatch(copyShapeAction(objectState)); dispatch(copyShapeAction(objectState));
@ -197,9 +197,10 @@ class ObjectItemContainer extends React.PureComponent<Props> {
const { const {
objectState, objectState,
removeObject, removeObject,
jobInstance,
} = this.props; } = this.props;
removeObject(objectState); removeObject(jobInstance, objectState);
}; };
private activate = (): void => { private activate = (): void => {

@ -9,6 +9,8 @@ import {
saveAnnotationsAsync, saveAnnotationsAsync,
collectStatisticsAsync, collectStatisticsAsync,
showStatistics as showStatisticsAction, showStatistics as showStatisticsAction,
undoActionAsync,
redoActionAsync,
} from 'actions/annotation-actions'; } from 'actions/annotation-actions';
import AnnotationTopBarComponent from 'components/annotation-page/top-bar/top-bar'; import AnnotationTopBarComponent from 'components/annotation-page/top-bar/top-bar';
@ -22,6 +24,8 @@ interface StateToProps {
saving: boolean; saving: boolean;
canvasIsReady: boolean; canvasIsReady: boolean;
savingStatuses: string[]; savingStatuses: string[];
undoAction?: string;
redoAction?: string;
} }
interface DispatchToProps { interface DispatchToProps {
@ -29,6 +33,8 @@ interface DispatchToProps {
onSwitchPlay(playing: boolean): void; onSwitchPlay(playing: boolean): void;
onSaveAnnotation(sessionInstance: any): void; onSaveAnnotation(sessionInstance: any): void;
showStatistics(sessionInstance: any): void; showStatistics(sessionInstance: any): void;
undo(sessionInstance: any, frameNumber: any): void;
redo(sessionInstance: any, frameNumber: any): void;
} }
function mapStateToProps(state: CombinedState): StateToProps { function mapStateToProps(state: CombinedState): StateToProps {
@ -45,6 +51,7 @@ function mapStateToProps(state: CombinedState): StateToProps {
uploading: saving, uploading: saving,
statuses: savingStatuses, statuses: savingStatuses,
}, },
history,
}, },
job: { job: {
instance: jobInstance, instance: jobInstance,
@ -68,6 +75,8 @@ function mapStateToProps(state: CombinedState): StateToProps {
savingStatuses, savingStatuses,
frameNumber, frameNumber,
jobInstance, jobInstance,
undoAction: history.undo[history.undo.length - 1],
redoAction: history.redo[history.redo.length - 1],
}; };
} }
@ -86,6 +95,12 @@ function mapDispatchToProps(dispatch: any): DispatchToProps {
dispatch(collectStatisticsAsync(sessionInstance)); dispatch(collectStatisticsAsync(sessionInstance));
dispatch(showStatisticsAction(true)); dispatch(showStatisticsAction(true));
}, },
undo(sessionInstance: any, frameNumber: any): void {
dispatch(undoActionAsync(sessionInstance, frameNumber));
},
redo(sessionInstance: any, frameNumber: any): void {
dispatch(redoActionAsync(sessionInstance, frameNumber));
},
}; };
} }
@ -115,6 +130,26 @@ class AnnotationTopBarContainer extends React.PureComponent<Props> {
} }
} }
private undo = (): void => {
const {
undo,
jobInstance,
frameNumber,
} = this.props;
undo(jobInstance, frameNumber);
};
private redo = (): void => {
const {
redo,
jobInstance,
frameNumber,
} = this.props;
redo(jobInstance, frameNumber);
};
private showStatistics = (): void => { private showStatistics = (): void => {
const { const {
jobInstance, jobInstance,
@ -300,6 +335,8 @@ class AnnotationTopBarContainer extends React.PureComponent<Props> {
stopFrame, stopFrame,
}, },
frameNumber, frameNumber,
undoAction,
redoAction,
} = this.props; } = this.props;
return ( return (
@ -321,6 +358,10 @@ class AnnotationTopBarContainer extends React.PureComponent<Props> {
startFrame={startFrame} startFrame={startFrame}
stopFrame={stopFrame} stopFrame={stopFrame}
frameNumber={frameNumber} frameNumber={frameNumber}
undoAction={undoAction}
redoAction={redoAction}
onUndoClick={this.undo}
onRedoClick={this.redo}
/> />
); );
} }

@ -53,6 +53,10 @@ const defaultState: AnnotationState = {
}, },
collapsed: {}, collapsed: {},
states: [], states: [],
history: {
undo: [],
redo: [],
},
}, },
propagate: { propagate: {
objectState: null, objectState: null,
@ -421,7 +425,10 @@ export default (state = defaultState, action: AnyAction): AnnotationState => {
}; };
} }
case AnnotationActionTypes.UPDATE_ANNOTATIONS_SUCCESS: { case AnnotationActionTypes.UPDATE_ANNOTATIONS_SUCCESS: {
const { states: updatedStates } = action.payload; const {
history,
states: updatedStates,
} = action.payload;
const { states: prevStates } = state.annotations; const { states: prevStates } = state.annotations;
const nextStates = [...prevStates]; const nextStates = [...prevStates];
@ -438,6 +445,7 @@ export default (state = defaultState, action: AnyAction): AnnotationState => {
annotations: { annotations: {
...state.annotations, ...state.annotations,
states: nextStates, states: nextStates,
history,
}, },
}; };
} }
@ -452,46 +460,62 @@ export default (state = defaultState, action: AnyAction): AnnotationState => {
}; };
} }
case AnnotationActionTypes.CREATE_ANNOTATIONS_SUCCESS: { case AnnotationActionTypes.CREATE_ANNOTATIONS_SUCCESS: {
const { states } = action.payload; const {
states,
history,
} = action.payload;
return { return {
...state, ...state,
annotations: { annotations: {
...state.annotations, ...state.annotations,
states, states,
history,
}, },
}; };
} }
case AnnotationActionTypes.MERGE_ANNOTATIONS_SUCCESS: { case AnnotationActionTypes.MERGE_ANNOTATIONS_SUCCESS: {
const { states } = action.payload; const {
states,
history,
} = action.payload;
return { return {
...state, ...state,
annotations: { annotations: {
...state.annotations, ...state.annotations,
states, states,
history,
}, },
}; };
} }
case AnnotationActionTypes.GROUP_ANNOTATIONS_SUCCESS: { case AnnotationActionTypes.GROUP_ANNOTATIONS_SUCCESS: {
const { states } = action.payload; const {
states,
history,
} = action.payload;
return { return {
...state, ...state,
annotations: { annotations: {
...state.annotations, ...state.annotations,
states, states,
history,
}, },
}; };
} }
case AnnotationActionTypes.SPLIT_ANNOTATIONS_SUCCESS: { case AnnotationActionTypes.SPLIT_ANNOTATIONS_SUCCESS: {
const { states } = action.payload; const {
states,
history,
} = action.payload;
return { return {
...state, ...state,
annotations: { annotations: {
...state.annotations, ...state.annotations,
states, states,
history,
}, },
}; };
} }
@ -499,6 +523,7 @@ export default (state = defaultState, action: AnyAction): AnnotationState => {
const { const {
label, label,
states, states,
history,
} = action.payload; } = action.payload;
const { instance: job } = state.job; const { instance: job } = state.job;
@ -515,6 +540,7 @@ export default (state = defaultState, action: AnyAction): AnnotationState => {
annotations: { annotations: {
...state.annotations, ...state.annotations,
states, states,
history,
}, },
}; };
} }
@ -547,12 +573,14 @@ export default (state = defaultState, action: AnyAction): AnnotationState => {
case AnnotationActionTypes.REMOVE_OBJECT_SUCCESS: { case AnnotationActionTypes.REMOVE_OBJECT_SUCCESS: {
const { const {
objectState, objectState,
history,
} = action.payload; } = action.payload;
return { return {
...state, ...state,
annotations: { annotations: {
...state.annotations, ...state.annotations,
history,
activatedStateID: null, activatedStateID: null,
states: state.annotations.states states: state.annotations.states
.filter((_objectState: any) => ( .filter((_objectState: any) => (
@ -617,8 +645,13 @@ export default (state = defaultState, action: AnyAction): AnnotationState => {
}; };
} }
case AnnotationActionTypes.PROPAGATE_OBJECT_SUCCESS: { case AnnotationActionTypes.PROPAGATE_OBJECT_SUCCESS: {
const { history } = action.payload;
return { return {
...state, ...state,
annotations: {
...state.annotations,
history,
},
propagate: { propagate: {
...state.propagate, ...state.propagate,
objectState: null, objectState: null,
@ -739,7 +772,11 @@ export default (state = defaultState, action: AnyAction): AnnotationState => {
}; };
} }
case AnnotationActionTypes.UPLOAD_JOB_ANNOTATIONS_SUCCESS: { case AnnotationActionTypes.UPLOAD_JOB_ANNOTATIONS_SUCCESS: {
const { states, job } = action.payload; const {
states,
job,
history,
} = action.payload;
const { loads } = state.activities; const { loads } = state.activities;
delete loads[job.id]; delete loads[job.id];
@ -754,6 +791,7 @@ export default (state = defaultState, action: AnyAction): AnnotationState => {
}, },
annotations: { annotations: {
...state.annotations, ...state.annotations,
history,
states, states,
selectedStatesID: [], selectedStatesID: [],
activatedStateID: null, activatedStateID: null,
@ -762,10 +800,12 @@ export default (state = defaultState, action: AnyAction): AnnotationState => {
}; };
} }
case AnnotationActionTypes.REMOVE_JOB_ANNOTATIONS_SUCCESS: { case AnnotationActionTypes.REMOVE_JOB_ANNOTATIONS_SUCCESS: {
const { history } = action.payload;
return { return {
...state, ...state,
annotations: { annotations: {
...state.annotations, ...state.annotations,
history,
selectedStatesID: [], selectedStatesID: [],
activatedStateID: null, activatedStateID: null,
collapsed: {}, collapsed: {},
@ -793,6 +833,27 @@ export default (state = defaultState, action: AnyAction): AnnotationState => {
}, },
}; };
} }
case AnnotationActionTypes.REDO_ACTION_SUCCESS:
case AnnotationActionTypes.UNDO_ACTION_SUCCESS: {
const {
history,
states,
} = action.payload;
const activatedStateID = states
.map((_state: any) => _state.clientID).includes(state.annotations.activatedStateID)
? state.annotations.activatedStateID : null;
return {
...state,
annotations: {
...state.annotations,
activatedStateID,
states,
history,
},
};
}
case AnnotationActionTypes.RESET_CANVAS: { case AnnotationActionTypes.RESET_CANVAS: {
return { return {
...state, ...state,

@ -312,6 +312,10 @@ export interface AnnotationState {
activatedStateID: number | null; activatedStateID: number | null;
collapsed: Record<number, boolean>; collapsed: Record<number, boolean>;
states: any[]; states: any[];
history: {
undo: string[];
redo: string[];
};
saving: { saving: {
uploading: boolean; uploading: boolean;
statuses: string[]; statuses: string[];

@ -8,7 +8,7 @@
}, },
"dependencies": {}, "dependencies": {},
"devDependencies": { "devDependencies": {
"eslint": "^6.1.0", "eslint": "^6.8.0",
"eslint-config-airbnb": "^18.0.1", "eslint-config-airbnb": "^18.0.1",
"eslint-plugin-import": "^2.18.2", "eslint-plugin-import": "^2.18.2",
"eslint-plugin-jsx-a11y": "^6.2.3", "eslint-plugin-jsx-a11y": "^6.2.3",

Loading…
Cancel
Save