diff --git a/cvat-core/src/annotations-collection.js b/cvat-core/src/annotations-collection.js index 2df189f9a1a0..560a79b9371d 100644 --- a/cvat-core/src/annotations-collection.js +++ b/cvat-core/src/annotations-collection.js @@ -32,6 +32,7 @@ } = require('./exceptions'); const { + HistoryActions, ObjectShape, ObjectType, colors, @@ -109,6 +110,7 @@ return labelAccumulator; }, {}); + this.history = data.history; this.shapes = {}; // key is a frame this.tags = {}; // key is a frame this.tracks = []; @@ -124,16 +126,25 @@ collectionZ: this.collectionZ, groups: this.groups, frameMeta: this.frameMeta, + history: this.history, }; } import(data) { + const result = { + tags: [], + shapes: [], + tracks: [], + }; + for (const tag of data.tags) { const clientID = ++this.count; const tagModel = new Tag(tag, clientID, this.injection); this.tags[tagModel.frame] = this.tags[tagModel.frame] || []; this.tags[tagModel.frame].push(tagModel); this.objects[clientID] = tagModel; + + result.tags.push(tagModel); } for (const shape of data.shapes) { @@ -142,6 +153,8 @@ this.shapes[shapeModel.frame] = this.shapes[shapeModel.frame] || []; this.shapes[shapeModel.frame].push(shapeModel); this.objects[clientID] = shapeModel; + + result.shapes.push(shapeModel); } for (const track of data.tracks) { @@ -152,10 +165,12 @@ if (trackModel) { this.tracks.push(trackModel); this.objects[clientID] = trackModel; + + result.tracks.push(trackModel); } } - return this; + return result; } export() { @@ -378,6 +393,18 @@ for (const object of objectsForMerge) { 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) { @@ -463,6 +490,16 @@ // Remove source object 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) { @@ -480,9 +517,21 @@ }); const groupIdx = reset ? 0 : ++this.groups.max; + const undoGroups = objectsForGroup.map((object) => object.group); for (const object of objectsForGroup) { 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; } @@ -704,7 +753,20 @@ } // 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) { diff --git a/cvat-core/src/annotations-history.js b/cvat-core/src/annotations-history.js new file mode 100644 index 000000000000..d98973e5f86d --- /dev/null +++ b/cvat-core/src/annotations-history.js @@ -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; diff --git a/cvat-core/src/annotations-objects.js b/cvat-core/src/annotations-objects.js index 6248ab70f659..cbd8c456220d 100644 --- a/cvat-core/src/annotations-objects.js +++ b/cvat-core/src/annotations-objects.js @@ -1,5 +1,5 @@ /* -* Copyright (C) 2019 Intel Corporation +* Copyright (C) 2019-2020 Intel Corporation * SPDX-License-Identifier: MIT */ @@ -17,6 +17,7 @@ ObjectShape, ObjectType, AttributeType, + HistoryActions, } = require('./enums'); const { @@ -139,6 +140,7 @@ class Annotation { constructor(data, clientID, injection) { this.taskLabels = injection.labels; + this.history = injection.history; this.clientID = clientID; this.serverID = data.id; this.group = data.group; @@ -156,6 +158,79 @@ 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) { const labelAttributes = label.attributes; for (const attribute of labelAttributes) { @@ -178,9 +253,15 @@ delete(force) { if (!this.lock || force) { 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]; } - validateStateBeforeSave(frame, data) { + _validateStateBeforeSave(frame, data, updated) { let fittedPoints = []; - const updated = data.updateFlags; if (updated.label) { checkObjectType('label', data.label, null, Label); @@ -255,7 +335,7 @@ } 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) { if (frame !== this.frame) { throw new ScriptingError( @@ -404,44 +523,40 @@ return objectStateFactory.call(this, frame, this.get(frame)); } - const fittedPoints = this.validateStateBeforeSave(frame, data); const updated = data.updateFlags; + const fittedPoints = this._validateStateBeforeSave(frame, data, updated); // Now when all fields are validated, we can apply them if (updated.label) { - this.label = data.label; - this.attributes = {}; - this.appendDefaultAttributes(data.label); + this._saveLabel(data.label); } if (updated.attributes) { - for (const attrID of Object.keys(data.attributes)) { - this.attributes[attrID] = data.attributes[attrID]; - } + this._saveAttributes(data.attributes); } if (updated.points && fittedPoints.length) { - this.points = [...fittedPoints]; + this._savePoints(fittedPoints); } if (updated.occluded) { - this.occluded = data.occluded; + this._saveOccluded(data.occluded); } if (updated.zOrder) { - this.zOrder = data.zOrder; + this._saveZOrder(data.zOrder); } if (updated.lock) { - this.lock = data.lock; + this._saveLock(data.lock); } if (updated.color) { - this.color = data.color; + this._saveColor(data.color); } if (updated.hidden) { - this.hidden = data.hidden; + this._saveHidden(data.hidden); } this.updateTimestamp(updated); @@ -622,87 +737,304 @@ return result; } - save(frame, data) { - if (this.lock && data.lock) { - return objectStateFactory.call(this, frame, this.get(frame)); + _saveLabel(label) { + const undoLabel = this.label; + 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 updated = data.updateFlags; + const redoAttributes = { + 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 labelAttributes = data.label.attributes + const labelAttributes = this.label.attributes .reduce((accumulator, value) => { accumulator[value.id] = value; return accumulator; }, {}); - if (updated.label) { - this.label = data.label; - this.attributes = {}; - for (const shape of Object.values(this.shapes)) { - shape.attributes = {}; + const wasKeyframe = frame in this.shapes; + const undoAttributes = this.attributes; + const undoShape = wasKeyframe ? this.shapes[frame] : undefined; + + 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; - if (updated.attributes) { - for (const attrID of Object.keys(data.attributes)) { - if (!labelAttributes[attrID].mutable) { - this.attributes[attrID] = data.attributes[attrID]; - this.attributes[attrID] = data.attributes[attrID]; - } else if (data.attributes[attrID] !== current.attributes[attrID]) { - mutableAttributesUpdated = mutableAttributesUpdated - // not keyframe yet - || !(frame in this.shapes) - // keyframe, but without this attrID - || !(attrID in this.shapes[frame]) - // keyframe with attrID, but with another value - || (this.shapes[frame][attrID] !== data.attributes[attrID]); - } + for (const attrID of Object.keys(attributes)) { + if (labelAttributes[attrID].mutable + && attributes[attrID] !== current.attributes[attrID]) { + redoShape.attributes[attrID] = attributes[attrID]; + } + } + + this.attributes = redoAttributes; + if (redoShape) { + this.shapes[frame] = redoShape; + } + + this.history.do(HistoryActions.CHANGED_ATTRIBUTES, () => { + 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) { - this.lock = data.lock; + this._saveLock(data.lock); } if (updated.color) { - this.color = data.color; + this._saveColor(data.color); } if (updated.hidden) { - this.hidden = 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, - }; + this._saveHidden(data.hidden); + } - for (const attrID of Object.keys(data.attributes)) { - if (labelAttributes[attrID].mutable - && data.attributes[attrID] !== current.attributes[attrID]) { - this.shapes[frame].attributes[attrID] = data.attributes[attrID]; - this.shapes[frame].attributes[attrID] = data.attributes[attrID]; - } - } + if (updated.points && fittedPoints.length) { + this._savePoints(frame, fittedPoints); + } - if (updated.keyframe && !data.keyframe) { - if (Object.keys(this.shapes).length === 1) { - throw new DataError('You are not able to remove the latest keyframe for a track. ' - + 'Consider removing a track instead'); - } else { - delete this.shapes[frame]; - } - } + if (updated.outside) { + this._saveOutside(frame, data.outside); + } + + if (updated.occluded) { + 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); @@ -752,14 +1084,6 @@ + `Interpolation impossible. Client ID: ${this.id}`, ); } - - delete(force) { - if (!this.lock || force) { - this.removed = true; - } - - return this.removed; - } } class Tag extends Annotation { @@ -810,7 +1134,7 @@ save(frame, data) { if (frame !== this.frame) { 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)); } - if (this.lock && data.lock) { - return objectStateFactory.call(this, frame, this.get(frame)); - } - const updated = data.updateFlags; - - // 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); - } + this._validateStateBeforeSave(frame, data, updated); // Now when all fields are validated, we can apply them if (updated.label) { - this.label = data.label; - this.attributes = {}; - this.appendDefaultAttributes(data.label); + this._saveLabel(data.label); } if (updated.attributes) { - for (const attrID of Object.keys(data.attributes)) { - this.attributes[attrID] = data.attributes[attrID]; - } + this._saveAttributes(data.attributes); } if (updated.lock) { - this.lock = data.lock; + this._saveLock(data.lock); } this.updateTimestamp(updated); diff --git a/cvat-core/src/annotations.js b/cvat-core/src/annotations.js index a892512c2811..516a8280b84c 100644 --- a/cvat-core/src/annotations.js +++ b/cvat-core/src/annotations.js @@ -1,5 +1,5 @@ /* -* Copyright (C) 2019 Intel Corporation +* Copyright (C) 2019-2020 Intel Corporation * SPDX-License-Identifier: MIT */ @@ -11,6 +11,7 @@ const serverProxy = require('./server-proxy'); const Collection = require('./annotations-collection'); const AnnotationsSaver = require('./annotations-saver'); + const AnnotationsHistory = require('./annotations-history'); const { checkObjectType } = require('./common'); const { Task } = require('./session'); const { @@ -56,27 +57,35 @@ frameMeta[i] = await session.frames.get(i); } + const history = new AnnotationsHistory(); const collection = new Collection({ labels: session.labels || session.task.labels, + history, startFrame, stopFrame, frameMeta, - }).import(rawAnnotations); + }); + collection.import(rawAnnotations); const saver = new AnnotationsSaver(rawAnnotations.version, collection, session); cache.set(session, { collection, saver, - + history, }); } } async function getAnnotations(session, frame, filter) { - await getAnnotationsFromServer(session); const sessionType = session instanceof Task ? 'task' : 'job'; 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); } @@ -244,6 +253,58 @@ 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 = { getAnnotations, putAnnotations, @@ -258,5 +319,9 @@ uploadAnnotations, dumpAnnotations, exportDataset, + undoActions, + redoActions, + clearActions, + getActions, }; })(); diff --git a/cvat-core/src/api.js b/cvat-core/src/api.js index 6414e1737ec0..7f9ad9c7448f 100644 --- a/cvat-core/src/api.js +++ b/cvat-core/src/api.js @@ -1,5 +1,5 @@ /* -* Copyright (C) 2019 Intel Corporation +* Copyright (C) 2019-2020 Intel Corporation * SPDX-License-Identifier: MIT */ @@ -28,6 +28,7 @@ function build() { ObjectType, ObjectShape, LogType, + HistoryActions, colors, } = require('./enums'); @@ -498,6 +499,7 @@ function build() { ObjectType, ObjectShape, LogType, + HistoryActions, colors, }, /** diff --git a/cvat-core/src/enums.js b/cvat-core/src/enums.js index 0e9b066b31db..df7c5ca4e9fa 100644 --- a/cvat-core/src/enums.js +++ b/cvat-core/src/enums.js @@ -1,5 +1,5 @@ /* -* Copyright (C) 2019 Intel Corporation +* Copyright (C) 2019-2020 Intel Corporation * SPDX-License-Identifier: MIT */ @@ -166,6 +166,45 @@ 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 * @type {module:API.cvat.classes.Loader[]} values @@ -189,6 +228,7 @@ ObjectType, ObjectShape, LogType, + HistoryActions, colors, }; })(); diff --git a/cvat-core/src/session.js b/cvat-core/src/session.js index f02e073cd62c..2a19364c1c49 100644 --- a/cvat-core/src/session.js +++ b/cvat-core/src/session.js @@ -1,5 +1,5 @@ /* -* Copyright (C) 2019 Intel Corporation +* Copyright (C) 2019-2020 Intel Corporation * SPDX-License-Identifier: MIT */ @@ -141,12 +141,12 @@ }), actions: Object.freeze({ value: { - async undo(count) { + async undo(count = 1) { const result = await PluginRegistry .apiWrapper.call(this, prototype.actions.undo, count); return result; }, - async redo(count) { + async redo(count = 1) { const result = await PluginRegistry .apiWrapper.call(this, prototype.actions.redo, count); return result; @@ -156,6 +156,11 @@ .apiWrapper.call(this, prototype.actions.clear); return result; }, + async get() { + const result = await PluginRegistry + .apiWrapper.call(this, prototype.actions.get); + return result; + }, }, writable: true, }), @@ -454,28 +459,48 @@ */ /** - * Is a dictionary of pairs "id:action" where "id" is an identifier of an object - * which has been affected by undo/redo and "action" is what exactly has been - * done with the object. Action can be: "created", "deleted", "updated". - * Size of an output array equal the param "count". - * @typedef {Object} HistoryAction + * @typedef {Object} HistoryActions + * @property {string[]} [undo] - array of possible actions to undo + * @property {string[]} [redo] - array of possible actions to redo * @global */ /** - * Undo actions + * Make undo * @method undo * @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.ArgumentError} * @instance * @async */ /** - * Redo actions + * Make redo * @method redo * @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.ArgumentError} * @instance * @async */ @@ -653,6 +678,13 @@ .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 = { get: Object.getPrototypeOf(this).frames.get.bind(this), preview: Object.getPrototypeOf(this).frames.preview.bind(this), @@ -1155,6 +1187,13 @@ .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 = { get: Object.getPrototypeOf(this).frames.get.bind(this), preview: Object.getPrototypeOf(this).frames.preview.bind(this), @@ -1217,6 +1256,10 @@ uploadAnnotations, dumpAnnotations, exportDataset, + undoActions, + redoActions, + clearActions, + getActions, } = require('./annotations'); buildDublicatedAPI(Job.prototype); @@ -1328,6 +1371,31 @@ 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) { // TODO: Add ability to change an owner and an assignee if (typeof (this.id) !== 'undefined') { @@ -1484,4 +1552,24 @@ const result = await exportDataset(this, format); 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; + }; })(); diff --git a/cvat-core/tests/api/annotations.js b/cvat-core/tests/api/annotations.js index 18f84f720dad..3400ea4efc1a 100644 --- a/cvat-core/tests/api/annotations.js +++ b/cvat-core/tests/api/annotations.js @@ -266,7 +266,7 @@ describe('Feature: check unsaved changes', () => { expect(await task.annotations.hasUnsavedChanges()).toBe(false); const annotations = await task.annotations.get(0); - annotations[0].keyframe = true; + annotations[0].keyframe = false; await annotations[0].save(); expect(await task.annotations.hasUnsavedChanges()).toBe(true); diff --git a/cvat-ui/src/actions/annotation-actions.ts b/cvat-ui/src/actions/annotation-actions.ts index 6f57ea46a1f1..fd70f460497d 100644 --- a/cvat-ui/src/actions/annotation-actions.ts +++ b/cvat-ui/src/actions/annotation-actions.ts @@ -74,6 +74,64 @@ export enum AnnotationActionTypes { REMOVE_JOB_ANNOTATIONS_SUCCESS = 'REMOVE_JOB_ANNOTATIONS_SUCCESS', REMOVE_JOB_ANNOTATIONS_FAILED = 'REMOVE_JOB_ANNOTATIONS_FAILED', 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, {}, {}, AnyAction> { + return async (dispatch: ActionCreator): Promise => { + 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, {}, {}, AnyAction> { + return async (dispatch: ActionCreator): Promise => { + 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 { @@ -91,11 +149,14 @@ export function removeAnnotationsAsync(sessionInstance: any): ThunkAction, {}, {}, AnyAction> { return async (dispatch: ActionCreator): Promise => { try { - sessionInstance.annotations.clear(); + await sessionInstance.annotations.clear(); + await sessionInstance.actions.clear(); + const history = await sessionInstance.actions.get(); + dispatch({ type: AnnotationActionTypes.REMOVE_JOB_ANNOTATIONS_SUCCESS, payload: { - sessionInstance, + history, }, }); } catch (error) { @@ -109,7 +170,6 @@ ThunkAction, {}, {}, AnyAction> { }; } - export function uploadJobAnnotationsAsync(job: any, loader: any, file: File): ThunkAction, {}, {}, AnyAction> { return async (dispatch: ActionCreator): Promise => { @@ -146,12 +206,15 @@ ThunkAction, {}, {}, AnyAction> { }); await job.annotations.clear(true); + await job.actions.clear(); + const history = await job.actions.get(); const states = await job.annotations.get(frame); setTimeout(() => { dispatch({ type: AnnotationActionTypes.UPLOAD_JOB_ANNOTATIONS_SUCCESS, payload: { + history, job, states, }, @@ -264,11 +327,13 @@ export function propagateObjectAsync( } await sessionInstance.annotations.put(states); + const history = await sessionInstance.actions.get(); dispatch({ type: AnnotationActionTypes.PROPAGATE_OBJECT_SUCCESS, payload: { objectState, + history, }, }); } 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, {}, {}, AnyAction> { return async (dispatch: ActionCreator): Promise => { try { const removed = await objectState.delete(force); + const history = await sessionInstance.actions.get(); + if (removed) { dispatch({ type: AnnotationActionTypes.REMOVE_OBJECT_SUCCESS, payload: { objectState, + history, }, }); } else { @@ -645,11 +713,13 @@ ThunkAction, {}, {}, AnyAction> { try { const promises = statesToUpdate.map((state: any): Promise => state.save()); const states = await Promise.all(promises); + const history = await sessionInstance.actions.get(); dispatch({ type: AnnotationActionTypes.UPDATE_ANNOTATIONS_SUCCESS, payload: { states, + history, }, }); } catch (error) { @@ -671,11 +741,13 @@ ThunkAction, {}, {}, AnyAction> { try { await sessionInstance.annotations.put(statesToCreate); const states = await sessionInstance.annotations.get(frame); + const history = await sessionInstance.actions.get(); dispatch({ type: AnnotationActionTypes.CREATE_ANNOTATIONS_SUCCESS, payload: { states, + history, }, }); } catch (error) { @@ -695,11 +767,13 @@ ThunkAction, {}, {}, AnyAction> { try { await sessionInstance.annotations.merge(statesToMerge); const states = await sessionInstance.annotations.get(frame); + const history = await sessionInstance.actions.get(); dispatch({ type: AnnotationActionTypes.MERGE_ANNOTATIONS_SUCCESS, payload: { states, + history, }, }); } catch (error) { @@ -719,11 +793,13 @@ ThunkAction, {}, {}, AnyAction> { try { await sessionInstance.annotations.group(statesToGroup); const states = await sessionInstance.annotations.get(frame); + const history = await sessionInstance.actions.get(); dispatch({ type: AnnotationActionTypes.GROUP_ANNOTATIONS_SUCCESS, payload: { states, + history, }, }); } catch (error) { @@ -743,11 +819,13 @@ ThunkAction, {}, {}, AnyAction> { try { await sessionInstance.annotations.split(stateToSplit, frame); const states = await sessionInstance.annotations.get(frame); + const history = await sessionInstance.actions.get(); dispatch({ type: AnnotationActionTypes.SPLIT_ANNOTATIONS_SUCCESS, payload: { states, + history, }, }); } catch (error) { @@ -772,11 +850,13 @@ export function changeLabelColorAsync( const updatedLabel = label; updatedLabel.color = color; const states = await sessionInstance.annotations.get(frameNumber); + const history = await sessionInstance.actions.get(); dispatch({ type: AnnotationActionTypes.CHANGE_LABEL_COLOR_SUCCESS, payload: { label: updatedLabel, + history, states, }, }); diff --git a/cvat-ui/src/components/annotation-page/standard-workspace/controls-side-bar/controls-side-bar.tsx b/cvat-ui/src/components/annotation-page/standard-workspace/controls-side-bar/controls-side-bar.tsx index 494718972e08..0870b53f3e2e 100644 --- a/cvat-ui/src/components/annotation-page/standard-workspace/controls-side-bar/controls-side-bar.tsx +++ b/cvat-ui/src/components/annotation-page/standard-workspace/controls-side-bar/controls-side-bar.tsx @@ -86,7 +86,7 @@ export default function ControlsSideBarComponent(props: Props): JSX.Element { isDrawing={activeControl === ActiveControl.DRAW_POINTS} /> - + diff --git a/cvat-ui/src/components/annotation-page/standard-workspace/controls-side-bar/cursor-control.tsx b/cvat-ui/src/components/annotation-page/standard-workspace/controls-side-bar/cursor-control.tsx index ae590cc44aa5..0ec834641e66 100644 --- a/cvat-ui/src/components/annotation-page/standard-workspace/controls-side-bar/cursor-control.tsx +++ b/cvat-ui/src/components/annotation-page/standard-workspace/controls-side-bar/cursor-control.tsx @@ -29,7 +29,7 @@ function CursorControl(props: Props): JSX.Element { } = props; return ( - + + canvasInstance.fit()} /> ); diff --git a/cvat-ui/src/components/annotation-page/standard-workspace/controls-side-bar/group-control.tsx b/cvat-ui/src/components/annotation-page/standard-workspace/controls-side-bar/group-control.tsx index f6b6c7175857..63268013b573 100644 --- a/cvat-ui/src/components/annotation-page/standard-workspace/controls-side-bar/group-control.tsx +++ b/cvat-ui/src/components/annotation-page/standard-workspace/controls-side-bar/group-control.tsx @@ -42,7 +42,7 @@ function GroupControl(props: Props): JSX.Element { }; return ( - + ); diff --git a/cvat-ui/src/components/annotation-page/standard-workspace/controls-side-bar/merge-control.tsx b/cvat-ui/src/components/annotation-page/standard-workspace/controls-side-bar/merge-control.tsx index 9e43855473ca..db268eac7d54 100644 --- a/cvat-ui/src/components/annotation-page/standard-workspace/controls-side-bar/merge-control.tsx +++ b/cvat-ui/src/components/annotation-page/standard-workspace/controls-side-bar/merge-control.tsx @@ -42,7 +42,7 @@ function MergeControl(props: Props): JSX.Element { }; return ( - + ); diff --git a/cvat-ui/src/components/annotation-page/standard-workspace/controls-side-bar/move-control.tsx b/cvat-ui/src/components/annotation-page/standard-workspace/controls-side-bar/move-control.tsx index e17659081ded..5379c72dbe0c 100644 --- a/cvat-ui/src/components/annotation-page/standard-workspace/controls-side-bar/move-control.tsx +++ b/cvat-ui/src/components/annotation-page/standard-workspace/controls-side-bar/move-control.tsx @@ -29,7 +29,7 @@ function MoveControl(props: Props): JSX.Element { } = props; return ( - + + - + canvasInstance @@ -40,7 +40,7 @@ function RotateControl(props: Props): JSX.Element { component={RotateIcon} /> - + canvasInstance diff --git a/cvat-ui/src/components/annotation-page/standard-workspace/controls-side-bar/split-control.tsx b/cvat-ui/src/components/annotation-page/standard-workspace/controls-side-bar/split-control.tsx index 6190d4dfd129..4b7700209d27 100644 --- a/cvat-ui/src/components/annotation-page/standard-workspace/controls-side-bar/split-control.tsx +++ b/cvat-ui/src/components/annotation-page/standard-workspace/controls-side-bar/split-control.tsx @@ -42,7 +42,7 @@ function SplitControl(props: Props): JSX.Element { }; return ( - + ); diff --git a/cvat-ui/src/components/annotation-page/top-bar/left-group.tsx b/cvat-ui/src/components/annotation-page/top-bar/left-group.tsx index c76cf232c767..9079a6cf958c 100644 --- a/cvat-ui/src/components/annotation-page/top-bar/left-group.tsx +++ b/cvat-ui/src/components/annotation-page/top-bar/left-group.tsx @@ -21,14 +21,22 @@ import { interface Props { saving: boolean; savingStatuses: string[]; + undoAction?: string; + redoAction?: string; onSaveAnnotation(): void; + onUndoClick(): void; + onRedoClick(): void; } function LeftGroup(props: Props): JSX.Element { const { saving, savingStatuses, + undoAction, + redoAction, onSaveAnnotation, + onUndoClick, + onRedoClick, } = props; return ( @@ -67,11 +75,25 @@ function LeftGroup(props: Props): JSX.Element { - - diff --git a/cvat-ui/src/components/annotation-page/top-bar/player-buttons.tsx b/cvat-ui/src/components/annotation-page/top-bar/player-buttons.tsx index 3c624c854dce..7234494353d0 100644 --- a/cvat-ui/src/components/annotation-page/top-bar/player-buttons.tsx +++ b/cvat-ui/src/components/annotation-page/top-bar/player-buttons.tsx @@ -42,19 +42,19 @@ function PlayerButtons(props: Props): JSX.Element { return ( - + - + - + {!playing ? ( - + ) : ( - + + - + - + diff --git a/cvat-ui/src/components/annotation-page/top-bar/player-navigation.tsx b/cvat-ui/src/components/annotation-page/top-bar/player-navigation.tsx index 8dcabee0bc68..20a6bd0ca4c5 100644 --- a/cvat-ui/src/components/annotation-page/top-bar/player-navigation.tsx +++ b/cvat-ui/src/components/annotation-page/top-bar/player-navigation.tsx @@ -44,7 +44,7 @@ function PlayerNavigation(props: Props): JSX.Element { - + filename.png diff --git a/cvat-ui/src/components/annotation-page/top-bar/statistics-modal.tsx b/cvat-ui/src/components/annotation-page/top-bar/statistics-modal.tsx index 1d8301ae07d0..71ff6765123f 100644 --- a/cvat-ui/src/components/annotation-page/top-bar/statistics-modal.tsx +++ b/cvat-ui/src/components/annotation-page/top-bar/statistics-modal.tsx @@ -90,7 +90,7 @@ export default function StatisticsModalComponent(props: Props): JSX.Element { }); const makeShapesTracksTitle = (title: string): JSX.Element => ( - + {title} diff --git a/cvat-ui/src/components/annotation-page/top-bar/top-bar.tsx b/cvat-ui/src/components/annotation-page/top-bar/top-bar.tsx index 399dc3f6ac09..f60faa4526b9 100644 --- a/cvat-ui/src/components/annotation-page/top-bar/top-bar.tsx +++ b/cvat-ui/src/components/annotation-page/top-bar/top-bar.tsx @@ -20,6 +20,8 @@ interface Props { frameNumber: number; startFrame: number; stopFrame: number; + undoAction?: string; + redoAction?: string; showStatistics(): void; onSwitchPlay(): void; onSaveAnnotation(): void; @@ -31,21 +33,16 @@ interface Props { onLastFrame(): void; onSliderChange(value: SliderValue): void; onInputChange(value: number | undefined): 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; + onUndoClick(): void; + onRedoClick(): void; } function AnnotationTopBarComponent(props: Props): JSX.Element { const { saving, savingStatuses, + undoAction, + redoAction, playing, frameNumber, startFrame, @@ -61,6 +58,8 @@ function AnnotationTopBarComponent(props: Props): JSX.Element { onLastFrame, onSliderChange, onInputChange, + onUndoClick, + onRedoClick, } = props; return ( @@ -70,6 +69,10 @@ function AnnotationTopBarComponent(props: Props): JSX.Element { saving={saving} savingStatuses={savingStatuses} onSaveAnnotation={onSaveAnnotation} + undoAction={undoAction} + redoAction={redoAction} + onUndoClick={onUndoClick} + onRedoClick={onRedoClick} /> @@ -98,4 +101,4 @@ function AnnotationTopBarComponent(props: Props): JSX.Element { ); } -export default React.memo(AnnotationTopBarComponent, propsAreEqual); +export default React.memo(AnnotationTopBarComponent); diff --git a/cvat-ui/src/components/create-model-page/create-model-content.tsx b/cvat-ui/src/components/create-model-page/create-model-content.tsx index 947122a9ccc3..dc61f3252592 100644 --- a/cvat-ui/src/components/create-model-page/create-model-content.tsx +++ b/cvat-ui/src/components/create-model-page/create-model-content.tsx @@ -107,7 +107,7 @@ export default class CreateModelContent extends React.PureComponent { return ( - + { // false positive diff --git a/cvat-ui/src/components/create-model-page/create-model-form.tsx b/cvat-ui/src/components/create-model-page/create-model-form.tsx index dcd8d7095147..d5d07dea230a 100644 --- a/cvat-ui/src/components/create-model-page/create-model-form.tsx +++ b/cvat-ui/src/components/create-model-page/create-model-form.tsx @@ -59,7 +59,7 @@ export class CreateModelForm extends React.PureComponent { - + { getFieldDecorator('global', { initialValue: false, valuePropName: 'checked', diff --git a/cvat-ui/src/components/create-task-page/advanced-configuration-form.tsx b/cvat-ui/src/components/create-task-page/advanced-configuration-form.tsx index 2c654661b751..22ad7d246450 100644 --- a/cvat-ui/src/components/create-task-page/advanced-configuration-form.tsx +++ b/cvat-ui/src/components/create-task-page/advanced-configuration-form.tsx @@ -85,7 +85,7 @@ class AdvancedConfigurationForm extends React.PureComponent { return ( Image quality}> - + {form.getFieldDecorator('imageQuality', { initialValue: 70, rules: [{ @@ -111,7 +111,7 @@ class AdvancedConfigurationForm extends React.PureComponent { return ( Overlap size}> - + {form.getFieldDecorator('overlapSize')( , )} @@ -125,7 +125,7 @@ class AdvancedConfigurationForm extends React.PureComponent { return ( Segment size}> - + {form.getFieldDecorator('segmentSize')( , )} diff --git a/cvat-ui/src/components/labels-editor/label-form.tsx b/cvat-ui/src/components/labels-editor/label-form.tsx index 22f5cb930c15..ef525d9b36fb 100644 --- a/cvat-ui/src/components/labels-editor/label-form.tsx +++ b/cvat-ui/src/components/labels-editor/label-form.tsx @@ -138,7 +138,7 @@ class LabelForm extends React.PureComponent { return ( - + { form.getFieldDecorator(`type[${key}]`, { initialValue: type, })( @@ -188,7 +188,7 @@ class LabelForm extends React.PureComponent { }; return ( - + { form.getFieldDecorator(`values[${key}]`, { initialValue: existedValues, @@ -215,7 +215,7 @@ class LabelForm extends React.PureComponent { const { form } = this.props; return ( - + { form.getFieldDecorator(`values[${key}]`, { initialValue: value, @@ -299,7 +299,7 @@ class LabelForm extends React.PureComponent { return ( - + { form.getFieldDecorator(`mutable[${key}]`, { initialValue: value, valuePropName: 'checked', @@ -316,7 +316,7 @@ class LabelForm extends React.PureComponent { return ( - +