Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
4 changes: 4 additions & 0 deletions changelog.d/20250703_113854_klakhov_add_dynamic_select.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
### Added

- Selector that allows inline editing of the following fields from card views: `assignee`, `state`, and `stage`.
(<https://github.com/cvat-ai/cvat/pull/9543>)
2 changes: 1 addition & 1 deletion cvat-core/src/common.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ export function isInteger(value): boolean {
}

export function isEmail(value): boolean {
return typeof value === 'string' && RegExp(/^[^\s@]+@[^\s@]+\.[^\s@]+$/).test(value);
return typeof value === 'string' && /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value);
}

// Called with specific Enum context
Expand Down
26 changes: 25 additions & 1 deletion cvat-ui/src/actions/projects-actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import {
} from 'reducers';
import { getTasksAsync } from 'actions/tasks-actions';
import { getCVATStore } from 'cvat-store';
import { getCore } from 'cvat-core-wrapper';
import { getCore, Project } from 'cvat-core-wrapper';
import { filterNull } from 'utils/filter-null';

const cvat = getCore();
Expand All @@ -30,6 +30,9 @@ export enum ProjectsActionTypes {
GET_PROJECT_PREVIEW = 'GET_PROJECT_PREVIEW',
GET_PROJECT_PREVIEW_SUCCESS = 'GET_PROJECT_PREVIEW_SUCCESS',
GET_PROJECT_PREVIEW_FAILED = 'GET_PROJECT_PREVIEW_FAILED',
UPDATE_PROJECT = 'UPDATE_PROJECT',
UPDATE_PROJECT_SUCCESS = 'UPDATE_PROJECT_SUCCESS',
UPDATE_PROJECT_FAILED = 'UPDATE_PROJECT_FAILED',
}

const projectActions = {
Expand Down Expand Up @@ -62,6 +65,11 @@ const projectActions = {
getProjectPreviewFailed: (projectID: number, error: any) => (
createAction(ProjectsActionTypes.GET_PROJECT_PREVIEW_FAILED, { projectID, error })
),
updateProject: (projectId: number) => createAction(ProjectsActionTypes.UPDATE_PROJECT, { projectId }),
updateProjectSuccess: (project: Project) => (
createAction(ProjectsActionTypes.UPDATE_PROJECT_SUCCESS, { project })
),
updateProjectFailed: (error: any) => createAction(ProjectsActionTypes.UPDATE_PROJECT_FAILED, { error }),
};

export type ProjectActions = ActionUnion<typeof projectActions>;
Expand Down Expand Up @@ -162,3 +170,19 @@ export const getProjectsPreviewAsync = (project: any): ThunkAction => async (dis
dispatch(projectActions.getProjectPreviewFailed(project.id, error));
}
};

export function updateProjectAsync(
projectInstance: Project,
): ThunkAction<Promise<Project>> {
return async (dispatch: ThunkDispatch): Promise<Project> => {
dispatch(projectActions.updateProject(projectInstance.id));
try {
const updatedProject = await projectInstance.save();
dispatch(projectActions.updateProjectSuccess(updatedProject));
return updatedProject;
} catch (error) {
dispatch(projectActions.updateProjectFailed(error));
throw error;
}
};
}
42 changes: 40 additions & 2 deletions cvat-ui/src/actions/tasks-actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,9 @@ export enum TasksActionTypes {
GET_TASK_PREVIEW = 'GET_TASK_PREVIEW',
GET_TASK_PREVIEW_SUCCESS = 'GET_TASK_PREVIEW_SUCCESS',
GET_TASK_PREVIEW_FAILED = 'GET_TASK_PREVIEW_FAILED',
UPDATE_TASK_IN_STATE = 'UPDATE_TASK_IN_STATE',
UPDATE_TASK = 'UPDATE_TASK',
UPDATE_TASK_SUCCESS = 'UPDATE_TASK_SUCCESS',
UPDATE_TASK_FAILED = 'UPDATE_TASK_FAILED',
}

