From 806ec8c18513b1d091ab77343e57e25ee6cd71ef Mon Sep 17 00:00:00 2001 From: Dmitry Kalinin Date: Tue, 29 Sep 2020 01:40:31 +0300 Subject: [PATCH 01/55] init server changes --- cvat/apps/engine/admin.py | 17 ++- .../migrations/0031_projects_adjastment.py | 32 +++++ cvat/apps/engine/models.py | 8 +- cvat/apps/engine/serializers.py | 117 ++++++++++++------ 4 files changed, 133 insertions(+), 41 deletions(-) create mode 100644 cvat/apps/engine/migrations/0031_projects_adjastment.py diff --git a/cvat/apps/engine/admin.py b/cvat/apps/engine/admin.py index 7336db03949b..ddacf69ab027 100644 --- a/cvat/apps/engine/admin.py +++ b/cvat/apps/engine/admin.py @@ -4,7 +4,7 @@ # SPDX-License-Identifier: MIT from django.contrib import admin -from .models import Task, Segment, Job, Label, AttributeSpec +from .models import Task, Segment, Job, Label, AttributeSpec, Project class JobInline(admin.TabularInline): model = Job @@ -54,6 +54,20 @@ def has_module_permission(self, request): JobInline ] +class ProjectAdmin(admin.ModelAdmin): + date_hierarchy = 'updated_date' + readonly_fields = ('created_date', 'updated_date', 'status') + fields = ('name', 'owner', 'created_date', 'updated_date', 'status') + search_fields = ('name', 'owner__username', 'owner__first_name', + 'owner__last_name', 'owner__email', 'assignee__username', 'assignee__first_name', + 'assignee__last_name') + inlines = [ + LabelInline + ] + + def has_add_permission(self, _request): + return False + class TaskAdmin(admin.ModelAdmin): date_hierarchy = 'updated_date' readonly_fields = ('created_date', 'updated_date', 'overlap') @@ -74,3 +88,4 @@ def has_add_permission(self, request): admin.site.register(Task, TaskAdmin) admin.site.register(Segment, SegmentAdmin) admin.site.register(Label, LabelAdmin) +admin.site.register(Project, ProjectAdmin) diff --git a/cvat/apps/engine/migrations/0031_projects_adjastment.py b/cvat/apps/engine/migrations/0031_projects_adjastment.py new file mode 100644 index 000000000000..9e89d845f104 --- /dev/null +++ b/cvat/apps/engine/migrations/0031_projects_adjastment.py @@ -0,0 +1,32 @@ +# Generated by Django 3.1.1 on 2020-09-24 12:44 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('engine', '0030_auto_20200914_1331'), + ] + + operations = [ + migrations.RemoveField( + model_name='project', + name='assignee', + ), + migrations.RemoveField( + model_name='project', + name='bug_tracker', + ), + migrations.AddField( + model_name='label', + name='project', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='engine.project'), + ), + migrations.AlterField( + model_name='label', + name='task', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='engine.task'), + ), + ] diff --git a/cvat/apps/engine/models.py b/cvat/apps/engine/models.py index 152fa4fa7d99..ad2306b0383f 100644 --- a/cvat/apps/engine/models.py +++ b/cvat/apps/engine/models.py @@ -143,9 +143,6 @@ class Project(models.Model): name = SafeCharField(max_length=256) owner = models.ForeignKey(User, null=True, blank=True, on_delete=models.SET_NULL, related_name="+") - assignee = models.ForeignKey(User, null=True, blank=True, - on_delete=models.SET_NULL, related_name="+") - bug_tracker = models.CharField(max_length=2000, blank=True, default="") created_date = models.DateTimeField(auto_now_add=True) updated_date = models.DateTimeField(auto_now_add=True) status = models.CharField(max_length=32, choices=StatusChoice.choices(), @@ -256,7 +253,8 @@ class Meta: default_permissions = () class Label(models.Model): - task = models.ForeignKey(Task, on_delete=models.CASCADE) + task = models.ForeignKey(Task, null=True, blank=True, on_delete=models.CASCADE) + project = models.ForeignKey(Project, null=True, blank=True, on_delete=models.CASCADE) name = SafeCharField(max_length=64) color = models.CharField(default='', max_length=8) @@ -312,7 +310,7 @@ class ShapeType(str, Enum): POLYGON = 'polygon' # (x0, y0, ..., xn, yn) POLYLINE = 'polyline' # (x0, y0, ..., xn, yn) POINTS = 'points' # (x0, y0, ..., xn, yn) - CUBOID = 'cuboid' + CUBOID = 'cuboid' # (x0, y0, ..., x7, y7) @classmethod def choices(cls): diff --git a/cvat/apps/engine/serializers.py b/cvat/apps/engine/serializers.py index bf31da29b1ea..1edbafd35fe1 100644 --- a/cvat/apps/engine/serializers.py +++ b/cvat/apps/engine/serializers.py @@ -44,6 +44,47 @@ class Meta: model = models.Label fields = ('id', 'name', 'color', 'attributes') + @staticmethod + def update_instance(validated_data, parent_instance): + attributes = validated_data.pop('attributespec_set', []) + instance = dict() + if isinstace(parent_instance. models.Project): + instance['project'] = parent_instance + logger = sLogger.project[parent_instace.id] + else: + instance['task'] = parent_instance + logger = sLogger.task[parent_instace.id] + (db_label, created) = models.Label.objects.get_or_create(name=label['name'], + **instance) + if created: + logger.info("New {} label was created".format(db_label.name)) + else: + logger.info("{} label was updated".format(db_label.name)) + if not label.get('color', None): + label_names = [l.name for l in + models.Label.objects.filter(task_id=instance.id).exclude(id=db_label.id).order_by('id') + ] + db_label.color = get_label_color(db_label.name, label_names) + else: + db_label.color = label.get('color', db_label.color) + db_label.save() + for attr in attributes: + (db_attr, created) = models.AttributeSpec.objects.get_or_create( + label=db_label, name=attr['name'], defaults=attr) + if created: + logger.info("New {} attribute for {} label was created" + .format(db_attr.name, db_label.name)) + else: + logger.info("{} attribute for {} label was updated" + .format(db_attr.name, db_label.name)) + + # FIXME: need to update only "safe" fields + db_attr.default_value = attr.get('default_value', db_attr.default_value) + db_attr.mutable = attr.get('mutable', db_attr.mutable) + db_attr.input_type = attr.get('input_type', db_attr.input_type) + db_attr.values = attr.get('values', db_attr.values) + db_attr.save() + class JobCommitSerializer(serializers.ModelSerializer): class Meta: model = models.JobCommit @@ -286,39 +327,7 @@ def update(self, instance, validated_data): instance.project = validated_data.get('project', instance.project) labels = validated_data.get('label_set', []) for label in labels: - attributes = label.pop('attributespec_set', []) - (db_label, created) = models.Label.objects.get_or_create(task=instance, - name=label['name']) - if created: - slogger.task[instance.id].info("New {} label was created" - .format(db_label.name)) - else: - slogger.task[instance.id].info("{} label was updated" - .format(db_label.name)) - if not label.get('color', None): - label_names = [l.name for l in - models.Label.objects.filter(task_id=instance.id).exclude(id=db_label.id).order_by('id') - ] - db_label.color = get_label_color(db_label.name, label_names) - else: - db_label.color = label.get('color', db_label.color) - db_label.save() - for attr in attributes: - (db_attr, created) = models.AttributeSpec.objects.get_or_create( - label=db_label, name=attr['name'], defaults=attr) - if created: - slogger.task[instance.id].info("New {} attribute for {} label was created" - .format(db_attr.name, db_label.name)) - else: - slogger.task[instance.id].info("{} attribute for {} label was updated" - .format(db_attr.name, db_label.name)) - - # FIXME: need to update only "safe" fields - db_attr.default_value = attr.get('default_value', db_attr.default_value) - db_attr.mutable = attr.get('mutable', db_attr.mutable) - db_attr.input_type = attr.get('input_type', db_attr.input_type) - db_attr.values = attr.get('values', db_attr.values) - db_attr.save() + LabelSerializer.update_instance(label, instance) instance.save() return instance @@ -333,13 +342,51 @@ def validate_labels(self, value): class ProjectSerializer(serializers.ModelSerializer): + labels = LabelSerializer(many=True, source='label_set', partial=True) + tasks = TaskSerializer(many=True, source='task_set', read_only=True) class Meta: model = models.Project - fields = ('url', 'id', 'name', 'owner', 'assignee', 'bug_tracker', - 'created_date', 'updated_date', 'status') + fields = ('url', 'id', 'name', 'labels', 'owner', + 'created_date', 'updated_date', 'status', 'tasks') read_only_fields = ('created_date', 'updated_date', 'status') ordering = ['-id'] + # pylint: disable=no-self-use + def create(self, validated_data): + labels = validated_data.pop('label_set') + db_project = models.Project.objects.create(**validated_data) + label_names = list() + for label in labels: + attributes = label.pop('attributespec_set') + if not label.get('color', None): + label['color'] = get_label_color(label['name'], label_names) + label_names.append(label['name']) + db_label = models.Label.objects.create(prject=db_project, **label) + for attr in attributes: + models.AttributeSpec.objects.create(label=db_label, **attr) + + db_project.save() + return db_project + + # pylint: disable=no-self-use + def update(self, instance, validated_data): + instance.name = validated_data.get('name', instance.name) + instance.owner = validated_data.get('owner', instance.owner) + labels = validated_data.get('label_set', []) + for label in labels: + LabelSerializer.update_instance(label, instance) + + instance.save() + return instance + + + def validate_labels(self, value): + if value: + label_names = [label['name'] for label in value] + if len(label_names) != len(set(label_names)): + raise serializers.ValidationError('All label names must be unique for the project') + return value + class BasicUserSerializer(serializers.ModelSerializer): def validate(self, data): if hasattr(self, 'initial_data'): From 58dfeb5528e2836483b865c7597c1e23d275b4b7 Mon Sep 17 00:00:00 2001 From: Dmitry Kalinin Date: Tue, 29 Sep 2020 01:43:52 +0300 Subject: [PATCH 02/55] init cvat-core implementation --- cvat-core/src/annotations-objects.js | 9 +- cvat-core/src/api-implementation.js | 93 +++++++++-- cvat-core/src/api.js | 45 +++++- cvat-core/src/enums.js | 6 +- cvat-core/src/server-proxy.js | 80 +++++++++- cvat-core/src/session.js | 228 ++++++++++++++++++++++++++- cvat-core/src/user.js | 2 +- 7 files changed, 436 insertions(+), 27 deletions(-) diff --git a/cvat-core/src/annotations-objects.js b/cvat-core/src/annotations-objects.js index 752670fee1cc..25f716c372c6 100644 --- a/cvat-core/src/annotations-objects.js +++ b/cvat-core/src/annotations-objects.js @@ -932,7 +932,14 @@ }, [this.clientID], frame); } - _appendShapeActionToHistory(actionType, frame, undoShape, redoShape, undoSource, redoSource) { + _appendShapeActionToHistory( + actionType, + frame, + undoShape, + redoShape, + undoSource, + redoSource, + ) { this.history.do(actionType, () => { if (!undoShape) { delete this.shapes[frame]; diff --git a/cvat-core/src/api-implementation.js b/cvat-core/src/api-implementation.js index 39c0d9112cb0..bae047a3a30c 100644 --- a/cvat-core/src/api-implementation.js +++ b/cvat-core/src/api-implementation.js @@ -26,14 +26,20 @@ const User = require('./user'); const { AnnotationFormats } = require('./annotation-formats.js'); const { ArgumentError } = require('./exceptions'); - const { Task } = require('./session'); + const { Task, Project } = require('./session'); - function attachUsers(task, users) { - if (task.assignee !== null) { - [task.assignee] = users.filter((user) => user.id === task.assignee); + function attachUsers(instance, users, instanceType) { + if (instance.owner !== null) { + [instance.owner] = users.filter((user) => user.id === instance.owner); } - for (const segment of task.segments) { + if (instanceType === 'project') return instance; + + if (instance.assignee !== null) { + [instance.assignee] = users.filter((user) => user.id === instance.assignee); + } + + for (const segment of instance.segments) { for (const job of segment.jobs) { if (job.assignee !== null) { [job.assignee] = users.filter((user) => user.id === job.assignee); @@ -41,11 +47,8 @@ } } - if (task.owner !== null) { - [task.owner] = users.filter((user) => user.id === task.owner); - } - return task; + return instance; } function implementAPI(cvat) { @@ -95,7 +98,11 @@ await serverProxy.server.logout(); }; - cvat.server.changePassword.implementation = async (oldPassword, newPassword1, newPassword2) => { + cvat.server.changePassword.implementation = async ( + oldPassword, + newPassword1, + newPassword2, + ) => { await serverProxy.server.changePassword(oldPassword, newPassword1, newPassword2); }; @@ -103,7 +110,12 @@ await serverProxy.server.requestPasswordReset(email); }; - cvat.server.resetPassword.implementation = async(newPassword1, newPassword2, uid, token) => { + cvat.server.resetPassword.implementation = async ( + newPassword1, + newPassword2, + uid, + token, + ) => { await serverProxy.server.resetPassword(newPassword1, newPassword2, uid, token); }; @@ -166,7 +178,7 @@ if (tasks !== null && tasks.length) { const users = (await serverProxy.users.getUsers()) .map((userData) => new User(userData)); - const task = new Task(attachUsers(tasks[0], users)); + const task = new Task(attachUsers(tasks[0], users, 'task')); return filter.jobID ? task.jobs .filter((job) => job.id === filter.jobID) : task.jobs; @@ -178,6 +190,7 @@ cvat.tasks.get.implementation = async (filter) => { checkFilter(filter, { page: isInteger, + projectId: isInteger, name: isString, id: isInteger, owner: isString, @@ -203,8 +216,14 @@ } } + if ('projectId' in filter && Object.keys(filter).length > 1) { + throw new ArgumentError( + 'Do not use the filter field "projectId" with other', + ); + } + const searchParams = new URLSearchParams(); - for (const field of ['name', 'owner', 'assignee', 'search', 'status', 'mode', 'id', 'page']) { + for (const field of ['name', 'owner', 'assignee', 'search', 'status', 'mode', 'id', 'page', 'projectId']) { if (Object.prototype.hasOwnProperty.call(filter, field)) { searchParams.set(field, filter[field]); } @@ -214,7 +233,7 @@ .map((userData) => new User(userData)); const tasksData = await serverProxy.tasks.getTasks(searchParams.toString()); const tasks = tasksData - .map((task) => attachUsers(task, users)) + .map((task) => attachUsers(task, users, 'task')) .map((task) => new Task(task)); @@ -223,6 +242,52 @@ return tasks; }; + cvat.projects.get.implementation = async (filter) => { + checkFilter(filter, { + id: isInteger, + page: isInteger, + name: isString, + owner: isString, + search: isString, + status: isEnum.bind(TaskStatus), + }); + + if ('search' in filter && Object.keys(filter).length > 1) { + if (!('page' in filter && Object.keys(filter).length === 2)) { + throw new ArgumentError( + 'Do not use the filter field "search" with others', + ); + } + } + + if ('id' in filter && Object.keys(filter).length > 1) { + if (!('page' in filter && Object.keys(filter).length === 2)) { + throw new ArgumentError( + 'Do not use the filter field "id" with others', + ); + } + } + + const searchParams = new URLSearchParams(); + // TODO: need to check search fields + for (const field of ['name', 'owner', 'search', 'status', 'id', 'page']) { + if (Object.prototype.hasOwnProperty.call(filter, field)) { + searchParams.set(field, filter[field]); + } + } + + const users = (await serverProxy.users.getUsers()) + .map((userData) => new User(userData)); + const projectsData = await serverProxy.projects.getProjects(searchParams.toString()); + const projects = projectsData + .map((project) => attachUsers(project, users, 'project')) + .map((project) => new Project(project)); + + projects.count = projectsData.count; + + return projects; + }; + return cvat; } diff --git a/cvat-core/src/api.js b/cvat-core/src/api.js index 1b70911839b2..5997647f8a41 100644 --- a/cvat-core/src/api.js +++ b/cvat-core/src/api.js @@ -18,7 +18,7 @@ function build() { const Log = require('./log'); const ObjectState = require('./object-state'); const Statistics = require('./statistics'); - const { Job, Task } = require('./session'); + const { Job, Task, Project } = require('./session'); const { Attribute, Label } = require('./labels'); const MLModel = require('./ml-model'); @@ -207,7 +207,8 @@ function build() { */ async changePassword(oldPassword, newPassword1, newPassword2) { const result = await PluginRegistry - .apiWrapper(cvat.server.changePassword, oldPassword, newPassword1, newPassword2); + .apiWrapper(cvat.server.changePassword, oldPassword, newPassword1, + newPassword2); return result; }, /** @@ -273,6 +274,42 @@ function build() { return result; }, }, + /** + * Namespace is used for getting projects + * @namespace projects + * @memberof module:API.cvat + */ + projects: { + /** + * @typedef {Object} ProjectFilter + * @property {string} name Check if name contains this value + * @property {module:API.cvat.enums.ProjectStatus} status + * Check if status contains this value + * @property {integer} id Check if id equals this value + * @property {integer} page Get specific page + * (default REST API returns 20 projects per request. + * In order to get more, it is need to specify next page) + * @property {string} owner Check if owner user contains this value + * @property {string} search Combined search of contains among all fields + * @global + */ + + /** + * Method returns list of projects corresponding to a filter + * @method get + * @async + * @memberof module:API.cvat.projects + * @param {ProjectFilter} [filter={}] project filter + * @returns {module:API.cvat.classes.Project[]} + * @throws {module:API.cvat.exceptions.PluginError} + * @throws {module:API.cvat.exceptions.ServerError} + */ + async get(filter = {}) { + const result = await PluginRegistry + .apiWrapper(cvat.projects.get, filter); + return result; + }, + }, /** * Namespace is used for getting tasks * @namespace tasks @@ -726,8 +763,9 @@ function build() { * @memberof module:API.cvat */ classes: { - Task, User, + Project, + Task, Job, Log, Attribute, @@ -739,6 +777,7 @@ function build() { }; cvat.server = Object.freeze(cvat.server); + cvat.projects = Object.freeze(cvat.projects); cvat.tasks = Object.freeze(cvat.tasks); cvat.jobs = Object.freeze(cvat.jobs); cvat.users = Object.freeze(cvat.users); diff --git a/cvat-core/src/enums.js b/cvat-core/src/enums.js index c7b6c5952fcb..91420f982c02 100644 --- a/cvat-core/src/enums.js +++ b/cvat-core/src/enums.js @@ -46,7 +46,7 @@ * @property {string} UNKNOWN 'unknown' * @readonly */ - const RQStatus = Object.freeze({ + const RQStatus = Object.freeze({ QUEUED: 'queued', STARTED: 'started', FINISHED: 'finished', @@ -134,8 +134,8 @@ * @readonly */ const Source = Object.freeze({ - MANUAL:'manual', - AUTO:'auto', + MANUAL: 'manual', + AUTO: 'auto', }); /** diff --git a/cvat-core/src/server-proxy.js b/cvat-core/src/server-proxy.js index 58724d35c092..4fa425f07ad0 100644 --- a/cvat-core/src/server-proxy.js +++ b/cvat-core/src/server-proxy.js @@ -251,7 +251,7 @@ const data = JSON.stringify({ old_password: oldPassword, new_password1: newPassword1, - new_password2:newPassword2, + new_password2: newPassword2, }); await Axios.post(`${config.backendAPI}/auth/password/change`, data, { proxy: config.proxy, @@ -280,13 +280,13 @@ } } - async function resetPassword(newPassword1, newPassword2, uid, token) { + async function resetPassword(newPassword1, newPassword2, uid, tokenString) { try { const data = JSON.stringify({ new_password1: newPassword1, new_password2: newPassword2, uid, - token, + tokenString, }); await Axios.post(`${config.backendAPI}/auth/password/reset/confirm`, data, { proxy: config.proxy, @@ -324,6 +324,63 @@ } } + async function getProjects(filter = '') { + const { backendAPI, proxy } = config; + + let response = null; + try { + response = await Axios.get(`${backendAPI}/projects?page_size=12&${filter}`, { + proxy, + }); + } catch (errorData) { + throw generateError(errorData); + } + + response.data.results.count = response.data.count; + return response.data.results; + } + + async function saveProject(id, projectData) { + const { backendAPI } = config; + + try { + await Axios.patch(`${backendAPI}/projects/${id}`, JSON.stringify(projectData), { + proxy: config.proxy, + headers: { + 'Content-Type': 'application/json', + }, + }); + } catch (errorData) { + throw generateError(errorData); + } + } + + async function deleteProject(id) { + const { backendAPI } = config; + + try { + await Axios.delete(`${backendAPI}/projects/${id}`); + } catch (errorData) { + throw generateError(errorData); + } + } + + async function createProject(projectSpec) { + const { backendAPI } = config; + + try { + const response = await Axios.post(`${backendAPI}/projects`, JSON.stringify(projectSpec), { + proxy: config.proxy, + headers: { + 'Content-Type': 'application/json', + }, + }); + return response.data; + } catch (errorData) { + throw generateError(errorData); + } + } + async function getTasks(filter = '') { const { backendAPI } = config; @@ -359,7 +416,12 @@ const { backendAPI } = config; try { - await Axios.delete(`${backendAPI}/tasks/${id}`); + await Axios.delete(`${backendAPI}/tasks/${id}`, { + proxy: config.proxy, + headers: { + 'Content-Type': 'application/json', + }, + }); } catch (errorData) { throw generateError(errorData); } @@ -832,6 +894,16 @@ writable: false, }, + projects: { + value: Object.freeze({ + getProjects, + saveProject, + createProject, + deleteProject, + }), + writable: false, + }, + tasks: { value: Object.freeze({ getTasks, diff --git a/cvat-core/src/session.js b/cvat-core/src/session.js index 8d0287258718..e8b719bc1eee 100644 --- a/cvat-core/src/session.js +++ b/cvat-core/src/session.js @@ -1400,9 +1400,210 @@ } } + /** + * Class representing a project + * @memberof module:API.cvat.classes + * @extends Session + */ + class Project extends Session { + /** + * In a fact you need use the constructor only if you want to create a project + * @param {object} initialData - Object which is used for initalization + *
It can contain keys: + *
  • name + *
  • labels + */ + constructor(initialData) { + super(); + const data = { + id: undefined, + name: undefined, + status: undefined, + owner: undefined, + created_date: undefined, + updated_date: undefined, + }; + + for (const property in data) { + if (Object.prototype.hasOwnProperty.call(data, property) + && property in initialData) { + data[property] = initialData[property]; + } + } + + data.labels = []; + data.tasks = []; + + if (Array.isArray(initialData.labels)) { + for (const label of initialData.labels) { + const classInstance = new Label(label); + data.labels.push(classInstance); + } + } + + if (Array.isArray(initialData.tasks)) { + for (const task of initialData.tasks) { + const taskInstance = new Task(task); + data.tasks.push(taskInstance); + } + } + + Object.defineProperties(this, Object.freeze({ + /** + * @name id + * @type {integer} + * @memberof module:API.cvat.classes.Project + * @readonly + * @instance + */ + id: { + get: () => data.id, + }, + /** + * @name name + * @type {string} + * @memberof module:API.cvat.classes.Project + * @instance + * @throws {module:API.cvat.exceptions.ArgumentError} + */ + name: { + get: () => data.name, + set: (value) => { + if (!value.trim().length) { + throw new ArgumentError( + 'Value must not be empty', + ); + } + data.name = value; + }, + }, + /** + * @name status + * @type {module:API.cvat.enums.TaskStatus} + * @memberof module:API.cvat.classes.Project + * @readonly + * @instance + */ + status: { + get: () => data.status, + }, + /** + * Instance of a user who has created the task + * @name owner + * @type {module:API.cvat.classes.User} + * @memberof module:API.cvat.classes.Project + * @readonly + * @instance + */ + owner: { + get: () => data.owner, + }, + /** + * @name createdDate + * @type {string} + * @memberof module:API.cvat.classes.Task + * @readonly + * @instance + */ + createdDate: { + get: () => data.created_date, + }, + /** + * @name updatedDate + * @type {string} + * @memberof module:API.cvat.classes.Task + * @readonly + * @instance + */ + updatedDate: { + get: () => data.updated_date, + }, + /** + * After project has been created value can be appended only. + * @name labels + * @type {module:API.cvat.classes.Label[]} + * @memberof module:API.cvat.classes.Project + * @instance + * @throws {module:API.cvat.exceptions.ArgumentError} + */ + labels: { + get: () => [...data.labels], + set: (labels) => { + if (!Array.isArray(labels)) { + throw new ArgumentError( + 'Value must be an array of Labels', + ); + } + + for (const label of labels) { + if (!(label instanceof Label)) { + throw new ArgumentError( + 'Each array value must be an instance of Label. ' + + `${typeof (label)} was found`, + ); + } + } + + data.labels = [...labels]; + }, + }, + /** + * Tasks linked with the project + * @name tasks + * @type {module:API.cvat.classes.Task[]} + * @memberof module:API.cvat.classes.Project + * @readonly + * @instance + */ + tasks: { + get: () => [...data.tasks], + }, + // TODO: Do we need logger here + })); + } + + /** + * Method updates data of a created project or creates new project from scratch + * @method save + * @returns {module:API.cvat.classes.Project} + * @memberof module:API.cvat.classes.Project + * @param {function} [onUpdate] - the function which is used only if project hasn't + * been created yet. It called in order to notify about creation status. + * It receives the string parameter which is a status message + * @readonly + * @instance + * @async + * @throws {module:API.cvat.exceptions.ServerError} + * @throws {module:API.cvat.exceptions.PluginError} + */ + async save(onUpdate = () => {}) { + const result = await PluginRegistry + .apiWrapper.call(this, Project.prototype.save, onUpdate); + return result; + } + + /** + * Method deletes a task from a server + * @method delete + * @memberof module:API.cvat.classes.Project + * @readonly + * @instance + * @async + * @throws {module:API.cvat.exceptions.ServerError} + * @throws {module:API.cvat.exceptions.PluginError} + */ + async delete() { + const result = await PluginRegistry + .apiWrapper.call(this, Project.prototype.delete); + return result; + } + } + + module.exports = { Job, Task, + Project, }; const { @@ -1647,7 +1848,7 @@ return this; }; - Task.prototype.save.implementation = async function saveTaskImplementation(onUpdate) { + Task.prototype.save.implementation = async function (onUpdate) { // TODO: Add ability to change an owner and an assignee if (typeof (this.id) !== 'undefined') { // If the task has been already created, we update it @@ -1906,4 +2107,29 @@ const result = await loggerStorage.log(logType, { ...payload, task_id: this.id }, wait); return result; }; + + Project.prototype.save.implementation = async function () { + if (typeof (this.id) !== 'undefined') { + const projectData = { + name: this.name, + labels: [...this.labels.map((el) => el.toJSON())], + }; + + await serverProxy.projects.saveProject(this.id, projectData); + return this; + } + + const projectSpec = { + name: this.name, + labels: [...this.labels.map((el) => el.toJSON())], + }; + + const project = await serverProxy.projects.createProject(projectSpec); + return new Project(project); + }; + + Project.prototype.delete.implementation = async function () { + const result = await serverProxy.projects.deleteProject(this.id); + return result; + }; })(); diff --git a/cvat-core/src/user.js b/cvat-core/src/user.js index 555ea83d2a76..f122d20b589d 100644 --- a/cvat-core/src/user.js +++ b/cvat-core/src/user.js @@ -152,7 +152,7 @@ * @readonly * @instance */ - get: () => !data.email_verification_required, + get: () => !data.email_verification_required, }, })); } From e1672f9347d99594148061b1db2c814c92f300ad Mon Sep 17 00:00:00 2001 From: Dmitry Kalinin Date: Tue, 29 Sep 2020 01:46:25 +0300 Subject: [PATCH 03/55] Added projects page in cvat-ui --- cvat-ui/src/actions/projects-actions.ts | 192 ++++++++++++++++++ .../create-project-content.tsx | 129 ++++++++++++ .../create-project-page.tsx | 21 ++ .../create-project-page/styles.scss | 38 ++++ .../create-task-page/create-task-content.tsx | 4 +- cvat-ui/src/components/cvat-app.tsx | 8 +- cvat-ui/src/components/header/header.tsx | 14 ++ .../components/project-page/project-page.tsx | 74 +++++++ .../src/components/project-page/styles.scss | 0 .../components/projects-page/actions-menu.tsx | 43 ++++ .../components/projects-page/empty-list.tsx | 53 +++++ .../components/projects-page/project-item.tsx | 91 +++++++++ .../components/projects-page/project-list.tsx | 59 ++++++ .../projects-page/projects-page.tsx | 54 +++++ .../src/components/projects-page/styles.scss | 89 ++++++++ .../src/components/projects-page/top-bar.tsx | 49 +++++ .../src/components/tasks-page/empty-list.tsx | 4 + cvat-ui/src/components/tasks-page/top-bar.tsx | 15 +- .../projects-page/projects-page.tsx | 47 +++++ cvat-ui/src/reducers/interfaces.ts | 38 ++++ cvat-ui/src/reducers/projects-reducer.ts | 165 +++++++++++++++ cvat-ui/src/reducers/root-reducer.ts | 2 + 22 files changed, 1178 insertions(+), 11 deletions(-) create mode 100644 cvat-ui/src/actions/projects-actions.ts create mode 100644 cvat-ui/src/components/create-project-page/create-project-content.tsx create mode 100644 cvat-ui/src/components/create-project-page/create-project-page.tsx create mode 100644 cvat-ui/src/components/create-project-page/styles.scss create mode 100644 cvat-ui/src/components/project-page/project-page.tsx create mode 100644 cvat-ui/src/components/project-page/styles.scss create mode 100644 cvat-ui/src/components/projects-page/actions-menu.tsx create mode 100644 cvat-ui/src/components/projects-page/empty-list.tsx create mode 100644 cvat-ui/src/components/projects-page/project-item.tsx create mode 100644 cvat-ui/src/components/projects-page/project-list.tsx create mode 100644 cvat-ui/src/components/projects-page/projects-page.tsx create mode 100644 cvat-ui/src/components/projects-page/styles.scss create mode 100644 cvat-ui/src/components/projects-page/top-bar.tsx create mode 100644 cvat-ui/src/containers/projects-page/projects-page.tsx create mode 100644 cvat-ui/src/reducers/projects-reducer.ts diff --git a/cvat-ui/src/actions/projects-actions.ts b/cvat-ui/src/actions/projects-actions.ts new file mode 100644 index 000000000000..9b27bf7924e0 --- /dev/null +++ b/cvat-ui/src/actions/projects-actions.ts @@ -0,0 +1,192 @@ +// Copyright (C) 2020 Intel Corporation +// +// SPDX-License-Identifier: MIT + +import { AnyAction, Dispatch, ActionCreator } from 'redux'; +import { ThunkAction } from 'redux-thunk'; + +import { CombinedState, ProjectsQuery } from 'reducers/interfaces'; +import { getCVATStore } from 'cvat-store'; +import getCore from 'cvat-core-wrapper'; + +const cvat = getCore(); + +export enum ProjectsActionTypes { + UPDATE_PROJECTS_GETTING_QUERY = 'UPDATE_PROJECTS_GETTING_QUERY', + GET_PROJECTS = 'GET_PROJECTS', + GET_PROJECTS_SUCCESS = 'GET_PROJECTS_SUCCESS', + GET_PROJECTS_FAILED = 'GET_PROJECTS_FAILED', + CREATE_PROJECT = 'CREATE_PROJECT', + CREATE_PROJECT_SUCCESS = 'CREATE_PROJECT_SUCCESS', + CREATE_PROJECT_FAILED = 'CREATE_PROJECT_FAILED', + DELETE_PROJECT = 'DELETE_PROJECT', + DELETE_PROJECT_SUCCESS = 'DELETE_PROJECT_SUCCESS', + DELETE_PROJECT_FAILED = 'DELETE_PROJECT_FAILED', +} + +export function updateProjectsGettingQuery(query: Partial): AnyAction { + const action = { + type: ProjectsActionTypes.UPDATE_PROJECTS_GETTING_QUERY, + payload: { + query, + }, + }; + + return action; +} + +function getProjects(): AnyAction { + const action = { + type: ProjectsActionTypes.GET_PROJECTS, + payload: {}, + }; + + return action; +} + +function getProjectsSuccess(array: any[], count: number): AnyAction { + const action = { + type: ProjectsActionTypes.GET_PROJECTS_SUCCESS, + payload: { + array, + count, + }, + }; + + return action; +} + +function getProjectsFailed(error: any): AnyAction { + const action = { + type: ProjectsActionTypes.GET_PROJECTS_FAILED, + payload: { + error, + }, + }; + + return action; +} + +export function getProjectsAsync(): +ThunkAction, {}, {}, AnyAction> { + return async (dispatch: ActionCreator): Promise => { + const { + projects: { + gettingQuery, + }, + } = getCVATStore().getState() as CombinedState; + + dispatch(getProjects()); + + // Clear query object from null fields + const filteredQuery = { ...gettingQuery }; + for (const key in filteredQuery) { + if (filteredQuery[key] === null) { + delete filteredQuery[key]; + } + } + + let result = null; + try { + result = await cvat.projects.get(filteredQuery); + } catch (error) { + dispatch(getProjectsFailed(error)); + return; + } + + const array = Array.from(result); + dispatch(getProjectsSuccess(array, result.count)); + }; +} + +function createProject(): AnyAction { + const action = { + type: ProjectsActionTypes.CREATE_PROJECT, + payload: {}, + }; + + return action; +} + +function createProjectSuccess(projectId: number): AnyAction { + const action = { + type: ProjectsActionTypes.CREATE_PROJECT_SUCCESS, + payload: { + projectId, + }, + }; + + return action; +} + +function createProjectFailed(error: any): AnyAction { + const action = { + type: ProjectsActionTypes.CREATE_PROJECT_FAILED, + payload: { + error, + }, + }; + + return action; +} + +export function createProjectAsync(data: any): +ThunkAction, {}, {}, AnyAction> { + return async (dispatch: ActionCreator): Promise => { + const projectInstance = new cvat.classes.Project(data); + + dispatch(createProject()); + try { + const savedProject = await projectInstance.save(); + dispatch(createProjectSuccess(savedProject.id)); + } catch (error) { + // FIXME: error length + dispatch(createProjectFailed(error)); + } + }; +} + +function deleteProject(projectId: number): AnyAction { + const action = { + type: ProjectsActionTypes.DELETE_PROJECT, + payload: { + projectId, + }, + }; + return action; +} + +function deleteProjectSuccess(projectId: number): AnyAction { + const action = { + type: ProjectsActionTypes.DELETE_PROJECT_SUCCESS, + payload: { + projectId, + }, + }; + return action; +} + +function deleteProjectFailed(projectId: number, error: any): AnyAction { + const action = { + type: ProjectsActionTypes.DELETE_PROJECT_FAILED, + payload: { + projectId, + error, + }, + }; + return action; +} + +export function deleteProjectAsync(projectInstance: any): +ThunkAction, {}, {}, AnyAction> { + return async (dispatch: ActionCreator): Promise => { + dispatch(deleteProject(projectInstance.id)); + try { + await projectInstance.delete(); + dispatch(deleteProjectSuccess(projectInstance.id)); + } catch (error) { + // FIXME: error length + dispatch(deleteProjectFailed(projectInstance.id, error)); + } + }; +} diff --git a/cvat-ui/src/components/create-project-page/create-project-content.tsx b/cvat-ui/src/components/create-project-page/create-project-content.tsx new file mode 100644 index 000000000000..2aa1c9baed93 --- /dev/null +++ b/cvat-ui/src/components/create-project-page/create-project-content.tsx @@ -0,0 +1,129 @@ +// Copyright (C) 2020 Intel Corporation +// +// SPDX-License-Identifier: MIT + +import React, { useState, useRef, useEffect } from 'react'; +import { useDispatch, useSelector } from 'react-redux'; +import { useHistory } from 'react-router'; +import { Col, Row } from 'antd/lib/grid'; +import Text from 'antd/lib/typography/Text'; +import Form, { FormComponentProps, WrappedFormUtils } from 'antd/lib/form/Form'; +import Button from 'antd/lib/button'; +import Input from 'antd/lib/input'; +import notification from 'antd/lib/notification'; + +import { CombinedState } from 'reducers/interfaces'; +import LabelsEditor from 'components/labels-editor/labels-editor'; +import { createProjectAsync } from 'actions/projects-actions'; + +const ProjectNameEditor = Form.create()( + (props: FormComponentProps): JSX.Element => { + const { form } = props; + const { getFieldDecorator } = form; + + return ( +
    e.preventDefault()} + > + Name}> + {getFieldDecorator('name', { + rules: [{ + required: true, + message: 'Please, specify a name', + }], + })( + , + )} + +
    + ); + }, +); + +export default function CreateProjectContent(): JSX.Element { + const [projectLabels, setProjectLabels] = useState([]); + const nameFormRef = useRef(null); + const dispatch = useDispatch(); + const history = useHistory(); + + const newProjectId = useSelector((state: CombinedState) => state.projects.creates.id); + const createProjectError = useSelector((state: CombinedState) => state.projects.creates.error); + + useEffect(() => { + if (Number.isInteger(newProjectId)) { + const btn = ( + + ); + + // Clear new project form + if (nameFormRef.current) nameFormRef.current.resetFields(); + setProjectLabels([]); + + notification.info({ + message: 'The task has been created', + btn, + }); + } + }, [newProjectId]); + + useEffect(() => { + if (createProjectError) { + notification.error({ + message: 'Could not create a task', + description: createProjectError, + }); + } + }, [createProjectError]); + + const onSumbit = (): void => { + let projectName = ''; + if (nameFormRef.current !== null) { + nameFormRef.current.validateFields((error, value) => { + if (!error) { + projectName = value.name; + } + }); + } + + if (!projectName) return; + + dispatch(createProjectAsync({ + name: projectName, + labels: projectLabels, + })); + }; + + return ( + + + + + + * + Labels: + { + setProjectLabels(newLabels); + } + } + /> + + + + + + ); +} diff --git a/cvat-ui/src/components/create-project-page/create-project-page.tsx b/cvat-ui/src/components/create-project-page/create-project-page.tsx new file mode 100644 index 000000000000..5072edde5869 --- /dev/null +++ b/cvat-ui/src/components/create-project-page/create-project-page.tsx @@ -0,0 +1,21 @@ +// Copyright (C) 2020 Intel Corporation +// +// SPDX-License-Identifier: MIT + +import './styles.scss'; +import React from 'react'; +import { Row, Col } from 'antd/lib/grid'; +import Text from 'antd/lib/typography/Text'; + +import CreateProjectContent from './create-project-content'; + +export default function CreateProjectPageComponent(): JSX.Element { + return ( + + + Create a new project + + + + ); +} diff --git a/cvat-ui/src/components/create-project-page/styles.scss b/cvat-ui/src/components/create-project-page/styles.scss new file mode 100644 index 000000000000..6cb9222b8b09 --- /dev/null +++ b/cvat-ui/src/components/create-project-page/styles.scss @@ -0,0 +1,38 @@ +// Copyright (C) 2020 Intel Corporation +// +// SPDX-License-Identifier: MIT + +@import '../../base.scss'; + +.cvat-create-project-form-wrapper { + text-align: center; + padding-top: 40px; + overflow-y: auto; + height: 90%; + position: fixed; + width: 100%; + + > div > span { + font-size: 36px; + } + + .cvat-create-project-content { + margin-top: 20px; + width: 100%; + height: auto; + border: 1px solid $border-color-1; + border-radius: 3px; + padding: 20px; + background: $background-color-1; + text-align: initial; + + > div:not(first-child) { + margin-top: 10px; + } + + > div:nth-child(7) > button { + float: right; + width: 120px; + } + } +} \ No newline at end of file diff --git a/cvat-ui/src/components/create-task-page/create-task-content.tsx b/cvat-ui/src/components/create-task-page/create-task-content.tsx index 3a1e311be97a..00fcb99e068f 100644 --- a/cvat-ui/src/components/create-task-page/create-task-content.tsx +++ b/cvat-ui/src/components/create-task-page/create-task-content.tsx @@ -13,10 +13,10 @@ import notification from 'antd/lib/notification'; import Text from 'antd/lib/typography/Text'; import ConnectedFileManager from 'containers/file-manager/file-manager'; +import LabelsEditor from 'components/labels-editor/labels-editor'; +import { Files } from 'components/file-manager/file-manager'; import BasicConfigurationForm, { BaseConfiguration } from './basic-configuration-form'; import AdvancedConfigurationForm, { AdvancedConfiguration } from './advanced-configuration-form'; -import LabelsEditor from '../labels-editor/labels-editor'; -import { Files } from '../file-manager/file-manager'; export interface CreateTaskData { basic: BaseConfiguration; diff --git a/cvat-ui/src/components/cvat-app.tsx b/cvat-ui/src/components/cvat-app.tsx index c32717c73dfa..3d745aafb15e 100644 --- a/cvat-ui/src/components/cvat-app.tsx +++ b/cvat-ui/src/components/cvat-app.tsx @@ -16,6 +16,9 @@ import notification from 'antd/lib/notification'; import GlobalErrorBoundary from 'components/global-error-boundary/global-error-boundary'; import ShorcutsDialog from 'components/shortcuts-dialog/shortcuts-dialog'; +import ProjectsPageComponent from 'components/projects-page/projects-page'; +import CreateProjectPageComponent from 'components/create-project-page/create-project-page'; +import ProjectPageComponent from 'components/project-page/project-page'; import TasksPageContainer from 'containers/tasks-page/tasks-page'; import CreateTaskPageContainer from 'containers/create-task-page/create-task-page'; import TaskPageContainer from 'containers/task-page/task-page'; @@ -312,12 +315,15 @@ class CVATApplication extends React.PureComponent + + + - + {/* eslint-disable-next-line */} diff --git a/cvat-ui/src/components/header/header.tsx b/cvat-ui/src/components/header/header.tsx index 379e7c70daa3..7b269f700e5a 100644 --- a/cvat-ui/src/components/header/header.tsx +++ b/cvat-ui/src/components/header/header.tsx @@ -262,6 +262,20 @@ function HeaderContainer(props: Props): JSX.Element {
    + + +
    + + )} + /> + + ); +} diff --git a/cvat-ui/src/components/projects-page/project-list.tsx b/cvat-ui/src/components/projects-page/project-list.tsx new file mode 100644 index 000000000000..c6f9ec5c32fe --- /dev/null +++ b/cvat-ui/src/components/projects-page/project-list.tsx @@ -0,0 +1,59 @@ +// Copyright (C) 2020 Intel Corporation +// +// SPDX-License-Identifier: MIT + +import React from 'react'; +import { useSelector } from 'react-redux'; +import { useHistory, useLocation } from 'react-router'; +import { Row, Col } from 'antd/lib/grid'; +import Pagination from 'antd/lib/pagination'; + +import { CombinedState } from 'reducers/interfaces'; +import ProjectItem from './project-item'; + +export default function ProjectListComponent(): JSX.Element { + const history = useHistory(); + const { search } = useLocation(); + const projectsCount = useSelector((state: CombinedState) => state.projects.count); + const { page } = useSelector((state: CombinedState) => state.projects.gettingQuery); + const projectInstances = useSelector((state: CombinedState) => state.projects.current); + + function changePage(p: number): void { + const URLparams = new URLSearchParams(search); + URLparams.set('page', p.toString()); + history.push({ + pathname: '/projects', + search: `?${URLparams.toString()}`, + }); + } + + return ( + <> + + + + {projectInstances.map( + (instance: any): JSX.Element => ( + + + + ), + )} + + + + + + + + + + ); +} diff --git a/cvat-ui/src/components/projects-page/projects-page.tsx b/cvat-ui/src/components/projects-page/projects-page.tsx new file mode 100644 index 000000000000..8c70b39f15c1 --- /dev/null +++ b/cvat-ui/src/components/projects-page/projects-page.tsx @@ -0,0 +1,54 @@ +// Copyright (C) 2020 Intel Corporation +// +// SPDX-License-Identifier: MIT + +import './styles.scss'; +import React, { useEffect } from 'react'; +import { useDispatch, useSelector } from 'react-redux'; +import { useLocation } from 'react-router'; +import Spin from 'antd/lib/spin'; + +import FeedbackComponent from 'components/feedback/feedback'; +import { CombinedState, ProjectsQuery } from 'reducers/interfaces'; +import { getProjectsAsync, updateProjectsGettingQuery } from 'actions/projects-actions'; +import EmptyListComponent from './empty-list'; +import TopBarComponent from './top-bar'; +import ProjectListComponent from './project-list'; + +export default function ProjectsPageComponent(): JSX.Element { + const { search } = useLocation(); + + const dispatch = useDispatch(); + const projectFetching = useSelector((state: CombinedState) => state.projects.fetching); + const projectsCount = useSelector((state: CombinedState) => state.projects.current.length); + + const anySearchQuery = !!Array.from(new URLSearchParams(search).keys()).filter((value) => value !== 'page').length; + + useEffect(() => { + const searchParams: Partial = {}; + for (const [param, value] of new URLSearchParams(search)) { + searchParams[param] = ['page', 'id'].includes(param) ? Number.parseInt(value, 10) : value; + } + dispatch(updateProjectsGettingQuery(searchParams)); + dispatch(getProjectsAsync()); + }, [search]); + + if (projectFetching) { + return ( + + ); + } + + return ( +
    + + { projectsCount + ? ( + + ) : ( + + )} + +
    + ); +} diff --git a/cvat-ui/src/components/projects-page/styles.scss b/cvat-ui/src/components/projects-page/styles.scss new file mode 100644 index 000000000000..61a409cb7224 --- /dev/null +++ b/cvat-ui/src/components/projects-page/styles.scss @@ -0,0 +1,89 @@ +// Copyright (C) 2020 Intel Corporation +// +// SPDX-License-Identifier: MIT + +@import '../../base.scss'; + +.cvat-projects-page { + padding-top: 15px; + padding-bottom: 40px; + height: 100%; + position: fixed; + width: 100%; + + + > div:nth-child(1) { + padding-bottom: 10px; + + div > { + span { + color: $text-color; + } + } + } +} + +/* empty-projects icon */ +.cvat-empty-projects-list { + > div:nth-child(1) { + margin-top: 50px; + } + + > div:nth-child(2) { + > div { + margin-top: 20px; + + /* No projects created yet */ + > span { + font-size: 20px; + color: $text-color; + } + } + } + + /* To get started with your annotation project .. */ + > div:nth-child(3) { + margin-top: 10px; + } +} + +.cvat-projects-top-bar { + > div:nth-child(1) { + display: flex; + + > span:nth-child(2) { + width: 200px; + margin-left: 10px; + } + } + + > div:nth-child(2) { + display: flex; + justify-content: flex-end; + } +} + +.cvat-create-project-button { + padding: 0 30px; +} + +.cvat-projects-pagination { + display: flex; + justify-content: center; +} + +.cvat-projects-project-item-title { + cursor: pointer; +} + +.cvat-porjects-project-item-description { + display: flex; + justify-content: space-between; + + // actions button + > div:nth-child(2) { + display: flex; + align-self: flex-end; + justify-content: center; + } +} \ No newline at end of file diff --git a/cvat-ui/src/components/projects-page/top-bar.tsx b/cvat-ui/src/components/projects-page/top-bar.tsx new file mode 100644 index 000000000000..968435d3b1ee --- /dev/null +++ b/cvat-ui/src/components/projects-page/top-bar.tsx @@ -0,0 +1,49 @@ +// Copyright (C) 2020 Intel Corporation +// +// SPDX-License-Identifier: MIT + +import React from 'react'; +import { useHistory } from 'react-router'; +import { Row, Col } from 'antd/lib/grid'; +import Button from 'antd/lib/button'; +import Input from 'antd/lib/input'; +import Text from 'antd/lib/typography/Text'; + + +export default function TopBarComponent(): JSX.Element { + const history = useHistory(); + + return ( + + + Projects + {}} + size='large' + placeholder='Search' + disabled + /> + + + + + + ); +} diff --git a/cvat-ui/src/components/tasks-page/empty-list.tsx b/cvat-ui/src/components/tasks-page/empty-list.tsx index eabd1f87ad8f..83c6783e22a0 100644 --- a/cvat-ui/src/components/tasks-page/empty-list.tsx +++ b/cvat-ui/src/components/tasks-page/empty-list.tsx @@ -31,6 +31,10 @@ export default function EmptyListComponent(): JSX.Element { create a new task +   + or try to +   + create a new project diff --git a/cvat-ui/src/components/tasks-page/top-bar.tsx b/cvat-ui/src/components/tasks-page/top-bar.tsx index 72216d73003a..67827b4d803d 100644 --- a/cvat-ui/src/components/tasks-page/top-bar.tsx +++ b/cvat-ui/src/components/tasks-page/top-bar.tsx @@ -3,8 +3,7 @@ // SPDX-License-Identifier: MIT import React from 'react'; -import { RouteComponentProps } from 'react-router'; -import { withRouter } from 'react-router-dom'; +import { useHistory } from 'react-router'; import { Row, Col } from 'antd/lib/grid'; import Button from 'antd/lib/button'; import Input from 'antd/lib/input'; @@ -15,20 +14,22 @@ interface VisibleTopBarProps { searchValue: string; } -function TopBarComponent(props: VisibleTopBarProps & RouteComponentProps): JSX.Element { +export default function TopBarComponent(props: VisibleTopBarProps): JSX.Element { const { searchValue, - history, onSearch, } = props; + const history = useHistory(); + return ( <> - + {/* Default project - + */} + Tasks @@ -61,5 +62,3 @@ function TopBarComponent(props: VisibleTopBarProps & RouteComponentProps): JSX.E ); } - -export default withRouter(TopBarComponent); diff --git a/cvat-ui/src/containers/projects-page/projects-page.tsx b/cvat-ui/src/containers/projects-page/projects-page.tsx new file mode 100644 index 000000000000..7ab9711e61a5 --- /dev/null +++ b/cvat-ui/src/containers/projects-page/projects-page.tsx @@ -0,0 +1,47 @@ +// Copyright (C) 2020 Intel Corporation +// +// SPDX-License-Identifier: MIT + +import { connect } from 'react-redux'; + +import { + ProjectsQuery, + CombinedState, +} from 'reducers/interfaces'; + +import ProjectsPageComponent from 'components/projects-page/projects-page'; + +import { getProjectsAsync } from 'actions/projects-actions'; + +interface StateToProps { + tasksFetching: boolean; + gettingQuery: ProjectsQuery; + numberOfProjects: number; +} + +interface DispatchToProps { + onGetProjects: (gettingQuery: ProjectsQuery) => void; +} + +function mapStateToProps(state: CombinedState): StateToProps { + const { projects } = state; + + return { + tasksFetching: projects.fetching, + gettingQuery: projects.gettingQuery, + numberOfProjects: projects.count, + }; +} + +function mapDispatchToProps(dispatch: any): DispatchToProps { + return { + onGetProjects: (query: ProjectsQuery): void => { + dispatch(getProjectsAsync(query)); + }, + }; +} + +export default connect( + mapStateToProps, + mapDispatchToProps, +)(ProjectsPageComponent); diff --git a/cvat-ui/src/reducers/interfaces.ts b/cvat-ui/src/reducers/interfaces.ts index 86dffa467978..62fe7f676ce9 100644 --- a/cvat-ui/src/reducers/interfaces.ts +++ b/cvat-ui/src/reducers/interfaces.ts @@ -21,6 +21,37 @@ export interface AuthState { allowResetPassword: boolean; } +export interface ProjectsQuery { + page: number; + id: number | null; + search: string | null; + owner: string | null; + name: string | null; + status: string | null; + [key: string]: string | number | null; +} + +export interface Project { + instance: any; // cvat-core instance +} + +export interface ProjectsState { + initialized: boolean; + fetching: boolean; + count: number; + current: Project[]; + gettingQuery: ProjectsQuery; + activities: { + creates: { + id: null | number; + error: string; + }; + deletes: { + [projectId: number]: boolean; // deleted (deleting if in dictionary) + }; + }; +} + export interface TasksQuery { page: number; id: number | null; @@ -193,6 +224,12 @@ export interface NotificationsState { resetPassword: null | ErrorState; loadAuthActions: null | ErrorState; }; + projects: { + fetching: null | ErrorState; + updating: null | ErrorState; + deleting: null | ErrorState; + creating: null | ErrorState; + }; tasks: { fetching: null | ErrorState; updating: null | ErrorState; @@ -479,6 +516,7 @@ export interface ShortcutsState { export interface CombinedState { auth: AuthState; + projects: ProjectsState; tasks: TasksState; users: UsersState; about: AboutState; diff --git a/cvat-ui/src/reducers/projects-reducer.ts b/cvat-ui/src/reducers/projects-reducer.ts new file mode 100644 index 000000000000..fc2a981ccaf2 --- /dev/null +++ b/cvat-ui/src/reducers/projects-reducer.ts @@ -0,0 +1,165 @@ +// Copyright (C) 2020 Intel Corporation +// +// SPDX-License-Identifier: MIT + +import { AnyAction } from 'redux'; +import { ProjectsActionTypes } from 'actions/projects-actions'; +import { BoundariesActionTypes } from 'actions/boundaries-actions'; +import { AuthActionTypes } from 'actions/auth-actions'; + +import { Project, ProjectsState } from './interfaces'; + +const defaultState: ProjectsState = { + initialized: false, + fetching: false, + count: 0, + current: [], + gettingQuery: { + page: 1, + id: null, + search: null, + owner: null, + name: null, + status: null, + }, + activities: { + deletes: {}, + creates: { + id: null, + error: '', + }, + }, +}; + +export default (state: ProjectsState = defaultState, action: AnyAction): ProjectsState => { + switch (action.type) { + case ProjectsActionTypes.UPDATE_PROJECTS_GETTING_QUERY: + return { + ...state, + gettingQuery: { + ...defaultState.gettingQuery, + ...action.payload.query, + }, + }; + case ProjectsActionTypes.GET_PROJECTS: + return { + ...state, + initialized: false, + fetching: true, + count: 0, + current: [], + }; + case ProjectsActionTypes.GET_PROJECTS_SUCCESS: { + const projects = action.payload.array.map( + (project: any): Project => ({ + instance: project, + }), + ); + + return { + ...state, + initialized: true, + fetching: false, + count: action.payload.count, + current: projects, + }; + } + case ProjectsActionTypes.GET_PROJECTS_FAILED: { + return { + ...state, + initialized: true, + fetching: false, + }; + } + case ProjectsActionTypes.CREATE_PROJECT: { + return { + ...state, + activities: { + ...state.activities, + creates: { + id: null, + error: '', + }, + }, + }; + } + case ProjectsActionTypes.CREATE_PROJECT_FAILED: { + return { + ...state, + activities: { + ...state.activities, + creates: { + ...state.activities.creates, + error: action.payload.error.toString(), + }, + }, + }; + } + case ProjectsActionTypes.CREATE_PROJECT_SUCCESS: { + return { + ...state, + activities: { + ...state.activities, + creates: { + id: action.payload.projectId, + error: '', + }, + }, + }; + } + case ProjectsActionTypes.DELETE_PROJECT: { + const { projectId } = action.payload; + const { deletes } = state.activities; + + deletes[projectId] = false; + + return { + ...state, + activities: { + ...state.activities, + deletes: { + ...deletes, + }, + }, + }; + } + case ProjectsActionTypes.DELETE_PROJECT_SUCCESS: { + const { projectId } = action.payload; + const { deletes } = state.activities; + + deletes[projectId] = true; + + return { + ...state, + activities: { + ...state.activities, + deletes: { + ...deletes, + }, + }, + }; + } + case ProjectsActionTypes.DELETE_PROJECT_FAILED: { + const { projectId } = action.payload; + const { deletes } = state.activities; + + delete deletes[projectId]; + + return { + ...state, + activities: { + ...state.activities, + deletes: { + ...deletes, + }, + }, + }; + } + case BoundariesActionTypes.RESET_AFTER_ERROR: + case AuthActionTypes.LOGOUT_SUCCESS: { + return { ...defaultState }; + } + default: + return state; + } +}; diff --git a/cvat-ui/src/reducers/root-reducer.ts b/cvat-ui/src/reducers/root-reducer.ts index e726b94e85ca..763a0b0578fe 100644 --- a/cvat-ui/src/reducers/root-reducer.ts +++ b/cvat-ui/src/reducers/root-reducer.ts @@ -4,6 +4,7 @@ import { combineReducers, Reducer } from 'redux'; import authReducer from './auth-reducer'; +import projectsReducer from './projects-reducer'; import tasksReducer from './tasks-reducer'; import usersReducer from './users-reducer'; import aboutReducer from './about-reducer'; @@ -20,6 +21,7 @@ import userAgreementsReducer from './useragreements-reducer'; export default function createRootReducer(): Reducer { return combineReducers({ auth: authReducer, + projects: projectsReducer, tasks: tasksReducer, users: usersReducer, about: aboutReducer, From 22f933587bd1e5245263039e51cfe998f9d9593d Mon Sep 17 00:00:00 2001 From: Dmitry Kalinin Date: Wed, 30 Sep 2020 00:09:24 +0300 Subject: [PATCH 04/55] Fixed label serializer --- cvat/apps/engine/serializers.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/cvat/apps/engine/serializers.py b/cvat/apps/engine/serializers.py index 1edbafd35fe1..945d8d2e8a08 100644 --- a/cvat/apps/engine/serializers.py +++ b/cvat/apps/engine/serializers.py @@ -48,25 +48,25 @@ class Meta: def update_instance(validated_data, parent_instance): attributes = validated_data.pop('attributespec_set', []) instance = dict() - if isinstace(parent_instance. models.Project): + if isinstance(parent_instance, models.Project): instance['project'] = parent_instance - logger = sLogger.project[parent_instace.id] + logger = slogger.project[parent_instance.id] else: instance['task'] = parent_instance - logger = sLogger.task[parent_instace.id] - (db_label, created) = models.Label.objects.get_or_create(name=label['name'], + logger = slogger.task[parent_instance.id] + (db_label, created) = models.Label.objects.get_or_create(name=validated_data['name'], **instance) if created: logger.info("New {} label was created".format(db_label.name)) else: logger.info("{} label was updated".format(db_label.name)) - if not label.get('color', None): + if not validated_data.get('color', None): label_names = [l.name for l in models.Label.objects.filter(task_id=instance.id).exclude(id=db_label.id).order_by('id') ] db_label.color = get_label_color(db_label.name, label_names) else: - db_label.color = label.get('color', db_label.color) + db_label.color = validated_data.get('color', db_label.color) db_label.save() for attr in attributes: (db_attr, created) = models.AttributeSpec.objects.get_or_create( From a21dbec19933cff081742c529b0074f6cf860e42 Mon Sep 17 00:00:00 2001 From: Dmitry Kalinin Date: Wed, 30 Sep 2020 00:11:27 +0300 Subject: [PATCH 05/55] moved bugtracker to separated component --- .../task-page/bug-tracker-editor.tsx | 93 ++++++++++++++++++ cvat-ui/src/components/task-page/details.tsx | 97 +++---------------- 2 files changed, 105 insertions(+), 85 deletions(-) create mode 100644 cvat-ui/src/components/task-page/bug-tracker-editor.tsx diff --git a/cvat-ui/src/components/task-page/bug-tracker-editor.tsx b/cvat-ui/src/components/task-page/bug-tracker-editor.tsx new file mode 100644 index 000000000000..263a4664b367 --- /dev/null +++ b/cvat-ui/src/components/task-page/bug-tracker-editor.tsx @@ -0,0 +1,93 @@ +// Copyright (C) 2020 Intel Corporation +// +// SPDX-License-Identifier: MIT + +import React, { useState } from 'react'; +import Button from 'antd/lib/button'; +import Modal from 'antd/lib/modal'; +import Text from 'antd/lib/typography/Text'; +import { Row, Col } from 'antd/lib/grid'; + +import patterns from 'utils/validation-patterns'; + +interface Props { + instance: any; + onChange: (bugTracker: string) => void; +} + +export default function BugTrackerEditorComponent(props: Props): JSX.Element { + const { + instance, + onChange, + } = props; + + const [bugTracker, setBugTracker] = useState(instance.bugTracker); + const [bugTrackerEditing, setBugTrackerEditing] = useState(false); + + const instanceType = Array.isArray(instance.tasks) ? 'project' : 'task'; + let shown = false; + + const onStart = (): void => setBugTrackerEditing(true); + const onChangeValue = (value: string): void => { + if (value && !patterns.validateURL.pattern.test(value)) { + if (!shown) { + Modal.error({ + title: `Could not update the ${instanceType} ${instance.id}`, + content: 'Issue tracker is expected to be URL', + onOk: (() => { + shown = false; + }), + }); + shown = true; + } + } else { + setBugTracker(value); + setBugTrackerEditing(false); + + instance.bugTracker = value; + onChange(instance); + } + }; + + if (bugTracker) { + return ( + + + Issue Tracker +
    + {bugTracker} + + +
    + ); + } + + return ( + + + Issue Tracker +
    + + {bugTrackerEditing ? '' : 'Not specified'} + + +
    + ); +} diff --git a/cvat-ui/src/components/task-page/details.tsx b/cvat-ui/src/components/task-page/details.tsx index aa3813d0ae15..a6e5680be9f1 100644 --- a/cvat-ui/src/components/task-page/details.tsx +++ b/cvat-ui/src/components/task-page/details.tsx @@ -7,17 +7,16 @@ import { Row, Col } from 'antd/lib/grid'; import Tag from 'antd/lib/tag'; import Icon from 'antd/lib/icon'; import Modal from 'antd/lib/modal'; -import Button from 'antd/lib/button'; import notification from 'antd/lib/notification'; import Text from 'antd/lib/typography/Text'; import Title from 'antd/lib/typography/Title'; import moment from 'moment'; import getCore from 'cvat-core-wrapper'; -import patterns from 'utils/validation-patterns'; import { getReposData, syncRepos } from 'utils/git-utils'; import { ActiveInference } from 'reducers/interfaces'; import AutomaticAnnotationProgress from 'components/tasks-page/automatic-annotation-progress'; +import BugTrackerEditor from './bug-tracker-editor'; import UserSelector from './user-selector'; import LabelsEditorComponent from '../labels-editor/labels-editor'; @@ -35,8 +34,6 @@ interface Props { interface State { name: string; - bugTracker: string; - bugTrackerEditing: boolean; repository: string; repositoryStatus: string; } @@ -56,8 +53,6 @@ export default class DetailsComponent extends React.PureComponent this.previewWrapperRef = React.createRef(); this.state = { name: taskInstance.name, - bugTracker: taskInstance.bugTracker, - bugTrackerEditing: false, repository: '', repositoryStatus: '', }; @@ -118,7 +113,6 @@ export default class DetailsComponent extends React.PureComponent if (prevProps !== this.props) { this.setState({ name: taskInstance.name, - bugTracker: taskInstance.bugTracker, }); } } @@ -312,82 +306,6 @@ export default class DetailsComponent extends React.PureComponent ); } - private renderBugTracker(): JSX.Element { - const { taskInstance, onTaskUpdate } = this.props; - const { bugTracker, bugTrackerEditing } = this.state; - - let shown = false; - const onStart = (): void => { - this.setState({ - bugTrackerEditing: true, - }); - }; - const onChangeValue = (value: string): void => { - if (value && !patterns.validateURL.pattern.test(value)) { - if (!shown) { - Modal.error({ - title: `Could not update the task ${taskInstance.id}`, - content: 'Issue tracker is expected to be URL', - onOk: (() => { - shown = false; - }), - }); - shown = true; - } - } else { - this.setState({ - bugTracker: value, - bugTrackerEditing: false, - }); - - taskInstance.bugTracker = value; - onTaskUpdate(taskInstance); - } - }; - - if (bugTracker) { - return ( - - - Issue Tracker -
    - {bugTracker} - - -
    - ); - } - - return ( - - - Issue Tracker -
    - - {bugTrackerEditing ? '' : 'Not specified'} - - -
    - ); - } - private renderLabelsEditor(): JSX.Element { const { taskInstance, onTaskUpdate } = this.props; @@ -410,7 +328,13 @@ export default class DetailsComponent extends React.PureComponent } public render(): JSX.Element { - const { activeInference, cancelAutoAnnotation } = this.props; + const { + activeInference, + cancelAutoAnnotation, + taskInstance, + onTaskUpdate, + } = this.props; + return (
    @@ -435,7 +359,10 @@ export default class DetailsComponent extends React.PureComponent { this.renderUsers() } - { this.renderBugTracker() } + Date: Wed, 30 Sep 2020 02:55:33 +0300 Subject: [PATCH 06/55] Added project page --- cvat-core/src/session.js | 15 ++++ cvat-ui/src/actions/projects-actions.ts | 58 +++++++++++++ .../src/components/project-page/details.tsx | 85 +++++++++++++++++++ .../components/project-page/project-page.tsx | 13 ++- .../src/components/project-page/styles.scss | 25 ++++++ cvat-ui/src/reducers/projects-reducer.ts | 35 ++++++++ .../migrations/0031_projects_adjastment.py | 4 - cvat/apps/engine/models.py | 1 + cvat/apps/engine/serializers.py | 3 +- 9 files changed, 230 insertions(+), 9 deletions(-) create mode 100644 cvat-ui/src/components/project-page/details.tsx diff --git a/cvat-core/src/session.js b/cvat-core/src/session.js index e8b719bc1eee..98e863e2eef5 100644 --- a/cvat-core/src/session.js +++ b/cvat-core/src/session.js @@ -1420,6 +1420,7 @@ name: undefined, status: undefined, owner: undefined, + bug_tracker: undefined, created_date: undefined, updated_date: undefined, }; @@ -1498,6 +1499,19 @@ owner: { get: () => data.owner, }, + /** + * @name bugTracker + * @type {string} + * @memberof module:API.cvat.classes.Project + * @instance + * @throws {module:API.cvat.exceptions.ArgumentError} + */ + bugTracker: { + get: () => data.bug_tracker, + set: (tracker) => { + data.bug_tracker = tracker; + }, + }, /** * @name createdDate * @type {string} @@ -2112,6 +2126,7 @@ if (typeof (this.id) !== 'undefined') { const projectData = { name: this.name, + bug_tracker: this.bugTracker, labels: [...this.labels.map((el) => el.toJSON())], }; diff --git a/cvat-ui/src/actions/projects-actions.ts b/cvat-ui/src/actions/projects-actions.ts index 9b27bf7924e0..4f9eeabb1b38 100644 --- a/cvat-ui/src/actions/projects-actions.ts +++ b/cvat-ui/src/actions/projects-actions.ts @@ -19,6 +19,9 @@ export enum ProjectsActionTypes { CREATE_PROJECT = 'CREATE_PROJECT', CREATE_PROJECT_SUCCESS = 'CREATE_PROJECT_SUCCESS', CREATE_PROJECT_FAILED = 'CREATE_PROJECT_FAILED', + UPDATE_PROJECT = 'UPDATE_PROJECT', + UPDATE_PROJECT_SUCCESS = 'UPDATE_PROJECT_SUCCESS', + UPDATE_PROJECT_FAILED = 'UPDATE_PROJECT_FAILED', DELETE_PROJECT = 'DELETE_PROJECT', DELETE_PROJECT_SUCCESS = 'DELETE_PROJECT_SUCCESS', DELETE_PROJECT_FAILED = 'DELETE_PROJECT_FAILED', @@ -146,6 +149,61 @@ ThunkAction, {}, {}, AnyAction> { }; } +function updateProject(): AnyAction { + const action = { + type: ProjectsActionTypes.UPDATE_PROJECT, + payload: {}, + }; + + return action; +} + +function updateProjectSuccess(project: any): AnyAction { + const action = { + type: ProjectsActionTypes.UPDATE_PROJECT_SUCCESS, + payload: { + project, + }, + }; + + return action; +} + +function updateProjectFailed(error: any, project: any): AnyAction { + const action = { + type: ProjectsActionTypes.UPDATE_PROJECT_FAILED, + payload: { + error, + project, + }, + }; + + return action; +} + +export function updateProjectAsync(projectInstance: any): +ThunkAction, {}, {}, AnyAction> { + return async (dispatch: ActionCreator): Promise => { + try { + dispatch(updateProject()); + await projectInstance.save(); + const [project] = await cvat.tasks.get({ id: projectInstance.id }); + dispatch(updateProjectSuccess(project)); + } catch (error) { + let project = null; + try { + [project] = await cvat.project.get({ id: projectInstance.id }); + } catch (fetchError) { + // FIXME: error length + dispatch(updateProjectFailed(error, projectInstance)); + return; + } + dispatch(updateProjectFailed(error, project)); + + } + }; +} + function deleteProject(projectId: number): AnyAction { const action = { type: ProjectsActionTypes.DELETE_PROJECT, diff --git a/cvat-ui/src/components/project-page/details.tsx b/cvat-ui/src/components/project-page/details.tsx new file mode 100644 index 000000000000..c14c6565e907 --- /dev/null +++ b/cvat-ui/src/components/project-page/details.tsx @@ -0,0 +1,85 @@ +// Copyright (C) 2020 Intel Corporation +// +// SPDX-License-Identifier: MIT + +import React, { useState } from 'react'; +import { useDispatch } from 'react-redux'; +import moment from 'moment'; +import { Row, Col } from 'antd/lib/grid'; +import Title from 'antd/lib/typography/Title'; +import Text from 'antd/lib/typography/Text'; +import Icon from 'antd/lib/icon'; +import Dropdown from 'antd/lib/dropdown'; + +import getCore from 'cvat-core-wrapper'; +import { Project } from 'reducers/interfaces'; +import { updateProjectAsync } from 'actions/projects-actions'; +import ActionsMenu from 'components/projects-page/actions-menu'; +import LabelsEditor from 'components/labels-editor/labels-editor'; +import BugTrackerEditor from 'components/task-page/bug-tracker-editor'; +import { MenuIcon } from 'icons'; + +const core = getCore(); + +interface DetailsComponentProps { + project: Project; +} + +export default function DetailsComponent(props: DetailsComponentProps): JSX.Element { + const { project } = props; + + const dispatch = useDispatch(); + const [projectName, setProjectName] = useState(project.instance.name); + + return ( +
    + + + { + setProjectName(value); + project.instance.name = value; + dispatch(updateProjectAsync(project.instance)); + }, + }} + className='cvat-text-color' + > + {projectName} + + + {`Project #${project.instance.id} created`} + {project.instance.owner ? ` by ${project.instance.owner.username}` : null} + {` on ${moment(project.instance.createdDate).format('MMMM Do YYYY')}`} + + { + dispatch(updateProjectAsync(_project)); + }} + /> + + +
    + Actions + }> + + +
    + +
    + label.toJSON(), + )} + onSubmit={(labels: any[]): void => { + project.instance.labels = labels + .map((labelData): any => new core.classes.Label(labelData)); + dispatch(updateProjectAsync(project.instance)); + }} + /> +
    + ); +} diff --git a/cvat-ui/src/components/project-page/project-page.tsx b/cvat-ui/src/components/project-page/project-page.tsx index 9f719f490f9c..8d3b9be31ee0 100644 --- a/cvat-ui/src/components/project-page/project-page.tsx +++ b/cvat-ui/src/components/project-page/project-page.tsx @@ -8,10 +8,12 @@ import React, { useEffect } from 'react'; import { useSelector, useDispatch } from 'react-redux'; import { useHistory, useParams } from 'react-router'; import Spin from 'antd/lib/spin'; +import { Row, Col } from 'antd/lib/grid'; +import Result from 'antd/lib/result'; import { CombinedState } from 'reducers/interfaces'; import { getProjectsAsync, updateProjectsGettingQuery } from 'actions/projects-actions'; -import Result from 'antd/lib/result'; +import DetailsComponent from './details'; interface ParamType { id: string; @@ -67,8 +69,11 @@ export default function TaskPageComponent(): JSX.Element { } return ( - - {project.instance.name} - + + + + {/* */} + + ); } diff --git a/cvat-ui/src/components/project-page/styles.scss b/cvat-ui/src/components/project-page/styles.scss index e69de29bb2d1..3b7118209f0c 100644 --- a/cvat-ui/src/components/project-page/styles.scss +++ b/cvat-ui/src/components/project-page/styles.scss @@ -0,0 +1,25 @@ +// Copyright (C) 2020 Intel Corporation +// +// SPDX-License-Identifier: MIT + +@import '../../base.scss'; + +.cvat-project-details { + width: 100%; + height: auto; + border: 1px solid $border-color-1; + border-radius: 3px; + padding: 16px; + margin-top: 16px; + background: $background-color-1; + + .ant-row-flex:nth-child(1) { + margin-bottom: 16px; + } + .cvat-project-details-actions { + > div { + display: flex; + align-items: center; + } + } +} \ No newline at end of file diff --git a/cvat-ui/src/reducers/projects-reducer.ts b/cvat-ui/src/reducers/projects-reducer.ts index fc2a981ccaf2..f1946aee5613 100644 --- a/cvat-ui/src/reducers/projects-reducer.ts +++ b/cvat-ui/src/reducers/projects-reducer.ts @@ -107,6 +107,41 @@ export default (state: ProjectsState = defaultState, action: AnyAction): Project }, }; } + case ProjectsActionTypes.UPDATE_PROJECT: { + return { + ...state, + }; + } + case ProjectsActionTypes.UPDATE_PROJECT_SUCCESS: { + return { + ...state, + current: state.current.map((project): Project => { + if (project.instance.id === action.payload.project.id) { + return { + ...project, + instance: action.payload.project, + }; + } + + return project; + }), + }; + } + case ProjectsActionTypes.UPDATE_PROJECT_FAILED: { + return { + ...state, + current: state.current.map((project): Project => { + if (project.instance.id === action.payload.project.id) { + return { + ...project, + instance: action.payload.project, + }; + } + + return project; + }), + }; + } case ProjectsActionTypes.DELETE_PROJECT: { const { projectId } = action.payload; const { deletes } = state.activities; diff --git a/cvat/apps/engine/migrations/0031_projects_adjastment.py b/cvat/apps/engine/migrations/0031_projects_adjastment.py index 9e89d845f104..5d1bc9943f13 100644 --- a/cvat/apps/engine/migrations/0031_projects_adjastment.py +++ b/cvat/apps/engine/migrations/0031_projects_adjastment.py @@ -15,10 +15,6 @@ class Migration(migrations.Migration): model_name='project', name='assignee', ), - migrations.RemoveField( - model_name='project', - name='bug_tracker', - ), migrations.AddField( model_name='label', name='project', diff --git a/cvat/apps/engine/models.py b/cvat/apps/engine/models.py index ad2306b0383f..8df231298f8f 100644 --- a/cvat/apps/engine/models.py +++ b/cvat/apps/engine/models.py @@ -143,6 +143,7 @@ class Project(models.Model): name = SafeCharField(max_length=256) owner = models.ForeignKey(User, null=True, blank=True, on_delete=models.SET_NULL, related_name="+") + bug_tracker = models.CharField(max_length=2000, blank=True, default="") created_date = models.DateTimeField(auto_now_add=True) updated_date = models.DateTimeField(auto_now_add=True) status = models.CharField(max_length=32, choices=StatusChoice.choices(), diff --git a/cvat/apps/engine/serializers.py b/cvat/apps/engine/serializers.py index 945d8d2e8a08..2ddf2a1a8df3 100644 --- a/cvat/apps/engine/serializers.py +++ b/cvat/apps/engine/serializers.py @@ -346,7 +346,7 @@ class ProjectSerializer(serializers.ModelSerializer): tasks = TaskSerializer(many=True, source='task_set', read_only=True) class Meta: model = models.Project - fields = ('url', 'id', 'name', 'labels', 'owner', + fields = ('url', 'id', 'name', 'labels', 'owner', 'bug_tracker', 'created_date', 'updated_date', 'status', 'tasks') read_only_fields = ('created_date', 'updated_date', 'status') ordering = ['-id'] @@ -372,6 +372,7 @@ def create(self, validated_data): def update(self, instance, validated_data): instance.name = validated_data.get('name', instance.name) instance.owner = validated_data.get('owner', instance.owner) + instance.bug_tracker = validated_data.get('bug_tracker', instance.bug_tracker) labels = validated_data.get('label_set', []) for label in labels: LabelSerializer.update_instance(label, instance) From 5612955cd828b563204b8d43f42264273e2730b9 Mon Sep 17 00:00:00 2001 From: Dmitry Kalinin Date: Wed, 30 Sep 2020 21:44:02 +0300 Subject: [PATCH 07/55] wip on creating task in projects --- cvat-core/src/session.js | 11 +++++ cvat-ui/src/actions/projects-actions.ts | 1 - .../create-project-content.tsx | 8 +++- .../basic-configuration-form.tsx | 10 +++++ .../create-task-page/create-task-content.tsx | 9 ++++- .../create-task-page/create-task-page.tsx | 11 ++++- cvat-ui/src/components/cvat-app.tsx | 2 +- .../src/components/project-page/details.tsx | 1 - .../components/project-page/project-page.tsx | 40 ++++++++++++++++++- .../src/components/project-page/styles.scss | 6 ++- .../components/projects-page/project-item.tsx | 6 +-- .../create-task-page/create-task-page.tsx | 3 +- cvat/apps/engine/models.py | 3 ++ cvat/apps/engine/serializers.py | 10 ++--- cvat/apps/engine/views.py | 2 +- 15 files changed, 102 insertions(+), 21 deletions(-) diff --git a/cvat-core/src/session.js b/cvat-core/src/session.js index 98e863e2eef5..60236c761953 100644 --- a/cvat-core/src/session.js +++ b/cvat-core/src/session.js @@ -814,6 +814,7 @@ const data = { id: undefined, name: undefined, + project_id: undefined, status: undefined, size: undefined, mode: undefined, @@ -906,6 +907,16 @@ data.name = value; }, }, + /** + * @name project_id + * @type {integer} + * @memberof module:API.cvat.classes.Task + * @readonly + * @instance + */ + project_id: { + get: () => data.project_id, + }, /** * @name status * @type {module:API.cvat.enums.TaskStatus} diff --git a/cvat-ui/src/actions/projects-actions.ts b/cvat-ui/src/actions/projects-actions.ts index 4f9eeabb1b38..be91666df982 100644 --- a/cvat-ui/src/actions/projects-actions.ts +++ b/cvat-ui/src/actions/projects-actions.ts @@ -199,7 +199,6 @@ ThunkAction, {}, {}, AnyAction> { return; } dispatch(updateProjectFailed(error, project)); - } }; } diff --git a/cvat-ui/src/components/create-project-page/create-project-content.tsx b/cvat-ui/src/components/create-project-page/create-project-content.tsx index 2aa1c9baed93..73d7e8b6df06 100644 --- a/cvat-ui/src/components/create-project-page/create-project-content.tsx +++ b/cvat-ui/src/components/create-project-page/create-project-content.tsx @@ -46,8 +46,12 @@ export default function CreateProjectContent(): JSX.Element { const dispatch = useDispatch(); const history = useHistory(); - const newProjectId = useSelector((state: CombinedState) => state.projects.creates.id); - const createProjectError = useSelector((state: CombinedState) => state.projects.creates.error); + const newProjectId = useSelector( + (state: CombinedState) => state.projects.activities.creates.id, + ); + const createProjectError = useSelector( + (state: CombinedState) => state.projects.activities.creates.error, + ); useEffect(() => { if (Number.isInteger(newProjectId)) { diff --git a/cvat-ui/src/components/create-task-page/basic-configuration-form.tsx b/cvat-ui/src/components/create-task-page/basic-configuration-form.tsx index f269c38d4cc4..af418442a6be 100644 --- a/cvat-ui/src/components/create-task-page/basic-configuration-form.tsx +++ b/cvat-ui/src/components/create-task-page/basic-configuration-form.tsx @@ -56,6 +56,16 @@ class BasicConfigurationForm extends React.PureComponent { , ) } + Project Id (for develping only)}> + { getFieldDecorator('project_id', { + rules: [{ + pattern: /^[1-9]+[0-9]*$/, + message: 'Please, specify valid positive number', + }], + })( + , + ) } + ); } diff --git a/cvat-ui/src/components/create-task-page/create-task-content.tsx b/cvat-ui/src/components/create-task-page/create-task-content.tsx index 00fcb99e068f..7f3b24279446 100644 --- a/cvat-ui/src/components/create-task-page/create-task-content.tsx +++ b/cvat-ui/src/components/create-task-page/create-task-content.tsx @@ -29,6 +29,7 @@ interface Props { onCreate: (data: CreateTaskData) => void; status: string; taskId: number | null; + projectId: number | null; installedGit: boolean; } @@ -37,6 +38,7 @@ type State = CreateTaskData; const defaultState = { basic: { name: '', + projectId: null, }, advanced: { zOrder: false, @@ -63,7 +65,12 @@ class CreateTaskContent extends React.PureComponent { if (error) { let errorCopy = error; @@ -69,6 +77,7 @@ export default function CreateTaskPage(props: Props): JSX.Element { Create a new task - + diff --git a/cvat-ui/src/components/project-page/details.tsx b/cvat-ui/src/components/project-page/details.tsx index c14c6565e907..f7844ecf0a3e 100644 --- a/cvat-ui/src/components/project-page/details.tsx +++ b/cvat-ui/src/components/project-page/details.tsx @@ -54,7 +54,6 @@ export default function DetailsComponent(props: DetailsComponentProps): JSX.Elem {` on ${moment(project.instance.createdDate).format('MMMM Do YYYY')}`} { dispatch(updateProjectAsync(_project)); diff --git a/cvat-ui/src/components/project-page/project-page.tsx b/cvat-ui/src/components/project-page/project-page.tsx index 8d3b9be31ee0..c226815cd28e 100644 --- a/cvat-ui/src/components/project-page/project-page.tsx +++ b/cvat-ui/src/components/project-page/project-page.tsx @@ -10,9 +10,13 @@ import { useHistory, useParams } from 'react-router'; import Spin from 'antd/lib/spin'; import { Row, Col } from 'antd/lib/grid'; import Result from 'antd/lib/result'; +import Button from 'antd/lib/button'; +import Title from 'antd/lib/typography/Title'; import { CombinedState } from 'reducers/interfaces'; import { getProjectsAsync, updateProjectsGettingQuery } from 'actions/projects-actions'; +import { cancelInferenceAsync } from 'actions/models-actions'; +import TaskItem from 'components/tasks-page/task-item'; import DetailsComponent from './details'; interface ParamType { @@ -27,6 +31,9 @@ export default function TaskPageComponent(): JSX.Element { const gettingQueryId = useSelector((state: CombinedState) => state.projects.gettingQuery.id); const projects = useSelector((state: CombinedState) => state.projects.current); const deletes = useSelector((state: CombinedState) => state.projects.activities.deletes); + const taskDeletes = useSelector((state: CombinedState) => state.tasks.activities.deletes); + const tasksActiveInferences = useSelector((state: CombinedState) => state.models.inferences); + const image = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAIAAAD8GO2jAAAJAElEQVRIiSWWSZNUV3pAv++785tyqiGBQlBQGnALtSRaEXb0xl44whv/Aa/8Nx3hfduhaNo9gBANSFRBVWVWDpWZ7+Wb7r2fF5z12Z+Do3Hu0oyZ99W+qVsAVpKEFEIIImKOn+AIABhjRELnTNd5ACQCKRVzBAAAYGbfe0BkZu8jMMcYpRDCOEMkvPdt03MMfR9CYCE4hACAgADMAAwAMTIiWKsBGAERyRillOTIMcZPUoyhqRtm5ggMJH3wdbUXQnatRwASRAKVEkTkPXofgAEQmREAECMAMGCMHEJHQYQQYmRgCIRWICAZa/NiJIQod+Vut5PHx5MI0NStczQYpNpIQvQ+MkOaJt77vvchxBCZCKRAa1SeJt7Hum6FoOA5+iACT+7cf/Tw5Mc//bWJwjmrpOi6XlSlfPrdsxjDx4v3XVNleU4CEQUw1PsSuDfWkZBSCCnBKEwTPT08mB6Oi0wLQkRSUtlE7ffq7a/7+c1KKdXsa0XOGjEeJ2lK0tkscnBJooS8e+eRcanWdnI4LcvVdn2J2PfNjkMLQhqtsiw/efB0kNuuvgzcIActNYvi1fnsv/77D9V6ORkOT598Tcaub6+QdJppGQE6H5AEc1jfXscV922MvlfauPR4NDnY71dXF+/Kbc9MA5+s/3JeltvN7UISZqk+nB6hjD/+4bmJ8Z9+//uzL55sW/6/l3/q+goAEFBaYwBBSlnHvm02xkitbKjLIYU/v/1FZKN7Dx5HdeQGABwDg5JOGswHsqn3myqU57v15nx9c/Uvz57+8P0PpadXL/7nZnEuRCQSEEFeX773MXrfO2edM0lisiyPjX9y9uTzsy9//Ps7ERrq9z7Cg9NTrZQPYX59ud/snNF6NNhX+77aH01G9+8co9Bv3r5eri6MRgbBzDGyGI0NRy+VSVNb5Fmep8NhPhpPDqeP7t49QYHRt+vVfPbxwrfNt7/9trxdPf/xf6tymybpaDSBGLebddO1qHXl4+zmY4w7JAwxxsAxsvjy69MiH1ibJIkbjfI8z4o8G44GKBUjLecff3rx1922nE7vfP+7f/z2ux/SxM1mV4Kga+vJoHj2u2cAcPnxkhRvt8sPHy925b5pPTMjIsconn73ZeJSqUxibVHkaZrkeTYsXJao1WJ9cXElhXn86OzpN7/9/IsvV+tVXZfj8UiQrMry/fu3bVenVvddzaQX62qxKsttu900VVkrKYQU+B//+e9pkgcWWtLhZFAUWZFno2HuXPr61fvdtn9weqa1Kctd3TT1vmWArmtCBEHyzeuXP//8UhEz8KoKSTFCov12U96uq3KnNd69dyCyLLu9Lau67/uwud3N56vC0Denx5mBalf2gUgmdevn8+s3f3+9XC5i9GVVNW3NMfR9t14v17eb2015fXXpuwYRAKHeV21TN02rFMlhMaz2Tb2tZAQwWmvje6x3rRDglNguL87Pr6cnD6uyms1ulBTWGmudVnK7We82S6NlCGFXNoQy9P1+u1FaCyQiCgC3tzv52cNTKaXRNnE2S02epWniFr1WaKIS3p8nVgfflW2c3H1cbW93ZXt0dAdi/3G7mc9mTdcmabqv+973SZq7NIEYBYkQegTkwDLNiyRxiXWJNc6ZNMlckijstNYEVBQDZfJ12Z6/fxcjt01b5KmxiW9KDD72/b5utXExBCVpOB4lad61bV2Vfe+cS2MIcjyYZFlqtHLOWOesSxWRJi21Aa6HRdE0/TDPTj+7P79ZHB8dTY+PMLb79Y3FMMmsMGq5qYVSD09OiuHEx6iYgR0KMiaVhHIymaZ5YSQaJaQUSkurlBaSCTDynbt3FotF1YSvzh6e3Dsp97WRsPh4vrz+AByLPFMo9/42SYtHZ1+0bXszm/VtI4jTxALgYDCQeV6kRaaJFXiJqI2T1hEiQBRSmjRVxWgxv51MJlJpIBoPB+9+evH8Ofz07tcuQISQDUbHR9Pp9KQqN3VVKopZZtrOdz2naSLTNLFaCO6t1No4klpIgQjMzIgCiKT8sNj88eUb5DAZD7/66vOL1Xrr4ej+o/WuWlzND49H//Cbp4eH06urD82+XM37vqutTWxitXHS5onFqGRiXSakQvqUe0Tk4DsIvaRaSUqybLVa//ph9s//+m/JYPKXF6+Gw5HSzikzHI6nR9MnT75WSl1e/Lpe3+y2q2I4HoyOWibppMyMQW1JCiRBSMyePxUePBMJZRQJjeSkuF7On//x+ffPnj3+/DdVVargh6PJaDSxSbrZ7TjEpi7L6rbuar9eSKUJG1m4XNskAEslBRFwAAZgjgyIMjKn2WA0HH04v/jwy9s+eOja0PrRwTGrtNptrDNpku2229n15WI+65o6L4bK2BCilAoYpXIZSSEFaamIgTlGCBxCjIGDD8Eb4+6cfDZfrNu63G421X4PEB7cn6YHzetf6tcvX8CDx8Yly/l1VW6tNdPpSQi+aZqm6ZqmkkwsJAkhhVZIAgBl9DH0se8gepQEQmSZk5KOp/emd+5td9vXr/52cHgwUeZ+ksRB1tfb4XBICHmejw8PlqslIcYYq7KezWYS+pophCiIe6UUCgEcJQFoKcg1XRNiL6Fvqtv5cjkcjhNrjVVv3v08v77Jk/zg4Mi4LMkGjHI0yMeHhygNMEiSy9V6tSmlkk6gQiSMEPsYAxMHKUQMje86I20ATDPI8iJezp2z44OJSzJnEo4hcmCA3W5Xt9w2LQ4L3+5zK1ForW2eZ5NxIQGRgZEECgItEQGDZ4TIBJEJAITQLisGYyKSUh1MJsaoLM0Pp3eddRT5/S/n2+pSKtKJpsQZIZUy2lqjzbibSACQSggpPn0lCSIhCCP3TEISAnMUHJ1VxhhjdOq00UocH1lnYwxt3SDh9eV5Psiy9BuTFa7QRltJGPu+aVoJwIQkhECE4DsOAZWIfcMcZZIzAMUI3b7d1ygEECD3kjmTrAvXM21JIGHbt6fTszzNlTYkrRAKOPYYMEQJhCyQiQmZJHCMvq9jAGsKRGIGlkIZ6yNK7UIIfduZ4cgYFX0o62Y2W253pVT68dkXw9FE25SF6pq2bRrf9wzw/8noFrOt3BOXAAAAAElFTkSuQmCC'; const filteredProjects = projects.filter( (project) => project.instance.id === id, @@ -48,7 +55,7 @@ export default function TaskPageComponent(): JSX.Element { }, [id, dispatch]); if (deleteActivity) { - history.push('projects'); + history.push('/projects'); } if (project === null) { @@ -69,10 +76,39 @@ export default function TaskPageComponent(): JSX.Element { } return ( - + + + + Tasks + + + + + {/* */} + {project.instance.tasks.map((task: any) => ( + ); diff --git a/cvat-ui/src/components/project-page/styles.scss b/cvat-ui/src/components/project-page/styles.scss index 3b7118209f0c..26fb8ca37e8f 100644 --- a/cvat-ui/src/components/project-page/styles.scss +++ b/cvat-ui/src/components/project-page/styles.scss @@ -10,7 +10,7 @@ border: 1px solid $border-color-1; border-radius: 3px; padding: 16px; - margin-top: 16px; + margin: 16px 0px; background: $background-color-1; .ant-row-flex:nth-child(1) { @@ -22,4 +22,8 @@ align-items: center; } } +} + +.cvat-project-page-tasks-bar { + margin-bottom: 16px; } \ No newline at end of file diff --git a/cvat-ui/src/components/projects-page/project-item.tsx b/cvat-ui/src/components/projects-page/project-item.tsx index 9894686a183e..042010cbc0a5 100644 --- a/cvat-ui/src/components/projects-page/project-item.tsx +++ b/cvat-ui/src/components/projects-page/project-item.tsx @@ -26,10 +26,8 @@ export default function ProjectItemComponent(props: Props): JSX.Element { const history = useHistory(); const ownerName = instance.owner ? instance.owner.username : null; const updated = moment(instance.updatedDate).fromNow(); - const deleted = useSelector((state: CombinedState) => { - const { deletes } = state.projects.activities; - return instance.id in deletes ? deletes[instance.id] : false; - }); + const deletes = useSelector((state: CombinedState) => state.projects.activities.deletes); + const deleted = instance.id in deletes ? deletes[instance.id] : false; const onOpenProject = (): void => { history.push(`/projects/${instance.id}`); diff --git a/cvat-ui/src/containers/create-task-page/create-task-page.tsx b/cvat-ui/src/containers/create-task-page/create-task-page.tsx index 472c86bb1a10..613171301623 100644 --- a/cvat-ui/src/containers/create-task-page/create-task-page.tsx +++ b/cvat-ui/src/containers/create-task-page/create-task-page.tsx @@ -3,6 +3,7 @@ // SPDX-License-Identifier: MIT import { connect } from 'react-redux'; +import { withRouter } from 'react-router'; import { CombinedState } from 'reducers/interfaces'; import CreateTaskComponent from 'components/create-task-page/create-task-page'; @@ -37,4 +38,4 @@ function mapStateToProps(state: CombinedState): StateToProps { export default connect( mapStateToProps, mapDispatchToProps, -)(CreateTaskComponent); +)(withRouter(CreateTaskComponent)); diff --git a/cvat/apps/engine/models.py b/cvat/apps/engine/models.py index 8df231298f8f..8433ae692b51 100644 --- a/cvat/apps/engine/models.py +++ b/cvat/apps/engine/models.py @@ -153,6 +153,9 @@ class Project(models.Model): class Meta: default_permissions = () + def __str__(self): + return self.name + class Task(models.Model): project = models.ForeignKey(Project, on_delete=models.CASCADE, null=True, blank=True, related_name="tasks", diff --git a/cvat/apps/engine/serializers.py b/cvat/apps/engine/serializers.py index 2ddf2a1a8df3..316436b275d7 100644 --- a/cvat/apps/engine/serializers.py +++ b/cvat/apps/engine/serializers.py @@ -283,10 +283,10 @@ class TaskSerializer(WriteOnceMixin, serializers.ModelSerializer): class Meta: model = models.Task - fields = ('url', 'id', 'name', 'mode', 'owner', 'assignee', + fields = ('url', 'id', 'name', 'project_id', 'mode', 'owner', 'assignee', 'bug_tracker', 'created_date', 'updated_date', 'overlap', 'segment_size', 'z_order', 'status', 'labels', 'segments', - 'project', 'data_chunk_size', 'data_compressed_chunk_type', 'data_original_chunk_type', 'size', 'image_quality', 'data') + 'data_chunk_size', 'data_compressed_chunk_type', 'data_original_chunk_type', 'size', 'image_quality', 'data') read_only_fields = ('mode', 'created_date', 'updated_date', 'status', 'data_chunk_size', 'data_compressed_chunk_type', 'data_original_chunk_type', 'size', 'image_quality', 'data') write_once_fields = ('overlap', 'segment_size') @@ -343,11 +343,11 @@ def validate_labels(self, value): class ProjectSerializer(serializers.ModelSerializer): labels = LabelSerializer(many=True, source='label_set', partial=True) - tasks = TaskSerializer(many=True, source='task_set', read_only=True) + tasks = TaskSerializer(many=True, read_only=True) class Meta: model = models.Project - fields = ('url', 'id', 'name', 'labels', 'owner', 'bug_tracker', - 'created_date', 'updated_date', 'status', 'tasks') + fields = ('url', 'id', 'name', 'labels', 'tasks', 'owner', + 'bug_tracker', 'created_date', 'updated_date', 'status') read_only_fields = ('created_date', 'updated_date', 'status') ordering = ['-id'] diff --git a/cvat/apps/engine/views.py b/cvat/apps/engine/views.py index 7e46d836e571..d95d650361ec 100644 --- a/cvat/apps/engine/views.py +++ b/cvat/apps/engine/views.py @@ -179,7 +179,7 @@ class Meta: fields = ("id", "name", "owner", "status", "assignee") @method_decorator(name='list', decorator=swagger_auto_schema( - operation_summary='Returns a paginated list of projects according to query parameters (10 projects per page)', + operation_summary='Returns a paginated list of projects according to query parameters (12 projects per page)', manual_parameters=[ openapi.Parameter('id', openapi.IN_QUERY, description="A unique number value identifying this project", type=openapi.TYPE_NUMBER), From 1138d4d2ce7a8a38c016918c5a476c2e438660e0 Mon Sep 17 00:00:00 2001 From: Dmitry Kalinin Date: Wed, 30 Sep 2020 21:51:55 +0300 Subject: [PATCH 08/55] Added optional chaining plugin --- cvat-canvas/package-lock.json | 66 ++++++++++++++++++++++++++++++++++- cvat-canvas/package.json | 3 +- cvat-canvas/webpack.config.js | 5 ++- cvat-ui/package-lock.json | 66 ++++++++++++++++++++++++++++++++++- cvat-ui/package.json | 3 +- cvat-ui/webpack.config.js | 10 ++++-- 6 files changed, 145 insertions(+), 8 deletions(-) diff --git a/cvat-canvas/package-lock.json b/cvat-canvas/package-lock.json index 5cf6beb4a2ab..da90cea8fdc5 100644 --- a/cvat-canvas/package-lock.json +++ b/cvat-canvas/package-lock.json @@ -1,6 +1,6 @@ { "name": "cvat-canvas", - "version": "2.1.1", + "version": "2.1.2", "lockfileVersion": 1, "requires": true, "dependencies": { @@ -277,6 +277,28 @@ "@babel/types": "^7.0.0" } }, + "@babel/helper-skip-transparent-expression-wrappers": { + "version": "7.11.0", + "resolved": "https://registry.npmjs.org/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.11.0.tgz", + "integrity": "sha512-0XIdiQln4Elglgjbwo9wuJpL/K7AGCY26kmEt0+pRP0TAj4jjyNq1MjoRvikrTVqKcx4Gysxt4cXvVFXP/JO2Q==", + "dev": true, + "requires": { + "@babel/types": "^7.11.0" + }, + "dependencies": { + "@babel/types": { + "version": "7.11.5", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.11.5.tgz", + "integrity": "sha512-bvM7Qz6eKnJVFIn+1LPtjlBFPVN5jNDc1XmN15vWe7Q3DPBufWWsLiIvUu7xW87uTG6QoggpIDnUgLQvPheU+Q==", + "dev": true, + "requires": { + "@babel/helper-validator-identifier": "^7.10.4", + "lodash": "^4.17.19", + "to-fast-properties": "^2.0.0" + } + } + } + }, "@babel/helper-split-export-declaration": { "version": "7.4.4", "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.4.4.tgz", @@ -286,6 +308,12 @@ "@babel/types": "^7.4.4" } }, + "@babel/helper-validator-identifier": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.10.4.tgz", + "integrity": "sha512-3U9y+43hz7ZM+rzG24Qe2mufW5KhvFg/NhnNph+i9mgCtdTCtMJuI1TMkrIUiK7Ix4PYlRF9I5dhqaLYA/ADXw==", + "dev": true + }, "@babel/helper-wrap-function": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/@babel/helper-wrap-function/-/helper-wrap-function-7.2.0.tgz", @@ -560,6 +588,25 @@ "@babel/plugin-syntax-optional-catch-binding": "^7.2.0" } }, + "@babel/plugin-proposal-optional-chaining": { + "version": "7.11.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-optional-chaining/-/plugin-proposal-optional-chaining-7.11.0.tgz", + "integrity": "sha512-v9fZIu3Y8562RRwhm1BbMRxtqZNFmFA2EG+pT2diuU8PT3H6T/KXoZ54KgYisfOFZHV6PfvAiBIZ9Rcz+/JCxA==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.10.4", + "@babel/helper-skip-transparent-expression-wrappers": "^7.11.0", + "@babel/plugin-syntax-optional-chaining": "^7.8.0" + }, + "dependencies": { + "@babel/helper-plugin-utils": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.10.4.tgz", + "integrity": "sha512-O4KCvQA6lLiMU9l2eawBPMf1xPP8xPfB3iEQw150hOVTqj/rfXz0ThTb4HEzqQfs2Bmo5Ay8BzxfzVtBrr9dVg==", + "dev": true + } + } + }, "@babel/plugin-proposal-unicode-property-regex": { "version": "7.4.4", "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-unicode-property-regex/-/plugin-proposal-unicode-property-regex-7.4.4.tgz", @@ -616,6 +663,23 @@ "@babel/helper-plugin-utils": "^7.0.0" } }, + "@babel/plugin-syntax-optional-chaining": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-chaining/-/plugin-syntax-optional-chaining-7.8.3.tgz", + "integrity": "sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "dependencies": { + "@babel/helper-plugin-utils": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.10.4.tgz", + "integrity": "sha512-O4KCvQA6lLiMU9l2eawBPMf1xPP8xPfB3iEQw150hOVTqj/rfXz0ThTb4HEzqQfs2Bmo5Ay8BzxfzVtBrr9dVg==", + "dev": true + } + } + }, "@babel/plugin-syntax-typescript": { "version": "7.3.3", "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.3.3.tgz", diff --git a/cvat-canvas/package.json b/cvat-canvas/package.json index 792ae9f32468..92efaf14bc65 100644 --- a/cvat-canvas/package.json +++ b/cvat-canvas/package.json @@ -1,6 +1,6 @@ { "name": "cvat-canvas", - "version": "2.1.1", + "version": "2.1.2", "description": "Part of Computer Vision Annotation Tool which presents its canvas library", "main": "src/canvas.ts", "scripts": { @@ -20,6 +20,7 @@ "@babel/cli": "^7.5.5", "@babel/core": "^7.5.5", "@babel/plugin-proposal-class-properties": "^7.8.3", + "@babel/plugin-proposal-optional-chaining": "^7.11.0", "@babel/preset-env": "^7.5.5", "@babel/preset-typescript": "^7.3.3", "@types/node": "^12.6.8", diff --git a/cvat-canvas/webpack.config.js b/cvat-canvas/webpack.config.js index 96723247d198..aa2dadd2c9fe 100644 --- a/cvat-canvas/webpack.config.js +++ b/cvat-canvas/webpack.config.js @@ -28,7 +28,10 @@ const nodeConfig = { use: { loader: 'babel-loader', options: { - plugins: ['@babel/plugin-proposal-class-properties'], + plugins: [ + '@babel/plugin-proposal-class-properties', + '@babel/plugin-proposal-optional-chaining' + ], presets: [ ['@babel/preset-env'], ['@babel/typescript'], diff --git a/cvat-ui/package-lock.json b/cvat-ui/package-lock.json index 2a64cbd0a5e9..97d0994a00d6 100644 --- a/cvat-ui/package-lock.json +++ b/cvat-ui/package-lock.json @@ -1,6 +1,6 @@ { "name": "cvat-ui", - "version": "1.9.8", + "version": "1.9.9", "lockfileVersion": 1, "requires": true, "dependencies": { @@ -278,6 +278,28 @@ "@babel/types": "^7.0.0" } }, + "@babel/helper-skip-transparent-expression-wrappers": { + "version": "7.11.0", + "resolved": "https://registry.npmjs.org/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.11.0.tgz", + "integrity": "sha512-0XIdiQln4Elglgjbwo9wuJpL/K7AGCY26kmEt0+pRP0TAj4jjyNq1MjoRvikrTVqKcx4Gysxt4cXvVFXP/JO2Q==", + "dev": true, + "requires": { + "@babel/types": "^7.11.0" + }, + "dependencies": { + "@babel/types": { + "version": "7.11.5", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.11.5.tgz", + "integrity": "sha512-bvM7Qz6eKnJVFIn+1LPtjlBFPVN5jNDc1XmN15vWe7Q3DPBufWWsLiIvUu7xW87uTG6QoggpIDnUgLQvPheU+Q==", + "dev": true, + "requires": { + "@babel/helper-validator-identifier": "^7.10.4", + "lodash": "^4.17.19", + "to-fast-properties": "^2.0.0" + } + } + } + }, "@babel/helper-split-export-declaration": { "version": "7.4.4", "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.4.4.tgz", @@ -287,6 +309,12 @@ "@babel/types": "^7.4.4" } }, + "@babel/helper-validator-identifier": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.10.4.tgz", + "integrity": "sha512-3U9y+43hz7ZM+rzG24Qe2mufW5KhvFg/NhnNph+i9mgCtdTCtMJuI1TMkrIUiK7Ix4PYlRF9I5dhqaLYA/ADXw==", + "dev": true + }, "@babel/helper-wrap-function": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/@babel/helper-wrap-function/-/helper-wrap-function-7.2.0.tgz", @@ -388,6 +416,25 @@ "@babel/plugin-syntax-optional-catch-binding": "^7.2.0" } }, + "@babel/plugin-proposal-optional-chaining": { + "version": "7.11.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-optional-chaining/-/plugin-proposal-optional-chaining-7.11.0.tgz", + "integrity": "sha512-v9fZIu3Y8562RRwhm1BbMRxtqZNFmFA2EG+pT2diuU8PT3H6T/KXoZ54KgYisfOFZHV6PfvAiBIZ9Rcz+/JCxA==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.10.4", + "@babel/helper-skip-transparent-expression-wrappers": "^7.11.0", + "@babel/plugin-syntax-optional-chaining": "^7.8.0" + }, + "dependencies": { + "@babel/helper-plugin-utils": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.10.4.tgz", + "integrity": "sha512-O4KCvQA6lLiMU9l2eawBPMf1xPP8xPfB3iEQw150hOVTqj/rfXz0ThTb4HEzqQfs2Bmo5Ay8BzxfzVtBrr9dVg==", + "dev": true + } + } + }, "@babel/plugin-proposal-unicode-property-regex": { "version": "7.6.2", "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-unicode-property-regex/-/plugin-proposal-unicode-property-regex-7.6.2.tgz", @@ -453,6 +500,23 @@ "@babel/helper-plugin-utils": "^7.0.0" } }, + "@babel/plugin-syntax-optional-chaining": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-chaining/-/plugin-syntax-optional-chaining-7.8.3.tgz", + "integrity": "sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "dependencies": { + "@babel/helper-plugin-utils": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.10.4.tgz", + "integrity": "sha512-O4KCvQA6lLiMU9l2eawBPMf1xPP8xPfB3iEQw150hOVTqj/rfXz0ThTb4HEzqQfs2Bmo5Ay8BzxfzVtBrr9dVg==", + "dev": true + } + } + }, "@babel/plugin-syntax-typescript": { "version": "7.3.3", "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.3.3.tgz", diff --git a/cvat-ui/package.json b/cvat-ui/package.json index 3bad59a771ae..e0043a0e597f 100644 --- a/cvat-ui/package.json +++ b/cvat-ui/package.json @@ -1,6 +1,6 @@ { "name": "cvat-ui", - "version": "1.9.8", + "version": "1.9.9", "description": "CVAT single-page application", "main": "src/index.tsx", "scripts": { @@ -16,6 +16,7 @@ "devDependencies": { "@babel/core": "^7.6.0", "@babel/plugin-proposal-class-properties": "^7.5.5", + "@babel/plugin-proposal-optional-chaining": "^7.11.0", "@babel/preset-env": "^7.6.0", "@babel/preset-react": "^7.0.0", "@babel/preset-typescript": "^7.6.0", diff --git a/cvat-ui/webpack.config.js b/cvat-ui/webpack.config.js index c7423db831ed..89caff6ed26a 100644 --- a/cvat-ui/webpack.config.js +++ b/cvat-ui/webpack.config.js @@ -39,9 +39,13 @@ module.exports = { use: { loader: 'babel-loader', options: { - plugins: ['@babel/plugin-proposal-class-properties', ['import', { - 'libraryName': 'antd', - }]], + plugins: [ + '@babel/plugin-proposal-class-properties', + '@babel/plugin-proposal-optional-chaining', + ['import', { + 'libraryName': 'antd', + }] + ], presets: [ ['@babel/preset-env', { targets: '> 2.5%', // https://github.com/browserslist/browserslist From 1ae57ba1bdd239460de8cce68427f11a669c89a4 Mon Sep 17 00:00:00 2001 From: Dmitry Kalinin Date: Wed, 30 Sep 2020 22:00:17 +0300 Subject: [PATCH 09/55] Added CHANGELOG.md --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index a8c70e3d4235..4349cfc9cf13 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -19,6 +19,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 It supports regular navigation, searching a frame according to annotations filters and searching the nearest frame without any annotations () - MacOS users notes in CONTRIBUTING.md +- Optional chaining plugin for cvat-canvas and cvat-ui () ### Changed - UI models (like DEXTR) were redesigned to be more interactive () From 3db94bd4172d54a763172eb3a28e5f02ede5b39c Mon Sep 17 00:00:00 2001 From: Dmitry Kalinin Date: Fri, 2 Oct 2020 09:00:48 +0300 Subject: [PATCH 10/55] wip --- .../create-project-page/styles.scss | 2 +- .../basic-configuration-form.tsx | 6 ++++- .../create-task-page/create-task-content.tsx | 26 +++++++++++++++++-- .../create-task-page/create-task-page.tsx | 4 +-- .../src/components/project-page/styles.scss | 15 ++++++++++- .../components/projects-page/actions-menu.tsx | 2 +- .../src/components/projects-page/styles.scss | 15 ++++++++++- 7 files changed, 61 insertions(+), 9 deletions(-) diff --git a/cvat-ui/src/components/create-project-page/styles.scss b/cvat-ui/src/components/create-project-page/styles.scss index 6cb9222b8b09..c12e4c546acf 100644 --- a/cvat-ui/src/components/create-project-page/styles.scss +++ b/cvat-ui/src/components/create-project-page/styles.scss @@ -35,4 +35,4 @@ width: 120px; } } -} \ No newline at end of file +} diff --git a/cvat-ui/src/components/create-task-page/basic-configuration-form.tsx b/cvat-ui/src/components/create-task-page/basic-configuration-form.tsx index af418442a6be..08e999e56d84 100644 --- a/cvat-ui/src/components/create-task-page/basic-configuration-form.tsx +++ b/cvat-ui/src/components/create-task-page/basic-configuration-form.tsx @@ -8,6 +8,7 @@ import Form, { FormComponentProps } from 'antd/lib/form/Form'; export interface BaseConfiguration { name: string; + project_id: string; } type Props = FormComponentProps & { @@ -26,6 +27,7 @@ class BasicConfigurationForm extends React.PureComponent { if (!error) { onSubmit({ name: values.name, + project_id: values.project_id, }); resolve(); } else { @@ -71,4 +73,6 @@ class BasicConfigurationForm extends React.PureComponent { } } -export default Form.create()(BasicConfigurationForm); +export default Form.create({ + onFieldsChange: (props, fields,) => console.log(props, fields, allFields), +})(BasicConfigurationForm); diff --git a/cvat-ui/src/components/create-task-page/create-task-content.tsx b/cvat-ui/src/components/create-task-page/create-task-content.tsx index 7f3b24279446..c8aa0778ac7b 100644 --- a/cvat-ui/src/components/create-task-page/create-task-content.tsx +++ b/cvat-ui/src/components/create-task-page/create-task-content.tsx @@ -29,7 +29,7 @@ interface Props { onCreate: (data: CreateTaskData) => void; status: string; taskId: number | null; - projectId: number | null; + projectId: string | null; installedGit: boolean; } @@ -64,12 +64,19 @@ class CreateTaskContent extends React.PureComponent + + * + Labels: + + + Project labels will be used + + + ); + } + return ( * diff --git a/cvat-ui/src/components/create-task-page/create-task-page.tsx b/cvat-ui/src/components/create-task-page/create-task-page.tsx index 7303762f0caa..6457bc629c4e 100644 --- a/cvat-ui/src/components/create-task-page/create-task-page.tsx +++ b/cvat-ui/src/components/create-task-page/create-task-page.tsx @@ -33,8 +33,8 @@ export default function CreateTaskPage(props: Props & RouteComponentProps): JSX. let projectId = null; const params = new URLSearchParams(location.search); - if (params.has('projectId') && params.get('projectId')?.match(/^[1-9]+[0-9]*$/)) { - projectId = +(params.get('projectId') || 0); + if (params.get('projectId')?.match(/^[1-9]+[0-9]*$/)) { + projectId = params.get('projectId'); } useEffect(() => { diff --git a/cvat-ui/src/components/project-page/styles.scss b/cvat-ui/src/components/project-page/styles.scss index 26fb8ca37e8f..441949b98cbb 100644 --- a/cvat-ui/src/components/project-page/styles.scss +++ b/cvat-ui/src/components/project-page/styles.scss @@ -26,4 +26,17 @@ .cvat-project-page-tasks-bar { margin-bottom: 16px; -} \ No newline at end of file +} + +.ant-menu.cvat-project-actions-menu { + box-shadow: 0 0 17px rgba(0, 0, 0, 0.2); + + > li:hover { + background-color: $hover-menu-color; + } + + .ant-menu-submenu-title { + margin: 0; + width: 13em; + } +} diff --git a/cvat-ui/src/components/projects-page/actions-menu.tsx b/cvat-ui/src/components/projects-page/actions-menu.tsx index 82ed197c4fb6..a1a4d49c387a 100644 --- a/cvat-ui/src/components/projects-page/actions-menu.tsx +++ b/cvat-ui/src/components/projects-page/actions-menu.tsx @@ -33,7 +33,7 @@ export default function ProjectActionsMenuComponent(props: Props): JSX.Element { }; return ( - +
    Delete diff --git a/cvat-ui/src/components/projects-page/styles.scss b/cvat-ui/src/components/projects-page/styles.scss index 61a409cb7224..c760e91abe8c 100644 --- a/cvat-ui/src/components/projects-page/styles.scss +++ b/cvat-ui/src/components/projects-page/styles.scss @@ -86,4 +86,17 @@ align-self: flex-end; justify-content: center; } -} \ No newline at end of file +} + +.ant-menu.cvat-project-actions-menu { + box-shadow: 0 0 17px rgba(0, 0, 0, 0.2); + + > li:hover { + background-color: $hover-menu-color; + } + + .ant-menu-submenu-title { + margin: 0; + width: 13em; + } +} From 9fe11432cfe802c44bc8be7e1bdf0956aab51c0e Mon Sep 17 00:00:00 2001 From: Dmitry Kalinin Date: Mon, 5 Oct 2020 02:21:48 +0300 Subject: [PATCH 11/55] Fixed task creation --- cvat-ui/src/actions/projects-actions.ts | 10 +-- cvat-ui/src/actions/tasks-actions.ts | 3 + .../create-project-content.tsx | 12 +++- .../basic-configuration-form.tsx | 19 +++-- .../create-task-page/create-task-content.tsx | 48 ++++++++++--- .../components/project-page/project-page.tsx | 12 ++-- cvat-ui/src/components/task-page/details.tsx | 2 +- cvat-ui/src/components/task-page/top-bar.tsx | 31 +++++++- cvat-ui/src/reducers/interfaces.ts | 3 + cvat-ui/src/reducers/notifications-reducer.ts | 71 +++++++++++++++++++ cvat-ui/src/reducers/projects-reducer.ts | 1 + cvat/apps/engine/serializers.py | 18 +++-- 12 files changed, 194 insertions(+), 36 deletions(-) diff --git a/cvat-ui/src/actions/projects-actions.ts b/cvat-ui/src/actions/projects-actions.ts index be91666df982..2264d11e352a 100644 --- a/cvat-ui/src/actions/projects-actions.ts +++ b/cvat-ui/src/actions/projects-actions.ts @@ -169,7 +169,7 @@ function updateProjectSuccess(project: any): AnyAction { return action; } -function updateProjectFailed(error: any, project: any): AnyAction { +function updateProjectFailed(project: any, error: any): AnyAction { const action = { type: ProjectsActionTypes.UPDATE_PROJECT_FAILED, payload: { @@ -187,18 +187,18 @@ ThunkAction, {}, {}, AnyAction> { try { dispatch(updateProject()); await projectInstance.save(); - const [project] = await cvat.tasks.get({ id: projectInstance.id }); + const [project] = await cvat.projects.get({ id: projectInstance.id }); dispatch(updateProjectSuccess(project)); } catch (error) { let project = null; try { - [project] = await cvat.project.get({ id: projectInstance.id }); + [project] = await cvat.projects.get({ id: projectInstance.id }); } catch (fetchError) { // FIXME: error length - dispatch(updateProjectFailed(error, projectInstance)); + dispatch(updateProjectFailed(projectInstance, error)); return; } - dispatch(updateProjectFailed(error, project)); + dispatch(updateProjectFailed(project, error)); } }; } diff --git a/cvat-ui/src/actions/tasks-actions.ts b/cvat-ui/src/actions/tasks-actions.ts index 4abb9dbdea89..61b627dde4cd 100644 --- a/cvat-ui/src/actions/tasks-actions.ts +++ b/cvat-ui/src/actions/tasks-actions.ts @@ -389,6 +389,9 @@ ThunkAction, {}, {}, AnyAction> { use_cache: data.advanced.useCache, }; + if (data.advanced.projectId) { + description.project_id = data.advanced.projectId; + } if (data.advanced.bugTracker) { description.bug_tracker = data.advanced.bugTracker; } diff --git a/cvat-ui/src/components/create-project-page/create-project-content.tsx b/cvat-ui/src/components/create-project-page/create-project-content.tsx index 73d7e8b6df06..c440fbff1ba5 100644 --- a/cvat-ui/src/components/create-project-page/create-project-content.tsx +++ b/cvat-ui/src/components/create-project-page/create-project-content.tsx @@ -2,7 +2,12 @@ // // SPDX-License-Identifier: MIT -import React, { useState, useRef, useEffect } from 'react'; +import React, { + useState, + useRef, + useEffect, + Component, +} from 'react'; import { useDispatch, useSelector } from 'react-redux'; import { useHistory } from 'react-router'; import { Col, Row } from 'antd/lib/grid'; @@ -16,6 +21,8 @@ import { CombinedState } from 'reducers/interfaces'; import LabelsEditor from 'components/labels-editor/labels-editor'; import { createProjectAsync } from 'actions/projects-actions'; +type NameFormRefType = Component, any, any> & WrappedFormUtils; + const ProjectNameEditor = Form.create()( (props: FormComponentProps): JSX.Element => { const { form } = props; @@ -42,7 +49,7 @@ const ProjectNameEditor = Form.create()( export default function CreateProjectContent(): JSX.Element { const [projectLabels, setProjectLabels] = useState([]); - const nameFormRef = useRef(null); + const nameFormRef = useRef(null); const dispatch = useDispatch(); const history = useHistory(); @@ -109,7 +116,6 @@ export default function CreateProjectContent(): JSX.Element { /> - * Labels: { @@ -27,7 +28,7 @@ class BasicConfigurationForm extends React.PureComponent { if (!error) { onSubmit({ name: values.name, - project_id: values.project_id, + projectId: values.projectId, }); resolve(); } else { @@ -59,7 +60,7 @@ class BasicConfigurationForm extends React.PureComponent { ) } Project Id (for develping only)}> - { getFieldDecorator('project_id', { + { getFieldDecorator('projectId', { rules: [{ pattern: /^[1-9]+[0-9]*$/, message: 'Please, specify valid positive number', @@ -74,5 +75,15 @@ class BasicConfigurationForm extends React.PureComponent { } export default Form.create({ - onFieldsChange: (props, fields,) => console.log(props, fields, allFields), + onFieldsChange: (props, fields) => { + const values: {[name: string]: string} = {}; + for (const field of Object.keys(fields)) { + if (!(fields[field].dirty || fields[field].errors)) { + values[field] = fields[field].value; + } + } + if (values) { + props.onChange(values); + } + }, })(BasicConfigurationForm); diff --git a/cvat-ui/src/components/create-task-page/create-task-content.tsx b/cvat-ui/src/components/create-task-page/create-task-content.tsx index c8aa0778ac7b..0f62c156eb0c 100644 --- a/cvat-ui/src/components/create-task-page/create-task-content.tsx +++ b/cvat-ui/src/components/create-task-page/create-task-content.tsx @@ -38,7 +38,7 @@ type State = CreateTaskData; const defaultState = { basic: { name: '', - projectId: null, + projectId: '', }, advanced: { zOrder: false, @@ -67,9 +67,11 @@ class CreateTaskContent extends React.PureComponent { - const { labels } = this.state; - return !!labels.length; + private validateLabelsOrProject = (): boolean => { + const { + basic: { + projectId, + }, + labels, + } = this.state; + return !!labels.length || !!projectId; }; private validateFiles = (): boolean => { @@ -123,6 +130,20 @@ class CreateTaskContent extends React.PureComponent { + for (const value of Object.keys(values)) { + if (value === 'projectId') { + this.setState((prevState) => ({ + ...prevState, + basic: { + ...prevState.basic, + projectId: values[value], + }, + })); + } + } + }; + private handleSubmitBasicConfiguration = (values: BaseConfiguration): void => { this.setState({ basic: { ...values }, @@ -136,10 +157,10 @@ class CreateTaskContent extends React.PureComponent { - if (!this.validateLabels()) { + if (!this.validateLabelsOrProject()) { notification.error({ message: 'Could not create a task', - description: 'A task must contain at least one label', + description: 'A task must contain at least one label or belong to some project', }); return; } @@ -179,6 +200,7 @@ class CreateTaskContent extends React.PureComponent { this.basicConfigurationComponent = component; } } + onChange={this.handleFormDataChange} onSubmit={this.handleSubmitBasicConfiguration} /> @@ -186,8 +208,12 @@ class CreateTaskContent extends React.PureComponent().id; const dispatch = useDispatch(); const history = useHistory(); - const gettingQueryId = useSelector((state: CombinedState) => state.projects.gettingQuery.id); const projects = useSelector((state: CombinedState) => state.projects.current); + const projectsFetching = useSelector((state: CombinedState) => state.projects.fetching); const deletes = useSelector((state: CombinedState) => state.projects.activities.deletes); const taskDeletes = useSelector((state: CombinedState) => state.tasks.activities.deletes); const tasksActiveInferences = useSelector((state: CombinedState) => state.models.inferences); @@ -38,8 +38,7 @@ export default function TaskPageComponent(): JSX.Element { const filteredProjects = projects.filter( (project) => project.instance.id === id, ); - const project = filteredProjects[0] || (gettingQueryId === id || Number.isNaN(id) - ? undefined : null); + const project = filteredProjects[0]; const deleteActivity = project && id in deletes ? deletes[id] : null; useEffect(() => { @@ -58,13 +57,13 @@ export default function TaskPageComponent(): JSX.Element { history.push('/projects'); } - if (project === null) { + if (projectsFetching) { return ( ); } - if (typeof (project) === 'undefined') { + if (!project) { return ( - {/* */} {project.instance.tasks.map((task: any) => ( { this.renderDatasetRepository() } - { this.renderLabelsEditor() } + { !taskInstance.project_id && this.renderLabelsEditor() }
    diff --git a/cvat-ui/src/components/task-page/top-bar.tsx b/cvat-ui/src/components/task-page/top-bar.tsx index 12fb78404991..51c6f121e30f 100644 --- a/cvat-ui/src/components/task-page/top-bar.tsx +++ b/cvat-ui/src/components/task-page/top-bar.tsx @@ -3,6 +3,7 @@ // SPDX-License-Identifier: MIT import React from 'react'; +import { useHistory } from 'react-router'; import { Row, Col } from 'antd/lib/grid'; import Button from 'antd/lib/button'; import Dropdown from 'antd/lib/dropdown'; @@ -20,10 +21,38 @@ export default function DetailsComponent(props: DetailsComponentProps): JSX.Elem const { taskInstance } = props; const { id } = taskInstance; + const history = useHistory(); + return ( - {`Task details #${id}`} + { taskInstance.project_id ? ( + + ): ( + + )} + + + + + {`Task details #${id}`} + + + project ${projectId}`, + reason: action.payload.error.toString(), + }, + }, + }, + }; + } + case ProjectsActionTypes.DELETE_PROJECT_FAILED: { + const { projectId } = action.payload; + return { + ...state, + errors: { + ...state.errors, + projects: { + ...state.errors.projects, + updating: { + message: 'Could not delete ' + + `project ${projectId}`, + reason: action.payload.error.toString(), + }, + }, + }, + }; + } case FormatsActionTypes.GET_FORMATS_FAILED: { return { ...state, diff --git a/cvat-ui/src/reducers/projects-reducer.ts b/cvat-ui/src/reducers/projects-reducer.ts index f1946aee5613..f31dea78ddc2 100644 --- a/cvat-ui/src/reducers/projects-reducer.ts +++ b/cvat-ui/src/reducers/projects-reducer.ts @@ -14,6 +14,7 @@ const defaultState: ProjectsState = { fetching: false, count: 0, current: [], + taskPreviews: {}, gettingQuery: { page: 1, id: null, diff --git a/cvat/apps/engine/serializers.py b/cvat/apps/engine/serializers.py index 3ef5ea7a9721..71e192df65c8 100644 --- a/cvat/apps/engine/serializers.py +++ b/cvat/apps/engine/serializers.py @@ -50,7 +50,8 @@ def update_instance(validated_data, parent_instance): instance = dict() if isinstance(parent_instance, models.Project): instance['project'] = parent_instance - logger = slogger.project[parent_instance.id] + # FIXME: + logger = slogger.glob else: instance['task'] = parent_instance logger = slogger.task[parent_instance.id] @@ -62,7 +63,7 @@ def update_instance(validated_data, parent_instance): logger.info("{} label was updated".format(db_label.name)) if not validated_data.get('color', None): label_names = [l.name for l in - models.Label.objects.filter(task_id=instance.id).exclude(id=db_label.id).order_by('id') + models.Label.objects.filter(**instance).exclude(id=db_label.id).order_by('id') ] db_label.color = get_label_color(db_label.name, label_names) else: @@ -316,6 +317,12 @@ def create(self, validated_data): db_task.save() return db_task + def to_representation(self, instance): + response = super().to_representation(instance) + if instance.project_id: + response["labels"] = LabelSerializer(many=True).to_representation(instance.project.label_set) + return response + # pylint: disable=no-self-use def update(self, instance, validated_data): instance.name = validated_data.get('name', instance.name) @@ -333,13 +340,16 @@ def update(self, instance, validated_data): return instance def validate_labels(self, value): - if not value: - raise serializers.ValidationError('Label set must not be empty') label_names = [label['name'] for label in value] if len(label_names) != len(set(label_names)): raise serializers.ValidationError('All label names must be unique for the task') return value + def validate(self, value): + if not value["labels"] or value["project_id"]: + raise serializers.ValidationError('Label set or project_id must be present') + return value + class ProjectSerializer(serializers.ModelSerializer): labels = LabelSerializer(many=True, source='label_set', partial=True) From 23fe77dfeecca55927243bac9fc6d3832f908d75 Mon Sep 17 00:00:00 2001 From: Dmitry Kalinin Date: Mon, 5 Oct 2020 14:42:41 +0300 Subject: [PATCH 12/55] Added project preview image --- cvat-core/src/session.js | 7 +++++-- cvat-ui/src/actions/projects-actions.ts | 21 +++++++++++++++++++ cvat-ui/src/actions/tasks-actions.ts | 4 ++-- .../components/project-page/project-page.tsx | 4 ++-- .../components/projects-page/project-item.tsx | 11 +++++++++- .../src/components/projects-page/styles.scss | 11 ++++++++++ cvat-ui/src/components/task-page/details.tsx | 2 +- cvat-ui/src/components/task-page/top-bar.tsx | 8 +++---- cvat-ui/src/reducers/projects-reducer.ts | 10 +++++++++ cvat/apps/dataset_manager/task.py | 3 ++- cvat/apps/engine/serializers.py | 8 ++++--- 11 files changed, 73 insertions(+), 16 deletions(-) diff --git a/cvat-core/src/session.js b/cvat-core/src/session.js index fc29424481c5..0028dbca068d 100644 --- a/cvat-core/src/session.js +++ b/cvat-core/src/session.js @@ -928,13 +928,13 @@ }, }, /** - * @name project_id + * @name projectId * @type {integer} * @memberof module:API.cvat.classes.Task * @readonly * @instance */ - project_id: { + projectId: { get: () => data.project_id, }, /** @@ -1949,6 +1949,9 @@ if (typeof (this.overlap) !== 'undefined') { taskSpec.overlap = this.overlap; } + if (typeof (this.projectId) !== 'undefined') { + taskSpec.project_id = this.projectId; + } const taskDataSpec = { client_files: this.clientFiles, diff --git a/cvat-ui/src/actions/projects-actions.ts b/cvat-ui/src/actions/projects-actions.ts index 2264d11e352a..df2f062683f0 100644 --- a/cvat-ui/src/actions/projects-actions.ts +++ b/cvat-ui/src/actions/projects-actions.ts @@ -13,6 +13,7 @@ const cvat = getCore(); export enum ProjectsActionTypes { UPDATE_PROJECTS_GETTING_QUERY = 'UPDATE_PROJECTS_GETTING_QUERY', + UPDATE_TASK_PREVIEW_IMAGE = 'UPDATE_TASK_PREVIEW_IMAGE', GET_PROJECTS = 'GET_PROJECTS', GET_PROJECTS_SUCCESS = 'GET_PROJECTS_SUCCESS', GET_PROJECTS_FAILED = 'GET_PROJECTS_FAILED', @@ -38,6 +39,18 @@ export function updateProjectsGettingQuery(query: Partial): AnyAc return action; } +function updateTaskImagePreview(taskId: number, image: string): AnyAction { + const action = { + type: ProjectsActionTypes.UPDATE_TASK_PREVIEW_IMAGE, + payload: { + taskId, + image, + }, + }; + + return action; +} + function getProjects(): AnyAction { const action = { type: ProjectsActionTypes.GET_PROJECTS, @@ -99,6 +112,14 @@ ThunkAction, {}, {}, AnyAction> { const array = Array.from(result); dispatch(getProjectsSuccess(array, result.count)); + + for (const project of array) { + for (const task of (project as any).tasks) { + (task as any).frames.preview().then((image: string) => { + dispatch(updateTaskImagePreview(task.id, image)); + }); + } + } }; } diff --git a/cvat-ui/src/actions/tasks-actions.ts b/cvat-ui/src/actions/tasks-actions.ts index 61b627dde4cd..2aa7b02e2603 100644 --- a/cvat-ui/src/actions/tasks-actions.ts +++ b/cvat-ui/src/actions/tasks-actions.ts @@ -389,8 +389,8 @@ ThunkAction, {}, {}, AnyAction> { use_cache: data.advanced.useCache, }; - if (data.advanced.projectId) { - description.project_id = data.advanced.projectId; + if (data.basic.projectId) { + description.project_id = +data.basic.projectId; } if (data.advanced.bugTracker) { description.bug_tracker = data.advanced.bugTracker; diff --git a/cvat-ui/src/components/project-page/project-page.tsx b/cvat-ui/src/components/project-page/project-page.tsx index 19466bc519a0..22a4efc316f6 100644 --- a/cvat-ui/src/components/project-page/project-page.tsx +++ b/cvat-ui/src/components/project-page/project-page.tsx @@ -33,7 +33,7 @@ export default function ProjectPageComponent(): JSX.Element { const deletes = useSelector((state: CombinedState) => state.projects.activities.deletes); const taskDeletes = useSelector((state: CombinedState) => state.tasks.activities.deletes); const tasksActiveInferences = useSelector((state: CombinedState) => state.models.inferences); - const image = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAIAAAD8GO2jAAAJAElEQVRIiSWWSZNUV3pAv++785tyqiGBQlBQGnALtSRaEXb0xl44whv/Aa/8Nx3hfduhaNo9gBANSFRBVWVWDpWZ7+Wb7r2fF5z12Z+Do3Hu0oyZ99W+qVsAVpKEFEIIImKOn+AIABhjRELnTNd5ACQCKRVzBAAAYGbfe0BkZu8jMMcYpRDCOEMkvPdt03MMfR9CYCE4hACAgADMAAwAMTIiWKsBGAERyRillOTIMcZPUoyhqRtm5ggMJH3wdbUXQnatRwASRAKVEkTkPXofgAEQmREAECMAMGCMHEJHQYQQYmRgCIRWICAZa/NiJIQod+Vut5PHx5MI0NStczQYpNpIQvQ+MkOaJt77vvchxBCZCKRAa1SeJt7Hum6FoOA5+iACT+7cf/Tw5Mc//bWJwjmrpOi6XlSlfPrdsxjDx4v3XVNleU4CEQUw1PsSuDfWkZBSCCnBKEwTPT08mB6Oi0wLQkRSUtlE7ffq7a/7+c1KKdXsa0XOGjEeJ2lK0tkscnBJooS8e+eRcanWdnI4LcvVdn2J2PfNjkMLQhqtsiw/efB0kNuuvgzcIActNYvi1fnsv/77D9V6ORkOT598Tcaub6+QdJppGQE6H5AEc1jfXscV922MvlfauPR4NDnY71dXF+/Kbc9MA5+s/3JeltvN7UISZqk+nB6hjD/+4bmJ8Z9+//uzL55sW/6/l3/q+goAEFBaYwBBSlnHvm02xkitbKjLIYU/v/1FZKN7Dx5HdeQGABwDg5JOGswHsqn3myqU57v15nx9c/Uvz57+8P0PpadXL/7nZnEuRCQSEEFeX773MXrfO2edM0lisiyPjX9y9uTzsy9//Ps7ERrq9z7Cg9NTrZQPYX59ud/snNF6NNhX+77aH01G9+8co9Bv3r5eri6MRgbBzDGyGI0NRy+VSVNb5Fmep8NhPhpPDqeP7t49QYHRt+vVfPbxwrfNt7/9trxdPf/xf6tymybpaDSBGLebddO1qHXl4+zmY4w7JAwxxsAxsvjy69MiH1ibJIkbjfI8z4o8G44GKBUjLecff3rx1922nE7vfP+7f/z2ux/SxM1mV4Kga+vJoHj2u2cAcPnxkhRvt8sPHy925b5pPTMjIsconn73ZeJSqUxibVHkaZrkeTYsXJao1WJ9cXElhXn86OzpN7/9/IsvV+tVXZfj8UiQrMry/fu3bVenVvddzaQX62qxKsttu900VVkrKYQU+B//+e9pkgcWWtLhZFAUWZFno2HuXPr61fvdtn9weqa1Kctd3TT1vmWArmtCBEHyzeuXP//8UhEz8KoKSTFCov12U96uq3KnNd69dyCyLLu9Lau67/uwud3N56vC0Denx5mBalf2gUgmdevn8+s3f3+9XC5i9GVVNW3NMfR9t14v17eb2015fXXpuwYRAKHeV21TN02rFMlhMaz2Tb2tZAQwWmvje6x3rRDglNguL87Pr6cnD6uyms1ulBTWGmudVnK7We82S6NlCGFXNoQy9P1+u1FaCyQiCgC3tzv52cNTKaXRNnE2S02epWniFr1WaKIS3p8nVgfflW2c3H1cbW93ZXt0dAdi/3G7mc9mTdcmabqv+973SZq7NIEYBYkQegTkwDLNiyRxiXWJNc6ZNMlckijstNYEVBQDZfJ12Z6/fxcjt01b5KmxiW9KDD72/b5utXExBCVpOB4lad61bV2Vfe+cS2MIcjyYZFlqtHLOWOesSxWRJi21Aa6HRdE0/TDPTj+7P79ZHB8dTY+PMLb79Y3FMMmsMGq5qYVSD09OiuHEx6iYgR0KMiaVhHIymaZ5YSQaJaQUSkurlBaSCTDynbt3FotF1YSvzh6e3Dsp97WRsPh4vrz+AByLPFMo9/42SYtHZ1+0bXszm/VtI4jTxALgYDCQeV6kRaaJFXiJqI2T1hEiQBRSmjRVxWgxv51MJlJpIBoPB+9+evH8Ofz07tcuQISQDUbHR9Pp9KQqN3VVKopZZtrOdz2naSLTNLFaCO6t1No4klpIgQjMzIgCiKT8sNj88eUb5DAZD7/66vOL1Xrr4ej+o/WuWlzND49H//Cbp4eH06urD82+XM37vqutTWxitXHS5onFqGRiXSakQvqUe0Tk4DsIvaRaSUqybLVa//ph9s//+m/JYPKXF6+Gw5HSzikzHI6nR9MnT75WSl1e/Lpe3+y2q2I4HoyOWibppMyMQW1JCiRBSMyePxUePBMJZRQJjeSkuF7On//x+ffPnj3+/DdVVargh6PJaDSxSbrZ7TjEpi7L6rbuar9eSKUJG1m4XNskAEslBRFwAAZgjgyIMjKn2WA0HH04v/jwy9s+eOja0PrRwTGrtNptrDNpku2229n15WI+65o6L4bK2BCilAoYpXIZSSEFaamIgTlGCBxCjIGDD8Eb4+6cfDZfrNu63G421X4PEB7cn6YHzetf6tcvX8CDx8Yly/l1VW6tNdPpSQi+aZqm6ZqmkkwsJAkhhVZIAgBl9DH0se8gepQEQmSZk5KOp/emd+5td9vXr/52cHgwUeZ+ksRB1tfb4XBICHmejw8PlqslIcYYq7KezWYS+pophCiIe6UUCgEcJQFoKcg1XRNiL6Fvqtv5cjkcjhNrjVVv3v08v77Jk/zg4Mi4LMkGjHI0yMeHhygNMEiSy9V6tSmlkk6gQiSMEPsYAxMHKUQMje86I20ATDPI8iJezp2z44OJSzJnEo4hcmCA3W5Xt9w2LQ4L3+5zK1ForW2eZ5NxIQGRgZEECgItEQGDZ4TIBJEJAITQLisGYyKSUh1MJsaoLM0Pp3eddRT5/S/n2+pSKtKJpsQZIZUy2lqjzbibSACQSggpPn0lCSIhCCP3TEISAnMUHJ1VxhhjdOq00UocH1lnYwxt3SDh9eV5Psiy9BuTFa7QRltJGPu+aVoJwIQkhECE4DsOAZWIfcMcZZIzAMUI3b7d1ygEECD3kjmTrAvXM21JIGHbt6fTszzNlTYkrRAKOPYYMEQJhCyQiQmZJHCMvq9jAGsKRGIGlkIZ6yNK7UIIfduZ4cgYFX0o62Y2W253pVT68dkXw9FE25SF6pq2bRrf9wzw/8noFrOt3BOXAAAAAElFTkSuQmCC'; + const previewImages = useSelector((state: CombinedState) => state.projects.taskPreviews); const filteredProjects = projects.filter( (project) => project.instance.id === id, @@ -103,7 +103,7 @@ export default function ProjectPageComponent(): JSX.Element { cancelAutoAnnotation={() => { dispatch(cancelInferenceAsync(task.id)); }} - previewImage={image} + previewImage={previewImages[task.id]} taskInstance={task} /> ))} diff --git a/cvat-ui/src/components/projects-page/project-item.tsx b/cvat-ui/src/components/projects-page/project-item.tsx index 042010cbc0a5..8226be8fb3b5 100644 --- a/cvat-ui/src/components/projects-page/project-item.tsx +++ b/cvat-ui/src/components/projects-page/project-item.tsx @@ -29,6 +29,13 @@ export default function ProjectItemComponent(props: Props): JSX.Element { const deletes = useSelector((state: CombinedState) => state.projects.activities.deletes); const deleted = instance.id in deletes ? deletes[instance.id] : false; + let projectPreview = null; + if (instance.tasks.length) { + projectPreview = useSelector((state: CombinedState) => ( + state.projects.taskPreviews[instance.tasks[0].id] + )); + } + const onOpenProject = (): void => { history.push(`/projects/${instance.id}`); }; @@ -42,7 +49,9 @@ export default function ProjectItemComponent(props: Props): JSX.Element { return ( } + cover={projectPreview ? ( + Preview + ) : ()} size='small' style={style} className='cvat-projects-project-item-card' diff --git a/cvat-ui/src/components/projects-page/styles.scss b/cvat-ui/src/components/projects-page/styles.scss index c760e91abe8c..1e724b731e2d 100644 --- a/cvat-ui/src/components/projects-page/styles.scss +++ b/cvat-ui/src/components/projects-page/styles.scss @@ -100,3 +100,14 @@ width: 13em; } } + +.cvat-projects-project-item-card { + .ant-empty { + margin: 8px; + } + + img { + height: 100%; + max-height: 146px; + } +} \ No newline at end of file diff --git a/cvat-ui/src/components/task-page/details.tsx b/cvat-ui/src/components/task-page/details.tsx index 75a8913b26c2..76c678995fdf 100644 --- a/cvat-ui/src/components/task-page/details.tsx +++ b/cvat-ui/src/components/task-page/details.tsx @@ -372,7 +372,7 @@ export default class DetailsComponent extends React.PureComponent { this.renderDatasetRepository() } - { !taskInstance.project_id && this.renderLabelsEditor() } + { !taskInstance.projectId && this.renderLabelsEditor() }
    diff --git a/cvat-ui/src/components/task-page/top-bar.tsx b/cvat-ui/src/components/task-page/top-bar.tsx index 51c6f121e30f..f3a20f8f08e4 100644 --- a/cvat-ui/src/components/task-page/top-bar.tsx +++ b/cvat-ui/src/components/task-page/top-bar.tsx @@ -26,16 +26,16 @@ export default function DetailsComponent(props: DetailsComponentProps): JSX.Elem return ( - { taskInstance.project_id ? ( + { taskInstance.projectId ? ( - ): ( + ) : ( ); diff --git a/cvat-ui/src/components/create-task-page/create-task-content.tsx b/cvat-ui/src/components/create-task-page/create-task-content.tsx index 8316cab2c880..506c65378c5b 100644 --- a/cvat-ui/src/components/create-task-page/create-task-content.tsx +++ b/cvat-ui/src/components/create-task-page/create-task-content.tsx @@ -70,9 +70,7 @@ class CreateTaskContent extends React.PureComponent - Labels: + Project: ([]); @@ -30,12 +31,13 @@ export default function ProjectSearchField(props: Props): JSX.Element { core.projects.searchNames(searchValue).then((result: Project[]) => { if (result) { setProjects(result); - setSearchPhrase(searchValue); } }); } else { setProjects([]); } + setSearchPhrase(searchValue); + onSelect(null); }; const handleFocus = (): void => { @@ -48,6 +50,11 @@ export default function ProjectSearchField(props: Props): JSX.Element { } }; + const handleSelect = (_value: SelectValue): void => { + setSearchPhrase(projects.filter((proj) => proj.id === +_value)[0].name); + onSelect(_value ? +_value : null); + }; + useEffect(() => { if (value && !projects.filter((project) => project.id === value).length) { core.projects.get({ id: value }).then((result: Project[]) => { @@ -56,6 +63,8 @@ export default function ProjectSearchField(props: Props): JSX.Element { id: project.id, name: project.name, }]); + setSearchPhrase(project.name); + onSelect(project.id); }); } }, [value]); @@ -65,7 +74,7 @@ export default function ProjectSearchField(props: Props): JSX.Element { value={searchPhrase} placeholder='Select project' onSearch={handleSearch} - onSelect={(_value) => onSelect(_value ? +_value : null)} + onSelect={handleSelect} className='cvat-project-search-field' onFocus={handleFocus} > diff --git a/cvat/apps/engine/serializers.py b/cvat/apps/engine/serializers.py index 09d9803e445f..d9160fcc6b86 100644 --- a/cvat/apps/engine/serializers.py +++ b/cvat/apps/engine/serializers.py @@ -380,7 +380,7 @@ def create(self, validated_data): if not label.get('color', None): label['color'] = get_label_color(label['name'], label_names) label_names.append(label['name']) - db_label = models.Label.objects.create(prject=db_project, **label) + db_label = models.Label.objects.create(project=db_project, **label) for attr in attributes: models.AttributeSpec.objects.create(label=db_label, **attr) From bc9b982bd6ef4bc22b9cf2ea8b869da1e80f615a Mon Sep 17 00:00:00 2001 From: Dmitry Kalinin Date: Mon, 12 Oct 2020 09:52:20 +0300 Subject: [PATCH 19/55] autodeleting not completed search field --- .../create-task-page/project-search-field.tsx | 23 +++++++++++-------- 1 file changed, 13 insertions(+), 10 deletions(-) diff --git a/cvat-ui/src/components/create-task-page/project-search-field.tsx b/cvat-ui/src/components/create-task-page/project-search-field.tsx index 27e2ddc66ef4..4b9cd303c375 100644 --- a/cvat-ui/src/components/create-task-page/project-search-field.tsx +++ b/cvat-ui/src/components/create-task-page/project-search-field.tsx @@ -40,14 +40,17 @@ export default function ProjectSearchField(props: Props): JSX.Element { onSelect(null); }; - const handleFocus = (): void => { - if (!projects.length) { + const handleFocus = (open: boolean): void => { + if (!projects.length && open) { core.projects.searchNames().then((result: Project[]) => { if (result) { setProjects(result); } }); } + if (!open && !value && searchPhrase) { + setSearchPhrase(''); + } }; const handleSelect = (_value: SelectValue): void => { @@ -76,13 +79,13 @@ export default function ProjectSearchField(props: Props): JSX.Element { onSearch={handleSearch} onSelect={handleSelect} className='cvat-project-search-field' - onFocus={handleFocus} - > - {projects.map((proj) => ( - - {proj.name} - - ))} - + onDropdownVisibleChange={handleFocus} + dataSource={ + projects.map((proj) => ({ + value: proj.id.toString(), + text: proj.name, + })) + } + /> ); } From 0c119472436067606d7a176b02d52faec79feb1c Mon Sep 17 00:00:00 2001 From: Dmitry Kalinin Date: Fri, 16 Oct 2020 01:42:54 +0300 Subject: [PATCH 20/55] trying to fix tests --- .gitignore | 4 + .../create-task-page/create-task-content.tsx | 103 ++++++------------ cvat/apps/engine/log.py | 9 +- .../migrations/0031_projects_adjastment.py | 4 - cvat/apps/engine/models.py | 2 + cvat/apps/engine/serializers.py | 9 +- .../case_2_register_user_change_pass.js | 4 +- .../case_4_assign_taks_job_users.js | 17 +-- .../issue_1498_message_ui_raw_labels_wrong.js | 1 + .../issue_1568_cuboid_dump_annotation.js | 4 +- .../issue_1599_ch_user_registration.js | 2 +- .../issue_1599_pl_user_registration.js | 2 +- .../integration/issue_1810_login_logout.js | 2 +- tests/cypress/support/commands.js | 3 + 14 files changed, 72 insertions(+), 94 deletions(-) diff --git a/.gitignore b/.gitignore index cbe91f69a9c3..e985f4efe802 100644 --- a/.gitignore +++ b/.gitignore @@ -30,3 +30,7 @@ yarn-debug.log* yarn-error.log* .DS_Store + +#Ignore Cypress tests temp files +/tests/cypress/fixtures +/tests/cypress/screenshots diff --git a/cvat-ui/src/components/create-task-page/create-task-content.tsx b/cvat-ui/src/components/create-task-page/create-task-content.tsx index 506c65378c5b..014c028e0955 100644 --- a/cvat-ui/src/components/create-task-page/create-task-content.tsx +++ b/cvat-ui/src/components/create-task-page/create-task-content.tsx @@ -75,20 +75,10 @@ class CreateTaskContent extends React.PureComponent history.push(`/tasks/${taskId}`)} - > - Open task - - ); + const btn = ; notification.info({ message: 'The task has been created', @@ -109,10 +99,7 @@ class CreateTaskContent extends React.PureComponent { - const { - projectId, - labels, - } = this.state; + const { projectId, labels } = this.state; return !!labels.length || !!projectId; }; @@ -121,9 +108,7 @@ class CreateTaskContent extends React.PureComponent acc + files[key].length, 0, - ); + const totalLen = Object.keys(files).reduce((acc, key) => acc + files[key].length, 0); return !!totalLen; }; @@ -163,7 +148,8 @@ class CreateTaskContent extends React.PureComponent { if (this.advancedConfigurationComponent) { return this.advancedConfigurationComponent.submit(); @@ -172,10 +158,12 @@ class CreateTaskContent extends React.PureComponent { resolve(); }); - }).then((): void => { + }) + .then((): void => { const { onCreate } = this.props; onCreate(this.state); - }).catch((error: Error): void => { + }) + .catch((error: Error): void => { notification.error({ message: 'Could not create a task', description: error.toString(), @@ -187,9 +175,9 @@ class CreateTaskContent extends React.PureComponent { this.basicConfigurationComponent = component; } - } + wrappedComponentRef={(component: any): void => { + this.basicConfigurationComponent = component; + }} onSubmit={this.handleSubmitBasicConfiguration} /> @@ -205,26 +193,19 @@ class CreateTaskContent extends React.PureComponentProject: - + ); } private renderLabelsBlock(): JSX.Element { - const { - projectId, - labels, - } = this.state; + const { projectId, labels } = this.state; if (projectId) { return ( <> - * Labels: @@ -240,13 +221,11 @@ class CreateTaskContent extends React.PureComponentLabels: { - this.setState({ - labels: newLabels, - }); - } - } + onSubmit={(newLabels): void => { + this.setState({ + labels: newLabels, + }); + }} /> ); @@ -258,9 +237,9 @@ class CreateTaskContent extends React.PureComponent* Select files: { this.fileManagerContainer = container; } - } + ref={(container: any): void => { + this.fileManagerContainer = container; + }} withRemote /> @@ -272,19 +251,12 @@ class CreateTaskContent extends React.PureComponent - Advanced configuration - } - > + Advanced configuration}> { - this.advancedConfigurationComponent = component; - } - } + wrappedComponentRef={(component: any): void => { + this.advancedConfigurationComponent = component; + }} onSubmit={this.handleSubmitAdvancedConfiguration} /> @@ -303,22 +275,15 @@ class CreateTaskContent extends React.PureComponentBasic configuration - { this.renderBasicBlock() } - { this.renderProjectBlock() } - { this.renderLabelsBlock() } - { this.renderFilesBlock() } - { this.renderAdvancedBlock() } + {this.renderBasicBlock()} + {this.renderProjectBlock()} + {this.renderLabelsBlock()} + {this.renderFilesBlock()} + {this.renderAdvancedBlock()} - - {loading ? : null} - + {loading ? : null} - diff --git a/cvat/apps/engine/log.py b/cvat/apps/engine/log.py index b3612eefb6a6..781b6dfb0234 100644 --- a/cvat/apps/engine/log.py +++ b/cvat/apps/engine/log.py @@ -29,9 +29,9 @@ def __init__(self): self._storage = dict() def __getitem__(self, pid): - """ + ''' Get ceratain storage object for some project - """ + ''' if pid not in self._storage: self._storage[pid] = self._create_project_logger(pid) return self._storage[pid] @@ -86,6 +86,9 @@ def __init__(self): self._storage = dict() def __getitem__(self, pid): + ''' + Get logger for exact task by id + ''' if pid not in self._storage: self._storage[pid] = self._create_client_logger(pid) return self._storage[pid] @@ -129,7 +132,7 @@ def _get_task_logger(self, jid): return clogger.task[job.segment.task.id] class dotdict(dict): - """dot.notation access to dictionary attributes""" + '''dot.notation access to dictionary attributes''' __getattr__ = dict.get __setattr__ = dict.__setitem__ __delattr__ = dict.__delitem__ diff --git a/cvat/apps/engine/migrations/0031_projects_adjastment.py b/cvat/apps/engine/migrations/0031_projects_adjastment.py index 5d1bc9943f13..339427d5fcc2 100644 --- a/cvat/apps/engine/migrations/0031_projects_adjastment.py +++ b/cvat/apps/engine/migrations/0031_projects_adjastment.py @@ -11,10 +11,6 @@ class Migration(migrations.Migration): ] operations = [ - migrations.RemoveField( - model_name='project', - name='assignee', - ), migrations.AddField( model_name='label', name='project', diff --git a/cvat/apps/engine/models.py b/cvat/apps/engine/models.py index 79570f5f3010..5885455fdae6 100644 --- a/cvat/apps/engine/models.py +++ b/cvat/apps/engine/models.py @@ -143,6 +143,8 @@ class Project(models.Model): name = SafeCharField(max_length=256) owner = models.ForeignKey(User, null=True, blank=True, on_delete=models.SET_NULL, related_name="+") + assignee = models.ForeignKey(User, null=True, blank=True, + on_delete=models.SET_NULL, related_name="+") bug_tracker = models.CharField(max_length=2000, blank=True, default="") created_date = models.DateTimeField(auto_now_add=True) updated_date = models.DateTimeField(auto_now_add=True) diff --git a/cvat/apps/engine/serializers.py b/cvat/apps/engine/serializers.py index d9160fcc6b86..675cf17c05a7 100644 --- a/cvat/apps/engine/serializers.py +++ b/cvat/apps/engine/serializers.py @@ -280,7 +280,7 @@ class TaskSerializer(WriteOnceMixin, serializers.ModelSerializer): size = serializers.ReadOnlyField(source='data.size') image_quality = serializers.ReadOnlyField(source='data.image_quality') data = serializers.ReadOnlyField(source='data.id') - project_id = serializers.IntegerField() + project_id = serializers.IntegerField(required=False) class Meta: model = models.Task @@ -345,9 +345,10 @@ def validate_labels(self, value): return value def validate(self, value): - if not (value.get("labels") or value.get("project_id")): + print(value) + if not (value.get("label_set") or value.get("project_id")): raise serializers.ValidationError('Label set or project_id must be present') - if value.get("labels") and value.get("project_id"): + if value.get("label_set") and value.get("project_id"): raise serializers.ValidationError('Project must have only one of Label set or project_id') return value @@ -365,7 +366,7 @@ class ProjectSerializer(serializers.ModelSerializer): tasks = TaskSerializer(many=True, read_only=True) class Meta: model = models.Project - fields = ('url', 'id', 'name', 'labels', 'tasks', 'owner', + fields = ('url', 'id', 'name', 'labels', 'tasks', 'owner', 'assignee', 'bug_tracker', 'created_date', 'updated_date', 'status') read_only_fields = ('created_date', 'updated_date', 'status') ordering = ['-id'] diff --git a/tests/cypress/integration/case_2_register_user_change_pass.js b/tests/cypress/integration/case_2_register_user_change_pass.js index d5691e9fb8b3..4d8ca90c8ff4 100644 --- a/tests/cypress/integration/case_2_register_user_change_pass.js +++ b/tests/cypress/integration/case_2_register_user_change_pass.js @@ -34,7 +34,7 @@ context('Register user, change password, login with new password', () => { describe(`Testing "Case ${caseId}"`, () => { it('Register user, change password', () => { cy.userRegistration(firstName, lastName, userName, emailAddr, password) - cy.url().should('include', '/tasks') + cy.url().should('include', '/projects') cy.get('.cvat-right-header') .find('.cvat-header-menu-dropdown') .should('have.text', userName) @@ -56,7 +56,7 @@ context('Register user, change password, login with new password', () => { }) it('Login with the new password', () => { cy.login(userName, newPassword) - cy.url().should('include', '/tasks') + cy.url().should('include', '/projects') }) }) }) diff --git a/tests/cypress/integration/case_4_assign_taks_job_users.js b/tests/cypress/integration/case_4_assign_taks_job_users.js index fd10407b1c67..1783825e956a 100644 --- a/tests/cypress/integration/case_4_assign_taks_job_users.js +++ b/tests/cypress/integration/case_4_assign_taks_job_users.js @@ -50,7 +50,7 @@ context('Multiple users. Assign task, job.', () => { cy.visit('auth/register') cy.url().should('include', '/auth/register') cy.userRegistration(secondUser.firstName, secondUser.lastName, secondUserName, secondUser.emailAddr, secondUser.password) - cy.url().should('include', '/tasks') + cy.url().should('include', '/projects') cy.logout(secondUserName) cy.url().should('include', '/auth/login') }) @@ -61,13 +61,13 @@ context('Multiple users. Assign task, job.', () => { closeModal() } cy.userRegistration(thirdUser.firstName, thirdUser.lastName, thirdUserName, thirdUser.emailAddr, thirdUser.password) - cy.url().should('include', '/tasks') + cy.url().should('include', '/projects') cy.logout(thirdUserName) cy.url().should('include', '/auth/login') }) it('First user login and create a task', () => { cy.login() - cy.url().should('include', '/tasks') + cy.url().should('include', '/projects') cy.imageGenerator('cypress/fixtures', image, width, height, color, posX, posY, labelName) cy.createAnnotationTask(taskName, labelName, attrName, textDefaultValue, image) }) @@ -83,7 +83,8 @@ context('Multiple users. Assign task, job.', () => { }) it('Second user login. The task can be opened. Logout', () => { cy.login(secondUserName, secondUser.password) - cy.url().should('include', '/tasks') + cy.url().should('include', '/projects') + cy.visit('tasks') cy.contains('strong', taskName) .should('exist') cy.openTask(taskName) @@ -91,14 +92,15 @@ context('Multiple users. Assign task, job.', () => { }) it('Third user login. The task not exist. Logout', () => { cy.login(thirdUserName, thirdUser.password) - cy.url().should('include', '/tasks') + cy.url().should('include', '/projects') + cy.visit('tasks') cy.contains('strong', taskName) .should('not.exist') cy.logout(thirdUserName) }) it('First user login and assign the job to the third user. Logout', () => { cy.login() - cy.url().should('include', '/tasks') + cy.url().should('include', '/projects') cy.openTask(taskName) cy.get('.cvat-task-job-list').within(() => { cy.get('.cvat-user-selector') @@ -110,7 +112,8 @@ context('Multiple users. Assign task, job.', () => { }) it('Third user login. The task can be opened.', () => { cy.login(thirdUserName, thirdUser.password) - cy.url().should('include', '/tasks') + cy.url().should('include', '/projects') + cy.visit('tasks') cy.contains('strong', taskName) .should('exist') cy.openTask(taskName) diff --git a/tests/cypress/integration/issue_1498_message_ui_raw_labels_wrong.js b/tests/cypress/integration/issue_1498_message_ui_raw_labels_wrong.js index ca6a50bb27f2..439afa0e4d18 100644 --- a/tests/cypress/integration/issue_1498_message_ui_raw_labels_wrong.js +++ b/tests/cypress/integration/issue_1498_message_ui_raw_labels_wrong.js @@ -34,6 +34,7 @@ context('Message in UI when raw labels are wrong.', () => { before(() => { cy.visit('auth/login') cy.login() + cy.visit('tasks') cy.get('#cvat-create-task-button').click() cy.url().should('include', '/tasks/create') cy.get('[role="tab"]').contains('Raw').click() diff --git a/tests/cypress/integration/issue_1568_cuboid_dump_annotation.js b/tests/cypress/integration/issue_1568_cuboid_dump_annotation.js index 2a7ea28377da..be076af7584a 100644 --- a/tests/cypress/integration/issue_1568_cuboid_dump_annotation.js +++ b/tests/cypress/integration/issue_1568_cuboid_dump_annotation.js @@ -60,8 +60,8 @@ context('Dump annotation if cuboid created', () => { .click() }) }) - it('Error notification is ot exists', () => { - cy.wait(5000) + it('Error notification is not exists', () => { + cy.wait(10000) cy.get('.ant-notification-notice') .should('not.exist') }) diff --git a/tests/cypress/integration/issue_1599_ch_user_registration.js b/tests/cypress/integration/issue_1599_ch_user_registration.js index 8bbe6c117a7b..31b3cf435fe0 100644 --- a/tests/cypress/integration/issue_1599_ch_user_registration.js +++ b/tests/cypress/integration/issue_1599_ch_user_registration.js @@ -44,7 +44,7 @@ context('Issue 1599 (Chinese alphabet).', () => { }) it('Successful registration', () => { - cy.url().should('include', '/tasks') + cy.url().should('include', '/projects') }) }) }) diff --git a/tests/cypress/integration/issue_1599_pl_user_registration.js b/tests/cypress/integration/issue_1599_pl_user_registration.js index 4551ac94fb9c..c8cfb41a2497 100644 --- a/tests/cypress/integration/issue_1599_pl_user_registration.js +++ b/tests/cypress/integration/issue_1599_pl_user_registration.js @@ -44,7 +44,7 @@ context('Issue 1599 (Polish alphabet).', () => { }) it('Successful registration', () => { - cy.url().should('include', '/tasks') + cy.url().should('include', '/projects') }) }) }) diff --git a/tests/cypress/integration/issue_1810_login_logout.js b/tests/cypress/integration/issue_1810_login_logout.js index 44a943e9ec8b..cba3ad4c7e5b 100644 --- a/tests/cypress/integration/issue_1810_login_logout.js +++ b/tests/cypress/integration/issue_1810_login_logout.js @@ -17,7 +17,7 @@ context('When clicking on the Logout button, get the user session closed.', () = describe(`Testing issue "${issueId}"`, () => { it('Login', () => { cy.login() - cy.url().should('include', '/tasks') + cy.url().should('include', '/projects') }) it('Logout', () => { cy.logout() diff --git a/tests/cypress/support/commands.js b/tests/cypress/support/commands.js index b96b40d69636..755148f2d957 100644 --- a/tests/cypress/support/commands.js +++ b/tests/cypress/support/commands.js @@ -14,6 +14,7 @@ Cypress.Commands.add('login', (username=Cypress.env('user'), password=Cypress.en cy.get('[placeholder="Username"]').type(username) cy.get('[placeholder="Password"]').type(password) cy.get('[type="submit"]').click() + cy.url().should('include', '/projects') }) Cypress.Commands.add('logout', (username=Cypress.env('user')) => { @@ -43,6 +44,7 @@ Cypress.Commands.add('createAnnotationTask', (taksName='New annotation task', multiAttrParams, advancedConfigurationParams ) => { + cy.visit('tasks') cy.get('#cvat-create-task-button').click() cy.url().should('include', '/tasks/create') cy.get('[id="name"]').type(taksName) @@ -68,6 +70,7 @@ Cypress.Commands.add('createAnnotationTask', (taksName='New annotation task', }) Cypress.Commands.add('openTask', (taskName) => { + cy.visit('tasks') cy.contains('strong', taskName) .parents('.cvat-tasks-list-item') .contains('a', 'Open') From 8677dbdc9b921d0b81e847e89b235e71f78bb703 Mon Sep 17 00:00:00 2001 From: Dmitry Kruchinin Date: Fri, 16 Oct 2020 13:27:26 +0300 Subject: [PATCH 21/55] Fix tests 1944, case 2. --- .../integration/case_4_assign_taks_job_users.js | 17 +++++++++++++++++ tests/cypress/support/commands.js | 1 - 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/tests/cypress/integration/case_4_assign_taks_job_users.js b/tests/cypress/integration/case_4_assign_taks_job_users.js index 1783825e956a..0120763f3b6f 100644 --- a/tests/cypress/integration/case_4_assign_taks_job_users.js +++ b/tests/cypress/integration/case_4_assign_taks_job_users.js @@ -68,6 +68,10 @@ context('Multiple users. Assign task, job.', () => { it('First user login and create a task', () => { cy.login() cy.url().should('include', '/projects') + cy.visit('tasks') + if (Cypress.browser.name === 'firefox') { + closeModal() + } cy.imageGenerator('cypress/fixtures', image, width, height, color, posX, posY, labelName) cy.createAnnotationTask(taskName, labelName, attrName, textDefaultValue, image) }) @@ -85,6 +89,9 @@ context('Multiple users. Assign task, job.', () => { cy.login(secondUserName, secondUser.password) cy.url().should('include', '/projects') cy.visit('tasks') + if (Cypress.browser.name === 'firefox') { + closeModal() + } cy.contains('strong', taskName) .should('exist') cy.openTask(taskName) @@ -94,6 +101,9 @@ context('Multiple users. Assign task, job.', () => { cy.login(thirdUserName, thirdUser.password) cy.url().should('include', '/projects') cy.visit('tasks') + if (Cypress.browser.name === 'firefox') { + closeModal() + } cy.contains('strong', taskName) .should('not.exist') cy.logout(thirdUserName) @@ -101,6 +111,10 @@ context('Multiple users. Assign task, job.', () => { it('First user login and assign the job to the third user. Logout', () => { cy.login() cy.url().should('include', '/projects') + cy.visit('tasks') + if (Cypress.browser.name === 'firefox') { + closeModal() + } cy.openTask(taskName) cy.get('.cvat-task-job-list').within(() => { cy.get('.cvat-user-selector') @@ -114,6 +128,9 @@ context('Multiple users. Assign task, job.', () => { cy.login(thirdUserName, thirdUser.password) cy.url().should('include', '/projects') cy.visit('tasks') + if (Cypress.browser.name === 'firefox') { + closeModal() + } cy.contains('strong', taskName) .should('exist') cy.openTask(taskName) diff --git a/tests/cypress/support/commands.js b/tests/cypress/support/commands.js index 755148f2d957..38545f5890b8 100644 --- a/tests/cypress/support/commands.js +++ b/tests/cypress/support/commands.js @@ -70,7 +70,6 @@ Cypress.Commands.add('createAnnotationTask', (taksName='New annotation task', }) Cypress.Commands.add('openTask', (taskName) => { - cy.visit('tasks') cy.contains('strong', taskName) .parents('.cvat-tasks-list-item') .contains('a', 'Open') From 69725dc8f0b5a7826781827ac68defeb2aeacd43 Mon Sep 17 00:00:00 2001 From: Dmitry Kalinin Date: Fri, 16 Oct 2020 14:36:02 +0300 Subject: [PATCH 22/55] Fixed serializer --- cvat/apps/engine/serializers.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cvat/apps/engine/serializers.py b/cvat/apps/engine/serializers.py index 675cf17c05a7..00b3bd14980f 100644 --- a/cvat/apps/engine/serializers.py +++ b/cvat/apps/engine/serializers.py @@ -362,7 +362,7 @@ class Meta: class ProjectSerializer(serializers.ModelSerializer): - labels = LabelSerializer(many=True, source='label_set', partial=True) + labels = LabelSerializer(many=True, source='label_set', partial=True, required=False) tasks = TaskSerializer(many=True, read_only=True) class Meta: model = models.Project From 5e5202aa79d1dcb27816a108a7b08d4d01f9d3a5 Mon Sep 17 00:00:00 2001 From: Dmitry Kalinin Date: Fri, 16 Oct 2020 14:40:54 +0300 Subject: [PATCH 23/55] Another serializer fix --- cvat/apps/engine/serializers.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cvat/apps/engine/serializers.py b/cvat/apps/engine/serializers.py index 00b3bd14980f..afdaf8ceecee 100644 --- a/cvat/apps/engine/serializers.py +++ b/cvat/apps/engine/serializers.py @@ -362,7 +362,7 @@ class Meta: class ProjectSerializer(serializers.ModelSerializer): - labels = LabelSerializer(many=True, source='label_set', partial=True, required=False) + labels = LabelSerializer(many=True, source='label_set', partial=True, default=[]) tasks = TaskSerializer(many=True, read_only=True) class Meta: model = models.Project From 7172826ce210ff5f9a677f232b0f15c0f190d088 Mon Sep 17 00:00:00 2001 From: Dmitry Kalinin Date: Fri, 16 Oct 2020 15:10:26 +0300 Subject: [PATCH 24/55] Added UI test lauch from launch.json --- .vscode/launch.json | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/.vscode/launch.json b/.vscode/launch.json index 48bc45835ce0..d6e8e878dec2 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -21,6 +21,21 @@ }, "smartStep": true, }, + { + "type": "node", + "request": "launch", + "name": "ui.js: test", + "cwd": "${workspaceRoot}/tests", + "runtimeExecutable": "${workspaceRoot}/tests/node_modules/.bin/cypress", + "args": [ + "run", + "--headless", + "--browser", + "chrome" + ], + "outputCapture": "std", + "console": "internalConsole" + }, { "name": "server: django", "type": "python", From dfe2988da4937b9fe9ec9dac8877bb718b9c8474 Mon Sep 17 00:00:00 2001 From: Dmitry Kruchinin Date: Fri, 16 Oct 2020 18:18:08 +0300 Subject: [PATCH 25/55] Apply comments --- .../case_4_assign_taks_job_users.js | 40 +++---------------- .../issue_1444_filter_property_shape.js | 1 + tests/cypress/support/commands.js | 2 +- tests/cypress/support/index.js | 2 +- 4 files changed, 9 insertions(+), 36 deletions(-) diff --git a/tests/cypress/integration/case_4_assign_taks_job_users.js b/tests/cypress/integration/case_4_assign_taks_job_users.js index 0120763f3b6f..43d29492df3c 100644 --- a/tests/cypress/integration/case_4_assign_taks_job_users.js +++ b/tests/cypress/integration/case_4_assign_taks_job_users.js @@ -34,16 +34,6 @@ context('Multiple users. Assign task, job.', () => { password: 'Fv5Df3#f55g' } - function closeModal() { - cy.get('.ant-modal-body').within(() => { - cy.get('.ant-modal-confirm-title') - .should('contain', 'Unsupported platform detected') - cy.get('.ant-modal-confirm-btns') - .contains('OK') - .click() - }) - } - describe(`Testing case "${caseId}"`, () => { // First user is "admin". it('Register second user and logout.', () => { @@ -55,11 +45,8 @@ context('Multiple users. Assign task, job.', () => { cy.url().should('include', '/auth/login') }) it('Register third user and logout.', () => { - cy.visit('auth/register') + cy.get('a[href="/auth/register"]').click() cy.url().should('include', '/auth/register') - if (Cypress.browser.name === 'firefox') { - closeModal() - } cy.userRegistration(thirdUser.firstName, thirdUser.lastName, thirdUserName, thirdUser.emailAddr, thirdUser.password) cy.url().should('include', '/projects') cy.logout(thirdUserName) @@ -68,10 +55,7 @@ context('Multiple users. Assign task, job.', () => { it('First user login and create a task', () => { cy.login() cy.url().should('include', '/projects') - cy.visit('tasks') - if (Cypress.browser.name === 'firefox') { - closeModal() - } + cy.get('[value="tasks"]').click() cy.imageGenerator('cypress/fixtures', image, width, height, color, posX, posY, labelName) cy.createAnnotationTask(taskName, labelName, attrName, textDefaultValue, image) }) @@ -88,10 +72,7 @@ context('Multiple users. Assign task, job.', () => { it('Second user login. The task can be opened. Logout', () => { cy.login(secondUserName, secondUser.password) cy.url().should('include', '/projects') - cy.visit('tasks') - if (Cypress.browser.name === 'firefox') { - closeModal() - } + cy.get('[value="tasks"]').click() cy.contains('strong', taskName) .should('exist') cy.openTask(taskName) @@ -100,10 +81,7 @@ context('Multiple users. Assign task, job.', () => { it('Third user login. The task not exist. Logout', () => { cy.login(thirdUserName, thirdUser.password) cy.url().should('include', '/projects') - cy.visit('tasks') - if (Cypress.browser.name === 'firefox') { - closeModal() - } + cy.get('[value="tasks"]').click() cy.contains('strong', taskName) .should('not.exist') cy.logout(thirdUserName) @@ -111,10 +89,7 @@ context('Multiple users. Assign task, job.', () => { it('First user login and assign the job to the third user. Logout', () => { cy.login() cy.url().should('include', '/projects') - cy.visit('tasks') - if (Cypress.browser.name === 'firefox') { - closeModal() - } + cy.get('[value="tasks"]').click() cy.openTask(taskName) cy.get('.cvat-task-job-list').within(() => { cy.get('.cvat-user-selector') @@ -127,10 +102,7 @@ context('Multiple users. Assign task, job.', () => { it('Third user login. The task can be opened.', () => { cy.login(thirdUserName, thirdUser.password) cy.url().should('include', '/projects') - cy.visit('tasks') - if (Cypress.browser.name === 'firefox') { - closeModal() - } + cy.get('[value="tasks"]').click() cy.contains('strong', taskName) .should('exist') cy.openTask(taskName) diff --git a/tests/cypress/integration/issue_1444_filter_property_shape.js b/tests/cypress/integration/issue_1444_filter_property_shape.js index 447e06f27865..735b7c297d08 100644 --- a/tests/cypress/integration/issue_1444_filter_property_shape.js +++ b/tests/cypress/integration/issue_1444_filter_property_shape.js @@ -62,6 +62,7 @@ context('Filter property "shape" work correctly', () => { }) it('Input filter "shape == "polygon""', () => { cy.get('.cvat-annotations-filters-input') + .click() .type('shape == "polygon"{Enter}') }) it('Only polygon is visible', () => { diff --git a/tests/cypress/support/commands.js b/tests/cypress/support/commands.js index 38545f5890b8..9e369ade70ad 100644 --- a/tests/cypress/support/commands.js +++ b/tests/cypress/support/commands.js @@ -44,7 +44,7 @@ Cypress.Commands.add('createAnnotationTask', (taksName='New annotation task', multiAttrParams, advancedConfigurationParams ) => { - cy.visit('tasks') + cy.get('[value="tasks"]').click() cy.get('#cvat-create-task-button').click() cy.url().should('include', '/tasks/create') cy.get('[id="name"]').type(taksName) diff --git a/tests/cypress/support/index.js b/tests/cypress/support/index.js index a86ace71db74..ff89ec2eb3c2 100644 --- a/tests/cypress/support/index.js +++ b/tests/cypress/support/index.js @@ -7,7 +7,7 @@ import './commands' before(() => { - if (Cypress.browser.name === 'firefox') { + if (Cypress.browser.family !== 'chromium') { cy.visit('/') cy.get('.ant-modal-body').within(() => { cy.get('.ant-modal-confirm-title') From 9d27d866e5624212e6c8a63342ede5de4a3c38c8 Mon Sep 17 00:00:00 2001 From: Dmitry Kalinin Date: Mon, 19 Oct 2020 00:25:39 +0300 Subject: [PATCH 26/55] Fixed API tests --- ...jastment.py => 0032_projects_adjastment.py} | 2 +- cvat/apps/engine/serializers.py | 18 +++++++----------- 2 files changed, 8 insertions(+), 12 deletions(-) rename cvat/apps/engine/migrations/{0031_projects_adjastment.py => 0032_projects_adjastment.py} (93%) diff --git a/cvat/apps/engine/migrations/0031_projects_adjastment.py b/cvat/apps/engine/migrations/0032_projects_adjastment.py similarity index 93% rename from cvat/apps/engine/migrations/0031_projects_adjastment.py rename to cvat/apps/engine/migrations/0032_projects_adjastment.py index 339427d5fcc2..2cc0c366106b 100644 --- a/cvat/apps/engine/migrations/0031_projects_adjastment.py +++ b/cvat/apps/engine/migrations/0032_projects_adjastment.py @@ -7,7 +7,7 @@ class Migration(migrations.Migration): dependencies = [ - ('engine', '0030_auto_20200914_1331'), + ('engine', '0031_auto_20201011_0220'), ] operations = [ diff --git a/cvat/apps/engine/serializers.py b/cvat/apps/engine/serializers.py index afdaf8ceecee..57bbdd1d08c6 100644 --- a/cvat/apps/engine/serializers.py +++ b/cvat/apps/engine/serializers.py @@ -272,7 +272,7 @@ def create(self, validated_data): return db_data class TaskSerializer(WriteOnceMixin, serializers.ModelSerializer): - labels = LabelSerializer(many=True, source='label_set', partial=True) + labels = LabelSerializer(many=True, source='label_set', partial=True, required=False) segments = SegmentSerializer(many=True, source='segment_set', read_only=True) data_chunk_size = serializers.ReadOnlyField(source='data.chunk_size') data_compressed_chunk_type = serializers.ReadOnlyField(source='data.compressed_chunk_type') @@ -295,7 +295,12 @@ class Meta: # pylint: disable=no-self-use def create(self, validated_data): - labels = validated_data.pop('label_set') + if not (validated_data.get("label_set") or validated_data.get("project_id")): + raise serializers.ValidationError('Label set or project_id must be present') + if validated_data.get("label_set") and validated_data.get("project_id"): + raise serializers.ValidationError('Project must have only one of Label set or project_id') + + labels = validated_data.pop('label_set', []) db_task = models.Task.objects.create(**validated_data) label_names = list() for label in labels: @@ -344,15 +349,6 @@ def validate_labels(self, value): raise serializers.ValidationError('All label names must be unique for the task') return value - def validate(self, value): - print(value) - if not (value.get("label_set") or value.get("project_id")): - raise serializers.ValidationError('Label set or project_id must be present') - if value.get("label_set") and value.get("project_id"): - raise serializers.ValidationError('Project must have only one of Label set or project_id') - return value - - class ProjectSearchSerializer(serializers.ModelSerializer): class Meta: model = models.Project From 3804f6170a68f433331176926c63e8b8f295d824 Mon Sep 17 00:00:00 2001 From: Dmitry Kalinin Date: Mon, 19 Oct 2020 01:12:41 +0300 Subject: [PATCH 27/55] Fixed codacy issues --- cvat/apps/engine/log.py | 5 ++--- cvat/apps/engine/serializers.py | 1 + 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/cvat/apps/engine/log.py b/cvat/apps/engine/log.py index 781b6dfb0234..f12a59914701 100644 --- a/cvat/apps/engine/log.py +++ b/cvat/apps/engine/log.py @@ -29,9 +29,7 @@ def __init__(self): self._storage = dict() def __getitem__(self, pid): - ''' - Get ceratain storage object for some project - ''' + '''Get ceratain storage object for some project''' if pid not in self._storage: self._storage[pid] = self._create_project_logger(pid) return self._storage[pid] @@ -132,6 +130,7 @@ def _get_task_logger(self, jid): return clogger.task[job.segment.task.id] class dotdict(dict): + '''dot.notation access to dictionary attributes''' __getattr__ = dict.get __setattr__ = dict.__setitem__ diff --git a/cvat/apps/engine/serializers.py b/cvat/apps/engine/serializers.py index 57bbdd1d08c6..78501d912acd 100644 --- a/cvat/apps/engine/serializers.py +++ b/cvat/apps/engine/serializers.py @@ -161,6 +161,7 @@ class RqStatusSerializer(serializers.Serializer): message = serializers.CharField(allow_blank=True, default="") class WriteOnceMixin: + """Adds support for write once fields to serializers. To use it, specify a list of fields as `write_once_fields` on the From 38a598ed7e0096a2795ee4b91913ecaf93caa81b Mon Sep 17 00:00:00 2001 From: Dmitry Kalinin Date: Mon, 19 Oct 2020 01:23:24 +0300 Subject: [PATCH 28/55] Fixed codacy issues --- cvat/apps/engine/log.py | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/cvat/apps/engine/log.py b/cvat/apps/engine/log.py index f12a59914701..8dcc47ffc941 100644 --- a/cvat/apps/engine/log.py +++ b/cvat/apps/engine/log.py @@ -29,7 +29,7 @@ def __init__(self): self._storage = dict() def __getitem__(self, pid): - '''Get ceratain storage object for some project''' + """Get ceratain storage object for some project""" if pid not in self._storage: self._storage[pid] = self._create_project_logger(pid) return self._storage[pid] @@ -84,9 +84,7 @@ def __init__(self): self._storage = dict() def __getitem__(self, pid): - ''' - Get logger for exact task by id - ''' + """Get logger for exact task by id""" if pid not in self._storage: self._storage[pid] = self._create_client_logger(pid) return self._storage[pid] @@ -130,8 +128,7 @@ def _get_task_logger(self, jid): return clogger.task[job.segment.task.id] class dotdict(dict): - - '''dot.notation access to dictionary attributes''' + """dot.notation access to dictionary attributes""" __getattr__ = dict.get __setattr__ = dict.__setitem__ __delattr__ = dict.__delitem__ From d05e49559714cf41f9e3ed094feffd63b41302e4 Mon Sep 17 00:00:00 2001 From: Dmitry Kalinin Date: Mon, 19 Oct 2020 12:30:02 +0300 Subject: [PATCH 29/55] Added API tests for projects --- cvat/apps/engine/log.py | 4 +-- cvat/apps/engine/tests/test_rest_api.py | 36 ++++++++++++++++++++++++- 2 files changed, 37 insertions(+), 3 deletions(-) diff --git a/cvat/apps/engine/log.py b/cvat/apps/engine/log.py index 8dcc47ffc941..d8804e26cd46 100644 --- a/cvat/apps/engine/log.py +++ b/cvat/apps/engine/log.py @@ -29,7 +29,7 @@ def __init__(self): self._storage = dict() def __getitem__(self, pid): - """Get ceratain storage object for some project""" + """Get ceratain storage object for some project.""" if pid not in self._storage: self._storage[pid] = self._create_project_logger(pid) return self._storage[pid] @@ -84,7 +84,7 @@ def __init__(self): self._storage = dict() def __getitem__(self, pid): - """Get logger for exact task by id""" + """Get logger for exact task by id.""" if pid not in self._storage: self._storage[pid] = self._create_client_logger(pid) return self._storage[pid] diff --git a/cvat/apps/engine/tests/test_rest_api.py b/cvat/apps/engine/tests/test_rest_api.py index a68531369b24..c101783ad42c 100644 --- a/cvat/apps/engine/tests/test_rest_api.py +++ b/cvat/apps/engine/tests/test_rest_api.py @@ -29,7 +29,7 @@ from rest_framework.test import APIClient, APITestCase from cvat.apps.engine.models import (AttributeType, Data, Job, Project, - Segment, StatusChoice, Task, StorageMethodChoice) + Segment, StatusChoice, Task, Label, StorageMethodChoice) from cvat.apps.engine.prepare import prepare_meta, prepare_meta_for_upload def create_db_users(cls): @@ -843,6 +843,10 @@ def _check_response(self, response, user, data): self.assertEqual(response.data["assignee"], data.get("assignee")) self.assertEqual(response.data["bug_tracker"], data.get("bug_tracker", "")) self.assertEqual(response.data["status"], StatusChoice.ANNOTATION) + self.assertListEqual( + [label["name"] for label in data.get("labels", [])], + [label["name"] for label in response.data["labels"]] + ) def _check_api_v1_projects(self, user, data): response = self._run_api_v1_projects(user, data) @@ -873,6 +877,14 @@ def test_api_v1_projects_admin(self): } self._check_api_v1_projects(self.admin, data) + data = { + "name": "Project with labels", + "labels": [{ + "name": "car", + }] + } + self._check_api_v1_projects(self.admin, data) + def test_api_v1_projects_user(self): data = { @@ -1331,6 +1343,16 @@ def test_api_v1_tasks_id_no_auth(self): class TaskCreateAPITestCase(APITestCase): def setUp(self): self.client = APIClient() + project = { + "name": "Project for task creation", + "owner": self.user, + } + self.project = Project.objects.create(**project) + label = { + "name": "car", + "project": self.project + } + Label.objects.create(**label) @classmethod def setUpTestData(cls): @@ -1346,6 +1368,7 @@ def _check_response(self, response, user, data): self.assertEqual(response.status_code, status.HTTP_201_CREATED) self.assertEqual(response.data["name"], data["name"]) self.assertEqual(response.data["mode"], "") + self.assertEqual(response.data["project_id"], data.get("project_id", None)) self.assertEqual(response.data["owner"], data.get("owner", user.id)) self.assertEqual(response.data["assignee"], data.get("assignee")) self.assertEqual(response.data["bug_tracker"], data.get("bug_tracker", "")) @@ -1399,6 +1422,17 @@ def test_api_v1_tasks_user(self): } self._check_api_v1_tasks(self.user, data) + def test_api_vi_tasks_user_project(self): + data = { + "name": "new name for the task", + "project_id": self.project.id, + } + response = self._run_api_v1_tasks(self.user, data) + data["labels"] = [{ + "name": "car" + }] + self._check_response(response, self.user, data) + def test_api_v1_tasks_observer(self): data = { "name": "new name for the task", From 7658fe0cc128dd13b609e8aa6f79f5e5524bcf31 Mon Sep 17 00:00:00 2001 From: Dmitry Kalinin Date: Wed, 21 Oct 2020 00:08:13 +0300 Subject: [PATCH 30/55] Added tests for cvat-core --- cvat-core/tests/api/projects.js | 168 +++++++++++++++++++++ cvat-core/tests/api/tasks.js | 13 ++ cvat-core/tests/mocks/dummy-data.mock.js | 166 ++++++++++++++++++++ cvat-core/tests/mocks/server-proxy.mock.js | 104 ++++++++++--- 4 files changed, 433 insertions(+), 18 deletions(-) create mode 100644 cvat-core/tests/api/projects.js diff --git a/cvat-core/tests/api/projects.js b/cvat-core/tests/api/projects.js new file mode 100644 index 000000000000..c377587f2bd6 --- /dev/null +++ b/cvat-core/tests/api/projects.js @@ -0,0 +1,168 @@ +/* + * Copyright (C) 2018 Intel Corporation + * SPDX-License-Identifier: MIT +*/ + +/* global + require:false + jest:false + describe:false +*/ + +// Setup mock for a server +jest.mock('../../src/server-proxy', () => { + const mock = require('../mocks/server-proxy.mock'); + return mock; +}); + +// Initialize api +window.cvat = require('../../src/api'); + +const { Task, Project } = require('../../src/session'); + +describe('Feature: get projects', () => { + test('get all projects', async () => { + const result = await window.cvat.projects.get(); + expect(Array.isArray(result)).toBeTruthy(); + expect(result).toHaveLength(2); + for (const el of result) { + expect(el).toBeInstanceOf(Project); + } + }); + + test('get project by id', async () => { + const result = await window.cvat.projects.get({ + id: 2, + }); + + expect(Array.isArray(result)).toBeTruthy(); + expect(result).toHaveLength(1); + expect(result[0]).toBeInstanceOf(Project); + expect(result[0].id).toBe(2); + expect(result[0].tasks).toHaveLength(1); + expect(result[0].tasks[0]).toBeInstanceOf(Task); + }); + + test('get a project by an unknown id', async () => { + const result = await window.cvat.projects.get({ + id: 1, + }); + expect(Array.isArray(result)).toBeTruthy(); + expect(result).toHaveLength(0); + }); + + test('get a project by an invalid id', async () => { + expect(window.cvat.projects.get({ + id: '1', + })).rejects.toThrow(window.cvat.exceptions.ArgumentError); + }); + + test('get projects by filters', async () => { + const result = await window.cvat.projects.get({ + status: 'completed', + }); + expect(Array.isArray(result)).toBeTruthy(); + expect(result).toHaveLength(1); + expect(result[0]).toBeInstanceOf(Project); + expect(result[0].id).toBe(2); + expect(result[0].status).toBe('completed'); + }); + + test('get projects by invalid filters', async () => { + expect(window.cvat.projects.get({ + unknown: '5', + })).rejects.toThrow(window.cvat.exceptions.ArgumentError); + }); +}); + +describe('Feature: save a project', () => { + test('save some changed fields in a project', async () => { + let result = await window.cvat.tasks.get({ + id: 2, + }); + + result[0].bugTracker = 'newBugTracker'; + result[0].name = 'New Project Name'; + + result[0].save(); + + result = await window.cvat.tasks.get({ + id: 2, + }); + + expect(result[0].bugTracker).toBe('newBugTracker'); + expect(result[0].name).toBe('New Project Name'); + }); + + test('save some new labels in a project', async () => { + let result = await window.cvat.projects.get({ + id: 6, + }); + + console.log(result[0].labels); + + const labelsLength = result[0].labels.length; + const newLabel = new window.cvat.classes.Label({ + name: 'My boss\'s car', + attributes: [{ + default_value: 'false', + input_type: 'checkbox', + mutable: true, + name: 'parked', + values: ['false'], + }], + }); + + result[0].labels = [...result[0].labels, newLabel]; + result[0].save(); + + result = await window.cvat.projects.get({ + id: 6, + }); + + expect(result[0].labels).toHaveLength(labelsLength + 1); + const appendedLabel = result[0].labels.filter((el) => el.name === 'My boss\'s car'); + expect(appendedLabel).toHaveLength(1); + expect(appendedLabel[0].attributes).toHaveLength(1); + expect(appendedLabel[0].attributes[0].name).toBe('parked'); + expect(appendedLabel[0].attributes[0].defaultValue).toBe('false'); + expect(appendedLabel[0].attributes[0].mutable).toBe(true); + expect(appendedLabel[0].attributes[0].inputType).toBe('checkbox'); + }); + + test('save new project without an id', async () => { + const project = new window.cvat.classes.Project({ + name: 'New Empty Project', + labels: [{ + name: 'car', + attributes: [{ + default_value: 'false', + input_type: 'checkbox', + mutable: true, + name: 'parked', + values: ['false'], + }], + }], + bug_tracker: 'bug tracker value', + }); + + const result = await project.save(); + expect(typeof (result.id)).toBe('number'); + }); +}); + +describe('Feature: delete a project', () => { + test('delete a project', async () => { + let result = await window.cvat.projects.get({ + id: 6, + }); + + await result[0].delete(); + result = await window.cvat.projects.get({ + id: 6, + }); + + expect(Array.isArray(result)).toBeTruthy(); + expect(result).toHaveLength(0); + }); +}); diff --git a/cvat-core/tests/api/tasks.js b/cvat-core/tests/api/tasks.js index 627bf7e85e86..bcea820cf841 100644 --- a/cvat-core/tests/api/tasks.js +++ b/cvat-core/tests/api/tasks.js @@ -167,6 +167,19 @@ describe('Feature: save a task', () => { const result = await task.save(); expect(typeof (result.id)).toBe('number'); }); + + test('save new task in project', async () => { + const task = new window.cvat.classes.Task({ + name: 'New Task', + project_id: 2, + bug_tracker: 'bug tracker value', + image_quality: 50, + z_order: true, + }); + + const result = await task.save(); + expect(result.projectId).toBe(2); + }); }); describe('Feature: delete a task', () => { diff --git a/cvat-core/tests/mocks/dummy-data.mock.js b/cvat-core/tests/mocks/dummy-data.mock.js index dd688e14b8bb..017003137d50 100644 --- a/cvat-core/tests/mocks/dummy-data.mock.js +++ b/cvat-core/tests/mocks/dummy-data.mock.js @@ -147,6 +147,171 @@ const shareDummyData = [ } ] +const projectsDummyData = { + "count": 2, + "next": null, + "previous": null, + "results": [ + { + "url":"http://192.168.0.139:7000/api/v1/projects/6", + "id":6, + "name":"Some empty project", + "labels":[], + "tasks":[], + "owner":2, + "assignee":2, + "bug_tracker":"", + "created_date":"2020-10-19T20:41:07.808029Z", + "updated_date":"2020-10-19T20:41:07.808084Z", + "status":"annotation" + },{ + "url":"http://192.168.0.139:7000/api/v1/projects/1", + "id":2, + "name":"Test project with roads", + "labels":[ + { + "id":1, + "name":"car", + "color":"#2080c0", + "attributes":[ + { + "id":199, + "name":"color", + "mutable":false, + "input_type":"select", + "default_value":"red", + "values":[ + "red", + "black", + "white", + "yellow", + "pink", + "green", + "blue", + "orange" + ] + } + ] + } + ], + "tasks":[ + { + "url":"http://192.168.0.139:7000/api/v1/tasks/2", + "id":2, + "name":"road 1", + "project_id":1, + "mode":"interpolation", + "owner":1, + "assignee":null, + "bug_tracker":"", + "created_date":"2020-10-12T08:59:59.878083Z", + "updated_date":"2020-10-18T21:02:20.831294Z", + "overlap":5, + "segment_size":100, + "z_order":false, + "status":"completed", + "labels":[ + { + "id":1, + "name":"car", + "color":"#2080c0", + "attributes":[ + { + "id":199, + "name":"color", + "mutable":false, + "input_type":"select", + "default_value":"red", + "values":[ + "red", + "black", + "white", + "yellow", + "pink", + "green", + "blue", + "orange" + ] + } + ] + } + ], + "segments":[ + { + "start_frame":0, + "stop_frame":99, + "jobs":[ + { + "url":"http://192.168.0.139:7000/api/v1/jobs/1", + "id":1, + "assignee":null, + "status":"completed" + } + ] + },{ + "start_frame":95, + "stop_frame":194, + "jobs":[ + { + "url":"http://192.168.0.139:7000/api/v1/jobs/2", + "id":2, + "assignee":null, + "status":"completed" + } + ] + },{ + "start_frame":190, + "stop_frame":289, + "jobs":[ + { + "url":"http://192.168.0.139:7000/api/v1/jobs/3", + "id":3, + "assignee":null, + "status":"completed" + } + ] + },{ + "start_frame":285, + "stop_frame":384, + "jobs":[ + { + "url":"http://192.168.0.139:7000/api/v1/jobs/4", + "id":4, + "assignee":null, + "status":"completed" + } + ] + },{ + "start_frame":380, + "stop_frame":431, + "jobs":[ + { + "url":"http://192.168.0.139:7000/api/v1/jobs/5", + "id":5, + "assignee":null, + "status":"completed" + } + ] + } + ], + "data_chunk_size":36, + "data_compressed_chunk_type":"imageset", + "data_original_chunk_type":"video", + "size":432, + "image_quality":100, + "data":1 + } + ], + "owner":1, + "assignee":null, + "bug_tracker":"", + "created_date":"2020-10-12T08:21:56.558898Z", + "updated_date":"2020-10-12T08:21:56.558982Z", + "status":"completed" + } + ], +} + const tasksDummyData = { "count": 5, "next": null, @@ -2638,6 +2803,7 @@ const frameMetaDummyData = { module.exports = { tasksDummyData, + projectsDummyData, aboutDummyData, shareDummyData, usersDummyData, diff --git a/cvat-core/tests/mocks/server-proxy.mock.js b/cvat-core/tests/mocks/server-proxy.mock.js index f189e25457f0..43ee7123ecde 100644 --- a/cvat-core/tests/mocks/server-proxy.mock.js +++ b/cvat-core/tests/mocks/server-proxy.mock.js @@ -11,6 +11,7 @@ const { tasksDummyData, + projectsDummyData, aboutDummyData, formatsDummyData, shareDummyData, @@ -20,6 +21,22 @@ const { frameMetaDummyData, } = require('./dummy-data.mock'); +function QueryStringToJSON(query) { + const pairs = [...new URLSearchParams(query).entries()]; + + const result = {}; + for (const pair of pairs) { + const [key, value] = pair; + if (['id'].includes(key)) { + result[key] = +value; + } else { + result[key] = value; + } + } + + return JSON.parse(JSON.stringify(result)); +} + class ServerProxy { constructor() { async function about() { @@ -34,7 +51,7 @@ class ServerProxy { const components = directory.split('/'); for (const component of components) { - const idx = position.map(x => x.name).indexOf(component); + const idx = position.map((x) => x.name).indexOf(component); if (idx !== -1 && 'children' in position[idx]) { position = position[idx].children; } else { @@ -65,23 +82,63 @@ class ServerProxy { return null; } - async function getTasks(filter = '') { - function QueryStringToJSON(query) { - const pairs = [...new URLSearchParams(query).entries()]; - - const result = {}; - for (const pair of pairs) { - const [key, value] = pair; - if (['id'].includes(key)) { - result[key] = +value; - } else { - result[key] = value; + async function getProjects(filter = '') { + const queries = QueryStringToJSON(filter); + const result = projectsDummyData.results.filter((x) => { + for (const key in queries) { + if (Object.prototype.hasOwnProperty.call(queries, key)) { + // TODO: Particular match for some fields is not checked + if (queries[key] !== x[key]) { + return false; + } } } - return JSON.parse(JSON.stringify(result)); + return true; + }); + + return result; + } + + async function saveProject(id, projectData) { + const object = projectsDummyData.results.filter((project) => project.id === id)[0]; + for (const prop in projectData) { + if (Object.prototype.hasOwnProperty.call(projectData, prop) + && Object.prototype.hasOwnProperty.call(object, prop)) { + object[prop] = projectData[prop]; + } + } + } + + async function createProject(projectData) { + const id = Math.max(...projectsDummyData.results.map((el) => el.id)) + 1; + projectsDummyData.results.push({ + id, + url: `http://localhost:7000/api/v1/projects/${id}`, + name: projectData.name, + owner: 1, + assignee: null, + bug_tracker: projectData.bug_tracker, + created_date: '2019-05-16T13:08:00.621747+03:00', + updated_date: '2019-05-16T13:08:00.621797+03:00', + status: 'annotation', + tasks: [], + labels: JSON.parse(JSON.stringify(projectData.labels)), + }); + + const createdProject = await getProjects(`?id=${id}`); + return createdProject[0]; + } + + async function deleteProject(id) { + const projects = projectsDummyData.results; + const project = projects.filter((el) => el.id === id)[0]; + if (project) { + projects.splice(projects.indexOf(project), 1); } + } + async function getTasks(filter = '') { // Emulation of a query filter const queries = QueryStringToJSON(filter); const result = tasksDummyData.results.filter((x) => { @@ -101,7 +158,7 @@ class ServerProxy { } async function saveTask(id, taskData) { - const object = tasksDummyData.results.filter(task => task.id === id)[0]; + const object = tasksDummyData.results.filter((task) => task.id === id)[0]; for (const prop in taskData) { if (Object.prototype.hasOwnProperty.call(taskData, prop) && Object.prototype.hasOwnProperty.call(object, prop)) { @@ -111,11 +168,12 @@ class ServerProxy { } async function createTask(taskData) { - const id = Math.max(...tasksDummyData.results.map(el => el.id)) + 1; + const id = Math.max(...tasksDummyData.results.map((el) => el.id)) + 1; tasksDummyData.results.push({ id, url: `http://localhost:7000/api/v1/tasks/${id}`, name: taskData.name, + project_id: taskData.project_id || null, size: 5000, mode: 'interpolation', owner: 2, @@ -138,7 +196,7 @@ class ServerProxy { async function deleteTask(id) { const tasks = tasksDummyData.results; - const task = tasks.filter(el => el.id === id)[0]; + const task = tasks.filter((el) => el.id === id)[0]; if (task) { tasks.splice(tasks.indexOf(task), 1); } @@ -158,7 +216,7 @@ class ServerProxy { } return acc; - }, []).filter(job => job.id === jobID); + }, []).filter((job) => job.id === jobID); return jobs[0] || { detail: 'Not found.', @@ -174,7 +232,7 @@ class ServerProxy { } return acc; - }, []).filter(job => job.id === id)[0]; + }, []).filter((job) => job.id === id)[0]; for (const prop in jobData) { if (Object.prototype.hasOwnProperty.call(jobData, prop) @@ -256,6 +314,16 @@ class ServerProxy { writable: false, }, + projects: { + value: Object.freeze({ + getProjects, + saveProject, + createProject, + deleteProject, + }), + writable: false, + }, + tasks: { value: Object.freeze({ getTasks, From 158fa5e0b9172b8136f0fd80f58ccf1d2390b4b1 Mon Sep 17 00:00:00 2001 From: Dmitry Kalinin Date: Fri, 23 Oct 2020 02:56:11 +0300 Subject: [PATCH 31/55] added assignee, fixed tasks usage --- cvat-core/src/api-implementation.js | 3 +- cvat-core/src/session.js | 23 +++++++++- cvat-core/tests/api/projects.js | 2 - cvat-ui/src/actions/projects-actions.ts | 45 +++++++++++-------- cvat-ui/src/actions/tasks-actions.ts | 22 ++------- .../src/components/project-page/details.tsx | 27 +++++++++-- .../components/project-page/project-page.tsx | 19 ++++---- .../src/components/project-page/styles.scss | 7 ++- .../components/projects-page/project-item.tsx | 2 +- cvat-ui/src/reducers/interfaces.ts | 3 -- cvat-ui/src/reducers/projects-reducer.ts | 11 ----- 11 files changed, 91 insertions(+), 73 deletions(-) diff --git a/cvat-core/src/api-implementation.js b/cvat-core/src/api-implementation.js index fde2beaa8f43..32b9f3fe9ed6 100644 --- a/cvat-core/src/api-implementation.js +++ b/cvat-core/src/api-implementation.js @@ -247,6 +247,7 @@ id: isInteger, page: isInteger, name: isString, + assignee: isString, owner: isString, search: isString, status: isEnum.bind(TaskStatus), @@ -270,7 +271,7 @@ const searchParams = new URLSearchParams(); // TODO: need to check search fields - for (const field of ['name', 'owner', 'search', 'status', 'id', 'page']) { + for (const field of ['name', 'assignee', 'owner', 'search', 'status', 'id', 'page']) { if (Object.prototype.hasOwnProperty.call(filter, field)) { searchParams.set(field, filter[field]); } diff --git a/cvat-core/src/session.js b/cvat-core/src/session.js index 0028dbca068d..3496483f78cf 100644 --- a/cvat-core/src/session.js +++ b/cvat-core/src/session.js @@ -1451,6 +1451,7 @@ id: undefined, name: undefined, status: undefined, + assignee: undefined, owner: undefined, bug_tracker: undefined, created_date: undefined, @@ -1521,7 +1522,26 @@ get: () => data.status, }, /** - * Instance of a user who has created the task + * Instance of a user who was assigned for the project + * @name assignee + * @type {module:API.cvat.classes.User} + * @memberof module:API.cvat.classes.Project + * @readonly + * @instance + */ + assignee: { + get: () => data.assignee, + set: (assignee) => { + if (assignee !== null && !(assignee instanceof User)) { + throw new ArgumentError( + 'Value must be a user instance', + ); + } + data.assignee = assignee; + }, + }, + /** + * Instance of a user who has created the project * @name owner * @type {module:API.cvat.classes.User} * @memberof module:API.cvat.classes.Project @@ -2208,6 +2228,7 @@ if (typeof (this.id) !== 'undefined') { const projectData = { name: this.name, + assignee: this.assignee ? this.assignee.id : null, bug_tracker: this.bugTracker, labels: [...this.labels.map((el) => el.toJSON())], }; diff --git a/cvat-core/tests/api/projects.js b/cvat-core/tests/api/projects.js index c377587f2bd6..1e8c1270adac 100644 --- a/cvat-core/tests/api/projects.js +++ b/cvat-core/tests/api/projects.js @@ -99,8 +99,6 @@ describe('Feature: save a project', () => { id: 6, }); - console.log(result[0].labels); - const labelsLength = result[0].labels.length; const newLabel = new window.cvat.classes.Label({ name: 'My boss\'s car', diff --git a/cvat-ui/src/actions/projects-actions.ts b/cvat-ui/src/actions/projects-actions.ts index 65eb41055e63..f6d3346a0669 100644 --- a/cvat-ui/src/actions/projects-actions.ts +++ b/cvat-ui/src/actions/projects-actions.ts @@ -6,13 +6,13 @@ import { AnyAction, Dispatch, ActionCreator } from 'redux'; import { ThunkAction } from 'redux-thunk'; import { ProjectsQuery } from 'reducers/interfaces'; +import { getTasksSuccess, updateTaskSuccess } from 'actions/tasks-actions'; import getCore from 'cvat-core-wrapper'; const cvat = getCore(); export enum ProjectsActionTypes { UPDATE_PROJECTS_GETTING_QUERY = 'UPDATE_PROJECTS_GETTING_QUERY', - UPDATE_TASK_PREVIEW_IMAGE = 'UPDATE_TASK_PREVIEW_IMAGE', GET_PROJECTS = 'GET_PROJECTS', GET_PROJECTS_SUCCESS = 'GET_PROJECTS_SUCCESS', GET_PROJECTS_FAILED = 'GET_PROJECTS_FAILED', @@ -38,18 +38,6 @@ function updateProjectsGettingQuery(query: Partial): AnyAction { return action; } -function updateTaskImagePreview(taskId: number, image: string): AnyAction { - const action = { - type: ProjectsActionTypes.UPDATE_TASK_PREVIEW_IMAGE, - payload: { - taskId, - image, - }, - }; - - return action; -} - function getProjects(): AnyAction { const action = { type: ProjectsActionTypes.GET_PROJECTS, @@ -110,13 +98,31 @@ ThunkAction, {}, {}, AnyAction> { const array = Array.from(result); dispatch(getProjectsSuccess(array, result.count)); + const tasks: any[] = []; + const taskPreviewPromises: Promise[] = []; + for (const project of array) { - for (const task of (project as any).tasks) { - (task as any).frames.preview().then((image: string) => { - dispatch(updateTaskImagePreview(task.id, image)); - }); - } + taskPreviewPromises.push(...(project as any).tasks.map((task: any): string => { + tasks.push(task); + return (task as any).frames.preview().catch(() => ''); + })); } + + dispatch(getTasksSuccess( + tasks, + await Promise.all(taskPreviewPromises), + tasks.length, + { + page: 1, + assignee: null, + id: null, + mode: null, + name: null, + owner: null, + search: null, + status: null, + } + )) }; } @@ -206,6 +212,9 @@ ThunkAction, {}, {}, AnyAction> { await projectInstance.save(); const [project] = await cvat.projects.get({ id: projectInstance.id }); dispatch(updateProjectSuccess(project)); + project.tasks.forEach((task: any) => { + dispatch(updateTaskSuccess(task)); + }); } catch (error) { let project = null; try { diff --git a/cvat-ui/src/actions/tasks-actions.ts b/cvat-ui/src/actions/tasks-actions.ts index fe8df7795c53..4988212682fc 100644 --- a/cvat-ui/src/actions/tasks-actions.ts +++ b/cvat-ui/src/actions/tasks-actions.ts @@ -49,7 +49,7 @@ function getTasks(): AnyAction { return action; } -function getTasksSuccess(array: any[], previews: string[], +export function getTasksSuccess(array: any[], previews: string[], count: number, query: TasksQuery): AnyAction { const action = { type: TasksActionTypes.GET_TASKS_SUCCESS, @@ -98,26 +98,12 @@ ThunkAction, {}, {}, AnyAction> { } const array = Array.from(result); - const previews = []; const promises = array - .map((task): string => (task as any).frames.preview()); + .map((task): string => (task as any).frames.preview().catch('')); dispatch(getInferenceStatusAsync()); - for (const promise of promises) { - try { - // a tricky moment - // await is okay in loop in this case, there aren't any performance bottleneck - // because all server requests have been already sent in parallel - - // eslint-disable-next-line no-await-in-loop - previews.push(await promise); - } catch (error) { - previews.push(''); - } - } - - dispatch(getTasksSuccess(array, previews, result.count, query)); + dispatch(getTasksSuccess(array, await Promise.all(promises), result.count, query)); }; } @@ -458,7 +444,7 @@ function updateTask(): AnyAction { return action; } -function updateTaskSuccess(task: any): AnyAction { +export function updateTaskSuccess(task: any): AnyAction { const action = { type: TasksActionTypes.UPDATE_TASK_SUCCESS, payload: { task }, diff --git a/cvat-ui/src/components/project-page/details.tsx b/cvat-ui/src/components/project-page/details.tsx index f7844ecf0a3e..0e6e651bc612 100644 --- a/cvat-ui/src/components/project-page/details.tsx +++ b/cvat-ui/src/components/project-page/details.tsx @@ -3,7 +3,7 @@ // SPDX-License-Identifier: MIT import React, { useState } from 'react'; -import { useDispatch } from 'react-redux'; +import { useDispatch, useSelector } from 'react-redux'; import moment from 'moment'; import { Row, Col } from 'antd/lib/grid'; import Title from 'antd/lib/typography/Title'; @@ -12,11 +12,12 @@ import Icon from 'antd/lib/icon'; import Dropdown from 'antd/lib/dropdown'; import getCore from 'cvat-core-wrapper'; -import { Project } from 'reducers/interfaces'; +import { Project, CombinedState } from 'reducers/interfaces'; import { updateProjectAsync } from 'actions/projects-actions'; import ActionsMenu from 'components/projects-page/actions-menu'; import LabelsEditor from 'components/labels-editor/labels-editor'; import BugTrackerEditor from 'components/task-page/bug-tracker-editor'; +import UserSelector from 'components/task-page/user-selector'; import { MenuIcon } from 'icons'; const core = getCore(); @@ -29,6 +30,7 @@ export default function DetailsComponent(props: DetailsComponentProps): JSX.Elem const { project } = props; const dispatch = useDispatch(); + const registeredUsers = useSelector((state: CombinedState) => state.users.users); const [projectName, setProjectName] = useState(project.instance.name); return ( @@ -60,13 +62,30 @@ export default function DetailsComponent(props: DetailsComponentProps): JSX.Elem }} /> - -
    + +
    Actions }>
    + { + let [userInstance] = registeredUsers + .filter((user: any) => user.username === value); + + if (userInstance === undefined) { + userInstance = null; + } + + project.instance.assignee = userInstance; + dispatch(updateProjectAsync(project.instance)); + } + } + /> state.projects.activities.deletes); const taskDeletes = useSelector((state: CombinedState) => state.tasks.activities.deletes); const tasksActiveInferences = useSelector((state: CombinedState) => state.models.inferences); - const previewImages = useSelector((state: CombinedState) => state.projects.taskPreviews); + const tasks = useSelector((state: CombinedState) => state.tasks.current); const filteredProjects = projects.filter( (project) => project.instance.id === id, @@ -88,17 +87,17 @@ export default function ProjectPageComponent(): JSX.Element { - {project.instance.tasks.map((task: any) => ( + {tasks.filter((task) => task.instance.projectId === project.instance.id).map((task: Task) => (
    - }> + }>
    diff --git a/cvat-ui/src/components/projects-page/project-list.tsx b/cvat-ui/src/components/projects-page/project-list.tsx index 9db7425f4dac..944e4faa0d66 100644 --- a/cvat-ui/src/components/projects-page/project-list.tsx +++ b/cvat-ui/src/components/projects-page/project-list.tsx @@ -19,10 +19,12 @@ export default function ProjectListComponent(): JSX.Element { const gettingQuery = useSelector((state: CombinedState) => state.projects.gettingQuery); function changePage(p: number): void { - dispatch(getProjectsAsync({ - ...gettingQuery, - page: p, - })); + dispatch( + getProjectsAsync({ + ...gettingQuery, + page: p, + }), + ); } return ( @@ -32,7 +34,7 @@ export default function ProjectListComponent(): JSX.Element { {projectInstances.map( (instance: any): JSX.Element => ( - + ), diff --git a/cvat-ui/src/components/projects-page/styles.scss b/cvat-ui/src/components/projects-page/styles.scss index 35c0b8f06aec..859454abda1e 100644 --- a/cvat-ui/src/components/projects-page/styles.scss +++ b/cvat-ui/src/components/projects-page/styles.scss @@ -5,14 +5,14 @@ @import '../../base.scss'; .cvat-projects-page { - padding-top: 15px; - padding-bottom: 40px; + padding-top: $grid-unit-size * 2; + padding-bottom: $grid-unit-size * 5; height: 100%; position: fixed; width: 100%; > div:nth-child(1) { - padding-bottom: 10px; + padding-bottom: $grid-unit-size; div > { span { @@ -25,12 +25,12 @@ /* empty-projects icon */ .cvat-empty-projects-list { > div:nth-child(1) { - margin-top: 50px; + margin-top: $grid-unit-size * 6; } > div:nth-child(2) { > div { - margin-top: 20px; + margin-top: $grid-unit-size * 3; /* No projects created yet */ > span { @@ -42,7 +42,7 @@ /* To get started with your annotation project .. */ > div:nth-child(3) { - margin-top: 10px; + margin-top: $grid-unit-size; } } @@ -52,7 +52,7 @@ > span:nth-child(2) { width: 200px; - margin-left: 10px; + margin-left: $grid-unit-size; } } @@ -63,7 +63,7 @@ } .cvat-create-project-button { - padding: 0 30px; + padding: 0 $grid-unit-size * 4; } .cvat-projects-pagination { @@ -108,7 +108,7 @@ .cvat-projects-project-item-card { .ant-empty { - margin: 8px; + margin: $grid-unit-size; } img { diff --git a/cvat-ui/src/components/projects-page/top-bar.tsx b/cvat-ui/src/components/projects-page/top-bar.tsx index 075de8156f99..8fcd2b69ad11 100644 --- a/cvat-ui/src/components/projects-page/top-bar.tsx +++ b/cvat-ui/src/components/projects-page/top-bar.tsx @@ -19,23 +19,16 @@ export default function TopBarComponent(): JSX.Element { Projects - + diff --git a/cvat-ui/src/components/task-page/bug-tracker-editor.tsx b/cvat-ui/src/components/task-page/bug-tracker-editor.tsx index 263a4664b367..4e605d3baa0f 100644 --- a/cvat-ui/src/components/task-page/bug-tracker-editor.tsx +++ b/cvat-ui/src/components/task-page/bug-tracker-editor.tsx @@ -16,10 +16,7 @@ interface Props { } export default function BugTrackerEditorComponent(props: Props): JSX.Element { - const { - instance, - onChange, - } = props; + const { instance, onChange } = props; const [bugTracker, setBugTracker] = useState(instance.bugTracker); const [bugTrackerEditing, setBugTrackerEditing] = useState(false); @@ -34,9 +31,9 @@ export default function BugTrackerEditorComponent(props: Props): JSX.Element { Modal.error({ title: `Could not update the ${instanceType} ${instance.id}`, content: 'Issue tracker is expected to be URL', - onOk: (() => { + onOk: () => { shown = false; - }), + }, }); shown = true; } @@ -51,9 +48,11 @@ export default function BugTrackerEditorComponent(props: Props): JSX.Element { if (bugTracker) { return ( - + - Issue Tracker + + Issue Tracker +
    {bugTracker}
    @@ -74,9 +73,11 @@ export default function BugTrackerEditorComponent(props: Props): JSX.Element { } return ( - + - Issue Tracker + + Issue Tracker +
    ); } - private renderUsers(): JSX.Element { + private renderDescription(): JSX.Element { const { taskInstance, registeredUsers, onTaskUpdate } = this.props; const owner = taskInstance.owner ? taskInstance.owner.username : null; const assignee = taskInstance.assignee ? taskInstance.assignee.username : null; @@ -213,7 +213,11 @@ export default class DetailsComponent extends React.PureComponent return ( - {owner && {`Created by ${owner} on ${created}`}} + + {owner && ( + {`Task #${taskInstance.id} сreated by ${owner} on ${created}`} + )} + Assigned to @@ -336,7 +340,7 @@ export default class DetailsComponent extends React.PureComponent - {this.renderUsers()} + {this.renderDescription()} diff --git a/cvat-ui/src/components/task-page/styles.scss b/cvat-ui/src/components/task-page/styles.scss index 2674edaca0c3..23e7c0b95289 100644 --- a/cvat-ui/src/components/task-page/styles.scss +++ b/cvat-ui/src/components/task-page/styles.scss @@ -72,8 +72,7 @@ } .cvat-task-top-bar { - margin-top: 20px; - margin-bottom: 10px; + margin: $grid-unit-size * 2 0; } .cvat-task-preview-wrapper { diff --git a/cvat-ui/src/components/task-page/top-bar.tsx b/cvat-ui/src/components/task-page/top-bar.tsx index 8445d362f2b3..ba87a6fe7747 100644 --- a/cvat-ui/src/components/task-page/top-bar.tsx +++ b/cvat-ui/src/components/task-page/top-bar.tsx @@ -19,7 +19,6 @@ interface DetailsComponentProps { export default function DetailsComponent(props: DetailsComponentProps): JSX.Element { const { taskInstance } = props; - const { id } = taskInstance; const history = useHistory(); @@ -33,7 +32,7 @@ export default function DetailsComponent(props: DetailsComponentProps): JSX.Elem size='large' > - {`Back to project #${taskInstance.projectId}`} + Back to project ) : ( )} - - - - {`Task details #${id}`} - - - }> - - {contextMenuFor && contextMenuFor.shapeType === 'polygon' && ( - - )} - , - window.document.body, - ) : - null; + return visible && contextMenuFor && type === ContextMenuType.CANVAS_SHAPE_POINT + ? ReactDOM.createPortal( +
    + + + + {contextMenuFor && contextMenuFor.shapeType === 'polygon' && ( + + )} +
    , + window.document.body, + ) + : null; } export default connect(mapStateToProps, mapDispatchToProps)(CanvasPointContextMenu); diff --git a/cvat-ui/src/components/annotation-page/standard-workspace/canvas-wrapper.tsx b/cvat-ui/src/components/annotation-page/standard-workspace/canvas-wrapper.tsx index da8b1cba031f..d760411ab66a 100644 --- a/cvat-ui/src/components/annotation-page/standard-workspace/canvas-wrapper.tsx +++ b/cvat-ui/src/components/annotation-page/standard-workspace/canvas-wrapper.tsx @@ -10,9 +10,7 @@ import Icon from 'antd/lib/icon'; import Layout from 'antd/lib/layout/layout'; import Slider, { SliderValue } from 'antd/lib/slider'; -import { - ColorBy, GridColor, ObjectType, ContextMenuType, Workspace, ShapeType, -} from 'reducers/interfaces'; +import { ColorBy, GridColor, ObjectType, ContextMenuType, Workspace, ShapeType } from 'reducers/interfaces'; import { LogType } from 'cvat-logger'; import { Canvas } from 'cvat-canvas-wrapper'; import getCore from 'cvat-core-wrapper'; @@ -306,9 +304,7 @@ export default class CanvasWrapperComponent extends React.PureComponent { } private onCanvasShapeDrawn = (event: any): void => { - const { - jobInstance, activeLabelID, activeObjectType, frame, onShapeDrawn, onCreateAnnotations, - } = this.props; + const { jobInstance, activeLabelID, activeObjectType, frame, onShapeDrawn, onCreateAnnotations } = this.props; if (!event.detail.continue) { onShapeDrawn(); @@ -331,9 +327,7 @@ export default class CanvasWrapperComponent extends React.PureComponent { }; private onCanvasObjectsMerged = (event: any): void => { - const { - jobInstance, frame, onMergeAnnotations, onMergeObjects, - } = this.props; + const { jobInstance, frame, onMergeAnnotations, onMergeObjects } = this.props; onMergeObjects(false); @@ -346,9 +340,7 @@ export default class CanvasWrapperComponent extends React.PureComponent { }; private onCanvasObjectsGroupped = (event: any): void => { - const { - jobInstance, frame, onGroupAnnotations, onGroupObjects, - } = this.props; + const { jobInstance, frame, onGroupAnnotations, onGroupObjects } = this.props; onGroupObjects(false); @@ -357,9 +349,7 @@ export default class CanvasWrapperComponent extends React.PureComponent { }; private onCanvasTrackSplitted = (event: any): void => { - const { - jobInstance, frame, onSplitAnnotations, onSplitTrack, - } = this.props; + const { jobInstance, frame, onSplitAnnotations, onSplitTrack } = this.props; onSplitTrack(false); @@ -439,9 +429,7 @@ export default class CanvasWrapperComponent extends React.PureComponent { }; private onCanvasCursorMoved = async (event: any): Promise => { - const { - jobInstance, activatedStateID, workspace, onActivateObject, - } = this.props; + const { jobInstance, activatedStateID, workspace, onActivateObject } = this.props; if (workspace !== Workspace.STANDARD) { return; @@ -572,9 +560,7 @@ export default class CanvasWrapperComponent extends React.PureComponent { } private updateShapesView(): void { - const { - annotations, opacity, colorBy, outlined, outlineColor, - } = this.props; + const { annotations, opacity, colorBy, outlined, outlineColor } = this.props; for (const state of annotations) { let shapeColor = ''; @@ -602,9 +588,7 @@ export default class CanvasWrapperComponent extends React.PureComponent { } private updateCanvas(): void { - const { - curZLayer, annotations, frameData, canvasInstance, - } = this.props; + const { curZLayer, annotations, frameData, canvasInstance } = this.props; if (frameData !== null) { canvasInstance.setup( 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 131c743e2013..b1a719d1ab75 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 @@ -24,9 +24,9 @@ function CursorControl(props: Props): JSX.Element { canvasInstance.cancel() : undefined} /> 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 7b4d01b3e3f8..640724401631 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 @@ -11,7 +11,6 @@ import { ShapeType } from 'reducers/interfaces'; import { CubeIcon } from 'icons'; -// eslint-disable-next-line max-len import DrawShapePopoverContainer from 'containers/annotation-page/standard-workspace/controls-side-bar/draw-shape-popover'; interface Props { @@ -22,22 +21,22 @@ interface Props { function DrawPolygonControl(props: Props): JSX.Element { const { canvasInstance, isDrawing } = props; - const dynamcPopoverPros = isDrawing ? - { - overlayStyle: { - display: 'none', - }, - } : - {}; - - const dynamicIconProps = isDrawing ? - { - className: 'cvat-active-canvas-control', - onClick: (): void => { - canvasInstance.draw({ enabled: false }); - }, - } : - {}; + const dynamcPopoverPros = isDrawing + ? { + overlayStyle: { + display: 'none', + }, + } + : {}; + + const dynamicIconProps = isDrawing + ? { + className: 'cvat-active-canvas-control', + onClick: (): void => { + canvasInstance.draw({ enabled: false }); + }, + } + : {}; return ( { - canvasInstance.draw({ enabled: false }); - }, - } : - {}; + const dynamcPopoverPros = isDrawing + ? { + overlayStyle: { + display: 'none', + }, + } + : {}; + + const dynamicIconProps = isDrawing + ? { + className: 'cvat-active-canvas-control', + onClick: (): void => { + canvasInstance.draw({ enabled: false }); + }, + } + : {}; return ( { - canvasInstance.draw({ enabled: false }); - }, - } : - {}; + const dynamcPopoverPros = isDrawing + ? { + overlayStyle: { + display: 'none', + }, + } + : {}; + + const dynamicIconProps = isDrawing + ? { + className: 'cvat-active-canvas-control', + onClick: (): void => { + canvasInstance.draw({ enabled: false }); + }, + } + : {}; return ( { - canvasInstance.draw({ enabled: false }); - }, - } : - {}; + const dynamcPopoverPros = isDrawing + ? { + overlayStyle: { + display: 'none', + }, + } + : {}; + + const dynamicIconProps = isDrawing + ? { + className: 'cvat-active-canvas-control', + onClick: (): void => { + canvasInstance.draw({ enabled: false }); + }, + } + : {}; return ( { - canvasInstance.draw({ enabled: false }); - }, - } : - {}; + const dynamcPopoverPros = isDrawing + ? { + overlayStyle: { + display: 'none', + }, + } + : {}; + + const dynamicIconProps = isDrawing + ? { + className: 'cvat-active-canvas-control', + onClick: (): void => { + canvasInstance.draw({ enabled: false }); + }, + } + : {}; return ( { - canvasInstance.group({ enabled: false }); - groupObjects(false); - }, - } : - { - className: 'cvat-group-control', - onClick: (): void => { - canvasInstance.cancel(); - canvasInstance.group({ enabled: true }); - groupObjects(true); - }, - }; + activeControl === ActiveControl.GROUP + ? { + className: 'cvat-group-control cvat-active-canvas-control', + onClick: (): void => { + canvasInstance.group({ enabled: false }); + groupObjects(false); + }, + } + : { + className: 'cvat-group-control', + onClick: (): void => { + canvasInstance.cancel(); + canvasInstance.group({ enabled: true }); + groupObjects(true); + }, + }; - const title = `Group shapes/tracks ${switchGroupShortcut}. Select and press ${resetGroupShortcut} to reset a group`; + const title = + `Group shapes/tracks ${switchGroupShortcut}.` + ` Select and press ${resetGroupShortcut} to reset a group`; 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 c2fa9c764695..d7537026d33a 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 @@ -18,27 +18,25 @@ interface Props { } function MergeControl(props: Props): JSX.Element { - const { - switchMergeShortcut, activeControl, canvasInstance, mergeObjects, - } = props; + const { switchMergeShortcut, activeControl, canvasInstance, mergeObjects } = props; const dynamicIconProps = - activeControl === ActiveControl.MERGE ? - { - className: 'cvat-merge-control cvat-active-canvas-control', - onClick: (): void => { - canvasInstance.merge({ enabled: false }); - mergeObjects(false); - }, - } : - { - className: 'cvat-merge-control', - onClick: (): void => { - canvasInstance.cancel(); - canvasInstance.merge({ enabled: true }); - mergeObjects(true); - }, - }; + activeControl === ActiveControl.MERGE + ? { + className: 'cvat-merge-control cvat-active-canvas-control', + onClick: (): void => { + canvasInstance.merge({ enabled: false }); + mergeObjects(false); + }, + } + : { + className: 'cvat-merge-control', + onClick: (): void => { + canvasInstance.cancel(); + canvasInstance.merge({ enabled: true }); + mergeObjects(true); + }, + }; 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 966484a62f92..798b868acc89 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 @@ -23,9 +23,9 @@ function MoveControl(props: Props): JSX.Element { { if (activeControl === ActiveControl.DRAG_CANVAS) { diff --git a/cvat-ui/src/components/annotation-page/standard-workspace/controls-side-bar/resize-control.tsx b/cvat-ui/src/components/annotation-page/standard-workspace/controls-side-bar/resize-control.tsx index 0bae44099bb5..c3b201d243c8 100644 --- a/cvat-ui/src/components/annotation-page/standard-workspace/controls-side-bar/resize-control.tsx +++ b/cvat-ui/src/components/annotation-page/standard-workspace/controls-side-bar/resize-control.tsx @@ -23,9 +23,9 @@ function ResizeControl(props: Props): JSX.Element { { if (activeControl === ActiveControl.ZOOM_CANVAS) { diff --git a/cvat-ui/src/components/annotation-page/standard-workspace/controls-side-bar/rotate-control.tsx b/cvat-ui/src/components/annotation-page/standard-workspace/controls-side-bar/rotate-control.tsx index fca523ca0d37..229353692840 100644 --- a/cvat-ui/src/components/annotation-page/standard-workspace/controls-side-bar/rotate-control.tsx +++ b/cvat-ui/src/components/annotation-page/standard-workspace/controls-side-bar/rotate-control.tsx @@ -23,7 +23,7 @@ function RotateControl(props: Props): JSX.Element { - )} + } trigger='hover' > diff --git a/cvat-ui/src/components/annotation-page/standard-workspace/controls-side-bar/setup-tag-control.tsx b/cvat-ui/src/components/annotation-page/standard-workspace/controls-side-bar/setup-tag-control.tsx index bc34bf33da5e..7817e798342d 100644 --- a/cvat-ui/src/components/annotation-page/standard-workspace/controls-side-bar/setup-tag-control.tsx +++ b/cvat-ui/src/components/annotation-page/standard-workspace/controls-side-bar/setup-tag-control.tsx @@ -9,7 +9,6 @@ import Icon from 'antd/lib/icon'; import { Canvas } from 'cvat-canvas-wrapper'; import { TagIcon } from 'icons'; -// eslint-disable-next-line max-len import SetupTagPopoverContainer from 'containers/annotation-page/standard-workspace/controls-side-bar/setup-tag-popover'; interface Props { @@ -20,13 +19,13 @@ interface Props { function SetupTagControl(props: Props): JSX.Element { const { isDrawing } = props; - const dynamcPopoverPros = isDrawing ? - { - overlayStyle: { - display: 'none', - }, - } : - {}; + const dynamcPopoverPros = isDrawing + ? { + overlayStyle: { + display: 'none', + }, + } + : {}; return ( 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 d8fb0c763ac7..3406a4a25598 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 @@ -18,27 +18,25 @@ interface Props { } function SplitControl(props: Props): JSX.Element { - const { - switchSplitShortcut, activeControl, canvasInstance, splitTrack, - } = props; + const { switchSplitShortcut, activeControl, canvasInstance, splitTrack } = props; const dynamicIconProps = - activeControl === ActiveControl.SPLIT ? - { - className: 'cvat-split-track-control cvat-active-canvas-control', - onClick: (): void => { - canvasInstance.split({ enabled: false }); - splitTrack(false); - }, - } : - { - className: 'cvat-split-track-control', - onClick: (): void => { - canvasInstance.cancel(); - canvasInstance.split({ enabled: true }); - splitTrack(true); - }, - }; + activeControl === ActiveControl.SPLIT + ? { + className: 'cvat-split-track-control cvat-active-canvas-control', + onClick: (): void => { + canvasInstance.split({ enabled: false }); + splitTrack(false); + }, + } + : { + className: 'cvat-split-track-control', + onClick: (): void => { + canvasInstance.cancel(); + canvasInstance.split({ enabled: true }); + splitTrack(true); + }, + }; return ( 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 a8954d040e5e..01f4658ab3ad 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 @@ -19,9 +19,7 @@ import { AIToolsIcon } from 'icons'; import { Canvas } from 'cvat-canvas-wrapper'; import range from 'utils/range'; import getCore from 'cvat-core-wrapper'; -import { - CombinedState, ActiveControl, Model, ObjectType, ShapeType, -} from 'reducers/interfaces'; +import { CombinedState, ActiveControl, Model, ObjectType, ShapeType } from 'reducers/interfaces'; import { interactWithCanvas, fetchAnnotationsAsync, @@ -180,9 +178,7 @@ export class ToolsControlComponent extends React.PureComponent { }; private cancelListener = async (): Promise => { - const { - isActivated, jobInstance, frame, fetchAnnotations, - } = this.props; + const { isActivated, jobInstance, frame, fetchAnnotations } = this.props; const { interactiveStateID, fetching } = this.state; if (isActivated) { @@ -317,9 +313,7 @@ export class ToolsControlComponent extends React.PureComponent { }; private onTracking = async (e: Event): Promise => { - const { - isActivated, jobInstance, frame, curZOrder, fetchAnnotations, - } = this.props; + const { isActivated, jobInstance, frame, curZOrder, fetchAnnotations } = this.props; const { activeLabelID } = this.state; const [label] = jobInstance.task.labels.filter((_label: any): boolean => _label.id === activeLabelID); @@ -492,12 +486,8 @@ export class ToolsControlComponent extends React.PureComponent { } private renderTrackerBlock(): JSX.Element { - const { - trackers, canvasInstance, jobInstance, frame, onInteractionStart, - } = this.props; - const { - activeTracker, activeLabelID, fetching, trackingFrames, - } = this.state; + const { trackers, canvasInstance, jobInstance, frame, onInteractionStart } = this.props; + const { activeTracker, activeLabelID, fetching, trackingFrames } = this.state; if (!trackers.length) { return ( @@ -660,9 +650,7 @@ export class ToolsControlComponent extends React.PureComponent { } private renderDetectorBlock(): JSX.Element { - const { - jobInstance, detectors, curZOrder, frame, fetchAnnotations, - } = this.props; + const { jobInstance, detectors, curZOrder, frame, fetchAnnotations } = this.props; if (!detectors.length) { return ( @@ -694,17 +682,18 @@ export class ToolsControlComponent extends React.PureComponent { }); const states = result.map( - (data: any): any => new core.classes.ObjectState({ - shapeType: data.type, - label: task.labels.filter((label: any): boolean => label.name === data.label)[0], - points: data.points, - objectType: ObjectType.SHAPE, - frame, - occluded: false, - source: 'auto', - attributes: {}, - zOrder: curZOrder, - }), + (data: any): any => + new core.classes.ObjectState({ + shapeType: data.type, + label: task.labels.filter((label: any): boolean => label.name === data.label)[0], + points: data.points, + objectType: ObjectType.SHAPE, + frame, + occluded: false, + source: 'auto', + attributes: {}, + zOrder: curZOrder, + }), ); await jobInstance.annotations.put(states); @@ -750,31 +739,29 @@ export class ToolsControlComponent extends React.PureComponent { } public render(): JSX.Element | null { - const { - interactors, detectors, trackers, isActivated, canvasInstance, - } = this.props; + const { interactors, detectors, trackers, isActivated, canvasInstance } = this.props; const { fetching, trackingProgress } = this.state; if (![...interactors, ...detectors, ...trackers].length) return null; - const dynamcPopoverPros = isActivated ? - { - overlayStyle: { - display: 'none', - }, - } : - {}; - - const dynamicIconProps = isActivated ? - { - className: 'cvat-active-canvas-control cvat-tools-control', - onClick: (): void => { - canvasInstance.interact({ enabled: false }); - }, - } : - { - className: 'cvat-tools-control', - }; + const dynamcPopoverPros = isActivated + ? { + overlayStyle: { + display: 'none', + }, + } + : {}; + + const dynamicIconProps = isActivated + ? { + className: 'cvat-active-canvas-control cvat-tools-control', + onClick: (): void => { + canvasInstance.interact({ enabled: false }); + }, + } + : { + className: 'cvat-tools-control', + }; return ( <> diff --git a/cvat-ui/src/components/annotation-page/standard-workspace/objects-side-bar/object-item-attribute.tsx b/cvat-ui/src/components/annotation-page/standard-workspace/objects-side-bar/object-item-attribute.tsx index 5a2c34401b99..01a7640a6635 100644 --- a/cvat-ui/src/components/annotation-page/standard-workspace/objects-side-bar/object-item-attribute.tsx +++ b/cvat-ui/src/components/annotation-page/standard-workspace/objects-side-bar/object-item-attribute.tsx @@ -36,9 +36,7 @@ function attrIsTheSame(prevProps: Props, nextProps: Props): boolean { } function ItemAttributeComponent(props: Props): JSX.Element { - const { - attrInputType, attrValues, attrValue, attrName, attrID, changeAttribute, - } = props; + const { attrInputType, attrValues, attrValue, attrName, attrID, changeAttribute } = props; const attrNameStyle: React.CSSProperties = { wordBreak: 'break-word', lineHeight: '1em' }; diff --git a/cvat-ui/src/components/annotation-page/standard-workspace/objects-side-bar/object-item-details.tsx b/cvat-ui/src/components/annotation-page/standard-workspace/objects-side-bar/object-item-details.tsx index 992aac595a34..6bae7966fbf0 100644 --- a/cvat-ui/src/components/annotation-page/standard-workspace/objects-side-bar/object-item-details.tsx +++ b/cvat-ui/src/components/annotation-page/standard-workspace/objects-side-bar/object-item-details.tsx @@ -35,9 +35,7 @@ function attrAreTheSame(prevProps: Props, nextProps: Props): boolean { } function ItemAttributesComponent(props: Props): JSX.Element { - const { - collapsed, attributes, values, changeAttribute, collapse, - } = props; + const { collapsed, attributes, values, changeAttribute, collapse } = props; const sorted = [...attributes].sort((a: any, b: any): number => a.inputType.localeCompare(b.inputType)); diff --git a/cvat-ui/src/components/annotation-page/standard-workspace/objects-side-bar/object-item-menu.tsx b/cvat-ui/src/components/annotation-page/standard-workspace/objects-side-bar/object-item-menu.tsx index ca86b3beb3d2..ce3a1db3fb0d 100644 --- a/cvat-ui/src/components/annotation-page/standard-workspace/objects-side-bar/object-item-menu.tsx +++ b/cvat-ui/src/components/annotation-page/standard-workspace/objects-side-bar/object-item-menu.tsx @@ -9,9 +9,7 @@ import Button from 'antd/lib/button'; import Modal from 'antd/lib/modal'; import Tooltip from 'antd/lib/tooltip'; -import { - BackgroundIcon, ForegroundIcon, ResetPerspectiveIcon, ColorizeIcon, -} from 'icons'; +import { BackgroundIcon, ForegroundIcon, ResetPerspectiveIcon, ColorizeIcon } from 'icons'; import { ObjectType, ShapeType, ColorBy } from 'reducers/interfaces'; import ColorPicker from './color-picker'; diff --git a/cvat-ui/src/components/annotation-page/standard-workspace/objects-side-bar/object-item.tsx b/cvat-ui/src/components/annotation-page/standard-workspace/objects-side-bar/object-item.tsx index cad3a3a9992a..c29ef720065a 100644 --- a/cvat-ui/src/components/annotation-page/standard-workspace/objects-side-bar/object-item.tsx +++ b/cvat-ui/src/components/annotation-page/standard-workspace/objects-side-bar/object-item.tsx @@ -96,13 +96,13 @@ function ObjectItemComponent(props: Props): JSX.Element { } = props; const type = - objectType === ObjectType.TAG ? - ObjectType.TAG.toUpperCase() : - `${shapeType.toUpperCase()} ${objectType.toUpperCase()}`; + objectType === ObjectType.TAG + ? ObjectType.TAG.toUpperCase() + : `${shapeType.toUpperCase()} ${objectType.toUpperCase()}`; - const className = !activated ? - 'cvat-objects-sidebar-state-item' : - 'cvat-objects-sidebar-state-item cvat-objects-sidebar-state-active-item'; + const className = !activated + ? 'cvat-objects-sidebar-state-item' + : 'cvat-objects-sidebar-state-item cvat-objects-sidebar-state-active-item'; return (
    diff --git a/cvat-ui/src/components/annotation-page/standard-workspace/objects-side-bar/objects-side-bar.tsx b/cvat-ui/src/components/annotation-page/standard-workspace/objects-side-bar/objects-side-bar.tsx index 330a79be83bd..4fc15bb1c211 100644 --- a/cvat-ui/src/components/annotation-page/standard-workspace/objects-side-bar/objects-side-bar.tsx +++ b/cvat-ui/src/components/annotation-page/standard-workspace/objects-side-bar/objects-side-bar.tsx @@ -58,9 +58,7 @@ function mapDispatchToProps(dispatch: Dispatch): DispatchToProps { } function ObjectsSideBar(props: StateToProps & DispatchToProps): JSX.Element { - const { - sidebarCollapsed, canvasInstance, collapseSidebar, updateTabContentHeight, - } = props; + const { sidebarCollapsed, canvasInstance, collapseSidebar, updateTabContentHeight } = props; useEffect(() => { const alignTabHeight = (): void => { diff --git a/cvat-ui/src/components/annotation-page/standard-workspace/standard-workspace.tsx b/cvat-ui/src/components/annotation-page/standard-workspace/standard-workspace.tsx index 868981390d2d..d57be1ec2249 100644 --- a/cvat-ui/src/components/annotation-page/standard-workspace/standard-workspace.tsx +++ b/cvat-ui/src/components/annotation-page/standard-workspace/standard-workspace.tsx @@ -7,7 +7,6 @@ import React from 'react'; import Layout from 'antd/lib/layout'; import CanvasWrapperContainer from 'containers/annotation-page/standard-workspace/canvas-wrapper'; -// eslint-disable-next-line max-len import ControlsSideBarContainer from 'containers/annotation-page/standard-workspace/controls-side-bar/controls-side-bar'; import PropagateConfirmContainer from 'containers/annotation-page/standard-workspace/propagate-confirm'; import CanvasContextMenuContainer from 'containers/annotation-page/standard-workspace/canvas-context-menu'; diff --git a/cvat-ui/src/components/annotation-page/top-bar/annotation-menu.tsx b/cvat-ui/src/components/annotation-page/top-bar/annotation-menu.tsx index 52a53ba41e0c..1866bd350631 100644 --- a/cvat-ui/src/components/annotation-page/top-bar/annotation-menu.tsx +++ b/cvat-ui/src/components/annotation-page/top-bar/annotation-menu.tsx @@ -30,9 +30,7 @@ export enum Actions { } export default function AnnotationMenuComponent(props: Props): JSX.Element { - const { - taskMode, loaders, dumpers, onClickMenu, loadActivity, dumpActivities, exportActivities, taskID, - } = props; + const { taskMode, loaders, dumpers, onClickMenu, loadActivity, dumpActivities, exportActivities, taskID } = props; let latestParams: ClickParam | null = null; function onClickMenuWrapper(params: ClickParam | null, file?: File): void { 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 bd958204f0ea..0829e3154ec8 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 @@ -11,9 +11,7 @@ import Timeline from 'antd/lib/timeline'; import Dropdown from 'antd/lib/dropdown'; import AnnotationMenuContainer from 'containers/annotation-page/top-bar/annotation-menu'; -import { - MainMenuIcon, SaveIcon, UndoIcon, RedoIcon, -} from 'icons'; +import { MainMenuIcon, SaveIcon, UndoIcon, RedoIcon } from 'icons'; interface Props { saving: boolean; 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 db1bb4a836c8..2903e4487d0a 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 @@ -109,7 +109,7 @@ function PlayerButtons(props: Props): JSX.Element { - )} + } > - )} + } > {nextButton} diff --git a/cvat-ui/src/components/change-password-modal/change-password-modal.tsx b/cvat-ui/src/components/change-password-modal/change-password-modal.tsx index 6533dc77240c..d3a3c0fee8bb 100644 --- a/cvat-ui/src/components/change-password-modal/change-password-modal.tsx +++ b/cvat-ui/src/components/change-password-modal/change-password-modal.tsx @@ -43,9 +43,7 @@ function mapDispatchToProps(dispatch: any): DispatchToProps { } function ChangePasswordComponent(props: ChangePasswordPageComponentProps): JSX.Element { - const { - fetching, onChangePassword, visible, onClose, - } = props; + const { fetching, onChangePassword, visible, onClose } = props; return ( This site uses cookies for functionality, analytics, and advertising purposes as described in our Cookie and Similar Technologies Notice. To see what cookies we serve and set your preferences, please visit our - Cookie Consent Tool - . By continuing to use our website, you + Cookie Consent Tool. By continuing to use our website, you agree to our use of cookies. - )} + } >
    Player - )} + } key='player' > Workspace - )} + } key='workspace' > diff --git a/cvat-ui/src/components/header/settings-modal/workspace-settings.tsx b/cvat-ui/src/components/header/settings-modal/workspace-settings.tsx index 423972e41bf3..9ccb0ae8570f 100644 --- a/cvat-ui/src/components/header/settings-modal/workspace-settings.tsx +++ b/cvat-ui/src/components/header/settings-modal/workspace-settings.tsx @@ -112,8 +112,7 @@ export default function WorkspaceSettingsComponent(props: Props): JSX.Element { {' '} - Show text for an object on the canvas not only when the object is activated - {' '} + Show text for an object on the canvas not only when the object is activated{' '} @@ -132,8 +131,7 @@ export default function WorkspaceSettingsComponent(props: Props): JSX.Element { {' '} - Enable automatic bordering for polygons and polylines during drawing/editing - {' '} + Enable automatic bordering for polygons and polylines during drawing/editing{' '} diff --git a/cvat-ui/src/components/labels-editor/common.ts b/cvat-ui/src/components/labels-editor/common.ts index d1b5f02c4f3f..aeec12b10755 100644 --- a/cvat-ui/src/components/labels-editor/common.ts +++ b/cvat-ui/src/components/labels-editor/common.ts @@ -26,20 +26,24 @@ function validateParsedAttribute(attr: Attribute): void { if (!['number', 'undefined'].includes(typeof attr.id)) { throw new Error( - `Attribute: "${attr.name}". Type of attribute id must be a number or undefined. Got value ${attr.id}`, + `Attribute: "${attr.name}". ` + `Type of attribute id must be a number or undefined. Got value ${attr.id}`, ); } if (!['checkbox', 'number', 'text', 'radio', 'select'].includes((attr.input_type || '').toLowerCase())) { - throw new Error(`Attribute: "${attr.name}". Unknown input type: ${attr.input_type}`); + throw new Error(`Attribute: "${attr.name}". ` + `Unknown input type: ${attr.input_type}`); } if (typeof attr.mutable !== 'boolean') { - throw new Error(`Attribute: "${attr.name}". Mutable flag must be a boolean value. Got value ${attr.mutable}`); + throw new Error( + `Attribute: "${attr.name}". ` + `Mutable flag must be a boolean value. Got value ${attr.mutable}`, + ); } if (!Array.isArray(attr.values)) { - throw new Error(`Attribute: "${attr.name}". Attribute values must be an array. Got type ${typeof attr.values}`); + throw new Error( + `Attribute: "${attr.name}". ` + `Attribute values must be an array. Got type ${typeof attr.values}`, + ); } if (!attr.values.length) { @@ -48,7 +52,7 @@ function validateParsedAttribute(attr: Attribute): void { for (const value of attr.values) { if (typeof value !== 'string') { - throw new Error(`Attribute: "${attr.name}". Each value must be a string. Got value ${value}`); + throw new Error(`Attribute: "${attr.name}". ` + `Each value must be a string. Got value ${value}`); } } } @@ -60,12 +64,12 @@ export function validateParsedLabel(label: Label): void { if (!['number', 'undefined'].includes(typeof label.id)) { throw new Error( - `Label "${label.name}". Type of label id must be only a number or undefined. Got value ${label.id}`, + `Label "${label.name}". ` + `Type of label id must be only a number or undefined. Got value ${label.id}`, ); } if (typeof label.color !== 'string') { - throw new Error(`Label "${label.name}". Label color must be a string. Got ${typeof label.color}`); + throw new Error(`Label "${label.name}". ` + `Label color must be a string. Got ${typeof label.color}`); } if (!label.color.match(/^#[0-9a-f]{6}$|^$/)) { @@ -76,7 +80,7 @@ export function validateParsedLabel(label: Label): void { } if (!Array.isArray(label.attributes)) { - throw new Error(`Label "${label.name}". attributes must be an array. Got type ${typeof label.attributes}`); + throw new Error(`Label "${label.name}". ` + `attributes must be an array. Got type ${typeof label.attributes}`); } for (const attr of label.attributes) { 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 0c30bbc0e485..380d405afd67 100644 --- a/cvat-ui/src/components/labels-editor/constructor-viewer-item.tsx +++ b/cvat-ui/src/components/labels-editor/constructor-viewer-item.tsx @@ -18,9 +18,7 @@ interface ConstructorViewerItemProps { } export default function ConstructorViewerItem(props: ConstructorViewerItemProps): JSX.Element { - const { - color, label, onUpdate, onDelete, - } = props; + const { color, label, onUpdate, onDelete } = props; return (
    diff --git a/cvat-ui/src/components/labels-editor/label-form.tsx b/cvat-ui/src/components/labels-editor/label-form.tsx index ed9440c0f77b..b292f41f6c45 100644 --- a/cvat-ui/src/components/labels-editor/label-form.tsx +++ b/cvat-ui/src/components/labels-editor/label-form.tsx @@ -18,9 +18,7 @@ import ColorPicker from 'components/annotation-page/standard-workspace/objects-s import { ColorizeIcon } from 'icons'; import patterns from 'utils/validation-patterns'; import consts from 'consts'; -import { - equalArrayHead, idGenerator, Label, Attribute, -} from './common'; +import { equalArrayHead, idGenerator, Label, Attribute } from './common'; export enum AttributeType { SELECT = 'SELECT', diff --git a/cvat-ui/src/components/labels-editor/labels-editor.tsx b/cvat-ui/src/components/labels-editor/labels-editor.tsx index e4fac40d9e21..9375189b80b7 100644 --- a/cvat-ui/src/components/labels-editor/labels-editor.tsx +++ b/cvat-ui/src/components/labels-editor/labels-editor.tsx @@ -185,16 +185,14 @@ export default class LabelsEditor extends React.PureComponent diff --git a/cvat-ui/src/components/create-task-page/styles.scss b/cvat-ui/src/components/create-task-page/styles.scss index 707fa1507f1c..59e89cd94540 100644 --- a/cvat-ui/src/components/create-task-page/styles.scss +++ b/cvat-ui/src/components/create-task-page/styles.scss @@ -30,7 +30,7 @@ margin-top: 10px; } - > div:nth-child(9) > button { + .cvat-create-task-submit-section > button { float: right; width: 120px; } diff --git a/cvat-ui/src/components/cvat-app.tsx b/cvat-ui/src/components/cvat-app.tsx index bf092d9793a1..b3c742cd9862 100644 --- a/cvat-ui/src/components/cvat-app.tsx +++ b/cvat-ui/src/components/cvat-app.tsx @@ -310,7 +310,7 @@ class CVATApplication extends React.PureComponent )} - + {/* eslint-disable-next-line */} diff --git a/cvat-ui/src/components/project-page/details.tsx b/cvat-ui/src/components/project-page/details.tsx index 85bebd3851ea..800bc7ef2770 100644 --- a/cvat-ui/src/components/project-page/details.tsx +++ b/cvat-ui/src/components/project-page/details.tsx @@ -3,14 +3,14 @@ // SPDX-License-Identifier: MIT import React, { useState } from 'react'; -import { useDispatch, useSelector } from 'react-redux'; +import { useDispatch } from 'react-redux'; import moment from 'moment'; import { Row, Col } from 'antd/lib/grid'; import Title from 'antd/lib/typography/Title'; import Text from 'antd/lib/typography/Text'; import getCore from 'cvat-core-wrapper'; -import { Project, CombinedState } from 'reducers/interfaces'; +import { Project } from 'reducers/interfaces'; import { updateProjectAsync } from 'actions/projects-actions'; import LabelsEditor from 'components/labels-editor/labels-editor'; import BugTrackerEditor from 'components/task-page/bug-tracker-editor'; @@ -26,7 +26,6 @@ export default function DetailsComponent(props: DetailsComponentProps): JSX.Elem const { project } = props; const dispatch = useDispatch(); - const registeredUsers = useSelector((state: CombinedState) => state.users.users); const [projectName, setProjectName] = useState(project.name); return ( @@ -57,8 +56,9 @@ export default function DetailsComponent(props: DetailsComponentProps): JSX.Elem { - dispatch(updateProjectAsync(_project)); + onChange={(bugTracker): void => { + project.bugTracker = bugTracker; + dispatch(updateProjectAsync(project)); }} /> @@ -66,15 +66,8 @@ export default function DetailsComponent(props: DetailsComponentProps): JSX.Elem Assigned to { - let [userInstance] = registeredUsers.filter((user: any) => user.username === value); - - if (userInstance === undefined) { - userInstance = null; - } - - project.assignee = userInstance; + onSelect={(user) => { + project.assignee = user; dispatch(updateProjectAsync(project)); }} /> diff --git a/cvat-ui/src/components/project-page/styles.scss b/cvat-ui/src/components/project-page/styles.scss index 120dda95be99..8d218fed4eb2 100644 --- a/cvat-ui/src/components/project-page/styles.scss +++ b/cvat-ui/src/components/project-page/styles.scss @@ -17,6 +17,10 @@ margin-bottom: $grid-unit-size * 2; } + .ant-row-flex:nth-child(2) .ant-col:nth-child(2) > span { + margin-right: $grid-unit-size; + } + .cvat-project-details-actions { display: flex; align-items: center; diff --git a/cvat-ui/src/components/task-page/bug-tracker-editor.tsx b/cvat-ui/src/components/task-page/bug-tracker-editor.tsx index 4e605d3baa0f..9e4b03cecede 100644 --- a/cvat-ui/src/components/task-page/bug-tracker-editor.tsx +++ b/cvat-ui/src/components/task-page/bug-tracker-editor.tsx @@ -40,9 +40,7 @@ export default function BugTrackerEditorComponent(props: Props): JSX.Element { } else { setBugTracker(value); setBugTrackerEditing(false); - - instance.bugTracker = value; - onChange(instance); + onChange(value); } }; diff --git a/cvat-ui/src/components/task-page/details.tsx b/cvat-ui/src/components/task-page/details.tsx index 500acce0ebc6..2abe8d7f0086 100644 --- a/cvat-ui/src/components/task-page/details.tsx +++ b/cvat-ui/src/components/task-page/details.tsx @@ -17,6 +17,7 @@ import { getReposData, syncRepos } from 'utils/git-utils'; import { ActiveInference } from 'reducers/interfaces'; import AutomaticAnnotationProgress from 'components/tasks-page/automatic-annotation-progress'; import UserSelector, { User } from './user-selector'; +import BugTrackerEditor from './bug-tracker-editor'; import LabelsEditorComponent from '../labels-editor/labels-editor'; const core = getCore(); @@ -204,7 +205,11 @@ export default class DetailsComponent extends React.PureComponent return ( - {owner && {`Task #${taskInstance.id} Created by ${owner} on ${created}`}} + + {owner && ( + {`Task #${taskInstance.id} Created by ${owner} on ${created}`} + )} + Assigned to {assigneeSelect} @@ -328,7 +333,13 @@ export default class DetailsComponent extends React.PureComponent {this.renderDescription()} - + { + taskInstance.bugTracker = bugTracker; + onTaskUpdate(taskInstance); + }} + /> create a new task -
    - or try to -
    + or try to create a new project
    diff --git a/cvat/apps/engine/serializers.py b/cvat/apps/engine/serializers.py index c3c21a1c980a..5223321bdf51 100644 --- a/cvat/apps/engine/serializers.py +++ b/cvat/apps/engine/serializers.py @@ -324,10 +324,10 @@ class TaskSerializer(WriteOnceMixin, serializers.ModelSerializer): class Meta: model = models.Task - fields = ('url', 'id', 'name', 'project_id' 'mode', 'owner', 'assignee', 'owner_id', 'assignee_id', + fields = ('url', 'id', 'name', 'project_id', 'mode', 'owner', 'assignee', 'owner_id', 'assignee_id', 'bug_tracker', 'created_date', 'updated_date', 'overlap', 'segment_size', 'status', 'labels', 'segments', - 'project', 'data_chunk_size', 'data_compressed_chunk_type', 'data_original_chunk_type', 'size', 'image_quality', 'data') + 'data_chunk_size', 'data_compressed_chunk_type', 'data_original_chunk_type', 'size', 'image_quality', 'data') read_only_fields = ('mode', 'created_date', 'updated_date', 'status', 'data_chunk_size', 'owner', 'asignee', 'data_compressed_chunk_type', 'data_original_chunk_type', 'size', 'image_quality', 'data') write_once_fields = ('overlap', 'segment_size', 'project_id') From 8d1407589872b48909502460715764d2fb739277 Mon Sep 17 00:00:00 2001 From: Dmitry Kalinin Date: Tue, 17 Nov 2020 00:01:43 +0300 Subject: [PATCH 48/55] Fixed PR issues and tests --- cvat-core/src/session.js | 1 - cvat-core/tests/api/projects.js | 75 ++++++++++++------------ cvat-core/tests/mocks/dummy-data.mock.js | 24 ++++++-- cvat-ui/src/actions/projects-actions.ts | 34 +++++------ cvat-ui/src/reducers/projects-reducer.ts | 14 ++--- cvat/apps/engine/serializers.py | 2 +- 6 files changed, 78 insertions(+), 72 deletions(-) diff --git a/cvat-core/src/session.js b/cvat-core/src/session.js index 6ca49d90d5dd..0a1e7643173e 100644 --- a/cvat-core/src/session.js +++ b/cvat-core/src/session.js @@ -1603,7 +1603,6 @@ tasks: { get: () => [...data.tasks], }, - // TODO: Do we need logger here }), ); } diff --git a/cvat-core/tests/api/projects.js b/cvat-core/tests/api/projects.js index 1e8c1270adac..737908d9f60f 100644 --- a/cvat-core/tests/api/projects.js +++ b/cvat-core/tests/api/projects.js @@ -1,13 +1,6 @@ -/* - * Copyright (C) 2018 Intel Corporation - * SPDX-License-Identifier: MIT -*/ - -/* global - require:false - jest:false - describe:false -*/ +// Copyright (C) 2019-2020 Intel Corporation +// +// SPDX-License-Identifier: MIT // Setup mock for a server jest.mock('../../src/server-proxy', () => { @@ -52,9 +45,11 @@ describe('Feature: get projects', () => { }); test('get a project by an invalid id', async () => { - expect(window.cvat.projects.get({ - id: '1', - })).rejects.toThrow(window.cvat.exceptions.ArgumentError); + expect( + window.cvat.projects.get({ + id: '1', + }), + ).rejects.toThrow(window.cvat.exceptions.ArgumentError); }); test('get projects by filters', async () => { @@ -69,9 +64,11 @@ describe('Feature: get projects', () => { }); test('get projects by invalid filters', async () => { - expect(window.cvat.projects.get({ - unknown: '5', - })).rejects.toThrow(window.cvat.exceptions.ArgumentError); + expect( + window.cvat.projects.get({ + unknown: '5', + }), + ).rejects.toThrow(window.cvat.exceptions.ArgumentError); }); }); @@ -101,14 +98,16 @@ describe('Feature: save a project', () => { const labelsLength = result[0].labels.length; const newLabel = new window.cvat.classes.Label({ - name: 'My boss\'s car', - attributes: [{ - default_value: 'false', - input_type: 'checkbox', - mutable: true, - name: 'parked', - values: ['false'], - }], + name: "My boss's car", + attributes: [ + { + default_value: 'false', + input_type: 'checkbox', + mutable: true, + name: 'parked', + values: ['false'], + }, + ], }); result[0].labels = [...result[0].labels, newLabel]; @@ -119,7 +118,7 @@ describe('Feature: save a project', () => { }); expect(result[0].labels).toHaveLength(labelsLength + 1); - const appendedLabel = result[0].labels.filter((el) => el.name === 'My boss\'s car'); + const appendedLabel = result[0].labels.filter((el) => el.name === "My boss's car"); expect(appendedLabel).toHaveLength(1); expect(appendedLabel[0].attributes).toHaveLength(1); expect(appendedLabel[0].attributes[0].name).toBe('parked'); @@ -131,21 +130,25 @@ describe('Feature: save a project', () => { test('save new project without an id', async () => { const project = new window.cvat.classes.Project({ name: 'New Empty Project', - labels: [{ - name: 'car', - attributes: [{ - default_value: 'false', - input_type: 'checkbox', - mutable: true, - name: 'parked', - values: ['false'], - }], - }], + labels: [ + { + name: 'car', + attributes: [ + { + default_value: 'false', + input_type: 'checkbox', + mutable: true, + name: 'parked', + values: ['false'], + }, + ], + }, + ], bug_tracker: 'bug tracker value', }); const result = await project.save(); - expect(typeof (result.id)).toBe('number'); + expect(typeof result.id).toBe('number'); }); }); diff --git a/cvat-core/tests/mocks/dummy-data.mock.js b/cvat-core/tests/mocks/dummy-data.mock.js index 084f6a7f7dde..115e04918b0b 100644 --- a/cvat-core/tests/mocks/dummy-data.mock.js +++ b/cvat-core/tests/mocks/dummy-data.mock.js @@ -154,8 +154,16 @@ const projectsDummyData = { name: 'Some empty project', labels: [], tasks: [], - owner: 2, - assignee: 2, + owner: { + url: 'http://localhost:7000/api/v1/users/2', + id: 2, + username: 'bsekache', + }, + assignee: { + url: 'http://localhost:7000/api/v1/users/2', + id: 2, + username: 'bsekache', + }, bug_tracker: '', created_date: '2020-10-19T20:41:07.808029Z', updated_date: '2020-10-19T20:41:07.808084Z', @@ -189,7 +197,11 @@ const projectsDummyData = { name: 'road 1', project_id: 1, mode: 'interpolation', - owner: 1, + owner: { + url: 'http://localhost:7000/api/v1/users/1', + id: 1, + username: 'admin', + }, assignee: null, bug_tracker: '', created_date: '2020-10-12T08:59:59.878083Z', @@ -285,7 +297,11 @@ const projectsDummyData = { data: 1, }, ], - owner: 1, + owner: { + url: 'http://localhost:7000/api/v1/users/1', + id: 1, + username: 'admin', + }, assignee: null, bug_tracker: '', created_date: '2020-10-12T08:21:56.558898Z', diff --git a/cvat-ui/src/actions/projects-actions.ts b/cvat-ui/src/actions/projects-actions.ts index dfcf5b1c514b..3607c4de44d8 100644 --- a/cvat-ui/src/actions/projects-actions.ts +++ b/cvat-ui/src/actions/projects-actions.ts @@ -71,8 +71,7 @@ function getProjectsFailed(error: any): AnyAction { return action; } -export function getProjectsAsync(query: Partial): -ThunkAction, {}, {}, AnyAction> { +export function getProjectsAsync(query: Partial): ThunkAction, {}, {}, AnyAction> { return async (dispatch: ActionCreator): Promise => { dispatch(getProjects()); dispatch(updateProjectsGettingQuery(query)); @@ -102,26 +101,24 @@ ThunkAction, {}, {}, AnyAction> { const taskPreviewPromises: Promise[] = []; for (const project of array) { - taskPreviewPromises.push(...(project as any).tasks.map((task: any): string => { - tasks.push(task); - return (task as any).frames.preview().catch(() => ''); - })); + taskPreviewPromises.push( + ...(project as any).tasks.map((task: any): string => { + tasks.push(task); + return (task as any).frames.preview().catch(() => ''); + }), + ); } const taskPreviews = await Promise.all(taskPreviewPromises); dispatch(getProjectsSuccess(array, result.count)); - const store = getCVATStore(); const state: CombinedState = store.getState(); if (!state.tasks.fetching) { - dispatch(getTasksSuccess( - tasks, - taskPreviews, - tasks.length, - { + dispatch( + getTasksSuccess(tasks, taskPreviews, tasks.length, { page: 1, assignee: null, id: null, @@ -130,8 +127,8 @@ ThunkAction, {}, {}, AnyAction> { owner: null, search: null, status: null, - } - )); + }), + ); } }; } @@ -167,8 +164,7 @@ function createProjectFailed(error: any): AnyAction { return action; } -export function createProjectAsync(data: any): -ThunkAction, {}, {}, AnyAction> { +export function createProjectAsync(data: any): ThunkAction, {}, {}, AnyAction> { return async (dispatch: ActionCreator): Promise => { const projectInstance = new cvat.classes.Project(data); @@ -214,8 +210,7 @@ function updateProjectFailed(project: any, error: any): AnyAction { return action; } -export function updateProjectAsync(projectInstance: any): -ThunkAction, {}, {}, AnyAction> { +export function updateProjectAsync(projectInstance: any): ThunkAction, {}, {}, AnyAction> { return async (dispatch: ActionCreator): Promise => { try { dispatch(updateProject()); @@ -269,8 +264,7 @@ function deleteProjectFailed(projectId: number, error: any): AnyAction { return action; } -export function deleteProjectAsync(projectInstance: any): -ThunkAction, {}, {}, AnyAction> { +export function deleteProjectAsync(projectInstance: any): ThunkAction, {}, {}, AnyAction> { return async (dispatch: ActionCreator): Promise => { dispatch(deleteProject(projectInstance.id)); try { diff --git a/cvat-ui/src/reducers/projects-reducer.ts b/cvat-ui/src/reducers/projects-reducer.ts index 072fff5b2246..d6a7c1eee491 100644 --- a/cvat-ui/src/reducers/projects-reducer.ts +++ b/cvat-ui/src/reducers/projects-reducer.ts @@ -111,11 +111,8 @@ export default (state: ProjectsState = defaultState, action: AnyAction): Project ...state, current: state.current.map( (project): Project => { - if (project.instance.id === action.payload.project.id) { - return { - ...project, - instance: action.payload.project, - }; + if (project.id === action.payload.project.id) { + return action.payload.project; } return project; @@ -128,11 +125,8 @@ export default (state: ProjectsState = defaultState, action: AnyAction): Project ...state, current: state.current.map( (project): Project => { - if (project.instance.id === action.payload.project.id) { - return { - ...project, - instance: action.payload.project, - }; + if (project.id === action.payload.project.id) { + return action.payload.project; } return project; diff --git a/cvat/apps/engine/serializers.py b/cvat/apps/engine/serializers.py index 5223321bdf51..5a8f647cef46 100644 --- a/cvat/apps/engine/serializers.py +++ b/cvat/apps/engine/serializers.py @@ -92,7 +92,7 @@ def update_instance(validated_data, parent_instance): logger.info("{} label was updated".format(db_label.name)) if not validated_data.get('color', None): label_names = [l.name for l in - instance.label_set.exclude(id=db_label.id).order_by('id') + instance[tuple(instance.keys())[0]].label_set.exclude(id=db_label.id).order_by('id') ] db_label.color = get_label_color(db_label.name, label_names) else: From f1788f9b393babe2819bf4174225da2315f57e25 Mon Sep 17 00:00:00 2001 From: Dmitry Kalinin Date: Tue, 17 Nov 2020 09:48:45 +0300 Subject: [PATCH 49/55] Fixed UI tests --- .../case_2_register_user_change_pass.js | 4 ++-- .../actions_users/case_4_assign_taks_job_users.js | 14 +++++++------- .../issue_1599_ch_user_registration.js | 2 +- .../issue_1599_pl_user_registration.js | 2 +- .../actions_users/issue_1810_login_logout.js | 2 +- 5 files changed, 12 insertions(+), 12 deletions(-) diff --git a/tests/cypress/integration/actions_users/case_2_register_user_change_pass.js b/tests/cypress/integration/actions_users/case_2_register_user_change_pass.js index 7834453e1245..91b025c10718 100644 --- a/tests/cypress/integration/actions_users/case_2_register_user_change_pass.js +++ b/tests/cypress/integration/actions_users/case_2_register_user_change_pass.js @@ -30,7 +30,7 @@ context('Register user, change password, login with new password', () => { describe(`Testing "Case ${caseId}"`, () => { it('Register user, change password', () => { cy.userRegistration(firstName, lastName, userName, emailAddr, password); - cy.url().should('include', '/projects'); + cy.url().should('include', '/tasks'); cy.get('.cvat-right-header') .find('.cvat-header-menu-dropdown') .should('have.text', userName) @@ -50,7 +50,7 @@ context('Register user, change password, login with new password', () => { }); it('Login with the new password', () => { cy.login(userName, newPassword); - cy.url().should('include', '/projects'); + cy.url().should('include', '/tasks'); }); }); }); diff --git a/tests/cypress/integration/actions_users/case_4_assign_taks_job_users.js b/tests/cypress/integration/actions_users/case_4_assign_taks_job_users.js index fd6d4731d18d..260263662190 100644 --- a/tests/cypress/integration/actions_users/case_4_assign_taks_job_users.js +++ b/tests/cypress/integration/actions_users/case_4_assign_taks_job_users.js @@ -57,7 +57,7 @@ context('Multiple users. Assign task, job.', () => { secondUser.emailAddr, secondUser.password, ); - cy.url().should('include', '/projects'); + cy.url().should('include', '/tasks'); cy.logout(secondUserName); cy.url().should('include', '/auth/login'); }); @@ -71,13 +71,13 @@ context('Multiple users. Assign task, job.', () => { thirdUser.emailAddr, thirdUser.password, ); - cy.url().should('include', '/projects'); + cy.url().should('include', '/tasks'); cy.logout(thirdUserName); cy.url().should('include', '/auth/login'); }); it('First user login and create a task', () => { cy.login(); - cy.url().should('include', '/projects'); + cy.url().should('include', '/tasks'); cy.goToTaskList(); cy.imageGenerator(imagesFolder, imageFileName, width, height, color, posX, posY, labelName, imagesCount); cy.createZipArchive(directoryToArchive, archivePath); @@ -93,7 +93,7 @@ context('Multiple users. Assign task, job.', () => { }); it('Second user login. The task can be opened. Logout', () => { cy.login(secondUserName, secondUser.password); - cy.url().should('include', '/projects'); + cy.url().should('include', '/tasks'); cy.goToTaskList(); cy.contains('strong', taskName).should('exist'); cy.openTask(taskName); @@ -101,14 +101,14 @@ context('Multiple users. Assign task, job.', () => { }); it('Third user login. The task not exist. Logout', () => { cy.login(thirdUserName, thirdUser.password); - cy.url().should('include', '/projects'); + cy.url().should('include', '/tasks'); cy.goToTaskList(); cy.contains('strong', taskName).should('not.exist'); cy.logout(thirdUserName); }); it('First user login and assign the job to the third user. Logout', () => { cy.login(); - cy.url().should('include', '/projects'); + cy.url().should('include', '/tasks'); cy.goToTaskList(); cy.openTask(taskName); cy.get('.cvat-task-job-list').within(() => { @@ -119,7 +119,7 @@ context('Multiple users. Assign task, job.', () => { }); it('Third user login. The task can be opened.', () => { cy.login(thirdUserName, thirdUser.password); - cy.url().should('include', '/projects'); + cy.url().should('include', '/tasks'); cy.goToTaskList(); cy.contains('strong', taskName).should('exist'); cy.openTask(taskName); diff --git a/tests/cypress/integration/actions_users/issue_1599_ch_user_registration.js b/tests/cypress/integration/actions_users/issue_1599_ch_user_registration.js index 6118e2dd8fef..08cda62cfead 100644 --- a/tests/cypress/integration/actions_users/issue_1599_ch_user_registration.js +++ b/tests/cypress/integration/actions_users/issue_1599_ch_user_registration.js @@ -40,7 +40,7 @@ context('Issue 1599 (Chinese alphabet).', () => { }); it('Successful registration', () => { - cy.url().should('include', '/projects'); + cy.url().should('include', '/tasks'); }); }); }); diff --git a/tests/cypress/integration/actions_users/issue_1599_pl_user_registration.js b/tests/cypress/integration/actions_users/issue_1599_pl_user_registration.js index d698253427a8..11a48a15aea7 100644 --- a/tests/cypress/integration/actions_users/issue_1599_pl_user_registration.js +++ b/tests/cypress/integration/actions_users/issue_1599_pl_user_registration.js @@ -40,7 +40,7 @@ context('Issue 1599 (Polish alphabet).', () => { }); it('Successful registration', () => { - cy.url().should('include', '/projects'); + cy.url().should('include', '/tasks'); }); }); }); diff --git a/tests/cypress/integration/actions_users/issue_1810_login_logout.js b/tests/cypress/integration/actions_users/issue_1810_login_logout.js index ee6a3207bd69..d2e8d01f32c0 100644 --- a/tests/cypress/integration/actions_users/issue_1810_login_logout.js +++ b/tests/cypress/integration/actions_users/issue_1810_login_logout.js @@ -14,7 +14,7 @@ context('When clicking on the Logout button, get the user session closed.', () = describe(`Testing issue "${issueId}"`, () => { it('Login', () => { cy.login(); - cy.url().should('include', '/projects'); + cy.url().should('include', '/tasks'); }); it('Logout', () => { cy.logout(); From 819a0de237e08a5769a99602e4520e4574669e1d Mon Sep 17 00:00:00 2001 From: Dmitry Kalinin Date: Wed, 18 Nov 2020 15:08:50 +0300 Subject: [PATCH 50/55] Fixed PR issues --- cvat-core/src/api-implementation.js | 13 +- cvat-core/src/api.js | 4 +- cvat-core/src/project.js | 265 ++++++++++++++++++ cvat-core/src/server-proxy.js | 10 +- cvat-core/src/session.js | 254 +---------------- cvat-core/tests/api/projects.js | 3 +- cvat-core/tests/mocks/server-proxy.mock.js | 8 +- cvat-ui/src/actions/annotation-actions.ts | 16 +- cvat-ui/src/actions/projects-actions.ts | 41 ++- .../create-project-content.tsx | 72 ++++- .../create-project-page/styles.scss | 14 +- .../src/components/projects-page/styles.scss | 4 +- cvat/apps/engine/serializers.py | 11 +- cvat/apps/engine/tests/test_rest_api.py | 50 ++-- 14 files changed, 442 insertions(+), 323 deletions(-) create mode 100644 cvat-core/src/project.js diff --git a/cvat-core/src/api-implementation.js b/cvat-core/src/api-implementation.js index e0698dc9b1be..3826873a87d5 100644 --- a/cvat-core/src/api-implementation.js +++ b/cvat-core/src/api-implementation.js @@ -15,7 +15,8 @@ const User = require('./user'); const { AnnotationFormats } = require('./annotation-formats'); const { ArgumentError } = require('./exceptions'); - const { Task, Project } = require('./session'); + const { Task } = require('./session'); + const { Project } = require('./project'); function implementAPI(cvat) { cvat.plugins.list.implementation = PluginRegistry.list; @@ -190,7 +191,10 @@ } } - if ('projectId' in filter && Object.keys(filter).length > 1) { + if ( + 'projectId' in filter + && (('page' in filter && Object.keys(filter).length > 2) || Object.keys(filter).length > 2) + ) { throw new ArgumentError('Do not use the filter field "projectId" with other'); } @@ -233,14 +237,13 @@ } const searchParams = new URLSearchParams(); - // TODO: need to check search fields for (const field of ['name', 'assignee', 'owner', 'search', 'status', 'id', 'page']) { if (Object.prototype.hasOwnProperty.call(filter, field)) { searchParams.set(field, filter[field]); } } - const projectsData = await serverProxy.projects.getProjects(searchParams.toString()); + const projectsData = await serverProxy.projects.get(searchParams.toString()); // prettier-ignore const projects = projectsData.map((project) => new Project(project)); @@ -249,7 +252,7 @@ return projects; }; - cvat.projects.searchNames.implementation = async (search, limit) => serverProxy.projects.searchProjectNames(search, limit); + cvat.projects.searchNames.implementation = async (search, limit) => serverProxy.projects.searchNames(search, limit); return cvat; } diff --git a/cvat-core/src/api.js b/cvat-core/src/api.js index 07619abc2590..a33ec2825939 100644 --- a/cvat-core/src/api.js +++ b/cvat-core/src/api.js @@ -13,7 +13,8 @@ function build() { const Log = require('./log'); const ObjectState = require('./object-state'); const Statistics = require('./statistics'); - const { Job, Task, Project } = require('./session'); + const { Job, Task } = require('./session'); + const { Project } = require('./project'); const { Attribute, Label } = require('./labels'); const MLModel = require('./ml-model'); @@ -345,6 +346,7 @@ function build() { * @property {integer} page Get specific page * (default REST API returns 20 tasks per request. * In order to get more, it is need to specify next page) + * @property {integer} projectId Check if project_id field contains this value * @property {string} owner Check if owner user contains this value * @property {string} assignee Check if assigneed contains this value * @property {string} search Combined search of contains among all fields diff --git a/cvat-core/src/project.js b/cvat-core/src/project.js new file mode 100644 index 000000000000..4d5e0aa48fc3 --- /dev/null +++ b/cvat-core/src/project.js @@ -0,0 +1,265 @@ +// Copyright (C) 2019-2020 Intel Corporation +// +// SPDX-License-Identifier: MIT + +(() => { + const PluginRegistry = require('./plugins'); + const serverProxy = require('./server-proxy'); + const { ArgumentError } = require('./exceptions'); + const { Task } = require('./session'); + const { Label } = require('./labels'); + const User = require('./user'); + + /** + * Class representing a project + * @memberof module:API.cvat.classes + */ + class Project { + /** + * In a fact you need use the constructor only if you want to create a project + * @param {object} initialData - Object which is used for initalization + *
    It can contain keys: + *
  • name + *
  • labels + */ + constructor(initialData) { + const data = { + id: undefined, + name: undefined, + status: undefined, + assignee: undefined, + owner: undefined, + bug_tracker: undefined, + created_date: undefined, + updated_date: undefined, + }; + + for (const property in data) { + if (Object.prototype.hasOwnProperty.call(data, property) && property in initialData) { + data[property] = initialData[property]; + } + } + + data.labels = []; + data.tasks = []; + + if (Array.isArray(initialData.labels)) { + for (const label of initialData.labels) { + const classInstance = new Label(label); + data.labels.push(classInstance); + } + } + + if (Array.isArray(initialData.tasks)) { + for (const task of initialData.tasks) { + const taskInstance = new Task(task); + data.tasks.push(taskInstance); + } + } + + Object.defineProperties( + this, + Object.freeze({ + /** + * @name id + * @type {integer} + * @memberof module:API.cvat.classes.Project + * @readonly + * @instance + */ + id: { + get: () => data.id, + }, + /** + * @name name + * @type {string} + * @memberof module:API.cvat.classes.Project + * @instance + * @throws {module:API.cvat.exceptions.ArgumentError} + */ + name: { + get: () => data.name, + set: (value) => { + if (!value.trim().length) { + throw new ArgumentError('Value must not be empty'); + } + data.name = value; + }, + }, + /** + * @name status + * @type {module:API.cvat.enums.TaskStatus} + * @memberof module:API.cvat.classes.Project + * @readonly + * @instance + */ + status: { + get: () => data.status, + }, + /** + * Instance of a user who was assigned for the project + * @name assignee + * @type {module:API.cvat.classes.User} + * @memberof module:API.cvat.classes.Project + * @readonly + * @instance + */ + assignee: { + get: () => data.assignee, + set: (assignee) => { + if (assignee !== null && !(assignee instanceof User)) { + throw new ArgumentError('Value must be a user instance'); + } + data.assignee = assignee; + }, + }, + /** + * Instance of a user who has created the project + * @name owner + * @type {module:API.cvat.classes.User} + * @memberof module:API.cvat.classes.Project + * @readonly + * @instance + */ + owner: { + get: () => data.owner, + }, + /** + * @name bugTracker + * @type {string} + * @memberof module:API.cvat.classes.Project + * @instance + * @throws {module:API.cvat.exceptions.ArgumentError} + */ + bugTracker: { + get: () => data.bug_tracker, + set: (tracker) => { + data.bug_tracker = tracker; + }, + }, + /** + * @name createdDate + * @type {string} + * @memberof module:API.cvat.classes.Task + * @readonly + * @instance + */ + createdDate: { + get: () => data.created_date, + }, + /** + * @name updatedDate + * @type {string} + * @memberof module:API.cvat.classes.Task + * @readonly + * @instance + */ + updatedDate: { + get: () => data.updated_date, + }, + /** + * After project has been created value can be appended only. + * @name labels + * @type {module:API.cvat.classes.Label[]} + * @memberof module:API.cvat.classes.Project + * @instance + * @throws {module:API.cvat.exceptions.ArgumentError} + */ + labels: { + get: () => [...data.labels], + set: (labels) => { + if (!Array.isArray(labels)) { + throw new ArgumentError('Value must be an array of Labels'); + } + + if (!Array.isArray(labels) || labels.some((label) => !(label instanceof Label))) { + throw new ArgumentError( + `Each array value must be an instance of Label. ${typeof label} was found`, + ); + } + + data.labels = [...labels]; + }, + }, + /** + * Tasks linked with the project + * @name tasks + * @type {module:API.cvat.classes.Task[]} + * @memberof module:API.cvat.classes.Project + * @readonly + * @instance + */ + tasks: { + get: () => [...data.tasks], + }, + }), + ); + } + + /** + * Method updates data of a created project or creates new project from scratch + * @method save + * @returns {module:API.cvat.classes.Project} + * @memberof module:API.cvat.classes.Project + * @readonly + * @instance + * @async + * @throws {module:API.cvat.exceptions.ServerError} + * @throws {module:API.cvat.exceptions.PluginError} + */ + async save() { + const result = await PluginRegistry.apiWrapper.call(this, Project.prototype.save); + return result; + } + + /** + * Method deletes a task from a server + * @method delete + * @memberof module:API.cvat.classes.Project + * @readonly + * @instance + * @async + * @throws {module:API.cvat.exceptions.ServerError} + * @throws {module:API.cvat.exceptions.PluginError} + */ + async delete() { + const result = await PluginRegistry.apiWrapper.call(this, Project.prototype.delete); + return result; + } + } + + module.exports = { + Project, + }; + + Project.prototype.save.implementation = async function () { + if (typeof this.id !== 'undefined') { + const projectData = { + name: this.name, + assignee: this.assignee ? this.assignee.id : null, + bug_tracker: this.bugTracker, + labels: [...this.labels.map((el) => el.toJSON())], + }; + + await serverProxy.projects.save(this.id, projectData); + return this; + } + + const projectSpec = { + name: this.name, + labels: [...this.labels.map((el) => el.toJSON())], + }; + + if (this.bugTracker) { + projectSpec.bug_tracker = this.bugTracker; + } + + const project = await serverProxy.projects.create(projectSpec); + return new Project(project); + }; + + Project.prototype.delete.implementation = async function () { + const result = await serverProxy.projects.delete(this.id); + return result; + }; +})(); diff --git a/cvat-core/src/server-proxy.js b/cvat-core/src/server-proxy.js index da0794ed075b..71e1cfbebf13 100644 --- a/cvat-core/src/server-proxy.js +++ b/cvat-core/src/server-proxy.js @@ -910,11 +910,11 @@ projects: { value: Object.freeze({ - getProjects, - searchProjectNames, - saveProject, - createProject, - deleteProject, + get: getProjects, + searchNames: searchProjectNames, + save: saveProject, + create: createProject, + delete: deleteProject, }), writable: false, }, diff --git a/cvat-core/src/session.js b/cvat-core/src/session.js index 0a1e7643173e..53b0ccddecd8 100644 --- a/cvat-core/src/session.js +++ b/cvat-core/src/session.js @@ -954,7 +954,7 @@ }, /** * @name projectId - * @type {integer} + * @type {integer|null} * @memberof module:API.cvat.classes.Task * @readonly * @instance @@ -1417,234 +1417,9 @@ } } - /** - * Class representing a project - * @memberof module:API.cvat.classes - * @extends Session - */ - class Project extends Session { - /** - * In a fact you need use the constructor only if you want to create a project - * @param {object} initialData - Object which is used for initalization - *
    It can contain keys: - *
  • name - *
  • labels - */ - constructor(initialData) { - super(); - const data = { - id: undefined, - name: undefined, - status: undefined, - assignee: undefined, - owner: undefined, - bug_tracker: undefined, - created_date: undefined, - updated_date: undefined, - }; - - for (const property in data) { - if (Object.prototype.hasOwnProperty.call(data, property) && property in initialData) { - data[property] = initialData[property]; - } - } - - data.labels = []; - data.tasks = []; - - if (Array.isArray(initialData.labels)) { - for (const label of initialData.labels) { - const classInstance = new Label(label); - data.labels.push(classInstance); - } - } - - if (Array.isArray(initialData.tasks)) { - for (const task of initialData.tasks) { - const taskInstance = new Task(task); - data.tasks.push(taskInstance); - } - } - - Object.defineProperties( - this, - Object.freeze({ - /** - * @name id - * @type {integer} - * @memberof module:API.cvat.classes.Project - * @readonly - * @instance - */ - id: { - get: () => data.id, - }, - /** - * @name name - * @type {string} - * @memberof module:API.cvat.classes.Project - * @instance - * @throws {module:API.cvat.exceptions.ArgumentError} - */ - name: { - get: () => data.name, - set: (value) => { - if (!value.trim().length) { - throw new ArgumentError('Value must not be empty'); - } - data.name = value; - }, - }, - /** - * @name status - * @type {module:API.cvat.enums.TaskStatus} - * @memberof module:API.cvat.classes.Project - * @readonly - * @instance - */ - status: { - get: () => data.status, - }, - /** - * Instance of a user who was assigned for the project - * @name assignee - * @type {module:API.cvat.classes.User} - * @memberof module:API.cvat.classes.Project - * @readonly - * @instance - */ - assignee: { - get: () => data.assignee, - set: (assignee) => { - if (assignee !== null && !(assignee instanceof User)) { - throw new ArgumentError('Value must be a user instance'); - } - data.assignee = assignee; - }, - }, - /** - * Instance of a user who has created the project - * @name owner - * @type {module:API.cvat.classes.User} - * @memberof module:API.cvat.classes.Project - * @readonly - * @instance - */ - owner: { - get: () => data.owner, - }, - /** - * @name bugTracker - * @type {string} - * @memberof module:API.cvat.classes.Project - * @instance - * @throws {module:API.cvat.exceptions.ArgumentError} - */ - bugTracker: { - get: () => data.bug_tracker, - set: (tracker) => { - data.bug_tracker = tracker; - }, - }, - /** - * @name createdDate - * @type {string} - * @memberof module:API.cvat.classes.Task - * @readonly - * @instance - */ - createdDate: { - get: () => data.created_date, - }, - /** - * @name updatedDate - * @type {string} - * @memberof module:API.cvat.classes.Task - * @readonly - * @instance - */ - updatedDate: { - get: () => data.updated_date, - }, - /** - * After project has been created value can be appended only. - * @name labels - * @type {module:API.cvat.classes.Label[]} - * @memberof module:API.cvat.classes.Project - * @instance - * @throws {module:API.cvat.exceptions.ArgumentError} - */ - labels: { - get: () => [...data.labels], - set: (labels) => { - if (!Array.isArray(labels)) { - throw new ArgumentError('Value must be an array of Labels'); - } - - for (const label of labels) { - if (!(label instanceof Label)) { - throw new ArgumentError( - `Each array value must be an instance of Label. ${typeof label} was found`, - ); - } - } - - data.labels = [...labels]; - }, - }, - /** - * Tasks linked with the project - * @name tasks - * @type {module:API.cvat.classes.Task[]} - * @memberof module:API.cvat.classes.Project - * @readonly - * @instance - */ - tasks: { - get: () => [...data.tasks], - }, - }), - ); - } - - /** - * Method updates data of a created project or creates new project from scratch - * @method save - * @returns {module:API.cvat.classes.Project} - * @memberof module:API.cvat.classes.Project - * been created yet. It called in order to notify about creation status. - * It receives the string parameter which is a status message - * @readonly - * @instance - * @async - * @throws {module:API.cvat.exceptions.ServerError} - * @throws {module:API.cvat.exceptions.PluginError} - */ - async save() { - const result = await PluginRegistry.apiWrapper.call(this, Project.prototype.save); - return result; - } - - /** - * Method deletes a task from a server - * @method delete - * @memberof module:API.cvat.classes.Project - * @readonly - * @instance - * @async - * @throws {module:API.cvat.exceptions.ServerError} - * @throws {module:API.cvat.exceptions.PluginError} - */ - async delete() { - const result = await PluginRegistry.apiWrapper.call(this, Project.prototype.delete); - return result; - } - } - module.exports = { Job, Task, - Project, }; const { @@ -2142,31 +1917,4 @@ const result = await loggerStorage.log(logType, { ...payload, task_id: this.id }, wait); return result; }; - - Project.prototype.save.implementation = async function () { - if (typeof this.id !== 'undefined') { - const projectData = { - name: this.name, - assignee: this.assignee ? this.assignee.id : null, - bug_tracker: this.bugTracker, - labels: [...this.labels.map((el) => el.toJSON())], - }; - - await serverProxy.projects.saveProject(this.id, projectData); - return this; - } - - const projectSpec = { - name: this.name, - labels: [...this.labels.map((el) => el.toJSON())], - }; - - const project = await serverProxy.projects.createProject(projectSpec); - return new Project(project); - }; - - Project.prototype.delete.implementation = async function () { - const result = await serverProxy.projects.deleteProject(this.id); - return result; - }; })(); diff --git a/cvat-core/tests/api/projects.js b/cvat-core/tests/api/projects.js index 737908d9f60f..5c1374301884 100644 --- a/cvat-core/tests/api/projects.js +++ b/cvat-core/tests/api/projects.js @@ -11,7 +11,8 @@ jest.mock('../../src/server-proxy', () => { // Initialize api window.cvat = require('../../src/api'); -const { Task, Project } = require('../../src/session'); +const { Task } = require('../../src/session'); +const { Project } = require('../../src/project'); describe('Feature: get projects', () => { test('get all projects', async () => { diff --git a/cvat-core/tests/mocks/server-proxy.mock.js b/cvat-core/tests/mocks/server-proxy.mock.js index 54764269376e..f9843b1c0b33 100644 --- a/cvat-core/tests/mocks/server-proxy.mock.js +++ b/cvat-core/tests/mocks/server-proxy.mock.js @@ -325,10 +325,10 @@ class ServerProxy { projects: { value: Object.freeze({ - getProjects, - saveProject, - createProject, - deleteProject, + get: getProjects, + save: saveProject, + create: createProject, + delete: deleteProject, }), writable: false, }, diff --git a/cvat-ui/src/actions/annotation-actions.ts b/cvat-ui/src/actions/annotation-actions.ts index f864bcacd033..44604467315e 100644 --- a/cvat-ui/src/actions/annotation-actions.ts +++ b/cvat-ui/src/actions/annotation-actions.ts @@ -2,7 +2,9 @@ // // SPDX-License-Identifier: MIT -import { AnyAction, Dispatch, ActionCreator, Store } from 'redux'; +import { + AnyAction, Dispatch, ActionCreator, Store, +} from 'redux'; import { ThunkAction } from 'utils/redux'; import { @@ -234,7 +236,9 @@ export function switchZLayer(cur: number): AnyAction { export function fetchAnnotationsAsync(): ThunkAction { return async (dispatch: ActionCreator): Promise => { try { - const { filters, frame, showAllInterpolationTracks, jobInstance } = receiveAnnotationsParameters(); + const { + filters, frame, showAllInterpolationTracks, jobInstance, + } = receiveAnnotationsParameters(); const states = await jobInstance.annotations.get(frame, showAllInterpolationTracks, filters); const [minZ, maxZ] = computeZRange(states); @@ -926,6 +930,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`); + } + 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 @@ -1077,7 +1085,9 @@ export function splitTrack(enabled: boolean): AnyAction { export function updateAnnotationsAsync(statesToUpdate: any[]): ThunkAction { return async (dispatch: ActionCreator): Promise => { - const { jobInstance, filters, frame, showAllInterpolationTracks } = receiveAnnotationsParameters(); + const { + jobInstance, filters, frame, showAllInterpolationTracks, + } = receiveAnnotationsParameters(); try { if (statesToUpdate.some((state: any): boolean => state.updateFlags.zOrder)) { diff --git a/cvat-ui/src/actions/projects-actions.ts b/cvat-ui/src/actions/projects-actions.ts index 3607c4de44d8..4d6d498eefcb 100644 --- a/cvat-ui/src/actions/projects-actions.ts +++ b/cvat-ui/src/actions/projects-actions.ts @@ -3,8 +3,8 @@ // SPDX-License-Identifier: MIT import { AnyAction, Dispatch, ActionCreator } from 'redux'; -import { ThunkAction } from 'redux-thunk'; +import { ActionUnion, createAction, ThunkAction } from 'utils/redux'; import { ProjectsQuery, CombinedState } from 'reducers/interfaces'; import { getTasksSuccess, updateTaskSuccess } from 'actions/tasks-actions'; import { getCVATStore } from 'cvat-store'; @@ -28,6 +28,37 @@ export enum ProjectsActionTypes { DELETE_PROJECT_FAILED = 'DELETE_PROJECT_FAILED', } +// prettier-ignore +const projectActions = { + getProjects: () => createAction(ProjectsActionTypes.GET_PROJECTS), + getProjectsSuccess: (array: any[], count: number) => ( + createAction(ProjectsActionTypes.GET_PROJECTS_SUCCESS, { array, count }) + ), + getProjectsFailed: (error: any) => createAction(ProjectsActionTypes.GET_PROJECTS_FAILED, { error }), + updateProjectsGettingQuery: (query: Partial) => ( + createAction(ProjectsActionTypes.UPDATE_PROJECTS_GETTING_QUERY, { query }) + ), + createProjects: () => createAction(ProjectsActionTypes.CREATE_PROJECT), + createProjectsSuccess: (projectId: number) => ( + createAction(ProjectsActionTypes.CREATE_PROJECT_SUCCESS, { projectId }) + ), + createProjectsFailed: (error: any) => createAction(ProjectsActionTypes.CREATE_PROJECT_FAILED, { error }), + updateProjects: () => createAction(ProjectsActionTypes.UPDATE_PROJECT), + updateProjectsSuccess: (project: any) => createAction(ProjectsActionTypes.UPDATE_PROJECT_SUCCESS, { project }), + updateProjectsFailed: (project: any, error: any) => ( + createAction(ProjectsActionTypes.UPDATE_PROJECT_FAILED, { project, error }) + ), + deleteProjects: (projectId: number) => createAction(ProjectsActionTypes.DELETE_PROJECT, { projectId }), + deleteProjectsSuccess: (projectId: number) => ( + createAction(ProjectsActionTypes.DELETE_PROJECT_SUCCESS, { projectId }) + ), + deleteProjectsFailed: (projectId: number, error: any) => ( + createAction(ProjectsActionTypes.DELETE_PROJECT_FAILED, { projectId, error }) + ), +}; + +export type ProjectActions = ActionUnion; + function updateProjectsGettingQuery(query: Partial): AnyAction { const action = { type: ProjectsActionTypes.UPDATE_PROJECTS_GETTING_QUERY, @@ -71,7 +102,7 @@ function getProjectsFailed(error: any): AnyAction { return action; } -export function getProjectsAsync(query: Partial): ThunkAction, {}, {}, AnyAction> { +export function getProjectsAsync(query: Partial): ThunkAction { return async (dispatch: ActionCreator): Promise => { dispatch(getProjects()); dispatch(updateProjectsGettingQuery(query)); @@ -164,7 +195,7 @@ function createProjectFailed(error: any): AnyAction { return action; } -export function createProjectAsync(data: any): ThunkAction, {}, {}, AnyAction> { +export function createProjectAsync(data: any): ThunkAction { return async (dispatch: ActionCreator): Promise => { const projectInstance = new cvat.classes.Project(data); @@ -210,7 +241,7 @@ function updateProjectFailed(project: any, error: any): AnyAction { return action; } -export function updateProjectAsync(projectInstance: any): ThunkAction, {}, {}, AnyAction> { +export function updateProjectAsync(projectInstance: any): ThunkAction { return async (dispatch: ActionCreator): Promise => { try { dispatch(updateProject()); @@ -264,7 +295,7 @@ function deleteProjectFailed(projectId: number, error: any): AnyAction { return action; } -export function deleteProjectAsync(projectInstance: any): ThunkAction, {}, {}, AnyAction> { +export function deleteProjectAsync(projectInstance: any): ThunkAction { return async (dispatch: ActionCreator): Promise => { dispatch(deleteProject(projectInstance.id)); try { diff --git a/cvat-ui/src/components/create-project-page/create-project-content.tsx b/cvat-ui/src/components/create-project-page/create-project-content.tsx index ba215de6d70e..b3f7a6a532f7 100644 --- a/cvat-ui/src/components/create-project-page/create-project-content.tsx +++ b/cvat-ui/src/components/create-project-page/create-project-content.tsx @@ -14,11 +14,12 @@ import Button from 'antd/lib/button'; import Input from 'antd/lib/input'; import notification from 'antd/lib/notification'; +import patterns from 'utils/validation-patterns'; import { CombinedState } from 'reducers/interfaces'; import LabelsEditor from 'components/labels-editor/labels-editor'; import { createProjectAsync } from 'actions/projects-actions'; -type NameFormRefType = Component, any, any> & WrappedFormUtils; +type FormRefType = Component, any, any> & WrappedFormUtils; const ProjectNameEditor = Form.create()( (props: FormComponentProps): JSX.Element => { @@ -42,10 +43,42 @@ const ProjectNameEditor = Form.create()( }, ); +const AdvanvedConfigurationForm = Form.create()( + (props: FormComponentProps): JSX.Element => { + const { form } = props; + const { getFieldDecorator } = form; + + return ( +
    e.preventDefault()}> + Issue tracker} + extra='Attach issue tracker where the project is described' + hasFeedback + > + {getFieldDecorator('bug_tracker', { + rules: [ + { + validator: (_, value, callback): void => { + if (value && !patterns.validateURL.pattern.test(value)) { + callback('Issue tracker must be URL'); + } else { + callback(); + } + }, + }, + ], + })()} + +
    + ); + }, +); + export default function CreateProjectContent(): JSX.Element { const [projectLabels, setProjectLabels] = useState([]); const shouldShowNotification = useRef(false); - const nameFormRef = useRef(null); + const nameFormRef = useRef(null); + const advancedFormRef = useRef(null); const dispatch = useDispatch(); const history = useHistory(); @@ -55,8 +88,9 @@ export default function CreateProjectContent(): JSX.Element { if (Number.isInteger(newProjectId) && shouldShowNotification.current) { const btn = ; - // Clear new project form + // Clear new project forms if (nameFormRef.current) nameFormRef.current.resetFields(); + if (advancedFormRef.current) advancedFormRef.current.resetFields(); setProjectLabels([]); notification.info({ @@ -69,23 +103,34 @@ export default function CreateProjectContent(): JSX.Element { }, [newProjectId]); const onSumbit = (): void => { - let projectName = ''; + interface Project { + [key: string]: any; + } + + const projectData: Project = {}; if (nameFormRef.current !== null) { nameFormRef.current.validateFields((error, value) => { if (!error) { - projectName = value.name; + projectData.name = value.name; + } + }); + } + + if (advancedFormRef.current !== null) { + advancedFormRef.current.validateFields((error, values) => { + if (!error) { + for (const [field, value] of Object.entries(values)) { + projectData[field] = value; + } } }); } - if (!projectName) return; + projectData.labels = projectLabels; - dispatch( - createProjectAsync({ - name: projectName, - labels: projectLabels, - }), - ); + if (!projectData.name) return; + + dispatch(createProjectAsync(projectData)); }; return ( @@ -102,6 +147,9 @@ export default function CreateProjectContent(): JSX.Element { }} /> + + +