diff --git a/CHANGELOG.md b/CHANGELOG.md index dd0148f52518..8f2d8faba22e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,7 +10,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - TDB ### Changed -- TDB +- Do not reload annotation view when renew the job or update job state () ### Deprecated - TDB diff --git a/cvat-core/src/session-implementation.ts b/cvat-core/src/session-implementation.ts index 6a064722aa2e..d21422477744 100644 --- a/cvat-core/src/session-implementation.ts +++ b/cvat-core/src/session-implementation.ts @@ -70,9 +70,21 @@ export function implementJob(Job) { jobData.assignee = jobData.assignee.id; } - const data = await serverProxy.jobs.save(this.id, jobData); - this._updateTrigger.reset(); - return new Job(data); + let updatedJob = null; + try { + const data = await serverProxy.jobs.save(this.id, jobData); + updatedJob = new Job(data); + this._updateTrigger.reset(); + } catch (error) { + updatedJob = new Job(this._initialData); + throw error; + } finally { + this.stage = updatedJob.stage; + this.state = updatedJob.state; + this.assignee = updatedJob.assignee; + } + + return this; } const jobSpec = { diff --git a/cvat-core/src/session.ts b/cvat-core/src/session.ts index 1c9189903868..5fc657fff922 100644 --- a/cvat-core/src/session.ts +++ b/cvat-core/src/session.ts @@ -381,7 +381,7 @@ export class Job extends Session { log: CallableFunction; }; - constructor(initialData: SerializedJob) { + constructor(initialData: Readonly) { super(); const data = { id: undefined, @@ -536,6 +536,9 @@ export class Job extends Session { _updateTrigger: { get: () => updateTrigger, }, + _initialData: { + get: () => initialData, + }, }), ); diff --git a/cvat-ui/src/actions/annotation-actions.ts b/cvat-ui/src/actions/annotation-actions.ts index 414dd911c5e0..076a35f3f04f 100644 --- a/cvat-ui/src/actions/annotation-actions.ts +++ b/cvat-ui/src/actions/annotation-actions.ts @@ -29,7 +29,7 @@ import { ShapeType, Workspace, } from 'reducers'; -import { updateJobAsync } from './tasks-actions'; +import { updateJobAsync } from './jobs-actions'; import { switchToolsBlockerState } from './settings-actions'; interface AnnotationsParameters { diff --git a/cvat-ui/src/actions/jobs-actions.ts b/cvat-ui/src/actions/jobs-actions.ts index 4386f3d8c0b3..37cba5999da7 100644 --- a/cvat-ui/src/actions/jobs-actions.ts +++ b/cvat-ui/src/actions/jobs-actions.ts @@ -19,6 +19,9 @@ export enum JobsActionTypes { GET_JOB_PREVIEW_SUCCESS = 'GET_JOB_PREVIEW_SUCCESS', GET_JOB_PREVIEW_FAILED = 'GET_JOB_PREVIEW_FAILED', CREATE_JOB_FAILED = 'CREATE_JOB_FAILED', + UPDATE_JOB = 'UPDATE_JOB', + UPDATE_JOB_SUCCESS = 'UPDATE_JOB_SUCCESS', + UPDATE_JOB_FAILED = 'UPDATE_JOB_FAILED', DELETE_JOB = 'DELETE_JOB', DELETE_JOB_SUCCESS = 'DELETE_JOB_SUCCESS', DELETE_JOB_FAILED = 'DELETE_JOB_FAILED', @@ -46,6 +49,15 @@ const jobsActions = { createJobFailed: (error: any) => ( createAction(JobsActionTypes.CREATE_JOB_FAILED, { error }) ), + updateJob: () => ( + createAction(JobsActionTypes.UPDATE_JOB) + ), + updateJobSuccess: (job: Job) => ( + createAction(JobsActionTypes.UPDATE_JOB_SUCCESS, { job }) + ), + updateJobFailed: (jobID: number, error: any) => ( + createAction(JobsActionTypes.UPDATE_JOB_FAILED, { jobID, error }) + ), deleteJob: (jobID: number) => ( createAction(JobsActionTypes.DELETE_JOB, { jobID }) ), @@ -93,6 +105,21 @@ export const createJobAsync = (data: JobData): ThunkAction => async (dispatch) = } }; +export function updateJobAsync(jobInstance: Job): ThunkAction> { + return async (dispatch): Promise => { + try { + dispatch(jobsActions.updateJob()); + const updated = await jobInstance.save(); + dispatch(jobsActions.updateJobSuccess(updated)); + } catch (error) { + dispatch(jobsActions.updateJobFailed(jobInstance.id, error)); + return false; + } + + return true; + }; +} + export const deleteJobAsync = (job: Job): ThunkAction => async (dispatch) => { dispatch(jobsActions.deleteJob(job.id)); try { diff --git a/cvat-ui/src/actions/tasks-actions.ts b/cvat-ui/src/actions/tasks-actions.ts index 3ab967e1a458..e5f9e5edcaf8 100644 --- a/cvat-ui/src/actions/tasks-actions.ts +++ b/cvat-ui/src/actions/tasks-actions.ts @@ -22,7 +22,6 @@ export enum TasksActionTypes { DELETE_TASK_SUCCESS = 'DELETE_TASK_SUCCESS', DELETE_TASK_FAILED = 'DELETE_TASK_FAILED', CREATE_TASK_FAILED = 'CREATE_TASK_FAILED', - UPDATE_JOB_FAILED = 'UPDATE_JOB_FAILED', SWITCH_MOVE_TASK_MODAL_VISIBLE = 'SWITCH_MOVE_TASK_MODAL_VISIBLE', GET_TASK_PREVIEW = 'GET_TASK_PREVIEW', GET_TASK_PREVIEW_SUCCESS = 'GET_TASK_PREVIEW_SUCCESS', @@ -294,25 +293,6 @@ ThunkAction, {}, {}, AnyAction> { }; } -function updateJobFailed(jobID: number, error: any): AnyAction { - const action = { - type: TasksActionTypes.UPDATE_JOB_FAILED, - payload: { jobID, error }, - }; - - return action; -} - -export function updateJobAsync(jobInstance: any): ThunkAction, {}, {}, AnyAction> { - return async (dispatch: ActionCreator): Promise => { - try { - await jobInstance.save(); - } catch (error) { - dispatch(updateJobFailed(jobInstance.id, error)); - } - }; -} - export function switchMoveTaskModalVisible(visible: boolean, taskId: number | null = null): AnyAction { const action = { type: TasksActionTypes.SWITCH_MOVE_TASK_MODAL_VISIBLE, diff --git a/cvat-ui/src/components/annotation-page/top-bar/annotation-menu.tsx b/cvat-ui/src/components/annotation-page/top-bar/annotation-menu.tsx index 75d608140161..d187f78e7b4b 100644 --- a/cvat-ui/src/components/annotation-page/top-bar/annotation-menu.tsx +++ b/cvat-ui/src/components/annotation-page/top-bar/annotation-menu.tsx @@ -16,8 +16,7 @@ import Collapse from 'antd/lib/collapse'; // eslint-disable-next-line import/no-extraneous-dependencies import { MenuInfo } from 'rc-menu/lib/interface'; import CVATTooltip from 'components/common/cvat-tooltip'; -import { getCore } from 'cvat-core-wrapper'; -import { JobStage } from 'reducers'; +import { getCore, JobStage } from 'cvat-core-wrapper'; const core = getCore(); @@ -213,7 +212,7 @@ function AnnotationMenuComponent(props: Props & RouteComponentProps): JSX.Elemen {JobState.COMPLETED} - {[JobStage.ANNOTATION, JobStage.REVIEW].includes(jobStage) ? + {[JobStage.ANNOTATION, JobStage.VALIDATION].includes(jobStage) ? Finish the job : null} {jobStage === JobStage.ACCEPTANCE ? Renew the job : null} diff --git a/cvat-ui/src/components/job-item/job-actions-menu.tsx b/cvat-ui/src/components/job-item/job-actions-menu.tsx index e7911dd05541..ed5646f3038a 100644 --- a/cvat-ui/src/components/job-item/job-actions-menu.tsx +++ b/cvat-ui/src/components/job-item/job-actions-menu.tsx @@ -1,6 +1,7 @@ // Copyright (C) 2023 CVAT.ai Corporation // // SPDX-License-Identifier: MIT + import React, { useCallback } from 'react'; import { useDispatch } from 'react-redux'; import { useHistory } from 'react-router'; @@ -15,13 +16,12 @@ import { } from 'cvat-core-wrapper'; import { deleteJobAsync } from 'actions/jobs-actions'; import { importActions } from 'actions/import-actions'; -import { updateJobAsync } from 'actions/tasks-actions'; const core = getCore(); interface Props { job: Job; - onJobUpdate?: (job: Job) => void; + onJobUpdate: (job: Job) => void; } function JobActionsMenu(props: Props): JSX.Element { @@ -62,19 +62,11 @@ function JobActionsMenu(props: Props): JSX.Element { } else if (action.key === 'renew_job') { job.state = core.enums.JobState.NEW; job.stage = JobStage.ANNOTATION; - if (onJobUpdate) { - onJobUpdate(job); - } else { - dispatch(updateJobAsync(job)); - } + onJobUpdate(job); } else if (action.key === 'finish_job') { job.stage = JobStage.ACCEPTANCE; job.state = core.enums.JobState.COMPLETED; - if (onJobUpdate) { - onJobUpdate(job); - } else { - dispatch(updateJobAsync(job)); - } + onJobUpdate(job); } }} > diff --git a/cvat-ui/src/components/job-item/job-item.tsx b/cvat-ui/src/components/job-item/job-item.tsx index 614d058d3426..312e65aa2b56 100644 --- a/cvat-ui/src/components/job-item/job-item.tsx +++ b/cvat-ui/src/components/job-item/job-item.tsx @@ -30,8 +30,8 @@ import CVATTooltip from 'components/common/cvat-tooltip'; import JobActionsMenu from './job-actions-menu'; interface Props { - job: Job, - task: Task, + job: Job; + task: Task; onJobUpdate: (job: Job) => void; } diff --git a/cvat-ui/src/components/jobs-page/job-card.tsx b/cvat-ui/src/components/jobs-page/job-card.tsx index 112662aa6706..7177a33e2858 100644 --- a/cvat-ui/src/components/jobs-page/job-card.tsx +++ b/cvat-ui/src/components/jobs-page/job-card.tsx @@ -1,5 +1,5 @@ // Copyright (C) 2022 Intel Corporation -// Copyright (C) 2022 CVAT.ai Corporation +// Copyright (C) 2022-2023 CVAT.ai Corporation // // SPDX-License-Identifier: MIT @@ -9,6 +9,8 @@ import Card from 'antd/lib/card'; import Descriptions from 'antd/lib/descriptions'; import { MoreOutlined } from '@ant-design/icons'; import Dropdown from 'antd/lib/dropdown'; + +import { Job } from 'cvat-core-wrapper'; import { useCardHeightHOC } from 'utils/hooks'; import Preview from 'components/common/preview'; import JobActionsMenu from 'components/job-item/job-actions-menu'; @@ -22,11 +24,12 @@ const useCardHeight = useCardHeightHOC({ }); interface Props { - job: any; + job: Job; + onJobUpdate: (job: Job) => void; } function JobCardComponent(props: Props): JSX.Element { - const { job } = props; + const { job, onJobUpdate } = props; const [expanded, setExpanded] = useState(false); const history = useHistory(); const height = useCardHeight(); @@ -73,7 +76,7 @@ function JobCardComponent(props: Props): JSX.Element { {job.assignee.username} ) : null} - }> + }> diff --git a/cvat-ui/src/components/jobs-page/jobs-content.tsx b/cvat-ui/src/components/jobs-page/jobs-content.tsx index db34eece8cfd..e9a4e266dab3 100644 --- a/cvat-ui/src/components/jobs-page/jobs-content.tsx +++ b/cvat-ui/src/components/jobs-page/jobs-content.tsx @@ -1,4 +1,5 @@ // Copyright (C) 2022 Intel Corporation +// Copyright (C) 2023 CVAT.ai Corporation // // SPDX-License-Identifier: MIT @@ -9,7 +10,12 @@ import { CombinedState } from 'reducers'; import { Job, JobType } from 'cvat-core-wrapper'; import JobCard from './job-card'; -function JobsContentComponent(): JSX.Element { +interface Props { + onJobUpdate(job: Job): void; +} + +function JobsContentComponent(props: Props): JSX.Element { + const { onJobUpdate } = props; const jobs = useSelector((state: CombinedState) => state.jobs.current); const dimensions = { md: 22, @@ -22,7 +28,7 @@ function JobsContentComponent(): JSX.Element { {jobs.filter((job: Job) => job.type === JobType.ANNOTATION).map((job: Job): JSX.Element => ( - + ))} diff --git a/cvat-ui/src/components/jobs-page/jobs-page.tsx b/cvat-ui/src/components/jobs-page/jobs-page.tsx index 0a6dea86241e..899075163d38 100644 --- a/cvat-ui/src/components/jobs-page/jobs-page.tsx +++ b/cvat-ui/src/components/jobs-page/jobs-page.tsx @@ -1,9 +1,10 @@ // Copyright (C) 2022 Intel Corporation +// Copyright (C) 2023 CVAT.ai Corporation // // SPDX-License-Identifier: MIT import './styles.scss'; -import React, { useEffect, useState } from 'react'; +import React, { useCallback, useEffect, useState } from 'react'; import { useHistory } from 'react-router'; import { useDispatch, useSelector } from 'react-redux'; import Spin from 'antd/lib/spin'; @@ -12,9 +13,10 @@ import Pagination from 'antd/lib/pagination'; import Empty from 'antd/lib/empty'; import Text from 'antd/lib/typography/Text'; +import { Job } from 'cvat-core-wrapper'; import { updateHistoryFromQuery } from 'components/resource-sorting-filtering'; import { CombinedState, Indexable } from 'reducers'; -import { getJobsAsync } from 'actions/jobs-actions'; +import { getJobsAsync, updateJobAsync } from 'actions/jobs-actions'; import TopBarComponent from './top-bar'; import JobsContentComponent from './jobs-content'; @@ -26,6 +28,9 @@ function JobsPageComponent(): JSX.Element { const query = useSelector((state: CombinedState) => state.jobs.query); const fetching = useSelector((state: CombinedState) => state.jobs.fetching); const count = useSelector((state: CombinedState) => state.jobs.count); + const onJobUpdate = useCallback((job: Job) => { + dispatch(updateJobAsync(job)); + }, []); const queryParams = new URLSearchParams(history.location.search); const updatedQuery = { ...query }; @@ -51,7 +56,7 @@ function JobsPageComponent(): JSX.Element { const content = count ? ( <> - + void; - showImportModal: (jobInstance: any) => void; + showExportModal: (jobInstance: Job) => void; + showImportModal: (jobInstance: Job) => void; removeAnnotations(startnumber: number, endnumber: number, delTrackKeyframesOnly: boolean): void; setForceExitAnnotationFlag(forceExit: boolean): void; - saveAnnotations(jobInstance: any, afterSave?: () => void): void; - updateJob(jobInstance: any): void; + saveAnnotations(jobInstance: Job, afterSave?: () => void): void; + updateJob(jobInstance: Job): Promise; } function mapStateToProps(state: CombinedState): StateToProps { @@ -55,10 +58,10 @@ function mapStateToProps(state: CombinedState): StateToProps { function mapDispatchToProps(dispatch: any): DispatchToProps { return { - showExportModal(jobInstance: any): void { + showExportModal(jobInstance: Job): void { dispatch(exportActions.openExportDatasetModal(jobInstance)); }, - showImportModal(jobInstance: any): void { + showImportModal(jobInstance: Job): void { dispatch(importActions.openImportDatasetModal(jobInstance)); }, removeAnnotations(startnumber: number, endnumber: number, delTrackKeyframesOnly:boolean) { @@ -67,11 +70,11 @@ function mapDispatchToProps(dispatch: any): DispatchToProps { setForceExitAnnotationFlag(forceExit: boolean): void { dispatch(setForceExitAnnotationFlagAction(forceExit)); }, - saveAnnotations(jobInstance: any, afterSave?: () => void): void { + saveAnnotations(jobInstance: Job, afterSave?: () => void): void { dispatch(saveAnnotationsAsync(jobInstance, afterSave)); }, - updateJob(jobInstance: any): void { - dispatch(updateJobAsync(jobInstance)); + updateJob(jobInstance: Job): Promise { + return dispatch(updateJobAsync(jobInstance)); }, }; } @@ -98,19 +101,28 @@ function AnnotationMenuContainer(props: Props): JSX.Element { } else if (action === Actions.RENEW_JOB) { jobInstance.state = core.enums.JobState.NEW; jobInstance.stage = JobStage.ANNOTATION; - updateJob(jobInstance); - window.location.reload(); + updateJob(jobInstance).then((success) => { + if (success) { + message.info('Job renewed', 2); + } + }); } else if (action === Actions.FINISH_JOB) { jobInstance.stage = JobStage.ACCEPTANCE; jobInstance.state = core.enums.JobState.COMPLETED; - updateJob(jobInstance); - history.push(`/tasks/${jobInstance.taskId}`); + updateJob(jobInstance).then((success) => { + if (success) { + history.push(`/tasks/${jobInstance.taskId}`); + } + }); } else if (action === Actions.OPEN_TASK) { history.push(`/tasks/${jobInstance.taskId}`); } else if (action.startsWith('state:')) { - [, jobInstance.state] = action.split(':'); - updateJob(jobInstance); - window.location.reload(); + [, jobInstance.state] = action.split(':') as [string, JobState]; + updateJob(jobInstance).then((success) => { + if (success) { + message.info('Job state updated', 2); + } + }); } else if (action === Actions.LOAD_JOB_ANNO) { showImportModal(jobInstance); } diff --git a/cvat-ui/src/reducers/annotation-reducer.ts b/cvat-ui/src/reducers/annotation-reducer.ts index d7d395adb6f1..ebe4c833d8da 100644 --- a/cvat-ui/src/reducers/annotation-reducer.ts +++ b/cvat-ui/src/reducers/annotation-reducer.ts @@ -5,11 +5,12 @@ import { AnyAction } from 'redux'; import { AnnotationActionTypes } from 'actions/annotation-actions'; +import { JobsActionTypes } from 'actions/jobs-actions'; import { AuthActionTypes } from 'actions/auth-actions'; import { BoundariesActionTypes } from 'actions/boundaries-actions'; import { Canvas, CanvasMode } from 'cvat-canvas-wrapper'; import { Canvas3d } from 'cvat-canvas3d-wrapper'; -import { DimensionType } from 'cvat-core-wrapper'; +import { DimensionType, JobStage } from 'cvat-core-wrapper'; import { clamp } from 'utils/math'; import { SettingsActionTypes } from 'actions/settings-actions'; @@ -17,7 +18,6 @@ import { ActiveControl, AnnotationState, ContextMenuType, - JobStage, ObjectType, ShapeType, Workspace, @@ -158,7 +158,7 @@ export default (state = defaultState, action: AnyAction): AnnotationState => { groundTruthJobFramesMeta, } = action.payload; - const isReview = job.stage === JobStage.REVIEW; + const isReview = job.stage === JobStage.VALIDATION; let workspaceSelected = Workspace.STANDARD; const defaultLabel = job.labels.length ? job.labels[0] : null; @@ -237,6 +237,15 @@ export default (state = defaultState, action: AnyAction): AnnotationState => { }, }; } + case JobsActionTypes.UPDATE_JOB_SUCCESS: { + return { + ...state, + job: { + ...state.job, + instance: action.payload.job, + }, + }; + } case AnnotationActionTypes.GET_DATA_FAILED: { return { ...state, diff --git a/cvat-ui/src/reducers/index.ts b/cvat-ui/src/reducers/index.ts index e542c31e57b6..ea91619f8aa3 100644 --- a/cvat-ui/src/reducers/index.ts +++ b/cvat-ui/src/reducers/index.ts @@ -375,12 +375,6 @@ export enum TaskStatus { COMPLETED = 'completed', } -export enum JobStage { - ANNOTATION = 'annotation', - REVIEW = 'validation', - ACCEPTANCE = 'acceptance', -} - export interface ActiveInference { status: RQStatus; progress: number; diff --git a/cvat-ui/src/reducers/jobs-reducer.ts b/cvat-ui/src/reducers/jobs-reducer.ts index 5be1f05ddc70..c7b07fc1fa30 100644 --- a/cvat-ui/src/reducers/jobs-reducer.ts +++ b/cvat-ui/src/reducers/jobs-reducer.ts @@ -135,6 +135,32 @@ export default (state: JobsState = defaultState, action: JobsActions): JobsState }, }; } + case JobsActionTypes.UPDATE_JOB: { + return { + ...state, + fetching: true, + }; + } + case JobsActionTypes.UPDATE_JOB_SUCCESS: { + return { + ...state, + current: state.current.includes(action.payload.job) ? ( + state.current.map((job) => { + if (job === action.payload.job) { + return action.payload.job; + } + return job; + }) + ) : state.current, + fetching: false, + }; + } + case JobsActionTypes.UPDATE_JOB_FAILED: { + return { + ...state, + fetching: false, + }; + } default: { return state; } diff --git a/cvat-ui/src/reducers/notifications-reducer.ts b/cvat-ui/src/reducers/notifications-reducer.ts index 727ccd140f54..71e3ed368bce 100644 --- a/cvat-ui/src/reducers/notifications-reducer.ts +++ b/cvat-ui/src/reducers/notifications-reducer.ts @@ -1638,6 +1638,22 @@ export default function (state = defaultState, action: AnyAction): Notifications }, }; } + case JobsActionTypes.UPDATE_JOB_FAILED: { + return { + ...state, + errors: { + ...state.errors, + jobs: { + ...state.errors.jobs, + updating: { + message: 'Could not update job', + reason: action.payload.error.toString(), + className: 'cvat-notification-notice-update-job-failed', + }, + }, + }, + }; + } case JobsActionTypes.DELETE_JOB_FAILED: { const { jobID } = action.payload; return {