Merge remote-tracking branch 'upstream/develop' into dkru/cypress-test-check-email-verification
commit
2d05706212
@ -0,0 +1 @@
|
||||
webpack.config.js
|
||||
@ -0,0 +1 @@
|
||||
webpack.config.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
|
||||
* <br> It can contain keys:
|
||||
* <br> <li style="margin-left: 10px;"> name
|
||||
* <br> <li style="margin-left: 10px;"> 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_id: 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;
|
||||
};
|
||||
})();
|
||||
@ -0,0 +1,170 @@
|
||||
// Copyright (C) 2019-2020 Intel Corporation
|
||||
//
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
// 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 } = require('../../src/session');
|
||||
const { Project } = require('../../src/project');
|
||||
|
||||
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,
|
||||
});
|
||||
|
||||
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);
|
||||
});
|
||||
});
|
||||
@ -1 +1,2 @@
|
||||
**/3rdparty/*.js
|
||||
webpack.config.js
|
||||
|
||||
@ -0,0 +1 @@
|
||||
webpack.config.js
|
||||
@ -0,0 +1,171 @@
|
||||
// Copyright (C) 2020 Intel Corporation
|
||||
//
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
import { Dispatch, ActionCreator } from 'redux';
|
||||
|
||||
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';
|
||||
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',
|
||||
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',
|
||||
}
|
||||
|
||||
// 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<ProjectsQuery>) => (
|
||||
createAction(ProjectsActionTypes.UPDATE_PROJECTS_GETTING_QUERY, { query })
|
||||
),
|
||||
createProject: () => createAction(ProjectsActionTypes.CREATE_PROJECT),
|
||||
createProjectSuccess: (projectId: number) => (
|
||||
createAction(ProjectsActionTypes.CREATE_PROJECT_SUCCESS, { projectId })
|
||||
),
|
||||
createProjectFailed: (error: any) => createAction(ProjectsActionTypes.CREATE_PROJECT_FAILED, { error }),
|
||||
updateProject: () => createAction(ProjectsActionTypes.UPDATE_PROJECT),
|
||||
updateProjectSuccess: (project: any) => createAction(ProjectsActionTypes.UPDATE_PROJECT_SUCCESS, { project }),
|
||||
updateProjectFailed: (project: any, error: any) => (
|
||||
createAction(ProjectsActionTypes.UPDATE_PROJECT_FAILED, { project, error })
|
||||
),
|
||||
deleteProject: (projectId: number) => createAction(ProjectsActionTypes.DELETE_PROJECT, { projectId }),
|
||||
deleteProjectSuccess: (projectId: number) => (
|
||||
createAction(ProjectsActionTypes.DELETE_PROJECT_SUCCESS, { projectId })
|
||||
),
|
||||
deleteProjectFailed: (projectId: number, error: any) => (
|
||||
createAction(ProjectsActionTypes.DELETE_PROJECT_FAILED, { projectId, error })
|
||||
),
|
||||
};
|
||||
|
||||
export type ProjectActions = ActionUnion<typeof projectActions>;
|
||||
|
||||
export function getProjectsAsync(query: Partial<ProjectsQuery>): ThunkAction {
|
||||
return async (dispatch: ActionCreator<Dispatch>): Promise<void> => {
|
||||
dispatch(projectActions.getProjects());
|
||||
dispatch(projectActions.updateProjectsGettingQuery(query));
|
||||
|
||||
// Clear query object from null fields
|
||||
const filteredQuery: Partial<ProjectsQuery> = {
|
||||
page: 1,
|
||||
...query,
|
||||
};
|
||||
for (const key in filteredQuery) {
|
||||
if (filteredQuery[key] === null || typeof filteredQuery[key] === 'undefined') {
|
||||
delete filteredQuery[key];
|
||||
}
|
||||
}
|
||||
|
||||
let result = null;
|
||||
try {
|
||||
result = await cvat.projects.get(filteredQuery);
|
||||
} catch (error) {
|
||||
dispatch(projectActions.getProjectsFailed(error));
|
||||
return;
|
||||
}
|
||||
|
||||
const array = Array.from(result);
|
||||
|
||||
const tasks: any[] = [];
|
||||
const taskPreviewPromises: Promise<any>[] = [];
|
||||
|
||||
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(() => '');
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
const taskPreviews = await Promise.all(taskPreviewPromises);
|
||||
|
||||
dispatch(projectActions.getProjectsSuccess(array, result.count));
|
||||
|
||||
const store = getCVATStore();
|
||||
const state: CombinedState = store.getState();
|
||||
|
||||
if (!state.tasks.fetching) {
|
||||
dispatch(
|
||||
getTasksSuccess(tasks, taskPreviews, tasks.length, {
|
||||
page: 1,
|
||||
assignee: null,
|
||||
id: null,
|
||||
mode: null,
|
||||
name: null,
|
||||
owner: null,
|
||||
search: null,
|
||||
status: null,
|
||||
}),
|
||||
);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export function createProjectAsync(data: any): ThunkAction {
|
||||
return async (dispatch: ActionCreator<Dispatch>): Promise<void> => {
|
||||
const projectInstance = new cvat.classes.Project(data);
|
||||
|
||||
dispatch(projectActions.createProject());
|
||||
try {
|
||||
const savedProject = await projectInstance.save();
|
||||
dispatch(projectActions.createProjectSuccess(savedProject.id));
|
||||
} catch (error) {
|
||||
dispatch(projectActions.createProjectFailed(error));
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export function updateProjectAsync(projectInstance: any): ThunkAction {
|
||||
return async (dispatch: ActionCreator<Dispatch>): Promise<void> => {
|
||||
try {
|
||||
dispatch(projectActions.updateProject());
|
||||
await projectInstance.save();
|
||||
const [project] = await cvat.projects.get({ id: projectInstance.id });
|
||||
dispatch(projectActions.updateProjectSuccess(project));
|
||||
project.tasks.forEach((task: any) => {
|
||||
dispatch(updateTaskSuccess(task));
|
||||
});
|
||||
} catch (error) {
|
||||
let project = null;
|
||||
try {
|
||||
[project] = await cvat.projects.get({ id: projectInstance.id });
|
||||
} catch (fetchError) {
|
||||
dispatch(projectActions.updateProjectFailed(projectInstance, error));
|
||||
return;
|
||||
}
|
||||
dispatch(projectActions.updateProjectFailed(project, error));
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export function deleteProjectAsync(projectInstance: any): ThunkAction {
|
||||
return async (dispatch: ActionCreator<Dispatch>): Promise<void> => {
|
||||
dispatch(projectActions.deleteProject(projectInstance.id));
|
||||
try {
|
||||
await projectInstance.delete();
|
||||
dispatch(projectActions.deleteProjectSuccess(projectInstance.id));
|
||||
} catch (error) {
|
||||
dispatch(projectActions.deleteProjectFailed(projectInstance.id, error));
|
||||
}
|
||||
};
|
||||
}
|
||||
@ -0,0 +1,160 @@
|
||||
// Copyright (C) 2020 Intel Corporation
|
||||
//
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
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';
|
||||
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 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 FormRefType = Component<FormComponentProps<any>, any, any> & WrappedFormUtils;
|
||||
|
||||
const ProjectNameEditor = Form.create<FormComponentProps>()(
|
||||
(props: FormComponentProps): JSX.Element => {
|
||||
const { form } = props;
|
||||
const { getFieldDecorator } = form;
|
||||
|
||||
return (
|
||||
<Form onSubmit={(e): void => e.preventDefault()}>
|
||||
<Form.Item hasFeedback label={<span>Name</span>}>
|
||||
{getFieldDecorator('name', {
|
||||
rules: [
|
||||
{
|
||||
required: true,
|
||||
message: 'Please, specify a name',
|
||||
},
|
||||
],
|
||||
})(<Input />)}
|
||||
</Form.Item>
|
||||
</Form>
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
const AdvanvedConfigurationForm = Form.create<FormComponentProps>()(
|
||||
(props: FormComponentProps): JSX.Element => {
|
||||
const { form } = props;
|
||||
const { getFieldDecorator } = form;
|
||||
|
||||
return (
|
||||
<Form onSubmit={(e): void => e.preventDefault()}>
|
||||
<Form.Item
|
||||
label={<span>Issue tracker</span>}
|
||||
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();
|
||||
}
|
||||
},
|
||||
},
|
||||
],
|
||||
})(<Input />)}
|
||||
</Form.Item>
|
||||
</Form>
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
export default function CreateProjectContent(): JSX.Element {
|
||||
const [projectLabels, setProjectLabels] = useState<any[]>([]);
|
||||
const shouldShowNotification = useRef(false);
|
||||
const nameFormRef = useRef<FormRefType>(null);
|
||||
const advancedFormRef = useRef<FormRefType>(null);
|
||||
const dispatch = useDispatch();
|
||||
const history = useHistory();
|
||||
|
||||
const newProjectId = useSelector((state: CombinedState) => state.projects.activities.creates.id);
|
||||
|
||||
useEffect(() => {
|
||||
if (Number.isInteger(newProjectId) && shouldShowNotification.current) {
|
||||
const btn = <Button onClick={() => history.push(`/projects/${newProjectId}`)}>Open project</Button>;
|
||||
|
||||
// Clear new project forms
|
||||
if (nameFormRef.current) nameFormRef.current.resetFields();
|
||||
if (advancedFormRef.current) advancedFormRef.current.resetFields();
|
||||
setProjectLabels([]);
|
||||
|
||||
notification.info({
|
||||
message: 'The project has been created',
|
||||
btn,
|
||||
});
|
||||
}
|
||||
|
||||
shouldShowNotification.current = true;
|
||||
}, [newProjectId]);
|
||||
|
||||
const onSumbit = (): void => {
|
||||
interface Project {
|
||||
[key: string]: any;
|
||||
}
|
||||
|
||||
const projectData: Project = {};
|
||||
if (nameFormRef.current !== null) {
|
||||
nameFormRef.current.validateFields((error, value) => {
|
||||
if (!error) {
|
||||
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;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
projectData.labels = projectLabels;
|
||||
|
||||
if (!projectData.name) return;
|
||||
|
||||
dispatch(createProjectAsync(projectData));
|
||||
};
|
||||
|
||||
return (
|
||||
<Row type='flex' justify='start' align='middle' className='cvat-create-project-content'>
|
||||
<Col span={24}>
|
||||
<ProjectNameEditor ref={nameFormRef} />
|
||||
</Col>
|
||||
<Col span={24}>
|
||||
<Text className='cvat-text-color'>Labels:</Text>
|
||||
<LabelsEditor
|
||||
labels={projectLabels}
|
||||
onSubmit={(newLabels): void => {
|
||||
setProjectLabels(newLabels);
|
||||
}}
|
||||
/>
|
||||
</Col>
|
||||
<Col span={24}>
|
||||
<AdvanvedConfigurationForm ref={advancedFormRef} />
|
||||
</Col>
|
||||
<Col span={24}>
|
||||
<Button type='primary' onClick={onSumbit}>
|
||||
Submit
|
||||
</Button>
|
||||
</Col>
|
||||
</Row>
|
||||
);
|
||||
}
|
||||
@ -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 (
|
||||
<Row type='flex' justify='center' align='top' className='cvat-create-task-form-wrapper'>
|
||||
<Col md={20} lg={16} xl={14} xxl={9}>
|
||||
<Text className='cvat-title'>Create a new project</Text>
|
||||
<CreateProjectContent />
|
||||
</Col>
|
||||
</Row>
|
||||
);
|
||||
}
|
||||
@ -0,0 +1,42 @@
|
||||
// Copyright (C) 2020 Intel Corporation
|
||||
//
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
@import '../../base.scss';
|
||||
|
||||
.cvat-create-project-form-wrapper {
|
||||
text-align: center;
|
||||
padding-top: $grid-unit-size * 5;
|
||||
overflow-y: auto;
|
||||
height: 90%;
|
||||
position: fixed;
|
||||
width: 100%;
|
||||
|
||||
> div > span {
|
||||
font-size: $grid-unit-size * 4;
|
||||
}
|
||||
}
|
||||
|
||||
.cvat-create-project-content {
|
||||
margin-top: $grid-unit-size * 2;
|
||||
width: 100%;
|
||||
height: auto;
|
||||
border: 1px solid $border-color-1;
|
||||
border-radius: 3px;
|
||||
padding: $grid-unit-size * 2;
|
||||
background: $background-color-1;
|
||||
text-align: initial;
|
||||
|
||||
> div:not(first-child) {
|
||||
margin-top: $grid-unit-size;
|
||||
}
|
||||
|
||||
> div:nth-child(4) {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
|
||||
> button {
|
||||
width: $grid-unit-size * 15;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,91 @@
|
||||
// Copyright (C) 2020 Intel Corporation
|
||||
//
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import Autocomplete from 'antd/lib/auto-complete';
|
||||
|
||||
import getCore from 'cvat-core-wrapper';
|
||||
import { SelectValue } from 'antd/lib/select';
|
||||
|
||||
const core = getCore();
|
||||
|
||||
type Props = {
|
||||
value: number | null;
|
||||
onSelect: (id: number | null) => void;
|
||||
};
|
||||
|
||||
type Project = {
|
||||
id: number;
|
||||
name: string;
|
||||
};
|
||||
|
||||
export default function ProjectSearchField(props: Props): JSX.Element {
|
||||
const { value, onSelect } = props;
|
||||
const [searchPhrase, setSearchPhrase] = useState('');
|
||||
|
||||
const [projects, setProjects] = useState<Project[]>([]);
|
||||
|
||||
const handleSearch = (searchValue: string): void => {
|
||||
if (searchValue) {
|
||||
core.projects.searchNames(searchValue).then((result: Project[]) => {
|
||||
if (result) {
|
||||
setProjects(result);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
setProjects([]);
|
||||
}
|
||||
setSearchPhrase(searchValue);
|
||||
onSelect(null);
|
||||
};
|
||||
|
||||
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 => {
|
||||
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[]) => {
|
||||
const [project] = result;
|
||||
setProjects([...projects, {
|
||||
id: project.id,
|
||||
name: project.name,
|
||||
}]);
|
||||
setSearchPhrase(project.name);
|
||||
onSelect(project.id);
|
||||
});
|
||||
}
|
||||
}, [value]);
|
||||
|
||||
return (
|
||||
<Autocomplete
|
||||
value={searchPhrase}
|
||||
placeholder='Select project'
|
||||
onSearch={handleSearch}
|
||||
onSelect={handleSelect}
|
||||
className='cvat-project-search-field'
|
||||
onDropdownVisibleChange={handleFocus}
|
||||
dataSource={
|
||||
projects.map((proj) => ({
|
||||
value: proj.id.toString(),
|
||||
text: proj.name,
|
||||
}))
|
||||
}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@ -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 getCore from 'cvat-core-wrapper';
|
||||
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';
|
||||
import UserSelector from 'components/task-page/user-selector';
|
||||
|
||||
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.name);
|
||||
|
||||
return (
|
||||
<div className='cvat-project-details'>
|
||||
<Row>
|
||||
<Col>
|
||||
<Title
|
||||
level={4}
|
||||
editable={{
|
||||
onChange: (value: string): void => {
|
||||
setProjectName(value);
|
||||
project.name = value;
|
||||
dispatch(updateProjectAsync(project));
|
||||
},
|
||||
}}
|
||||
className='cvat-text-color'
|
||||
>
|
||||
{projectName}
|
||||
</Title>
|
||||
</Col>
|
||||
</Row>
|
||||
<Row type='flex' justify='space-between'>
|
||||
<Col>
|
||||
<Text type='secondary'>
|
||||
{`Project #${project.id} created`}
|
||||
{project.owner ? ` by ${project.owner.username}` : null}
|
||||
{` on ${moment(project.createdDate).format('MMMM Do YYYY')}`}
|
||||
</Text>
|
||||
<BugTrackerEditor
|
||||
instance={project}
|
||||
onChange={(bugTracker): void => {
|
||||
project.bugTracker = bugTracker;
|
||||
dispatch(updateProjectAsync(project));
|
||||
}}
|
||||
/>
|
||||
</Col>
|
||||
<Col>
|
||||
<Text type='secondary'>Assigned to</Text>
|
||||
<UserSelector
|
||||
value={project.assignee}
|
||||
onSelect={(user) => {
|
||||
project.assignee = user;
|
||||
dispatch(updateProjectAsync(project));
|
||||
}}
|
||||
/>
|
||||
</Col>
|
||||
</Row>
|
||||
<LabelsEditor
|
||||
labels={project.labels.map((label: any): string => label.toJSON())}
|
||||
onSubmit={(labels: any[]): void => {
|
||||
project.labels = labels.map((labelData): any => new core.classes.Label(labelData));
|
||||
dispatch(updateProjectAsync(project));
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@ -0,0 +1,107 @@
|
||||
// Copyright (C) 2020 Intel Corporation
|
||||
//
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
import './styles.scss';
|
||||
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 Button from 'antd/lib/button';
|
||||
import Title from 'antd/lib/typography/Title';
|
||||
|
||||
import { CombinedState, Task } from 'reducers/interfaces';
|
||||
import { getProjectsAsync } from 'actions/projects-actions';
|
||||
import { cancelInferenceAsync } from 'actions/models-actions';
|
||||
import TaskItem from 'components/tasks-page/task-item';
|
||||
import DetailsComponent from './details';
|
||||
import ProjectTopBar from './top-bar';
|
||||
|
||||
interface ParamType {
|
||||
id: string;
|
||||
}
|
||||
|
||||
export default function ProjectPageComponent(): JSX.Element {
|
||||
const id = +useParams<ParamType>().id;
|
||||
const dispatch = useDispatch();
|
||||
const history = useHistory();
|
||||
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);
|
||||
const tasks = useSelector((state: CombinedState) => state.tasks.current);
|
||||
|
||||
const filteredProjects = projects.filter((project) => project.id === id);
|
||||
const project = filteredProjects[0];
|
||||
const deleteActivity = project && id in deletes ? deletes[id] : null;
|
||||
|
||||
useEffect(() => {
|
||||
dispatch(
|
||||
getProjectsAsync({
|
||||
id,
|
||||
}),
|
||||
);
|
||||
}, [id, dispatch]);
|
||||
|
||||
if (deleteActivity) {
|
||||
history.push('/projects');
|
||||
}
|
||||
|
||||
if (projectsFetching) {
|
||||
return <Spin size='large' className='cvat-spinner' />;
|
||||
}
|
||||
|
||||
if (!project) {
|
||||
return (
|
||||
<Result
|
||||
className='cvat-not-found'
|
||||
status='404'
|
||||
title='Sorry, but this project was not found'
|
||||
subTitle='Please, be sure information you tried to get exist and you have access'
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Row type='flex' justify='center' align='top' className='cvat-project-page'>
|
||||
<Col md={22} lg={18} xl={16} xxl={14}>
|
||||
<ProjectTopBar projectInstance={project} />
|
||||
<DetailsComponent project={project} />
|
||||
<Row type='flex' justify='space-between' align='middle' className='cvat-project-page-tasks-bar'>
|
||||
<Col>
|
||||
<Title level={4}>Tasks</Title>
|
||||
</Col>
|
||||
<Col>
|
||||
<Button
|
||||
size='large'
|
||||
type='primary'
|
||||
icon='plus'
|
||||
id='cvat-create-task-button'
|
||||
onClick={() => history.push(`/tasks/create?projectId=${id}`)}
|
||||
>
|
||||
Create new task
|
||||
</Button>
|
||||
</Col>
|
||||
</Row>
|
||||
{tasks
|
||||
.filter((task) => task.instance.projectId === project.id)
|
||||
.map((task: Task) => (
|
||||
<TaskItem
|
||||
key={task.instance.id}
|
||||
deleted={task.instance.id in taskDeletes ? taskDeletes[task.instance.id] : false}
|
||||
hidden={false}
|
||||
activeInference={tasksActiveInferences[task.instance.id] || null}
|
||||
cancelAutoAnnotation={() => {
|
||||
dispatch(cancelInferenceAsync(task.instance.id));
|
||||
}}
|
||||
previewImage={task.preview}
|
||||
taskInstance={task.instance}
|
||||
/>
|
||||
))}
|
||||
</Col>
|
||||
</Row>
|
||||
);
|
||||
}
|
||||
@ -0,0 +1,56 @@
|
||||
// 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: $grid-unit-size * 2;
|
||||
margin: $grid-unit-size * 2 0;
|
||||
background: $background-color-1;
|
||||
|
||||
.ant-row-flex:nth-child(1) {
|
||||
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;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.cvat-issue-tracker {
|
||||
margin-top: $grid-unit-size * 2;
|
||||
margin-bottom: $grid-unit-size * 2;
|
||||
}
|
||||
}
|
||||
|
||||
.cvat-project-page-tasks-bar {
|
||||
margin: $grid-unit-size * 2 0;
|
||||
}
|
||||
|
||||
.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;
|
||||
}
|
||||
}
|
||||
|
||||
.cvat-project-top-bar-actions > button {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
@ -0,0 +1,44 @@
|
||||
// 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 Dropdown from 'antd/lib/dropdown';
|
||||
import Icon from 'antd/lib/icon';
|
||||
import Text from 'antd/lib/typography/Text';
|
||||
|
||||
import { Project } from 'reducers/interfaces';
|
||||
import ActionsMenu from 'components/projects-page/actions-menu';
|
||||
import { MenuIcon } from 'icons';
|
||||
|
||||
interface DetailsComponentProps {
|
||||
projectInstance: Project;
|
||||
}
|
||||
|
||||
export default function ProjectTopBar(props: DetailsComponentProps): JSX.Element {
|
||||
const { projectInstance } = props;
|
||||
|
||||
const history = useHistory();
|
||||
|
||||
return (
|
||||
<Row className='cvat-task-top-bar' type='flex' justify='space-between' align='middle'>
|
||||
<Col>
|
||||
<Button onClick={() => history.push('/projects')} type='link' size='large'>
|
||||
<Icon type='left' />
|
||||
Back to projects
|
||||
</Button>
|
||||
</Col>
|
||||
<Col className='cvat-project-top-bar-actions'>
|
||||
<Dropdown overlay={<ActionsMenu projectInstance={projectInstance.instance} />}>
|
||||
<Button size='large'>
|
||||
<Text className='cvat-text-color'>Actions</Text>
|
||||
<Icon className='cvat-menu-icon' component={MenuIcon} />
|
||||
</Button>
|
||||
</Dropdown>
|
||||
</Col>
|
||||
</Row>
|
||||
);
|
||||
}
|
||||
@ -0,0 +1,40 @@
|
||||
// Copyright (C) 2020 Intel Corporation
|
||||
//
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
import React from 'react';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import Modal from 'antd/lib/modal';
|
||||
import Menu from 'antd/lib/menu';
|
||||
|
||||
import { deleteProjectAsync } from 'actions/projects-actions';
|
||||
|
||||
interface Props {
|
||||
projectInstance: any;
|
||||
}
|
||||
|
||||
export default function ProjectActionsMenuComponent(props: Props): JSX.Element {
|
||||
const { projectInstance } = props;
|
||||
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const onDeleteProject = (): void => {
|
||||
Modal.confirm({
|
||||
title: `The project ${projectInstance.id} will be deleted`,
|
||||
content: 'All related data (images, annotations) will be lost. Continue?',
|
||||
onOk: () => {
|
||||
dispatch(deleteProjectAsync(projectInstance));
|
||||
},
|
||||
okButtonProps: {
|
||||
type: 'danger',
|
||||
},
|
||||
okText: 'Delete',
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<Menu className='cvat-project-actions-menu'>
|
||||
<Menu.Item onClick={onDeleteProject}>Delete</Menu.Item>
|
||||
</Menu>
|
||||
);
|
||||
}
|
||||
@ -0,0 +1,53 @@
|
||||
// Copyright (C) 2020 Intel Corporation
|
||||
//
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
import React from 'react';
|
||||
import { Link } from 'react-router-dom';
|
||||
import Text from 'antd/lib/typography/Text';
|
||||
import { Row, Col } from 'antd/lib/grid';
|
||||
import Icon from 'antd/lib/icon';
|
||||
|
||||
import { EmptyTasksIcon } from 'icons';
|
||||
|
||||
interface Props {
|
||||
notFound?: boolean;
|
||||
}
|
||||
|
||||
export default function EmptyListComponent(props: Props): JSX.Element {
|
||||
const { notFound } = props;
|
||||
return (
|
||||
<div className='cvat-empty-projects-list'>
|
||||
<Row type='flex' justify='center' align='middle'>
|
||||
<Col>
|
||||
<Icon className='cvat-empty-projects-icon' component={EmptyTasksIcon} />
|
||||
</Col>
|
||||
</Row>
|
||||
{notFound ? (
|
||||
<Row type='flex' justify='center' align='middle'>
|
||||
<Col>
|
||||
<Text strong>No results matched your search...</Text>
|
||||
</Col>
|
||||
</Row>
|
||||
) : (
|
||||
<>
|
||||
<Row type='flex' justify='center' align='middle'>
|
||||
<Col>
|
||||
<Text strong>No projects created yet ...</Text>
|
||||
</Col>
|
||||
</Row>
|
||||
<Row type='flex' justify='center' align='middle'>
|
||||
<Col>
|
||||
<Text type='secondary'>To get started with your annotation project</Text>
|
||||
</Col>
|
||||
</Row>
|
||||
<Row type='flex' justify='center' align='middle'>
|
||||
<Col>
|
||||
<Link to='/projects/create'>create a new one</Link>
|
||||
</Col>
|
||||
</Row>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@ -0,0 +1,99 @@
|
||||
// Copyright (C) 2020 Intel Corporation
|
||||
//
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
import React from 'react';
|
||||
import moment from 'moment';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { useHistory } from 'react-router';
|
||||
import Text from 'antd/lib/typography/Text';
|
||||
import Empty from 'antd/lib/empty';
|
||||
import Card from 'antd/lib/card';
|
||||
import Meta from 'antd/lib/card/Meta';
|
||||
import Dropdown from 'antd/lib/dropdown';
|
||||
import Button from 'antd/lib/button';
|
||||
|
||||
import { CombinedState, Project } from 'reducers/interfaces';
|
||||
import ProjectActionsMenuComponent from './actions-menu';
|
||||
|
||||
interface Props {
|
||||
projectInstance: Project;
|
||||
}
|
||||
|
||||
export default function ProjectItemComponent(props: Props): JSX.Element {
|
||||
const { projectInstance } = props;
|
||||
|
||||
const history = useHistory();
|
||||
const ownerName = projectInstance.owner ? projectInstance.owner.username : null;
|
||||
const updated = moment(projectInstance.updatedDate).fromNow();
|
||||
const deletes = useSelector((state: CombinedState) => state.projects.activities.deletes);
|
||||
const deleted = projectInstance.id in deletes ? deletes[projectInstance.id] : false;
|
||||
|
||||
let projectPreview = null;
|
||||
if (projectInstance.tasks.length) {
|
||||
// prettier-ignore
|
||||
projectPreview = useSelector((state: CombinedState) => (
|
||||
state.tasks.current.find((task) => task.instance.id === projectInstance.tasks[0].id)?.preview
|
||||
));
|
||||
}
|
||||
|
||||
const onOpenProject = (): void => {
|
||||
history.push(`/projects/${projectInstance.id}`);
|
||||
};
|
||||
|
||||
const style: React.CSSProperties = {};
|
||||
|
||||
if (deleted) {
|
||||
style.pointerEvents = 'none';
|
||||
style.opacity = 0.5;
|
||||
}
|
||||
|
||||
return (
|
||||
<Card
|
||||
cover={
|
||||
projectPreview ? (
|
||||
<img
|
||||
className='cvat-projects-project-item-card-preview'
|
||||
src={projectPreview}
|
||||
alt='Preview'
|
||||
onClick={onOpenProject}
|
||||
aria-hidden
|
||||
/>
|
||||
) : (
|
||||
<div className='cvat-projects-project-item-card-preview' onClick={onOpenProject} aria-hidden>
|
||||
<Empty description='No tasks' />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
size='small'
|
||||
style={style}
|
||||
className='cvat-projects-project-item-card'
|
||||
>
|
||||
<Meta
|
||||
title={(
|
||||
<span onClick={onOpenProject} className='cvat-projects-project-item-title' aria-hidden>
|
||||
{projectInstance.name}
|
||||
</span>
|
||||
)}
|
||||
description={(
|
||||
<div className='cvat-porjects-project-item-description'>
|
||||
<div>
|
||||
{ownerName && (
|
||||
<>
|
||||
<Text type='secondary'>{`Created ${ownerName ? `by ${ownerName}` : ''}`}</Text>
|
||||
<br />
|
||||
</>
|
||||
)}
|
||||
<Text type='secondary'>{`Last updated ${updated}`}</Text>
|
||||
</div>
|
||||
<div>
|
||||
<Dropdown overlay={<ProjectActionsMenuComponent projectInstance={projectInstance} />}>
|
||||
<Button type='link' size='large' icon='more' />
|
||||
</Dropdown>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
/>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
@ -0,0 +1,59 @@
|
||||
// Copyright (C) 2020 Intel Corporation
|
||||
//
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
import React from 'react';
|
||||
import { useSelector, useDispatch } from 'react-redux';
|
||||
import { Row, Col } from 'antd/lib/grid';
|
||||
import Pagination from 'antd/lib/pagination';
|
||||
|
||||
import { getProjectsAsync } from 'actions/projects-actions';
|
||||
import { CombinedState } from 'reducers/interfaces';
|
||||
import ProjectItem from './project-item';
|
||||
|
||||
export default function ProjectListComponent(): JSX.Element {
|
||||
const dispatch = useDispatch();
|
||||
const projectsCount = useSelector((state: CombinedState) => state.projects.count);
|
||||
const { page } = useSelector((state: CombinedState) => state.projects.gettingQuery);
|
||||
const projectInstances = useSelector((state: CombinedState) => state.projects.current);
|
||||
const gettingQuery = useSelector((state: CombinedState) => state.projects.gettingQuery);
|
||||
|
||||
function changePage(p: number): void {
|
||||
dispatch(
|
||||
getProjectsAsync({
|
||||
...gettingQuery,
|
||||
page: p,
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Row type='flex' justify='center' align='middle'>
|
||||
<Col className='cvat-projects-list' md={22} lg={18} xl={16} xxl={14}>
|
||||
<Row gutter={[8, 8]}>
|
||||
{projectInstances.map(
|
||||
(instance: any): JSX.Element => (
|
||||
<Col xs={8} sm={8} xl={6} key={instance.id}>
|
||||
<ProjectItem projectInstance={instance} />
|
||||
</Col>
|
||||
),
|
||||
)}
|
||||
</Row>
|
||||
</Col>
|
||||
</Row>
|
||||
<Row type='flex' justify='center' align='middle'>
|
||||
<Col md={22} lg={18} xl={16} xxl={14}>
|
||||
<Pagination
|
||||
className='cvat-projects-pagination'
|
||||
onChange={changePage}
|
||||
total={projectsCount}
|
||||
pageSize={12}
|
||||
current={page}
|
||||
showQuickJumper
|
||||
/>
|
||||
</Col>
|
||||
</Row>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@ -0,0 +1,67 @@
|
||||
// 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, useHistory } from 'react-router';
|
||||
import Spin from 'antd/lib/spin';
|
||||
|
||||
import FeedbackComponent from 'components/feedback/feedback';
|
||||
import { CombinedState, ProjectsQuery } from 'reducers/interfaces';
|
||||
import { getProjectsAsync } 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 history = useHistory();
|
||||
const dispatch = useDispatch();
|
||||
const projectFetching = useSelector((state: CombinedState) => state.projects.fetching);
|
||||
const projectsCount = useSelector((state: CombinedState) => state.projects.current.length);
|
||||
const gettingQuery = useSelector((state: CombinedState) => state.projects.gettingQuery);
|
||||
|
||||
const anySearchQuery = !!Array.from(new URLSearchParams(search).keys()).filter((value) => value !== 'page').length;
|
||||
|
||||
useEffect(() => {
|
||||
const searchParams: Partial<ProjectsQuery> = {};
|
||||
for (const [param, value] of new URLSearchParams(search)) {
|
||||
searchParams[param] = ['page', 'id'].includes(param) ? Number.parseInt(value, 10) : value;
|
||||
}
|
||||
dispatch(getProjectsAsync(searchParams));
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
const searchParams = new URLSearchParams();
|
||||
for (const [name, value] of Object.entries(gettingQuery)) {
|
||||
if (value !== null && typeof value !== 'undefined') {
|
||||
searchParams.append(name, value.toString());
|
||||
}
|
||||
}
|
||||
history.push({
|
||||
pathname: '/projects',
|
||||
search: `?${searchParams.toString()}`,
|
||||
});
|
||||
}, [gettingQuery]);
|
||||
|
||||
if (projectFetching) {
|
||||
return (
|
||||
<Spin size='large' className='cvat-spinner' />
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className='cvat-projects-page'>
|
||||
<TopBarComponent />
|
||||
{ projectsCount
|
||||
? (
|
||||
<ProjectListComponent />
|
||||
) : (
|
||||
<EmptyListComponent notFound={anySearchQuery} />
|
||||
)}
|
||||
<FeedbackComponent />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@ -0,0 +1,81 @@
|
||||
// Copyright (C) 2020 Intel Corporation
|
||||
//
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
import './styles.scss';
|
||||
import React from 'react';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import Search from 'antd/lib/input/Search';
|
||||
|
||||
import { CombinedState, ProjectsQuery } from 'reducers/interfaces';
|
||||
import { getProjectsAsync } from 'actions/projects-actions';
|
||||
|
||||
function getSearchField(gettingQuery: ProjectsQuery): string {
|
||||
let searchString = '';
|
||||
for (const field of Object.keys(gettingQuery)) {
|
||||
if (gettingQuery[field] !== null && field !== 'page') {
|
||||
if (field === 'search') {
|
||||
return (gettingQuery[field] as any) as string;
|
||||
}
|
||||
|
||||
// not constant condition
|
||||
// eslint-disable-next-line
|
||||
if (typeof (gettingQuery[field] === 'number')) {
|
||||
searchString += `${field}:${gettingQuery[field]} AND `;
|
||||
} else {
|
||||
searchString += `${field}:"${gettingQuery[field]}" AND `;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return searchString.slice(0, -5);
|
||||
}
|
||||
|
||||
export default function ProjectSearchField(): JSX.Element {
|
||||
const dispatch = useDispatch();
|
||||
const gettingQuery = useSelector((state: CombinedState) => state.projects.gettingQuery);
|
||||
|
||||
const handleSearch = (value: string): void => {
|
||||
const query = { ...gettingQuery };
|
||||
const search = value.replace(/\s+/g, ' ').replace(/\s*:+\s*/g, ':').trim();
|
||||
|
||||
const fields = Object.keys(query).filter((key) => key !== 'page');
|
||||
for (const field of fields) {
|
||||
query[field] = null;
|
||||
}
|
||||
query.search = null;
|
||||
|
||||
let specificRequest = false;
|
||||
for (const param of search.split(/[\s]+and[\s]+|[\s]+AND[\s]+/)) {
|
||||
if (param.includes(':')) {
|
||||
const [field, fieldValue] = param.split(':');
|
||||
if (fields.includes(field) && !!fieldValue) {
|
||||
specificRequest = true;
|
||||
if (field === 'id') {
|
||||
if (Number.isInteger(+fieldValue)) {
|
||||
query[field] = +fieldValue;
|
||||
}
|
||||
} else {
|
||||
query[field] = fieldValue;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
query.page = 1;
|
||||
if (!specificRequest && value) {
|
||||
query.search = value;
|
||||
}
|
||||
|
||||
dispatch(getProjectsAsync(query));
|
||||
};
|
||||
|
||||
return (
|
||||
<Search
|
||||
defaultValue={getSearchField(gettingQuery)}
|
||||
onSearch={handleSearch}
|
||||
size='large'
|
||||
placeholder='Search'
|
||||
/>
|
||||
);
|
||||
}
|
||||
@ -0,0 +1,119 @@
|
||||
// Copyright (C) 2020 Intel Corporation
|
||||
//
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
@import '../../base.scss';
|
||||
|
||||
.cvat-projects-page {
|
||||
padding-top: $grid-unit-size * 2;
|
||||
padding-bottom: $grid-unit-size * 5;
|
||||
height: 100%;
|
||||
position: fixed;
|
||||
width: 100%;
|
||||
|
||||
> div:nth-child(1) {
|
||||
padding-bottom: $grid-unit-size;
|
||||
|
||||
div > {
|
||||
span {
|
||||
color: $text-color;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* empty-projects icon */
|
||||
.cvat-empty-projects-list {
|
||||
> div:nth-child(1) {
|
||||
margin-top: $grid-unit-size * 6;
|
||||
}
|
||||
|
||||
> div:nth-child(2) {
|
||||
> div {
|
||||
margin-top: $grid-unit-size * 3;
|
||||
|
||||
/* No projects created yet */
|
||||
> span {
|
||||
font-size: 20px;
|
||||
color: $text-color;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* To get started with your annotation project .. */
|
||||
> div:nth-child(3) {
|
||||
margin-top: $grid-unit-size;
|
||||
}
|
||||
}
|
||||
|
||||
.cvat-projects-top-bar {
|
||||
> div:nth-child(1) {
|
||||
display: flex;
|
||||
|
||||
> span:nth-child(2) {
|
||||
width: $grid-unit-size * 25;
|
||||
margin-left: $grid-unit-size;
|
||||
}
|
||||
}
|
||||
|
||||
> div:nth-child(2) {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
}
|
||||
|
||||
.cvat-create-project-button {
|
||||
padding: 0 $grid-unit-size * 4;
|
||||
}
|
||||
|
||||
.cvat-projects-pagination {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.cvat-projects-project-item-title,
|
||||
.cvat-projects-project-item-card-preview {
|
||||
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;
|
||||
|
||||
> button {
|
||||
color: $text-color;
|
||||
width: inherit;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.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;
|
||||
}
|
||||
}
|
||||
|
||||
.cvat-projects-project-item-card {
|
||||
.ant-empty {
|
||||
margin: $grid-unit-size;
|
||||
}
|
||||
|
||||
img {
|
||||
height: 100%;
|
||||
max-height: $grid-unit-size * 18;
|
||||
object-fit: cover;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,36 @@
|
||||
// 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 Text from 'antd/lib/typography/Text';
|
||||
|
||||
import SearchField from './search-field';
|
||||
|
||||
export default function TopBarComponent(): JSX.Element {
|
||||
const history = useHistory();
|
||||
|
||||
return (
|
||||
<Row type='flex' justify='center' align='middle' className='cvat-projects-top-bar'>
|
||||
<Col md={11} lg={9} xl={8} xxl={7}>
|
||||
<Text className='cvat-title'>Projects</Text>
|
||||
<SearchField />
|
||||
</Col>
|
||||
<Col md={{ span: 11 }} lg={{ span: 9 }} xl={{ span: 8 }} xxl={{ span: 7 }}>
|
||||
<Button
|
||||
size='large'
|
||||
id='cvat-create-project-button'
|
||||
className='cvat-create-project-button'
|
||||
type='primary'
|
||||
onClick={(): void => history.push('/projects/create')}
|
||||
icon='plus'
|
||||
>
|
||||
Create new project
|
||||
</Button>
|
||||
</Col>
|
||||
</Row>
|
||||
);
|
||||
}
|
||||
@ -0,0 +1,92 @@
|
||||
// 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);
|
||||
onChange(value);
|
||||
}
|
||||
};
|
||||
|
||||
if (bugTracker) {
|
||||
return (
|
||||
<Row className='cvat-issue-tracker'>
|
||||
<Col>
|
||||
<Text strong className='cvat-text-color'>
|
||||
Issue Tracker
|
||||
</Text>
|
||||
<br />
|
||||
<Text editable={{ onChange: onChangeValue }}>{bugTracker}</Text>
|
||||
<Button
|
||||
type='ghost'
|
||||
size='small'
|
||||
onClick={(): void => {
|
||||
// false positive
|
||||
// eslint-disable-next-line
|
||||
window.open(bugTracker, '_blank');
|
||||
}}
|
||||
className='cvat-open-bug-tracker-button'
|
||||
>
|
||||
Open the issue
|
||||
</Button>
|
||||
</Col>
|
||||
</Row>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Row className='cvat-issue-tracker'>
|
||||
<Col>
|
||||
<Text strong className='cvat-text-color'>
|
||||
Issue Tracker
|
||||
</Text>
|
||||
<br />
|
||||
<Text
|
||||
editable={{
|
||||
editing: bugTrackerEditing,
|
||||
onStart,
|
||||
onChange: onChangeValue,
|
||||
}}
|
||||
>
|
||||
{bugTrackerEditing ? '' : 'Not specified'}
|
||||
</Text>
|
||||
</Col>
|
||||
</Row>
|
||||
);
|
||||
}
|
||||
@ -0,0 +1,192 @@
|
||||
// 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: {
|
||||
return {
|
||||
...state,
|
||||
initialized: true,
|
||||
fetching: false,
|
||||
count: action.payload.count,
|
||||
current: action.payload.array,
|
||||
};
|
||||
}
|
||||
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.UPDATE_PROJECT: {
|
||||
return {
|
||||
...state,
|
||||
};
|
||||
}
|
||||
case ProjectsActionTypes.UPDATE_PROJECT_SUCCESS: {
|
||||
return {
|
||||
...state,
|
||||
current: state.current.map(
|
||||
(project): Project => {
|
||||
if (project.id === action.payload.project.id) {
|
||||
return action.payload.project;
|
||||
}
|
||||
|
||||
return project;
|
||||
},
|
||||
),
|
||||
};
|
||||
}
|
||||
case ProjectsActionTypes.UPDATE_PROJECT_FAILED: {
|
||||
return {
|
||||
...state,
|
||||
current: state.current.map(
|
||||
(project): Project => {
|
||||
if (project.id === action.payload.project.id) {
|
||||
return action.payload.project;
|
||||
}
|
||||
|
||||
return project;
|
||||
},
|
||||
),
|
||||
};
|
||||
}
|
||||
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;
|
||||
}
|
||||
};
|
||||
@ -0,0 +1,24 @@
|
||||
# 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', '0032_remove_task_z_order'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
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'),
|
||||
),
|
||||
]
|
||||
@ -0,0 +1,60 @@
|
||||
// Copyright (C) 2020 Intel Corporation
|
||||
//
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
/// <reference types="cypress" />
|
||||
|
||||
import { taskName, labelName } from '../../support/const';
|
||||
|
||||
context('Check hide/unhide functionality from label tab for object and tag with a same label.', () => {
|
||||
const issueId = '2418';
|
||||
const createRectangleShape2Points = {
|
||||
points: 'By 2 Points',
|
||||
type: 'Shape',
|
||||
labelName: labelName,
|
||||
firstX: 260,
|
||||
firstY: 200,
|
||||
secondX: 360,
|
||||
secondY: 250,
|
||||
};
|
||||
|
||||
before(() => {
|
||||
cy.openTaskJob(taskName);
|
||||
});
|
||||
|
||||
describe(`Testing issue "${issueId}"`, () => {
|
||||
it('Crearte an object. Create a tag.', () => {
|
||||
cy.createRectangle(createRectangleShape2Points);
|
||||
cy.createTag(labelName);
|
||||
});
|
||||
it('Go to "Labels" tab.', () => {
|
||||
cy.get('.cvat-objects-sidebar').within(() => {
|
||||
cy.contains('Labels').click();
|
||||
});
|
||||
});
|
||||
it('Hide object by label name.', () => {
|
||||
cy.get('.cvat-objects-sidebar-labels-list').within(() => {
|
||||
cy.contains(labelName)
|
||||
.parents('.cvat-objects-sidebar-label-item')
|
||||
.within(() => {
|
||||
cy.get('.cvat-label-item-button-hidden')
|
||||
.click()
|
||||
.should('have.class', 'cvat-label-item-button-hidden-enabled');
|
||||
});
|
||||
});
|
||||
cy.get('#cvat_canvas_shape_1').should('be.hidden');
|
||||
});
|
||||
it('Unhide object by label name.', () => {
|
||||
cy.get('.cvat-objects-sidebar-labels-list').within(() => {
|
||||
cy.contains(labelName)
|
||||
.parents('.cvat-objects-sidebar-label-item')
|
||||
.within(() => {
|
||||
cy.get('.cvat-label-item-button-hidden')
|
||||
.click()
|
||||
.should('not.have.class', 'cvat-label-item-button-hidden-enabled');
|
||||
});
|
||||
});
|
||||
cy.get('#cvat_canvas_shape_1').should('be.visible');
|
||||
});
|
||||
});
|
||||
});
|
||||
@ -0,0 +1,45 @@
|
||||
// Copyright (C) 2020 Intel Corporation
|
||||
//
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
/// <reference types="cypress" />
|
||||
|
||||
import { taskName } from '../../support/const';
|
||||
|
||||
context('Value must be a user instance.', () => {
|
||||
const issueId = '2440';
|
||||
|
||||
before(() => {
|
||||
cy.openTask(taskName);
|
||||
});
|
||||
|
||||
describe(`Testing issue "${issueId}"`, () => {
|
||||
it('Assign a task to a user', () => {
|
||||
cy.get('.cvat-task-details-user-block').within(() => {
|
||||
cy.get('.cvat-user-search-field').click();
|
||||
});
|
||||
cy.get('.ant-select-dropdown')
|
||||
.not('.ant-select-dropdown-hidden')
|
||||
.contains(new RegExp(`^${Cypress.env('user')}$`, 'g'))
|
||||
.click();
|
||||
cy.get('.cvat-spinner').should('exist');
|
||||
});
|
||||
it('Assign the task to the same user again', () => {
|
||||
cy.get('.cvat-task-details-user-block').within(() => {
|
||||
cy.get('.cvat-user-search-field').click();
|
||||
});
|
||||
cy.get('.ant-select-dropdown')
|
||||
.not('.ant-select-dropdown-hidden')
|
||||
.contains(new RegExp(`^${Cypress.env('user')}$`, 'g'))
|
||||
.click();
|
||||
// Before fix:
|
||||
// The following error originated from your application code, not from Cypress.
|
||||
// > Value must be a user instance
|
||||
cy.get('.cvat-spinner').should('exist');
|
||||
// Remove the user's assignment for next tests.
|
||||
cy.get('.cvat-task-details-user-block').within(() => {
|
||||
cy.get('[type="text"]').click().clear().type('{Enter}');
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
Loading…
Reference in New Issue