From 8b05aa5c75f40771c61e5129b5b988b827cec501 Mon Sep 17 00:00:00 2001 From: Dmitry Kalinin Date: Mon, 1 Mar 2021 03:13:40 +0300 Subject: [PATCH 01/11] Added label deleting --- cvat-core/src/project.js | 19 +++++++++- cvat-core/src/session.js | 31 +++++++++++---- .../labels-editor/constructor-viewer-item.tsx | 22 +++++------ .../labels-editor/labels-editor.tsx | 38 ++++++++++++------- cvat/apps/engine/serializers.py | 13 ++++++- 5 files changed, 89 insertions(+), 34 deletions(-) diff --git a/cvat-core/src/project.js b/cvat-core/src/project.js index ba06b1fe3154..b165f867ab26 100644 --- a/cvat-core/src/project.js +++ b/cvat-core/src/project.js @@ -43,6 +43,7 @@ data.labels = []; data.tasks = []; + data.deleted_labels = []; if (Array.isArray(initialData.labels)) { for (const label of initialData.labels) { @@ -186,6 +187,12 @@ ); } + for (const label of data.labels) { + if (!labels.filter((_label) => _label.id === label.id).length) { + data.deleted_labels.push(label); + } + } + data.labels = [...labels]; }, }, @@ -211,6 +218,9 @@ subsets: { get: () => [...data.task_subsets], }, + _deletedLabels: { + get: () => [...data.deleted_labels], + }, }), ); } @@ -257,7 +267,14 @@ name: this.name, assignee_id: this.assignee ? this.assignee.id : null, bug_tracker: this.bugTracker, - labels: [...this.labels.map((el) => el.toJSON())], + labels: [ + ...this.labels.map((el) => el.toJSON()), + ...this._deletedLabels.map((label) => { + const labelJSON = label.toJSON(); + labelJSON.deleted = true; + return labelJSON; + }), + ], }; await serverProxy.projects.save(this.id, projectData); diff --git a/cvat-core/src/session.js b/cvat-core/src/session.js index ddc7b7a22eaf..aef65c4176e4 100644 --- a/cvat-core/src/session.js +++ b/cvat-core/src/session.js @@ -1006,6 +1006,7 @@ if (data.owner) data.owner = new User(data.owner); data.labels = []; + data.deleted_labels = []; data.jobs = []; data.files = Object.freeze({ server_files: [], @@ -1318,6 +1319,12 @@ } } + for (const label of data.labels) { + if (!labels.filter((_label) => _label.id === label.id).length) { + data.deleted_labels.push(label); + } + } + updatedFields.labels = true; data.labels = [...labels]; }, @@ -1485,12 +1492,6 @@ dataChunkType: { get: () => data.data_compressed_chunk_type, }, - __updatedFields: { - get: () => updatedFields, - set: (fields) => { - updatedFields = fields; - }, - }, dimension: { /** * @name enabled @@ -1501,6 +1502,15 @@ */ get: () => data.dimension, }, + __updatedFields: { + get: () => updatedFields, + set: (fields) => { + updatedFields = fields; + }, + }, + _deletedLabels: { + get: () => [...data.deleted_labels], + }, }), ); @@ -1920,7 +1930,14 @@ taskData.subset = this.subset; break; case 'labels': - taskData.labels = [...this.labels.map((el) => el.toJSON())]; + taskData.labels = [ + ...this.labels.map((el) => el.toJSON()), + ...this._deletedLabels.map((label) => { + const labelJSON = label.toJSON(); + labelJSON.deleted = true; + return labelJSON; + }), + ]; break; default: break; diff --git a/cvat-ui/src/components/labels-editor/constructor-viewer-item.tsx b/cvat-ui/src/components/labels-editor/constructor-viewer-item.tsx index 32c56dd7677e..fe436f610a9d 100644 --- a/cvat-ui/src/components/labels-editor/constructor-viewer-item.tsx +++ b/cvat-ui/src/components/labels-editor/constructor-viewer-item.tsx @@ -35,18 +35,16 @@ export default function ConstructorViewerItem(props: ConstructorViewerItemProps) - {label.id < 0 && ( - - onDelete(label)} - onKeyPress={(): boolean => false} - > - - - - )} + + onDelete(label)} + onKeyPress={(): boolean => false} + > + + + ); } diff --git a/cvat-ui/src/components/labels-editor/labels-editor.tsx b/cvat-ui/src/components/labels-editor/labels-editor.tsx index df92fc416feb..2eacf74a9b31 100644 --- a/cvat-ui/src/components/labels-editor/labels-editor.tsx +++ b/cvat-ui/src/components/labels-editor/labels-editor.tsx @@ -6,10 +6,12 @@ import './styles.scss'; import React from 'react'; import Tabs from 'antd/lib/tabs'; import Button from 'antd/lib/button'; -import notification from 'antd/lib/notification'; import Text from 'antd/lib/typography/Text'; +import ModalConfirm from 'antd/lib/modal/confirm'; import copy from 'copy-to-clipboard'; -import { CopyOutlined, EditOutlined, BuildOutlined } from '@ant-design/icons'; +import { + CopyOutlined, EditOutlined, BuildOutlined, ExclamationCircleOutlined, +} from '@ant-design/icons'; import CVATTooltip from 'components/common/cvat-tooltip'; import RawViewer from './raw-viewer'; @@ -144,20 +146,30 @@ export default class LabelsEditor extends React.PureComponent { - // the label is saved on the server, cannot delete it - if (typeof label.id !== 'undefined' && label.id >= 0) { - notification.error({ - message: 'Could not delete the label', - description: 'It has been already saved on the server', - }); - } + const deleteLabel = (): void => { + const { unsavedLabels, savedLabels } = this.state; - const { unsavedLabels, savedLabels } = this.state; + const filteredUnsavedLabels = unsavedLabels.filter((_label: Label): boolean => _label.id !== label.id); + const filteredSavedLabels = savedLabels.filter((_label: Label): boolean => _label.id !== label.id); - const filteredUnsavedLabels = unsavedLabels.filter((_label: Label): boolean => _label.id !== label.id); + this.setState({ savedLabels: filteredSavedLabels, unsavedLabels: filteredUnsavedLabels }); + this.handleSubmit(filteredSavedLabels, filteredUnsavedLabels); + }; - this.setState({ unsavedLabels: filteredUnsavedLabels }); - this.handleSubmit(savedLabels, filteredUnsavedLabels); + if (typeof label.id !== 'undefined' && label.id >= 0) { + ModalConfirm({ + title: `Do you want to delete "${label.name}" label?`, + icon: , + content: 'This action is irreversible. Annotation corresponding with this label will be deleted.', + type: 'warning', + okType: 'danger', + onOk() { + deleteLabel(); + }, + }); + } else { + deleteLabel(); + } }; private handleSubmit(savedLabels: Label[], unsavedLabels: Label[]): void { diff --git a/cvat/apps/engine/serializers.py b/cvat/apps/engine/serializers.py index c617b01fff3c..cf0ea9cd1d56 100644 --- a/cvat/apps/engine/serializers.py +++ b/cvat/apps/engine/serializers.py @@ -5,6 +5,7 @@ import os import re import shutil +from typing import Dict from rest_framework import serializers, exceptions from django.contrib.auth.models import User, Group @@ -70,10 +71,17 @@ class LabelSerializer(serializers.ModelSerializer): attributes = AttributeSerializer(many=True, source='attributespec_set', default=[]) color = serializers.CharField(allow_blank=True, required=False) + deleted = serializers.BooleanField(required=False) class Meta: model = models.Label - fields = ('id', 'name', 'color', 'attributes') + fields = ('id', 'name', 'color', 'attributes', 'deleted') + + def validate(self, attrs): + if attrs.get('deleted') == True and attrs.get('id') is None: + raise serializers.ValidationError('Deleted label must have an ID') + + return attrs @staticmethod def update_instance(validated_data, parent_instance): @@ -96,6 +104,9 @@ def update_instance(validated_data, parent_instance): else: db_label = models.Label.objects.create(name=validated_data.get('name'), **instance) logger.info("New {} label was created".format(db_label.name)) + if validated_data.get('deleted') == True: + db_label.delete() + return if not validated_data.get('color', None): label_names = [l.name for l in instance[tuple(instance.keys())[0]].label_set.exclude(id=db_label.id).order_by('id') From f53f9a862a843f484c5e33fac90d3ccda99512f5 Mon Sep 17 00:00:00 2001 From: Dmitry Kalinin Date: Fri, 5 Mar 2021 18:34:51 +0300 Subject: [PATCH 02/11] Added label deletion for server mock cvat-core tests --- cvat-core/tests/mocks/server-proxy.mock.js | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/cvat-core/tests/mocks/server-proxy.mock.js b/cvat-core/tests/mocks/server-proxy.mock.js index d52f3aede177..4357e1769efa 100644 --- a/cvat-core/tests/mocks/server-proxy.mock.js +++ b/cvat-core/tests/mocks/server-proxy.mock.js @@ -1,4 +1,4 @@ -// Copyright (C) 2020 Intel Corporation +// Copyright (C) 2020-2021 Intel Corporation // // SPDX-License-Identifier: MIT @@ -97,7 +97,11 @@ class ServerProxy { Object.prototype.hasOwnProperty.call(projectData, prop) && Object.prototype.hasOwnProperty.call(object, prop) ) { - object[prop] = projectData[prop]; + if (prop === 'labels') { + object[prop] = projectData[prop].filter((label) => !label.deleted); + } else { + object[prop] = projectData[prop]; + } } } } @@ -156,7 +160,11 @@ class ServerProxy { Object.prototype.hasOwnProperty.call(taskData, prop) && Object.prototype.hasOwnProperty.call(object, prop) ) { - object[prop] = taskData[prop]; + if (prop === 'labels') { + object[prop] = taskData[prop].filter((label) => !label.deleted); + } else { + object[prop] = taskData[prop]; + } } } } From 7913304b5afd7ba76f0387c86356436d517654af Mon Sep 17 00:00:00 2001 From: Dmitry Kalinin Date: Fri, 5 Mar 2021 18:35:46 +0300 Subject: [PATCH 03/11] vscode settings adjustments --- .vscode/settings.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.vscode/settings.json b/.vscode/settings.json index 7efcd4e6a035..c4fb22e0a673 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,6 +1,5 @@ { "python.pythonPath": ".env/bin/python", - "eslint.enable": true, "eslint.probe": [ "javascript", "typescript", @@ -19,8 +18,9 @@ "!cwd": true } ], + "python.linting.enabled": true, "python.linting.pylintEnabled": true, - "python.testing.unittestEnabled": true, + "python.linting.pycodestyleEnabled": false, "licenser.license": "Custom", "licenser.customHeader": "Copyright (C) @YEAR@ Intel Corporation\n\nSPDX-License-Identifier: MIT", "files.trimTrailingWhitespace": true From bbc578b846f0cd7b5941feb009adba52e16ae7cc Mon Sep 17 00:00:00 2001 From: Dmitry Kalinin Date: Fri, 12 Mar 2021 12:24:52 +0300 Subject: [PATCH 04/11] Added server tests --- cvat/apps/engine/tests/test_rest_api.py | 226 +++++++++++++++++++++++- cvat/settings/testing.py | 3 + 2 files changed, 221 insertions(+), 8 deletions(-) diff --git a/cvat/apps/engine/tests/test_rest_api.py b/cvat/apps/engine/tests/test_rest_api.py index d9d2c182a8e0..d1878a18414c 100644 --- a/cvat/apps/engine/tests/test_rest_api.py +++ b/cvat/apps/engine/tests/test_rest_api.py @@ -30,7 +30,7 @@ from rest_framework import status from rest_framework.test import APIClient, APITestCase -from cvat.apps.engine.models import (AttributeType, Data, Job, Project, +from cvat.apps.engine.models import (AttributeSpec, AttributeType, Data, Job, Project, Segment, StatusChoice, Task, Label, StorageMethodChoice, StorageChoice) from cvat.apps.engine.prepare import prepare_meta, prepare_meta_for_upload from cvat.apps.engine.media_extractors import ValidateDimension @@ -74,6 +74,7 @@ def create_db_task(data): os.makedirs(db_data.get_data_dirname()) os.makedirs(db_data.get_upload_dirname()) + labels = data.pop('labels', None) db_task = Task.objects.create(**data) shutil.rmtree(db_task.get_task_dirname(), ignore_errors=True) os.makedirs(db_task.get_task_dirname()) @@ -82,6 +83,17 @@ def create_db_task(data): db_task.data = db_data db_task.save() + if not labels is None: + for label_data in labels: + attributes = label_data.pop('attributes', None) + db_label = Label(task=db_task, **label_data) + db_label.save() + + if not attributes is None: + for attribute_data in attributes: + db_attribute = AttributeSpec(label=db_label, **attribute_data) + db_attribute.save() + for x in range(0, db_task.data.size, db_task.segment_size): start_frame = x stop_frame = min(x + db_task.segment_size - 1, db_task.data.size - 1) @@ -98,6 +110,26 @@ def create_db_task(data): return db_task +def create_db_project(data): + labels = data.pop('labels', None) + db_project = Project.objects.create(**data) + shutil.rmtree(db_project.get_project_dirname(), ignore_errors=True) + os.makedirs(db_project.get_project_dirname()) + os.makedirs(db_project.get_project_logs_dirname()) + + if not labels is None: + for label_data in labels: + attributes = label_data.pop('attributes', None) + db_label = Label(project=db_project, **label_data) + db_label.save() + + if not attributes is None: + for attribute_data in attributes: + db_attribute = AttributeSpec(label=db_label, **attribute_data) + db_attribute.save() + + return db_project + def create_dummy_db_tasks(obj, project=None): tasks = [] @@ -161,14 +193,14 @@ def create_dummy_db_projects(obj): "owner": obj.owner, "assignee": obj.assignee, } - db_project = Project.objects.create(**data) + db_project = create_db_project(data) projects.append(db_project) data = { "name": "my project without assignee", "owner": obj.user, } - db_project = Project.objects.create(**data) + db_project = create_db_project(data) create_dummy_db_tasks(obj, db_project) projects.append(db_project) @@ -177,14 +209,14 @@ def create_dummy_db_projects(obj): "owner": obj.owner, "assignee": obj.assignee, } - db_project = Project.objects.create(**data) + db_project = create_db_project(data) create_dummy_db_tasks(obj, db_project) projects.append(db_project) data = { "name": "public project", } - db_project = Project.objects.create(**data) + db_project = create_db_project(data) create_dummy_db_tasks(obj, db_project) projects.append(db_project) @@ -193,7 +225,7 @@ def create_dummy_db_projects(obj): "owner": obj.admin, "assignee": obj.assignee, } - db_project = Project.objects.create(**data) + db_project = create_db_project(data) create_dummy_db_tasks(obj, db_project) projects.append(db_project) @@ -1159,7 +1191,7 @@ def _run_api_v1_projects_id(self, pid, user, data): def _check_response(self, response, db_project, data): self.assertEqual(response.status_code, status.HTTP_200_OK) - name = data.get("name", data.get("name", db_project.name)) + name = data.get("name", db_project.name) self.assertEqual(response.data["name"], name) response_owner = response.data["owner"]["id"] if response.data["owner"] else None db_owner = db_project.owner.id if db_project.owner else None @@ -1169,6 +1201,16 @@ def _check_response(self, response, db_project, data): self.assertEqual(response_assignee, data.get("assignee_id", db_assignee)) self.assertEqual(response.data["status"], data.get("status", db_project.status)) self.assertEqual(response.data["bug_tracker"], data.get("bug_tracker", db_project.bug_tracker)) + if data.get("labels"): + self.assertListEqual( + [label["name"] for label in data.get("labels") if not label.get("deleted", False)], + [label["name"] for label in response.data["labels"]] + ) + else: + self.assertListEqual( + [label.name for label in db_project.label_set.all()], + [label["name"] for label in response.data["labels"]] + ) def _check_api_v1_projects_id(self, user, data): for db_project in self.projects: @@ -1182,9 +1224,13 @@ def _check_api_v1_projects_id(self, user, data): def test_api_v1_projects_id_admin(self): data = { - "name": "new name for the project", + "name": "project with some labels", "owner_id": self.owner.id, "bug_tracker": "https://new.bug.tracker", + "labels": [ + {"name": "car"}, + {"name": "person"} + ], } self._check_api_v1_projects_id(self.admin, data) @@ -1207,6 +1253,103 @@ def test_api_v1_projects_id_no_auth(self): } self._check_api_v1_projects_id(None, data) +class UpdateLabelsAPITestCase(APITestCase): + def setUp(self): + self.client = APIClient() + + def assertLabelsEqual(self, label1, label2): + self.assertEqual(label1.get("name", label2.get("name")), label2.get("name")) + self.assertEqual(label1.get("color", label2.get("color")), label2.get("color")) + + def _check_response(self, response, db_object, data): + self.assertEqual(response.status_code, status.HTTP_200_OK) + db_labels = db_object.label_set.all() + response_labels = response.data["labels"] + for label in data["labels"]: + if label.get("id", None) is None: + self.assertLabelsEqual( + label, + [l for l in response_labels if label.get("name") == l.get("name")][0], + ) + db_labels = [l for l in db_labels if label.get("name") != l.name] + response_labels = [l for l in response_labels if label.get("name") != l.get("name")] + else: + if not label.get("deleted", False): + self.assertLabelsEqual( + label, + [l for l in response_labels if label.get("id") == l.get("id")][0], + ) + response_labels = [l for l in response_labels if label.get("id") != l.get("id")] + db_labels = [l for l in db_labels if label.get("id") != l.id] + else: + self.assertEqual( + len([l for l in response_labels if label.get("id") == l.get("id")]), 0 + ) + self.assertEqual(len(response_labels), len(db_labels)) + +class ProjectUpdateLabelsAPITestCase(UpdateLabelsAPITestCase): + @classmethod + def setUpTestData(cls): + project_data = { + "name": "Project with labels", + "bug_tracker": "https://new.bug.tracker", + "labels": [{ + "name": "car", + "color": "#ff00ff", + "attributes": [{ + "name": "bool_attribute", + "mutable": True, + "input_type": AttributeType.CHECKBOX, + "default_value": "true" + }], + }, { + "name": "person", + }] + } + + create_db_users(cls) + db_project = create_db_project(project_data) + create_dummy_db_tasks(cls, db_project) + cls.project = db_project + + def _check_api_v1_project(self, data): + response = self._run_api_v1_project_id(self.project.id, self.admin, data) + self._check_response(response, self.project, data) + + def _run_api_v1_project_id(self, pid, user, data): + with ForceLogin(user, self.client): + response = self.client.patch('/api/v1/projects/{}'.format(pid), + data=data, format="json") + + return response + + def test_api_v1_projects_create_label(self): + data = { + "labels": [{ + "name": "new label", + }], + } + self._check_api_v1_project(data) + + def test_api_v1_projects_edit_label(self): + data = { + "labels": [{ + "id": 1, + "name": "New name for label", + "color": "#fefefe", + }], + } + self._check_api_v1_project(data) + + def test_api_v1_projects_delete_label(self): + data = { + "labels": [{ + "id": 2, + "name": "Label for deletion", + "deleted": True + }] + } + self._check_api_v1_project(data) class ProjectListOfTasksAPITestCase(APITestCase): def setUp(self): self.client = APIClient() @@ -1568,6 +1711,73 @@ def test_api_v1_tasks_id_no_auth(self): } self._check_api_v1_tasks_id(None, data) +class TaskUpdateLabelsAPITestCase(UpdateLabelsAPITestCase): + @classmethod + def setUpTestData(cls): + task_data = { + "name": "Project with labels", + "bug_tracker": "https://new.bug.tracker", + "overlap": 0, + "segment_size": 100, + "image_quality": 75, + "size": 100, + "labels": [{ + "name": "car", + "color": "#ff00ff", + "attributes": [{ + "name": "bool_attribute", + "mutable": True, + "input_type": AttributeType.CHECKBOX, + "default_value": "true" + }], + }, { + "name": "person", + }] + } + + create_db_users(cls) + db_task = create_db_task(task_data) + cls.task = db_task + + def _check_api_v1_task(self, data): + response = self._run_api_v1_task_id(self.task.id, self.admin, data) + self._check_response(response, self.task, data) + + def _run_api_v1_task_id(self, tid, user, data): + with ForceLogin(user, self.client): + response = self.client.patch('/api/v1/tasks/{}'.format(tid), + data=data, format="json") + + return response + + def test_api_v1_tasks_create_label(self): + data = { + "labels": [{ + "name": "new label", + }], + } + self._check_api_v1_task(data) + + def test_api_v1_tasks_edit_label(self): + data = { + "labels": [{ + "id": 1, + "name": "New name for label", + "color": "#fefefe", + }], + } + self._check_api_v1_task(data) + + def test_api_v1_tasks_delete_label(self): + data = { + "labels": [{ + "id": 2, + "name": "Label for deletion", + "deleted": True + }] + } + self._check_api_v1_task(data) + class TaskCreateAPITestCase(APITestCase): def setUp(self): self.client = APIClient() diff --git a/cvat/settings/testing.py b/cvat/settings/testing.py index 9825349fd8b7..c55e6f421126 100644 --- a/cvat/settings/testing.py +++ b/cvat/settings/testing.py @@ -19,6 +19,9 @@ TASKS_ROOT = os.path.join(DATA_ROOT, 'tasks') os.makedirs(TASKS_ROOT, exist_ok=True) +PROJECTS_ROOT = os.path.join(DATA_ROOT, 'projects') +os.makedirs(PROJECTS_ROOT, exist_ok=True) + MODELS_ROOT = os.path.join(DATA_ROOT, 'models') os.makedirs(MODELS_ROOT, exist_ok=True) From 0f8c3baacd21a756f41057b17d3f2348be9fcabc Mon Sep 17 00:00:00 2001 From: Dmitry Kalinin Date: Mon, 15 Mar 2021 11:53:32 +0300 Subject: [PATCH 05/11] Removed unused import --- cvat/apps/engine/serializers.py | 1 - 1 file changed, 1 deletion(-) diff --git a/cvat/apps/engine/serializers.py b/cvat/apps/engine/serializers.py index cf0ea9cd1d56..4aab32f4f4b9 100644 --- a/cvat/apps/engine/serializers.py +++ b/cvat/apps/engine/serializers.py @@ -5,7 +5,6 @@ import os import re import shutil -from typing import Dict from rest_framework import serializers, exceptions from django.contrib.auth.models import User, Group From fd2bf6bc8915e1cda2f36927899be10d532301b4 Mon Sep 17 00:00:00 2001 From: Dmitry Kalinin Date: Mon, 15 Mar 2021 12:05:42 +0300 Subject: [PATCH 06/11] Added CHANGELOG and increased npm version --- CHANGELOG.md | 1 + cvat-ui/package-lock.json | 2 +- cvat-ui/package.json | 2 +- 3 files changed, 3 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7bb9bdc3e86c..0347e1c3fc53 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -20,6 +20,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Pre-built [cvat_server](https://hub.docker.com/r/openvino/cvat_server) and [cvat_ui](https://hub.docker.com/r/openvino/cvat_ui) images were published on DockerHub () - Project task subsets () +- Label deletion from tasks and projects () ### Changed diff --git a/cvat-ui/package-lock.json b/cvat-ui/package-lock.json index a51ba06021b3..88465e0711b4 100644 --- a/cvat-ui/package-lock.json +++ b/cvat-ui/package-lock.json @@ -1,6 +1,6 @@ { "name": "cvat-ui", - "version": "1.15.2", + "version": "1.15.3", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/cvat-ui/package.json b/cvat-ui/package.json index 027fffe58974..dff1015e2fc8 100644 --- a/cvat-ui/package.json +++ b/cvat-ui/package.json @@ -1,6 +1,6 @@ { "name": "cvat-ui", - "version": "1.15.2", + "version": "1.15.3", "description": "CVAT single-page application", "main": "src/index.tsx", "scripts": { From bb1da4db878c3b9198aa5b8d8d98c2ed0557691d Mon Sep 17 00:00:00 2001 From: Dmitry Kalinin Date: Mon, 15 Mar 2021 12:25:06 +0300 Subject: [PATCH 07/11] Added ingoring npm scripts for non-project directories --- .vscode/settings.json | 1 + 1 file changed, 1 insertion(+) diff --git a/.vscode/settings.json b/.vscode/settings.json index c4fb22e0a673..5718c4b7c14d 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -18,6 +18,7 @@ "!cwd": true } ], + "npm.exclude": "**/.env/**", "python.linting.enabled": true, "python.linting.pylintEnabled": true, "python.linting.pycodestyleEnabled": false, From 93a04ad4371a7691377ca7f893918b789a4efc71 Mon Sep 17 00:00:00 2001 From: Dmitry Kalinin Date: Mon, 15 Mar 2021 12:57:54 +0300 Subject: [PATCH 08/11] Added dummy no labels wrapper --- cvat-ui/src/actions/annotation-actions.ts | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/cvat-ui/src/actions/annotation-actions.ts b/cvat-ui/src/actions/annotation-actions.ts index 194edc1c06ae..3fad874e6f4f 100644 --- a/cvat-ui/src/actions/annotation-actions.ts +++ b/cvat-ui/src/actions/annotation-actions.ts @@ -5,6 +5,7 @@ import { AnyAction, Dispatch, ActionCreator, Store, } from 'redux'; +import { MutableRefObject } from 'react'; import { ThunkAction } from 'utils/redux'; import { @@ -26,7 +27,6 @@ import getCore from 'cvat-core-wrapper'; import logger, { LogType } from 'cvat-logger'; import { RectDrawingMethod } from 'cvat-canvas-wrapper'; import { getCVATStore } from 'cvat-store'; -import { MutableRefObject } from 'react'; interface AnnotationsParameters { filters: string[]; @@ -919,8 +919,10 @@ export function getJobAsync(tid: number, jid: number, initialFrame: number, init throw new Error(`Task ${tid} doesn't contain the job ${jid}`); } - if (!task.labels.length && task.projectId) { - throw new Error(`Project ${task.projectId} does not contain any label`); + if (!task.labels.length) { + throw new Error( + `${task.projectId ? 'Project' : 'Task'} ${task.projectId || task.id} does not contain any label`, + ); } const frameNumber = Math.max(Math.min(job.stopFrame, initialFrame), job.startFrame); From 267af2efa46a3ad955e5d223b54e8d809123b6a6 Mon Sep 17 00:00:00 2001 From: Dmitry Kalinin Date: Tue, 16 Mar 2021 02:38:14 +0300 Subject: [PATCH 09/11] Added handling no labels jobs --- cvat-ui/src/actions/annotation-actions.ts | 11 ++++++--- .../controls-side-bar/controls-side-bar.tsx | 21 ++++++++++++++--- .../controls-side-bar/draw-cuboid-control.tsx | 7 ++++-- .../controls-side-bar/draw-points-control.tsx | 7 ++++-- .../draw-polygon-control.tsx | 7 ++++-- .../draw-polyline-control.tsx | 7 ++++-- .../draw-rectangle-control.tsx | 7 ++++-- .../controls-side-bar/group-control.tsx | 7 ++++-- .../controls-side-bar/merge-control.tsx | 7 ++++-- .../controls-side-bar/opencv-control.tsx | 10 ++++---- .../controls-side-bar/setup-tag-control.tsx | 7 ++++-- .../controls-side-bar/split-control.tsx | 7 ++++-- .../controls-side-bar/tools-control.tsx | 14 ++++++----- .../standard-workspace/styles.scss | 8 +++++-- .../tag-annotation-workspace/styles.scss | 4 ++++ .../tag-annotation-sidebar.tsx | 23 ++++++++++++++++--- .../controls-side-bar/controls-side-bar.tsx | 5 +++- .../controls-side-bar/draw-shape-popover.tsx | 4 ++-- cvat-ui/src/reducers/annotation-reducer.ts | 2 +- 19 files changed, 122 insertions(+), 43 deletions(-) diff --git a/cvat-ui/src/actions/annotation-actions.ts b/cvat-ui/src/actions/annotation-actions.ts index 3fad874e6f4f..a5be24340b5e 100644 --- a/cvat-ui/src/actions/annotation-actions.ts +++ b/cvat-ui/src/actions/annotation-actions.ts @@ -6,6 +6,7 @@ import { AnyAction, Dispatch, ActionCreator, Store, } from 'redux'; import { MutableRefObject } from 'react'; +import notification from 'antd/lib/notification'; import { ThunkAction } from 'utils/redux'; import { @@ -920,9 +921,13 @@ export function getJobAsync(tid: number, jid: number, initialFrame: number, init } if (!task.labels.length) { - throw new Error( - `${task.projectId ? 'Project' : 'Task'} ${task.projectId || task.id} does not contain any label`, - ); + notification.warning({ + message: 'No labels', + description: `${task.projectId ? 'Project' : 'Task'} ${ + task.projectId || task.id + } does not contain any label`, + placement: 'topRight', + }); } const frameNumber = Math.max(Math.min(job.stopFrame, initialFrame), job.startFrame); 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 2ac25ef204dd..f1016b56cf40 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 @@ -3,8 +3,8 @@ // SPDX-License-Identifier: MIT import React from 'react'; -import GlobalHotKeys, { KeyMap } from 'utils/mousetrap-react'; import Layout from 'antd/lib/layout'; +import GlobalHotKeys, { KeyMap } from 'utils/mousetrap-react'; import { ActiveControl, Rotation } from 'reducers/interfaces'; import { Canvas } from 'cvat-canvas-wrapper'; @@ -32,6 +32,7 @@ interface Props { activeControl: ActiveControl; keyMap: KeyMap; normalizedKeyMap: Record; + labels: any[]; mergeObjects(enabled: boolean): void; groupObjects(enabled: boolean): void; @@ -64,10 +65,11 @@ const ObservedSplitControl = ControlVisibilityObserver(SplitC export default function ControlsSideBarComponent(props: Props): JSX.Element { const { - canvasInstance, activeControl, + canvasInstance, normalizedKeyMap, keyMap, + labels, mergeObjects, groupObjects, splitTrack, @@ -114,6 +116,8 @@ export default function ControlsSideBarComponent(props: Props): JSX.Element { ActiveControl.OPENCV_TOOLS, ].includes(activeControl); + if (!labels.length) return; + if (!drawing) { canvasInstance.cancel(); // repeateDrawShapes gets all the latest parameters @@ -136,6 +140,7 @@ export default function ControlsSideBarComponent(props: Props): JSX.Element { }, SWITCH_MERGE_MODE: (event: KeyboardEvent | undefined) => { preventDefault(event); + if (!labels.length) return; const merging = activeControl === ActiveControl.MERGE; if (!merging) { canvasInstance.cancel(); @@ -145,6 +150,7 @@ export default function ControlsSideBarComponent(props: Props): JSX.Element { }, SWITCH_SPLIT_MODE: (event: KeyboardEvent | undefined) => { preventDefault(event); + if (!labels.length) return; const splitting = activeControl === ActiveControl.SPLIT; if (!splitting) { canvasInstance.cancel(); @@ -154,6 +160,7 @@ export default function ControlsSideBarComponent(props: Props): JSX.Element { }, SWITCH_GROUP_MODE: (event: KeyboardEvent | undefined) => { preventDefault(event); + if (!labels.length) return; const grouping = activeControl === ActiveControl.GROUP; if (!grouping) { canvasInstance.cancel(); @@ -213,24 +220,29 @@ export default function ControlsSideBarComponent(props: Props): JSX.Element { - +
@@ -239,6 +251,7 @@ export default function ControlsSideBarComponent(props: Props): JSX.Element { canvasInstance={canvasInstance} activeControl={activeControl} mergeObjects={mergeObjects} + disabled={!labels.length} /> diff --git a/cvat-ui/src/components/annotation-page/standard-workspace/controls-side-bar/draw-cuboid-control.tsx b/cvat-ui/src/components/annotation-page/standard-workspace/controls-side-bar/draw-cuboid-control.tsx index 50e28fecae54..29e5ee0f0d56 100644 --- a/cvat-ui/src/components/annotation-page/standard-workspace/controls-side-bar/draw-cuboid-control.tsx +++ b/cvat-ui/src/components/annotation-page/standard-workspace/controls-side-bar/draw-cuboid-control.tsx @@ -17,11 +17,12 @@ import withVisibilityHandling from './handle-popover-visibility'; export interface Props { canvasInstance: Canvas; isDrawing: boolean; + disabled?: boolean; } const CustomPopover = withVisibilityHandling(Popover, 'draw-cuboid'); function DrawPolygonControl(props: Props): JSX.Element { - const { canvasInstance, isDrawing } = props; + const { canvasInstance, isDrawing, disabled } = props; const dynamcPopoverPros = isDrawing ? { overlayStyle: { @@ -41,7 +42,9 @@ function DrawPolygonControl(props: Props): JSX.Element { className: 'cvat-draw-cuboid-control', }; - return ( + return disabled ? ( + + ) : ( + ) : ( + ) : ( + ) : ( + ) : ( + ) : ( 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 e1c96c3baf82..804aa38bf5c8 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 @@ -14,12 +14,13 @@ export interface Props { canvasInstance: Canvas; activeControl: ActiveControl; switchMergeShortcut: string; + disabled?: boolean; mergeObjects(enabled: boolean): void; } function MergeControl(props: Props): JSX.Element { const { - switchMergeShortcut, activeControl, canvasInstance, mergeObjects, + switchMergeShortcut, activeControl, canvasInstance, mergeObjects, disabled, } = props; const dynamicIconProps = @@ -40,7 +41,9 @@ function MergeControl(props: Props): JSX.Element { }, }; - return ( + return disabled ? ( + + ) : ( diff --git a/cvat-ui/src/components/annotation-page/standard-workspace/controls-side-bar/opencv-control.tsx b/cvat-ui/src/components/annotation-page/standard-workspace/controls-side-bar/opencv-control.tsx index 8bb4ea8740c8..d8c9c7615ddf 100644 --- a/cvat-ui/src/components/annotation-page/standard-workspace/controls-side-bar/opencv-control.tsx +++ b/cvat-ui/src/components/annotation-page/standard-workspace/controls-side-bar/opencv-control.tsx @@ -108,7 +108,7 @@ class OpenCVControlComponent extends React.PureComponent { canvasInstance.interact({ enabled: false }); }, @@ -403,7 +403,9 @@ class OpenCVControlComponent extends React.PureComponent + ) : ( + ) : ( }> 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 c68774596986..4cc8057f7c4e 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 @@ -14,12 +14,13 @@ export interface Props { canvasInstance: Canvas; activeControl: ActiveControl; switchSplitShortcut: string; + disabled?: boolean; splitTrack(enabled: boolean): void; } function SplitControl(props: Props): JSX.Element { const { - switchSplitShortcut, activeControl, canvasInstance, splitTrack, + switchSplitShortcut, activeControl, canvasInstance, splitTrack, disabled, } = props; const dynamicIconProps = @@ -40,7 +41,9 @@ function SplitControl(props: Props): JSX.Element { }, }; - return ( + return disabled ? ( + + ) : ( diff --git a/cvat-ui/src/components/annotation-page/standard-workspace/controls-side-bar/tools-control.tsx b/cvat-ui/src/components/annotation-page/standard-workspace/controls-side-bar/tools-control.tsx index 8ff6aae647bb..f19971f15225 100644 --- a/cvat-ui/src/components/annotation-page/standard-workspace/controls-side-bar/tools-control.tsx +++ b/cvat-ui/src/components/annotation-page/standard-workspace/controls-side-bar/tools-control.tsx @@ -111,7 +111,7 @@ export class ToolsControlComponent extends React.PureComponent { this.state = { activeInteractor: props.interactors.length ? props.interactors[0] : null, activeTracker: props.trackers.length ? props.trackers[0] : null, - activeLabelID: props.labels[0].id, + activeLabelID: props.labels.length ? props.labels[0].id : null, interactiveStateID: null, trackingProgress: null, trackingFrames: 10, @@ -239,7 +239,7 @@ export class ToolsControlComponent extends React.PureComponent { const object = new core.classes.ObjectState({ frame, objectType: ObjectType.SHAPE, - label: labels.filter((label: any) => label.id === activeLabelID)[0], + label: labels.length ? labels.filter((label: any) => label.id === activeLabelID)[0] : null, shapeType: ShapeType.POLYGON, points: result.flat(), occluded: false, @@ -257,7 +257,7 @@ export class ToolsControlComponent extends React.PureComponent { const object = new core.classes.ObjectState({ frame, objectType: ObjectType.SHAPE, - label: labels.filter((label: any) => label.id === activeLabelID)[0], + label: labels.length ? labels.filter((label: any) => label.id === activeLabelID)[0] : null, shapeType: ShapeType.POLYGON, points: result.flat(), occluded: false, @@ -716,7 +716,7 @@ export class ToolsControlComponent extends React.PureComponent { public render(): JSX.Element | null { const { - interactors, detectors, trackers, isActivated, canvasInstance, + interactors, detectors, trackers, isActivated, canvasInstance, labels, } = this.props; const { fetching, trackingProgress } = this.state; @@ -732,7 +732,7 @@ export class ToolsControlComponent extends React.PureComponent { const dynamicIconProps = isActivated ? { - className: 'cvat-active-canvas-control cvat-tools-control', + className: 'cvat-tools-control cvat-active-canvas-control', onClick: (): void => { canvasInstance.interact({ enabled: false }); }, @@ -741,7 +741,9 @@ export class ToolsControlComponent extends React.PureComponent { className: 'cvat-tools-control', }; - return ( + return !labels.length ? ( + + ) : ( <> svg { + filter: opacity(0.45); +} + .cvat-rotate-canvas-controls-left, .cvat-rotate-canvas-controls-right { transform: scale(0.65); diff --git a/cvat-ui/src/components/annotation-page/tag-annotation-workspace/styles.scss b/cvat-ui/src/components/annotation-page/tag-annotation-workspace/styles.scss index 0f15c96830e3..ce04f7048cae 100644 --- a/cvat-ui/src/components/annotation-page/tag-annotation-workspace/styles.scss +++ b/cvat-ui/src/components/annotation-page/tag-annotation-workspace/styles.scss @@ -45,3 +45,7 @@ padding: 5px 10px; } } + +.labels-tag-annotation-sidebar-not-found-wrapper { + margin-top: $grid-unit-size * 4; +} diff --git a/cvat-ui/src/components/annotation-page/tag-annotation-workspace/tag-annotation-sidebar/tag-annotation-sidebar.tsx b/cvat-ui/src/components/annotation-page/tag-annotation-workspace/tag-annotation-sidebar/tag-annotation-sidebar.tsx index 775d13c42e96..f54b405e7643 100644 --- a/cvat-ui/src/components/annotation-page/tag-annotation-workspace/tag-annotation-sidebar/tag-annotation-sidebar.tsx +++ b/cvat-ui/src/components/annotation-page/tag-annotation-workspace/tag-annotation-sidebar/tag-annotation-sidebar.tsx @@ -4,7 +4,6 @@ import React, { useState, useEffect } from 'react'; import { connect } from 'react-redux'; -import GlobalHotKeys, { KeyMap } from 'utils/mousetrap-react'; import { Action } from 'redux'; import { ThunkDispatch } from 'redux-thunk'; import { Row, Col } from 'antd/lib/grid'; @@ -21,6 +20,7 @@ import { changeFrameAsync, rememberObject, } from 'actions/annotation-actions'; +import GlobalHotKeys, { KeyMap } from 'utils/mousetrap-react'; import { Canvas } from 'cvat-canvas-wrapper'; import { CombinedState, ObjectType } from 'reducers/interfaces'; import LabelSelector from 'components/label-selector/label-selector'; @@ -107,7 +107,7 @@ function TagAnnotationSidebar(props: StateToProps & DispatchToProps): JSX.Elemen } }; - const defaultLabelID = labels[0].id; + const defaultLabelID = labels.length ? labels[0].id : null; const [sidebarCollapsed, setSidebarCollapsed] = useState(false); const [frameTags, setFrameTags] = useState([] as any[]); @@ -196,7 +196,24 @@ function TagAnnotationSidebar(props: StateToProps & DispatchToProps): JSX.Elemen }, }; - return ( + return !labels.length ? ( + + {/* eslint-disable-next-line */} + setSidebarCollapsed(!sidebarCollapsed)} + > + {sidebarCollapsed ? : } + + + + No labels are available. + + + + ) : ( <> diff --git a/cvat-ui/src/containers/annotation-page/standard-workspace/controls-side-bar/controls-side-bar.tsx b/cvat-ui/src/containers/annotation-page/standard-workspace/controls-side-bar/controls-side-bar.tsx index 22a33f9dc497..f057ae6413d0 100644 --- a/cvat-ui/src/containers/annotation-page/standard-workspace/controls-side-bar/controls-side-bar.tsx +++ b/cvat-ui/src/containers/annotation-page/standard-workspace/controls-side-bar/controls-side-bar.tsx @@ -2,7 +2,6 @@ // // SPDX-License-Identifier: MIT -import { KeyMap } from 'utils/mousetrap-react'; import { connect } from 'react-redux'; import { Canvas } from 'cvat-canvas-wrapper'; @@ -18,6 +17,7 @@ import { } from 'actions/annotation-actions'; import ControlsSideBarComponent from 'components/annotation-page/standard-workspace/controls-side-bar/controls-side-bar'; import { ActiveControl, CombinedState, Rotation } from 'reducers/interfaces'; +import { KeyMap } from 'utils/mousetrap-react'; interface StateToProps { canvasInstance: Canvas; @@ -25,6 +25,7 @@ interface StateToProps { activeControl: ActiveControl; keyMap: KeyMap; normalizedKeyMap: Record; + labels: any[]; } interface DispatchToProps { @@ -42,6 +43,7 @@ function mapStateToProps(state: CombinedState): StateToProps { const { annotation: { canvas: { instance: canvasInstance, activeControl }, + job: { labels }, }, settings: { player: { rotateAll }, @@ -53,6 +55,7 @@ function mapStateToProps(state: CombinedState): StateToProps { rotateAll, canvasInstance, activeControl, + labels, normalizedKeyMap, keyMap, }; diff --git a/cvat-ui/src/containers/annotation-page/standard-workspace/controls-side-bar/draw-shape-popover.tsx b/cvat-ui/src/containers/annotation-page/standard-workspace/controls-side-bar/draw-shape-popover.tsx index 937b61f0f06c..151be9789114 100644 --- a/cvat-ui/src/containers/annotation-page/standard-workspace/controls-side-bar/draw-shape-popover.tsx +++ b/cvat-ui/src/containers/annotation-page/standard-workspace/controls-side-bar/draw-shape-popover.tsx @@ -1,4 +1,4 @@ -// Copyright (C) 2020 Intel Corporation +// Copyright (C) 2020-2021 Intel Corporation // // SPDX-License-Identifier: MIT @@ -79,7 +79,7 @@ class DrawShapePopoverContainer extends React.PureComponent { super(props); const { shapeType } = props; - const defaultLabelID = props.labels[0].id; + const defaultLabelID = props.labels.length ? props.labels[0].id : null; const defaultRectDrawingMethod = RectDrawingMethod.CLASSIC; const defaultCuboidDrawingMethod = CuboidDrawingMethod.CLASSIC; this.state = { diff --git a/cvat-ui/src/reducers/annotation-reducer.ts b/cvat-ui/src/reducers/annotation-reducer.ts index 0db2fbaa3492..3da1900848d4 100644 --- a/cvat-ui/src/reducers/annotation-reducer.ts +++ b/cvat-ui/src/reducers/annotation-reducer.ts @@ -182,7 +182,7 @@ export default (state = defaultState, action: AnyAction): AnnotationState => { }, drawing: { ...state.drawing, - activeLabelID: job.task.labels[0].id, + activeLabelID: job.task.labels.length ? job.task.labels[0].id : null, activeObjectType: job.task.mode === 'interpolation' ? ObjectType.TRACK : ObjectType.SHAPE, }, canvas: { From 21e15058dc9486cd81bb419a23843a4230dff293 Mon Sep 17 00:00:00 2001 From: Dmitry Kalinin Date: Thu, 18 Mar 2021 13:59:48 +0300 Subject: [PATCH 10/11] Fixed PR comments --- cvat-core/src/labels.js | 11 ++ cvat-core/src/project.js | 26 +-- cvat-core/src/session.js | 30 ++- cvat-ui/src/actions/annotation-actions.ts | 13 +- .../annotation-page/annotation-page.tsx | 24 +++ .../controls-side-bar/controls-side-bar.tsx | 178 +++++++++--------- cvat-ui/src/utils/hooks.ts | 13 ++ 7 files changed, 160 insertions(+), 135 deletions(-) create mode 100644 cvat-ui/src/utils/hooks.ts diff --git a/cvat-core/src/labels.js b/cvat-core/src/labels.js index 7e673245d119..e78224694d3a 100644 --- a/cvat-core/src/labels.js +++ b/cvat-core/src/labels.js @@ -133,6 +133,7 @@ id: undefined, name: undefined, color: undefined, + deleted: false, }; for (const key in data) { @@ -208,6 +209,12 @@ attributes: { get: () => [...data.attributes], }, + deleted: { + get: () => data.deleted, + set: (value) => { + data.deleted = value; + }, + }, }), ); } @@ -223,6 +230,10 @@ object.id = this.id; } + if (this.deleted) { + object.deleted = this.deleted; + } + return object; } } diff --git a/cvat-core/src/project.js b/cvat-core/src/project.js index b165f867ab26..f389205c6f48 100644 --- a/cvat-core/src/project.js +++ b/cvat-core/src/project.js @@ -43,7 +43,6 @@ data.labels = []; data.tasks = []; - data.deleted_labels = []; if (Array.isArray(initialData.labels)) { for (const label of initialData.labels) { @@ -187,13 +186,13 @@ ); } - for (const label of data.labels) { - if (!labels.filter((_label) => _label.id === label.id).length) { - data.deleted_labels.push(label); - } - } + const IDs = labels.map((_label) => _label.id); + const deletedLabels = data.labels.filter((_label) => !IDs.includes(_label.id)); + deletedLabels.forEach((_label) => { + _label.deleted = true; + }); - data.labels = [...labels]; + data.labels = [...deletedLabels, ...labels]; }, }, /** @@ -218,8 +217,8 @@ subsets: { get: () => [...data.task_subsets], }, - _deletedLabels: { - get: () => [...data.deleted_labels], + _internalData: { + get: () => data, }, }), ); @@ -267,14 +266,7 @@ name: this.name, assignee_id: this.assignee ? this.assignee.id : null, bug_tracker: this.bugTracker, - labels: [ - ...this.labels.map((el) => el.toJSON()), - ...this._deletedLabels.map((label) => { - const labelJSON = label.toJSON(); - labelJSON.deleted = true; - return labelJSON; - }), - ], + labels: [...this._internalData.labels.map((el) => el.toJSON())], }; await serverProxy.projects.save(this.id, projectData); diff --git a/cvat-core/src/session.js b/cvat-core/src/session.js index aef65c4176e4..fbc708e842af 100644 --- a/cvat-core/src/session.js +++ b/cvat-core/src/session.js @@ -1006,7 +1006,6 @@ if (data.owner) data.owner = new User(data.owner); data.labels = []; - data.deleted_labels = []; data.jobs = []; data.files = Object.freeze({ server_files: [], @@ -1305,7 +1304,7 @@ * @throws {module:API.cvat.exceptions.ArgumentError} */ labels: { - get: () => [...data.labels], + get: () => data.labels.filter((_label) => !_label.deleted), set: (labels) => { if (!Array.isArray(labels)) { throw new ArgumentError('Value must be an array of Labels'); @@ -1319,14 +1318,14 @@ } } - for (const label of data.labels) { - if (!labels.filter((_label) => _label.id === label.id).length) { - data.deleted_labels.push(label); - } - } + const IDs = labels.map((_label) => _label.id); + const deletedLabels = data.labels.filter((_label) => !IDs.includes(_label.id)); + deletedLabels.forEach((_label) => { + _label.deleted = true; + }); updatedFields.labels = true; - data.labels = [...labels]; + data.labels = [...deletedLabels, ...labels]; }, }, /** @@ -1502,15 +1501,15 @@ */ get: () => data.dimension, }, + _internalData: { + get: () => data, + }, __updatedFields: { get: () => updatedFields, set: (fields) => { updatedFields = fields; }, }, - _deletedLabels: { - get: () => [...data.deleted_labels], - }, }), ); @@ -1930,14 +1929,7 @@ taskData.subset = this.subset; break; case 'labels': - taskData.labels = [ - ...this.labels.map((el) => el.toJSON()), - ...this._deletedLabels.map((label) => { - const labelJSON = label.toJSON(); - labelJSON.deleted = true; - return labelJSON; - }), - ]; + taskData.labels = [...this._internalData.labels.map((el) => el.toJSON())]; break; default: break; diff --git a/cvat-ui/src/actions/annotation-actions.ts b/cvat-ui/src/actions/annotation-actions.ts index a5be24340b5e..40cc070d5e58 100644 --- a/cvat-ui/src/actions/annotation-actions.ts +++ b/cvat-ui/src/actions/annotation-actions.ts @@ -2,11 +2,10 @@ // // SPDX-License-Identifier: MIT +import { MutableRefObject } from 'react'; import { AnyAction, Dispatch, ActionCreator, Store, } from 'redux'; -import { MutableRefObject } from 'react'; -import notification from 'antd/lib/notification'; import { ThunkAction } from 'utils/redux'; import { @@ -920,16 +919,6 @@ export function getJobAsync(tid: number, jid: number, initialFrame: number, init throw new Error(`Task ${tid} doesn't contain the job ${jid}`); } - if (!task.labels.length) { - notification.warning({ - message: 'No labels', - description: `${task.projectId ? 'Project' : 'Task'} ${ - task.projectId || task.id - } does not contain any label`, - placement: 'topRight', - }); - } - const frameNumber = Math.max(Math.min(job.stopFrame, initialFrame), job.startFrame); const frameData = await job.frames.get(frameNumber); // call first getting of frame data before rendering interface diff --git a/cvat-ui/src/components/annotation-page/annotation-page.tsx b/cvat-ui/src/components/annotation-page/annotation-page.tsx index 484998597582..92410833bf7e 100644 --- a/cvat-ui/src/components/annotation-page/annotation-page.tsx +++ b/cvat-ui/src/components/annotation-page/annotation-page.tsx @@ -8,8 +8,10 @@ import { useHistory } from 'react-router'; import Layout from 'antd/lib/layout'; import Spin from 'antd/lib/spin'; import Result from 'antd/lib/result'; +import notification from 'antd/lib/notification'; import { Workspace } from 'reducers/interfaces'; +import { usePrevious } from 'utils/hooks'; import AnnotationTopBarContainer from 'containers/annotation-page/top-bar/top-bar'; import StatisticsModalContainer from 'containers/annotation-page/top-bar/statistics-modal'; import StandardWorkspaceComponent from 'components/annotation-page/standard-workspace/standard-workspace'; @@ -33,6 +35,8 @@ export default function AnnotationPageComponent(props: Props): JSX.Element { const { job, fetching, getJob, closeJob, saveLogs, workspace, } = props; + const prevJob = usePrevious(job); + const prevFetching = usePrevious(fetching); const history = useHistory(); useEffect(() => { @@ -60,6 +64,26 @@ export default function AnnotationPageComponent(props: Props): JSX.Element { } }, [job, fetching]); + useEffect(() => { + if (prevFetching && !fetching && !prevJob && job && !job.task.labels.length) { + notification.warning({ + message: 'No labels', + description: ( + + {`${job.task.projectId ? 'Project' : 'Task'} ${ + job.task.projectId || job.task.id + } does not contain any label. `} + + Add + + {' the first one for editing annotation.'} + + ), + placement: 'topRight', + }); + } + }, [job, fetching, prevJob, prevFetching]); + if (job === null) { return ; } 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 f1016b56cf40..87017d5a5bd1 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,98 +86,13 @@ export default function ControlsSideBarComponent(props: Props): JSX.Element { } }; - const subKeyMap = { - PASTE_SHAPE: keyMap.PASTE_SHAPE, - SWITCH_DRAW_MODE: keyMap.SWITCH_DRAW_MODE, - SWITCH_MERGE_MODE: keyMap.SWITCH_MERGE_MODE, - SWITCH_SPLIT_MODE: keyMap.SWITCH_SPLIT_MODE, - SWITCH_GROUP_MODE: keyMap.SWITCH_GROUP_MODE, - RESET_GROUP: keyMap.RESET_GROUP, + let subKeyMap: any = { CANCEL: keyMap.CANCEL, CLOCKWISE_ROTATION: keyMap.CLOCKWISE_ROTATION, ANTICLOCKWISE_ROTATION: keyMap.ANTICLOCKWISE_ROTATION, }; - const handlers = { - PASTE_SHAPE: (event: KeyboardEvent | undefined) => { - preventDefault(event); - canvasInstance.cancel(); - pasteShape(); - }, - SWITCH_DRAW_MODE: (event: KeyboardEvent | undefined) => { - preventDefault(event); - const drawing = [ - ActiveControl.DRAW_POINTS, - ActiveControl.DRAW_POLYGON, - ActiveControl.DRAW_POLYLINE, - ActiveControl.DRAW_RECTANGLE, - ActiveControl.DRAW_CUBOID, - ActiveControl.AI_TOOLS, - ActiveControl.OPENCV_TOOLS, - ].includes(activeControl); - - if (!labels.length) return; - - if (!drawing) { - canvasInstance.cancel(); - // repeateDrawShapes gets all the latest parameters - // and calls canvasInstance.draw() with them - - if (event && event.shiftKey) { - redrawShape(); - } else { - repeatDrawShape(); - } - } else { - if ([ActiveControl.AI_TOOLS, ActiveControl.OPENCV_TOOLS].includes(activeControl)) { - // separated API method - canvasInstance.interact({ enabled: false }); - return; - } - - canvasInstance.draw({ enabled: false }); - } - }, - SWITCH_MERGE_MODE: (event: KeyboardEvent | undefined) => { - preventDefault(event); - if (!labels.length) return; - const merging = activeControl === ActiveControl.MERGE; - if (!merging) { - canvasInstance.cancel(); - } - canvasInstance.merge({ enabled: !merging }); - mergeObjects(!merging); - }, - SWITCH_SPLIT_MODE: (event: KeyboardEvent | undefined) => { - preventDefault(event); - if (!labels.length) return; - const splitting = activeControl === ActiveControl.SPLIT; - if (!splitting) { - canvasInstance.cancel(); - } - canvasInstance.split({ enabled: !splitting }); - splitTrack(!splitting); - }, - SWITCH_GROUP_MODE: (event: KeyboardEvent | undefined) => { - preventDefault(event); - if (!labels.length) return; - const grouping = activeControl === ActiveControl.GROUP; - if (!grouping) { - canvasInstance.cancel(); - } - canvasInstance.group({ enabled: !grouping }); - groupObjects(!grouping); - }, - RESET_GROUP: (event: KeyboardEvent | undefined) => { - preventDefault(event); - const grouping = activeControl === ActiveControl.GROUP; - if (!grouping) { - return; - } - resetGroup(); - canvasInstance.group({ enabled: false }); - groupObjects(false); - }, + let handlers: any = { CANCEL: (event: KeyboardEvent | undefined) => { preventDefault(event); if (activeControl !== ActiveControl.CURSOR) { @@ -194,6 +109,95 @@ export default function ControlsSideBarComponent(props: Props): JSX.Element { }, }; + if (labels.length) { + handlers = { + ...handlers, + PASTE_SHAPE: (event: KeyboardEvent | undefined) => { + preventDefault(event); + canvasInstance.cancel(); + pasteShape(); + }, + SWITCH_DRAW_MODE: (event: KeyboardEvent | undefined) => { + preventDefault(event); + const drawing = [ + ActiveControl.DRAW_POINTS, + ActiveControl.DRAW_POLYGON, + ActiveControl.DRAW_POLYLINE, + ActiveControl.DRAW_RECTANGLE, + ActiveControl.DRAW_CUBOID, + ActiveControl.AI_TOOLS, + ActiveControl.OPENCV_TOOLS, + ].includes(activeControl); + + if (!drawing) { + canvasInstance.cancel(); + // repeateDrawShapes gets all the latest parameters + // and calls canvasInstance.draw() with them + + if (event && event.shiftKey) { + redrawShape(); + } else { + repeatDrawShape(); + } + } else { + if ([ActiveControl.AI_TOOLS, ActiveControl.OPENCV_TOOLS].includes(activeControl)) { + // separated API method + canvasInstance.interact({ enabled: false }); + return; + } + + canvasInstance.draw({ enabled: false }); + } + }, + SWITCH_MERGE_MODE: (event: KeyboardEvent | undefined) => { + preventDefault(event); + const merging = activeControl === ActiveControl.MERGE; + if (!merging) { + canvasInstance.cancel(); + } + canvasInstance.merge({ enabled: !merging }); + mergeObjects(!merging); + }, + SWITCH_SPLIT_MODE: (event: KeyboardEvent | undefined) => { + preventDefault(event); + const splitting = activeControl === ActiveControl.SPLIT; + if (!splitting) { + canvasInstance.cancel(); + } + canvasInstance.split({ enabled: !splitting }); + splitTrack(!splitting); + }, + SWITCH_GROUP_MODE: (event: KeyboardEvent | undefined) => { + preventDefault(event); + const grouping = activeControl === ActiveControl.GROUP; + if (!grouping) { + canvasInstance.cancel(); + } + canvasInstance.group({ enabled: !grouping }); + groupObjects(!grouping); + }, + RESET_GROUP: (event: KeyboardEvent | undefined) => { + preventDefault(event); + const grouping = activeControl === ActiveControl.GROUP; + if (!grouping) { + return; + } + resetGroup(); + canvasInstance.group({ enabled: false }); + groupObjects(false); + }, + }; + subKeyMap = { + ...subKeyMap, + PASTE_SHAPE: keyMap.PASTE_SHAPE, + SWITCH_DRAW_MODE: keyMap.SWITCH_DRAW_MODE, + SWITCH_MERGE_MODE: keyMap.SWITCH_MERGE_MODE, + SWITCH_SPLIT_MODE: keyMap.SWITCH_SPLIT_MODE, + SWITCH_GROUP_MODE: keyMap.SWITCH_GROUP_MODE, + RESET_GROUP: keyMap.RESET_GROUP, + }; + } + return ( diff --git a/cvat-ui/src/utils/hooks.ts b/cvat-ui/src/utils/hooks.ts new file mode 100644 index 000000000000..d61fdb07dba5 --- /dev/null +++ b/cvat-ui/src/utils/hooks.ts @@ -0,0 +1,13 @@ +// Copyright (C) 2021 Intel Corporation +// +// SPDX-License-Identifier: MIT +import { useRef, useEffect } from 'react'; + +// eslint-disable-next-line import/prefer-default-export +export function usePrevious(value: any): any { + const ref = useRef(); + useEffect(() => { + ref.current = value; + }); + return ref.current; +} From c38f3f7070c75514643d7e3a8f7cb0dba2560a09 Mon Sep 17 00:00:00 2001 From: Dmitry Kalinin Date: Thu, 18 Mar 2021 14:20:37 +0300 Subject: [PATCH 11/11] Added generic usage to the hook --- cvat-ui/src/utils/hooks.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/cvat-ui/src/utils/hooks.ts b/cvat-ui/src/utils/hooks.ts index d61fdb07dba5..74b58a1d233f 100644 --- a/cvat-ui/src/utils/hooks.ts +++ b/cvat-ui/src/utils/hooks.ts @@ -4,8 +4,8 @@ import { useRef, useEffect } from 'react'; // eslint-disable-next-line import/prefer-default-export -export function usePrevious(value: any): any { - const ref = useRef(); +export function usePrevious(value: T): T | undefined { + const ref = useRef(); useEffect(() => { ref.current = value; });