function getTasks(query: Partial<TasksQuery>, updateQuery: boolean, fetchingTimestamp: number): AnyAction {
Expand Down Expand Up @@ -205,7 +207,7 @@ export function getTaskPreviewAsync(taskInstance: any): ThunkAction {

export function updateTaskInState(task: Task): AnyAction {
const action = {
type: TasksActionTypes.UPDATE_TASK_IN_STATE,
type: TasksActionTypes.UPDATE_TASK_SUCCESS,
payload: { task },
};

Expand Down Expand Up @@ -318,6 +320,42 @@ ThunkAction {
};
}

function updateTask(taskId: number): AnyAction {
return {
type: TasksActionTypes.UPDATE_TASK,
payload: {
taskId,
},
};
}

function updateTaskFailed(taskId: number, error: any): AnyAction {
return {
type: TasksActionTypes.UPDATE_TASK_FAILED,
payload: {
taskId,
error,
},
};
}

export function updateTaskAsync(
taskInstance: Task,
fields: Parameters<Task['save']>[0],
): ThunkAction<Promise<Task>> {
return async (dispatch: ThunkDispatch): Promise<Task> => {
try {
dispatch(updateTask(taskInstance.id));
const updated = await taskInstance.save(fields);
dispatch(updateTaskInState(updated));
return updated;
} catch (error) {
dispatch(updateTaskFailed(taskInstance.id, error));
throw error;
}
};
}

export function switchMoveTaskModalVisible(visible: boolean, taskId: number | null = null): AnyAction {
const action = {
type: TasksActionTypes.SWITCH_MOVE_TASK_MODAL_VISIBLE,
Expand Down
23 changes: 23 additions & 0 deletions cvat-ui/src/components/common/cvat-menu-edit-label.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
// Copyright (C) CVAT.ai Corporation
//
// SPDX-License-Identifier: MIT

import React from 'react';
import { RightOutlined } from '@ant-design/icons';
import Text from 'antd/lib/typography/Text';

import './styles.scss';

interface CVATMenuEditLabelProps {
children: React.ReactNode;
}

export function CVATMenuEditLabel(props: CVATMenuEditLabelProps): JSX.Element {
const { children } = props;
return (
<div className='cvat-menu-edit-label'>
<Text>{children}</Text>
<RightOutlined />
</div>
);
}
5 changes: 5 additions & 0 deletions cvat-ui/src/components/common/styles.scss
Original file line number Diff line number Diff line change
Expand Up @@ -55,3 +55,8 @@
max-height: $grid-unit-size * 40;
overflow-y: auto;
}

.cvat-menu-edit-label {
display: flex;
justify-content: space-between;
}
41 changes: 9 additions & 32 deletions cvat-ui/src/components/job-item/job-item.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@ import moment from 'moment';
import { Col, Row } from 'antd/lib/grid';
import Card from 'antd/lib/card';
import Text from 'antd/lib/typography/Text';
import Select from 'antd/lib/select';
import Icon from '@ant-design/icons';
import {
BorderOutlined,
Expand All @@ -29,6 +28,7 @@ import { CombinedState } from 'reducers';
import Collapse from 'antd/lib/collapse';
import CVATTag, { TagType } from 'components/common/cvat-tag';
import JobActionsComponent from 'components/jobs-page/actions-menu';
import { JobStageSelector, JobStateSelector } from './job-selectors';

function formatDate(value: moment.Moment): string {
return value.format('MMM Do YYYY HH:mm');
Expand Down Expand Up @@ -114,7 +114,7 @@ function JobItem(props: Props): JSX.Element {
const deletes = useSelector((state: CombinedState) => state.jobs.activities.deletes);
const deleted = job.id in deletes ? deletes[job.id] === true : false;

const { stage } = job;
const { stage, state } = job;
const created = moment(job.createdDate);
const updated = moment(job.updatedDate);
const now = moment(moment.now());
Expand Down Expand Up @@ -208,48 +208,25 @@ function JobItem(props: Props): JSX.Element {
<Text>Stage:</Text>
</Col>
</Row>
<Select
className='cvat-job-item-stage'
popupClassName='cvat-job-item-stage-dropdown'
<JobStageSelector
value={stage}
onChange={(newValue: JobStage) => {
onSelect={(newValue: JobStage) => {
onJobUpdate(job, { stage: newValue });
}}
>
<Select.Option value={JobStage.ANNOTATION}>
{JobStage.ANNOTATION}
</Select.Option>
<Select.Option value={JobStage.VALIDATION}>
{JobStage.VALIDATION}
</Select.Option>
<Select.Option value={JobStage.ACCEPTANCE}>
{JobStage.ACCEPTANCE}
</Select.Option>
</Select>
/>
</Col>
<Col className='cvat-job-item-select'>
<Row justify='space-between' align='middle'>
<Col>
<Text>State:</Text>
</Col>
</Row>
<Select
className='cvat-job-item-state'
popupClassName='cvat-job-item-state-dropdown'
value={job.state}
onChange={(newValue: JobState) => {
<JobStateSelector
value={state}
onSelect={(newValue: JobState) => {
onJobUpdate(job, { state: newValue });
}}
>
<Select.Option value={JobState.NEW}>{JobState.NEW}</Select.Option>
<Select.Option value={JobState.IN_PROGRESS}>
{JobState.IN_PROGRESS}
</Select.Option>
<Select.Option value={JobState.REJECTED}>{JobState.REJECTED}</Select.Option>
<Select.Option value={JobState.COMPLETED}>
{JobState.COMPLETED}
</Select.Option>
</Select>
/>
</Col>
</Row>
</Col>
Expand Down
54 changes: 54 additions & 0 deletions cvat-ui/src/components/job-item/job-selectors.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
// Copyright (C) CVAT.ai Corporation
//
// SPDX-License-Identifier: MIT

import React from 'react';
import Select from 'antd/lib/select';
import { JobStage, JobState } from 'cvat-core-wrapper';

interface JobStateSelectorProps {
value: JobState;
onSelect: (newValue: JobState) => void;
}

export function JobStateSelector({ value, onSelect }: JobStateSelectorProps): JSX.Element {
return (
<Select
className='cvat-job-item-state'
popupClassName='cvat-job-item-state-dropdown'
value={value}
onChange={onSelect}
>
<Select.Option value={JobState.NEW}>{JobState.NEW}</Select.Option>
<Select.Option value={JobState.IN_PROGRESS}>{JobState.IN_PROGRESS}</Select.Option>
<Select.Option value={JobState.REJECTED}>{JobState.REJECTED}</Select.Option>
<Select.Option value={JobState.COMPLETED}>{JobState.COMPLETED}</Select.Option>
</Select>
);
}

interface JobStageSelectorProps {
value: JobStage;
onSelect: (newValue: JobStage) => void;
}

export function JobStageSelector({ value, onSelect }: JobStageSelectorProps): JSX.Element {
return (
<Select
className='cvat-job-item-stage'
popupClassName='cvat-job-item-stage-dropdown'
value={value}
onChange={onSelect}
>
<Select.Option value={JobStage.ANNOTATION}>
{JobStage.ANNOTATION}
</Select.Option>
<Select.Option value={JobStage.VALIDATION}>
{JobStage.VALIDATION}
</Select.Option>
<Select.Option value={JobStage.ACCEPTANCE}>
{JobStage.ACCEPTANCE}
</Select.Option>
</Select>
);
}
4 changes: 4 additions & 0 deletions cvat-ui/src/components/job-item/styles.scss
Original file line number Diff line number Diff line change
Expand Up @@ -90,3 +90,7 @@
.ant-menu.cvat-job-item-menu {
box-shadow: $box-shadow-base;
}

.cvat-job-item-stage, .cvat-job-item-state {
width: $grid-unit-size * 16;
}
47 changes: 34 additions & 13 deletions cvat-ui/src/components/jobs-page/actions-menu-items.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,28 +7,31 @@ import { Link } from 'react-router-dom';
import { MenuProps } from 'antd/lib/menu';
import { LoadingOutlined } from '@ant-design/icons';
import { usePlugins } from 'utils/hooks';
import { CVATMenuEditLabel } from 'components/common/cvat-menu-edit-label';

interface MenuItemsData {
jobID: number;
taskID: number;
projectID: number | null;
jobId: number;
taskId: number;
projectId: number | null;
pluginActions: ReturnType<typeof usePlugins>;
isMergingConsensusEnabled: boolean;
onOpenBugTracker: (() => void) | null;
onImportAnnotations: () => void;
onExportAnnotations: () => void;
onMergeConsensusJob: (() => void) | null;
onDeleteJob: (() => void) | null;
startEditField: (key: string) => void;
}

export default function JobActionsItems(
menuItemsData: MenuItemsData,
jobMenuProps: unknown,
): MenuProps['items'] {
const {
jobID,
taskID,
projectID,
startEditField,
jobId,
taskId,
projectId,
pluginActions,
isMergingConsensusEnabled,
onOpenBugTracker,
Expand All @@ -42,13 +45,13 @@ export default function JobActionsItems(

menuItems.push([{
key: 'task',
label: <Link to={`/tasks/${taskID}`}>Go to the task</Link>,
label: <Link to={`/tasks/${taskId}`}>Go to the task</Link>,
}, 0]);

if (projectID) {
if (projectId) {
menuItems.push([{
key: 'project',
label: <Link to={`/projects/${projectID}`}>Go to the project</Link>,
label: <Link to={`/projects/${projectId}`}>Go to the project</Link>,
}, 10]);
}

Expand Down Expand Up @@ -83,17 +86,35 @@ export default function JobActionsItems(
}

menuItems.push([{
key: 'view-analytics',
label: <Link to={`/tasks/${taskID}/jobs/${jobID}/analytics`}>View analytics</Link>,
key: 'edit_assignee',
onClick: () => startEditField('assignee'),
label: <CVATMenuEditLabel>Assignee</CVATMenuEditLabel>,
}, 60]);

menuItems.push([{
key: 'edit_state',
onClick: () => startEditField('state'),
label: <CVATMenuEditLabel>State</CVATMenuEditLabel>,
}, 70]);

menuItems.push([{
key: 'edit_stage',
onClick: () => startEditField('stage'),
label: <CVATMenuEditLabel>Stage</CVATMenuEditLabel>,
}, 80]);

menuItems.push([{
key: 'view-analytics',
label: <Link to={`/tasks/${taskId}/jobs/${jobId}/analytics`}>View analytics</Link>,
}, 90]);

if (onDeleteJob) {
menuItems.push([{ type: 'divider' }, 69]);
menuItems.push([{ type: 'divider' }, 99]);
menuItems.push([{
key: 'delete',
onClick: onDeleteJob,
label: 'Delete',
}, 70]);
}, 100]);
}

menuItems.push(
Expand Down
Loading
Loading