Merge branch 'develop' into mk/share_without_copying_
commit
c1da108f6c
@ -0,0 +1 @@
|
|||||||
|
webpack.config.js
|
||||||
@ -0,0 +1 @@
|
|||||||
|
webpack.config.js
|
||||||
File diff suppressed because it is too large
Load Diff
@ -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
|
**/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));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
@ -1,36 +0,0 @@
|
|||||||
// Copyright (C) 2020 Intel Corporation
|
|
||||||
//
|
|
||||||
// SPDX-License-Identifier: MIT
|
|
||||||
|
|
||||||
import { ActionUnion, createAction, ThunkAction } from 'utils/redux';
|
|
||||||
import getCore from 'cvat-core-wrapper';
|
|
||||||
|
|
||||||
const core = getCore();
|
|
||||||
|
|
||||||
export enum UsersActionTypes {
|
|
||||||
GET_USERS = 'GET_USERS',
|
|
||||||
GET_USERS_SUCCESS = 'GET_USERS_SUCCESS',
|
|
||||||
GET_USERS_FAILED = 'GET_USERS_FAILED',
|
|
||||||
}
|
|
||||||
|
|
||||||
const usersActions = {
|
|
||||||
getUsers: () => createAction(UsersActionTypes.GET_USERS),
|
|
||||||
getUsersSuccess: (users: any[]) => createAction(UsersActionTypes.GET_USERS_SUCCESS, { users }),
|
|
||||||
getUsersFailed: (error: any) => createAction(UsersActionTypes.GET_USERS_FAILED, { error }),
|
|
||||||
};
|
|
||||||
|
|
||||||
export type UsersActions = ActionUnion<typeof usersActions>;
|
|
||||||
|
|
||||||
export function getUsersAsync(): ThunkAction {
|
|
||||||
return async (dispatch): Promise<void> => {
|
|
||||||
dispatch(usersActions.getUsers());
|
|
||||||
|
|
||||||
try {
|
|
||||||
const users = await core.users.get();
|
|
||||||
const wrappedUsers = users.map((userData: any): any => new core.classes.User(userData));
|
|
||||||
dispatch(usersActions.getUsersSuccess(wrappedUsers));
|
|
||||||
} catch (error) {
|
|
||||||
dispatch(usersActions.getUsersFailed(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;
|
||||||
|
}
|
||||||
|
};
|
||||||
@ -1,48 +0,0 @@
|
|||||||
// Copyright (C) 2020 Intel Corporation
|
|
||||||
//
|
|
||||||
// SPDX-License-Identifier: MIT
|
|
||||||
|
|
||||||
import { BoundariesActionTypes, BoundariesActions } from 'actions/boundaries-actions';
|
|
||||||
import { AuthActionTypes, AuthActions } from 'actions/auth-actions';
|
|
||||||
import { UsersActionTypes, UsersActions } from 'actions/users-actions';
|
|
||||||
import { UsersState } from './interfaces';
|
|
||||||
|
|
||||||
const defaultState: UsersState = {
|
|
||||||
users: [],
|
|
||||||
fetching: false,
|
|
||||||
initialized: false,
|
|
||||||
};
|
|
||||||
|
|
||||||
export default function (
|
|
||||||
state: UsersState = defaultState,
|
|
||||||
action: UsersActions | AuthActions | BoundariesActions,
|
|
||||||
): UsersState {
|
|
||||||
switch (action.type) {
|
|
||||||
case UsersActionTypes.GET_USERS: {
|
|
||||||
return {
|
|
||||||
...state,
|
|
||||||
fetching: true,
|
|
||||||
initialized: false,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
case UsersActionTypes.GET_USERS_SUCCESS:
|
|
||||||
return {
|
|
||||||
...state,
|
|
||||||
fetching: false,
|
|
||||||
initialized: true,
|
|
||||||
users: action.payload.users,
|
|
||||||
};
|
|
||||||
case UsersActionTypes.GET_USERS_FAILED:
|
|
||||||
return {
|
|
||||||
...state,
|
|
||||||
fetching: false,
|
|
||||||
initialized: true,
|
|
||||||
};
|
|
||||||
case BoundariesActionTypes.RESET_AFTER_ERROR:
|
|
||||||
case AuthActionTypes.LOGOUT_SUCCESS: {
|
|
||||||
return { ...defaultState };
|
|
||||||
}
|
|
||||||
default:
|
|
||||||
return state;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,5 +1,16 @@
|
|||||||
{% load i18n %}{% autoescape off %} {% blocktrans %}You're receiving this email because you requested a password reset
|
{% load i18n %}{% autoescape off %}
|
||||||
for your user account at {{ site_name }}.{% endblocktrans %} {% trans "Please go to the following page and choose a new
|
{% blocktrans %}
|
||||||
password:" %} {% block reset_link %} {{ protocol }}://{{ domain }}/auth/password/reset/confirm?uid={{ uid }}&token={{
|
You're receiving this email because you requested a password reset for your user account at {{ site_name }}.
|
||||||
token }} {% endblock %} {% trans "Your username, in case you've forgotten:" %} {{ user.get_username }} {% trans "Thanks
|
{% endblocktrans %}
|
||||||
for using our site!" %} {% blocktrans %}The {{ site_name }} team{% endblocktrans %} {% endautoescape %}
|
|
||||||
|
{% trans "Please go to the following page and choose a new password:" %}
|
||||||
|
{% block reset_link %}
|
||||||
|
{{ protocol }}://{{ domain }}/auth/password/reset/confirm?uid={{ uid }}&token={{ token }}
|
||||||
|
{% endblock %}
|
||||||
|
{% trans "Your username, in case you've forgotten:" %} {{ user.get_username }}
|
||||||
|
|
||||||
|
{% trans "Thanks for using our site!" %}
|
||||||
|
|
||||||
|
{% blocktrans %}The {{ site_name }} team{% endblocktrans %}
|
||||||
|
|
||||||
|
{% endautoescape %}
|
||||||
|
|||||||
@ -1,7 +1,14 @@
|
|||||||
<!--
|
<!--
|
||||||
Copyright (C) 2018 Intel Corporation
|
Copyright (C) 2018-2020 Intel Corporation
|
||||||
|
|
||||||
SPDX-License-Identifier: MIT
|
SPDX-License-Identifier: MIT
|
||||||
-->
|
-->
|
||||||
{% extends 'documentation/base_page.html' %} {% block title %} CVAT User Guide {% endblock %} {% block content %} {{
|
{% extends 'documentation/base_page.html' %}
|
||||||
user_guide }} {% endblock %}
|
|
||||||
|
{% block title %}
|
||||||
|
CVAT User Guide
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
{{ user_guide }}
|
||||||
|
{% endblock %}
|
||||||
|
|||||||
@ -1,7 +1,8 @@
|
|||||||
<!--
|
<!--
|
||||||
Copyright (C) 2018 Intel Corporation
|
Copyright (C) 2018-2020 Intel Corporation
|
||||||
|
|
||||||
SPDX-License-Identifier: MIT
|
SPDX-License-Identifier: MIT
|
||||||
-->
|
-->
|
||||||
{% extends 'documentation/base_page.html' %} {% block title %} CVAT XML format {% endblock %} {% block content %} {{
|
{% extends 'documentation/base_page.html' %}
|
||||||
xml_format }} {% endblock %}
|
{% block title %} CVAT XML format {% endblock %}
|
||||||
|
{% block content %} {{ xml_format }} {% endblock %}
|
||||||
|
|||||||
@ -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'),
|
||||||
|
),
|
||||||
|
]
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue