Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Project tasks pagination #3910

Merged
merged 17 commits into from
Nov 26, 2021
Merged
Show file tree
Hide file tree
Changes from 15 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Add Cityscapes format (<https://github.com/openvinotoolkit/cvat/pull/3758>)
- Add Open Images V6 format (<https://github.com/openvinotoolkit/cvat/pull/3679>)
- Rotated bounding boxes (<https://github.com/openvinotoolkit/cvat/pull/3832>)
- Add project tasks paginations (<https://github.com/openvinotoolkit/cvat/pull/3910>)

### Changed
- TDB
Expand Down
4 changes: 2 additions & 2 deletions cvat-core/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion cvat-core/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "cvat-core",
"version": "3.19.0",
"version": "3.20.0",
"description": "Part of Computer Vision Tool which presents an interface for client-side integration",
"main": "babel.config.js",
"scripts": {
Expand Down
26 changes: 7 additions & 19 deletions cvat-core/src/api-implementation.js
Original file line number Diff line number Diff line change
Expand Up @@ -188,6 +188,7 @@
owner: isString,
assignee: isString,
search: isString,
ordering: isString,
status: isEnum.bind(TaskStatus),
mode: isEnum.bind(TaskMode),
dimension: isEnum.bind(DimensionType),
Expand All @@ -196,11 +197,13 @@
checkExclusiveFields(filter, ['id', 'search', 'projectId'], ['page']);

const searchParams = new URLSearchParams();

for (const field of [
'name',
'owner',
'assignee',
'search',
'ordering',
'status',
'mode',
'id',
Expand All @@ -209,7 +212,7 @@
'dimension',
]) {
if (Object.prototype.hasOwnProperty.call(filter, field)) {
searchParams.set(field, filter[field]);
searchParams.set(camelToSnake(field), filter[field]);
}
}

Expand All @@ -230,35 +233,20 @@
owner: isString,
search: isString,
status: isEnum.bind(TaskStatus),
withoutTasks: isBoolean,
});

checkExclusiveFields(filter, ['id', 'search'], ['page', 'withoutTasks']);

if (typeof filter.withoutTasks === 'undefined') {
if (typeof filter.id === 'undefined') {
filter.withoutTasks = true;
} else {
filter.withoutTasks = false;
}
}
checkExclusiveFields(filter, ['id', 'search'], ['page']);

const searchParams = new URLSearchParams();
for (const field of ['name', 'assignee', 'owner', 'search', 'status', 'id', 'page', 'withoutTasks']) {
for (const field of ['name', 'assignee', 'owner', 'search', 'status', 'id', 'page']) {
if (Object.prototype.hasOwnProperty.call(filter, field)) {
searchParams.set(camelToSnake(field), filter[field]);
}
}

const projectsData = await serverProxy.projects.get(searchParams.toString());
// prettier-ignore
const projects = projectsData.map((project) => {
if (filter.withoutTasks) {
project.task_ids = project.tasks;
project.tasks = [];
} else {
project.task_ids = project.tasks.map((task) => task.id);
}
project.task_ids = project.tasks;
return project;
}).map((project) => new Project(project));

Expand Down
26 changes: 0 additions & 26 deletions cvat-core/src/project.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@
(() => {
const PluginRegistry = require('./plugins');
const { ArgumentError } = require('./exceptions');
const { Task } = require('./session');
const { Label } = require('./labels');
const User = require('./user');

Expand Down Expand Up @@ -44,7 +43,6 @@
}

data.labels = [];
data.tasks = [];

if (Array.isArray(initialData.labels)) {
for (const label of initialData.labels) {
Expand All @@ -53,19 +51,6 @@
}
}

if (Array.isArray(initialData.tasks)) {
for (const task of initialData.tasks) {
const taskInstance = new Task(task);
data.tasks.push(taskInstance);
}
}
if (!data.task_subsets) {
const subsetsSet = new Set();
for (const task of data.tasks) {
if (task.subset) subsetsSet.add(task.subset);
}
data.task_subsets = Array.from(subsetsSet);
}
if (typeof initialData.training_project === 'object') {
data.training_project = { ...initialData.training_project };
}
Expand Down Expand Up @@ -212,17 +197,6 @@
data.labels = [...deletedLabels, ...labels];
},
},
/**
* Tasks related with the project
* @name tasks
* @type {module:API.cvat.classes.Task[]}
* @memberof module:API.cvat.classes.Project
* @readonly
* @instance
*/
tasks: {
get: () => [...data.tasks],
},
/**
* Subsets array for related tasks
* @name subsets
Expand Down
7 changes: 3 additions & 4 deletions cvat-core/tests/api/projects.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,11 @@ jest.mock('../../src/server-proxy', () => {
// 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({ withoutTasks: false });
const result = await window.cvat.projects.get();
expect(Array.isArray(result)).toBeTruthy();
expect(result).toHaveLength(2);
for (const el of result) {
Expand All @@ -33,8 +32,8 @@ describe('Feature: get projects', () => {
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);
// eslint-disable-next-line no-underscore-dangle
expect(result[0]._internalData.task_ids).toHaveLength(1);
});

test('get a project by an unknown id', async () => {
Expand Down
4 changes: 2 additions & 2 deletions cvat-ui/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion cvat-ui/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "cvat-ui",
"version": "1.26.0",
"version": "1.26.1",
"description": "CVAT single-page application",
"main": "src/index.tsx",
"scripts": {
Expand Down
80 changes: 36 additions & 44 deletions cvat-ui/src/actions/projects-actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,9 @@
import { Dispatch, ActionCreator } from 'redux';

import { ActionUnion, createAction, ThunkAction } from 'utils/redux';
import { ProjectsQuery } from 'reducers/interfaces';
import { getTasksSuccess, updateTaskSuccess } from 'actions/tasks-actions';
import { ProjectsQuery, TasksQuery, CombinedState } from 'reducers/interfaces';
import { getTasksAsync } from 'actions/tasks-actions';
import { getCVATStore } from 'cvat-store';
import getCore from 'cvat-core-wrapper';

const cvat = getCore();
Expand Down Expand Up @@ -34,8 +35,8 @@ const projectActions = {
createAction(ProjectsActionTypes.GET_PROJECTS_SUCCESS, { array, previews, count })
),
getProjectsFailed: (error: any) => createAction(ProjectsActionTypes.GET_PROJECTS_FAILED, { error }),
updateProjectsGettingQuery: (query: Partial<ProjectsQuery>) => (
createAction(ProjectsActionTypes.UPDATE_PROJECTS_GETTING_QUERY, { query })
updateProjectsGettingQuery: (query: Partial<ProjectsQuery>, tasksQuery: Partial<TasksQuery> = {}) => (
createAction(ProjectsActionTypes.UPDATE_PROJECTS_GETTING_QUERY, { query, tasksQuery })
),
createProject: () => createAction(ProjectsActionTypes.CREATE_PROJECT),
createProjectSuccess: (projectId: number) => (
Expand All @@ -58,10 +59,27 @@ const projectActions = {

export type ProjectActions = ActionUnion<typeof projectActions>;

export function getProjectsAsync(query: Partial<ProjectsQuery>): ThunkAction {
return async (dispatch: ActionCreator<Dispatch>, getState): Promise<void> => {
export function getProjectTasksAsync(tasksQuery: Partial<TasksQuery> = {}): ThunkAction<void> {
return (dispatch: ActionCreator<Dispatch>): void => {
const store = getCVATStore();
const state: CombinedState = store.getState();
dispatch(projectActions.updateProjectsGettingQuery({}, tasksQuery));
const query: Partial<TasksQuery> = {
...state.projects.tasksGettingQuery,
page: 1,
...tasksQuery,
};

dispatch(getTasksAsync(query));
};
}

export function getProjectsAsync(
query: Partial<ProjectsQuery>, tasksQuery: Partial<TasksQuery> = {},
): ThunkAction {
return async (dispatch: ActionCreator<Dispatch>): Promise<void> => {
dispatch(projectActions.getProjects());
dispatch(projectActions.updateProjectsGettingQuery(query));
dispatch(projectActions.updateProjectsGettingQuery(query, tasksQuery));

// Clear query object from null fields
const filteredQuery: Partial<ProjectsQuery> = {
Expand All @@ -85,38 +103,15 @@ export function getProjectsAsync(query: Partial<ProjectsQuery>): ThunkAction {

const array = Array.from(result);

// Appropriate tasks fetching proccess needs with retrieving only a single project
if (Object.keys(filteredQuery).includes('id')) {
const tasks: any[] = [];
const [project] = array;
const taskPreviewPromises: Promise<string>[] = (project as any).tasks.map((task: any): string => {
tasks.push(task);
return (task as any).frames.preview().catch(() => '');
});
const previewPromises = array.map((project): string => (project as any).preview().catch(() => ''));
dispatch(projectActions.getProjectsSuccess(array, await Promise.all(previewPromises), result.count));

const taskPreviews = await Promise.all(taskPreviewPromises);

const state = getState();

dispatch(projectActions.getProjectsSuccess(array, taskPreviews, result.count));

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,
}),
);
}
} else {
const previewPromises = array.map((project): string => (project as any).preview().catch(() => ''));
dispatch(projectActions.getProjectsSuccess(array, await Promise.all(previewPromises), result.count));
// Appropriate tasks fetching proccess needs with retrieving only a single project
if (Object.keys(filteredQuery).includes('id') && typeof filteredQuery.id === 'number') {
dispatch(getProjectTasksAsync({
...tasksQuery,
projectId: filteredQuery.id,
}));
}
};
}
Expand All @@ -136,17 +131,14 @@ export function createProjectAsync(data: any): ThunkAction {
}

export function updateProjectAsync(projectInstance: any): ThunkAction {
return async (dispatch: ActionCreator<Dispatch>): Promise<void> => {
return async (dispatch, getState): Promise<void> => {
try {
const state = getState();
dispatch(projectActions.updateProject());
await projectInstance.save();
const [project] = await cvat.projects.get({ id: projectInstance.id });
// TODO: Check case when a project is not available anymore after update
// (assignee changes assignee and project is not public)
dispatch(projectActions.updateProjectSuccess(project));
project.tasks.forEach((task: any) => {
dispatch(updateTaskSuccess(task, task.id));
});
dispatch(getProjectTasksAsync(state.projects.tasksGettingQuery));
} catch (error) {
let project = null;
try {
Expand Down
10 changes: 6 additions & 4 deletions cvat-ui/src/actions/tasks-actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,9 @@ function getTasks(): AnyAction {
return action;
}

export function getTasksSuccess(array: any[], previews: string[], count: number, query: TasksQuery): AnyAction {
export function getTasksSuccess(
array: any[], previews: string[], count: number, query: Partial<TasksQuery>,
): AnyAction {
const action = {
type: TasksActionTypes.GET_TASKS_SUCCESS,
payload: {
Expand All @@ -61,7 +63,7 @@ export function getTasksSuccess(array: any[], previews: string[], count: number,
return action;
}

function getTasksFailed(error: any, query: TasksQuery): AnyAction {
function getTasksFailed(error: any, query: Partial<TasksQuery>): AnyAction {
const action = {
type: TasksActionTypes.GET_TASKS_FAILED,
payload: {
Expand All @@ -73,7 +75,7 @@ function getTasksFailed(error: any, query: TasksQuery): AnyAction {
return action;
}

export function getTasksAsync(query: TasksQuery): ThunkAction<Promise<void>, {}, {}, AnyAction> {
export function getTasksAsync(query: Partial<TasksQuery>): ThunkAction<Promise<void>, {}, {}, AnyAction> {
return async (dispatch: ActionCreator<Dispatch>): Promise<void> => {
dispatch(getTasks());

Expand Down Expand Up @@ -248,7 +250,7 @@ export function exportTaskAsync(taskInstance: any): ThunkAction<Promise<void>, {
downloadAnchor.click();
dispatch(exportTaskSuccess(taskInstance.id));
} catch (error) {
dispatch(exportTaskFailed(taskInstance.id, error));
dispatch(exportTaskFailed(taskInstance.id, error as Error));
}
};
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ export default function ProjectSubsetField(props: Props): JSX.Element {

useEffect(() => {
if (!projectSubsets?.length && projectId) {
core.projects.get({ id: projectId, withoutTasks: true }).then((response: ProjectPartialWithSubsets[]) => {
core.projects.get({ id: projectId }).then((response: ProjectPartialWithSubsets[]) => {
if (response.length) {
const [project] = response;
setInternalSubsets(
Expand Down
3 changes: 1 addition & 2 deletions cvat-ui/src/components/project-page/details.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@ 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';
Expand All @@ -19,7 +18,7 @@ import UserSelector from 'components/task-page/user-selector';
const core = getCore();

interface DetailsComponentProps {
project: Project;
project: any;
}

export default function DetailsComponent(props: DetailsComponentProps): JSX.Element {
Expand Down
Loading