diff --git a/CHANGELOG.md b/CHANGELOG.md index 5fca7f447207fffe7fedf83264312082aacddcad..62ba12929b4fac8f241d455809765f24cc9626bf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,7 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added - Support of attributes returned by serverless functions () based on () - Project/task backups uploading via chunk uploads () - +- Fixed UX bug when jobs pagination is reset after changing a job () ### Changed - Bumped nuclio version to 1.8.14 () diff --git a/cvat-ui/package.json b/cvat-ui/package.json index 97daf345104c311ddc8262f415ec3ff6b9733173..5f16c80fc23942bd1c759f2780e5e7660fcf6276 100644 --- a/cvat-ui/package.json +++ b/cvat-ui/package.json @@ -1,6 +1,6 @@ { "name": "cvat-ui", - "version": "1.38.0", + "version": "1.38.1", "description": "CVAT single-page application", "main": "src/index.tsx", "scripts": { diff --git a/cvat-ui/src/actions/tasks-actions.ts b/cvat-ui/src/actions/tasks-actions.ts index f99e04c5f9975ded799b431dea5033acc7d9a165..98ae49d223449a6a760ef2690824396499b0cfa3 100644 --- a/cvat-ui/src/actions/tasks-actions.ts +++ b/cvat-ui/src/actions/tasks-actions.ts @@ -441,37 +441,37 @@ export function updateTaskSuccess(task: any, taskID: number): AnyAction { return action; } -function updateJob(): AnyAction { +function updateTaskFailed(error: any, task: any): AnyAction { const action = { - type: TasksActionTypes.UPDATE_JOB, - payload: { }, + type: TasksActionTypes.UPDATE_TASK_FAILED, + payload: { error, task }, }; return action; } -function updateJobSuccess(jobInstance: any): AnyAction { +function updateJob(jobID: number): AnyAction { const action = { - type: TasksActionTypes.UPDATE_JOB_SUCCESS, - payload: { jobInstance }, + type: TasksActionTypes.UPDATE_JOB, + payload: { jobID }, }; return action; } -function updateJobFailed(jobID: number, error: any): AnyAction { +function updateJobSuccess(jobInstance: any, jobID: number): AnyAction { const action = { - type: TasksActionTypes.UPDATE_JOB_FAILED, - payload: { jobID, error }, + type: TasksActionTypes.UPDATE_JOB_SUCCESS, + payload: { jobID, jobInstance }, }; return action; } -function updateTaskFailed(error: any, task: any): AnyAction { +function updateJobFailed(jobID: number, error: any): AnyAction { const action = { - type: TasksActionTypes.UPDATE_TASK_FAILED, - payload: { error, task }, + type: TasksActionTypes.UPDATE_JOB_FAILED, + payload: { jobID, error }, }; return action; @@ -503,9 +503,9 @@ export function updateTaskAsync(taskInstance: any): ThunkAction, C export function updateJobAsync(jobInstance: any): ThunkAction, {}, {}, AnyAction> { return async (dispatch: ActionCreator): Promise => { try { - dispatch(updateJob()); + dispatch(updateJob(jobInstance.id)); const newJob = await jobInstance.save(); - dispatch(updateJobSuccess(newJob)); + dispatch(updateJobSuccess(newJob, newJob.id)); } catch (error) { dispatch(updateJobFailed(jobInstance.id, error)); } diff --git a/cvat-ui/src/components/common/loading-spinner.tsx b/cvat-ui/src/components/common/loading-spinner.tsx new file mode 100644 index 0000000000000000000000000000000000000000..c6f0e8f58ed4b351e1a41721fc2a4aa37dac64de --- /dev/null +++ b/cvat-ui/src/components/common/loading-spinner.tsx @@ -0,0 +1,16 @@ +// Copyright (C) 2022 Intel Corporation +// +// SPDX-License-Identifier: MIT + +import React from 'react'; +import Spin, { SpinProps } from 'antd/lib/spin'; + +function CVATLoadingSpinner(props: SpinProps): JSX.Element { + return ( +
+ +
+ ); +} + +export default React.memo(CVATLoadingSpinner); diff --git a/cvat-ui/src/components/task-page/details.tsx b/cvat-ui/src/components/task-page/details.tsx index a2d2c55e9f7def94c039998415a9a464ff145d2d..179f467e57118f5c99ac5f1bbd2562e4926c15bc 100644 --- a/cvat-ui/src/components/task-page/details.tsx +++ b/cvat-ui/src/components/task-page/details.tsx @@ -241,6 +241,7 @@ export default class DetailsComponent extends React.PureComponent { + if (taskInstance?.assignee?.id === value?.id) return; taskInstance.assignee = value; onTaskUpdate(taskInstance); }} diff --git a/cvat-ui/src/components/task-page/job-list.tsx b/cvat-ui/src/components/task-page/job-list.tsx index d90fe98419af271106d1e117452b779877850ea6..67b113e741f962fee50e15cb2a0254e3f4cacac3 100644 --- a/cvat-ui/src/components/task-page/job-list.tsx +++ b/cvat-ui/src/components/task-page/job-list.tsx @@ -1,4 +1,4 @@ -// Copyright (C) 2020-2021 Intel Corporation +// Copyright (C) 2020-2022 Intel Corporation // // SPDX-License-Identifier: MIT @@ -234,6 +234,7 @@ function JobListComponent(props: Props & RouteComponentProps): JSX.Element { className='cvat-job-assignee-selector' value={jobInstance.assignee} onSelect={(value: User | null): void => { + if (jobInstance?.assignee?.id === value?.id) return; jobInstance.assignee = value; onJobUpdate(jobInstance); }} diff --git a/cvat-ui/src/components/task-page/styles.scss b/cvat-ui/src/components/task-page/styles.scss index c5296c3acce820bf96c159e2997c0962d4a4b6a2..0fa391fafab89dbb5530c0ce7b319e80a9e18107 100644 --- a/cvat-ui/src/components/task-page/styles.scss +++ b/cvat-ui/src/components/task-page/styles.scss @@ -78,6 +78,11 @@ width: 100%; } +.cvat-task-page { + position: relative; + height: 100%; +} + .cvat-task-job-list { width: 100%; height: auto; diff --git a/cvat-ui/src/components/task-page/task-page.tsx b/cvat-ui/src/components/task-page/task-page.tsx index cfd526032999746f96a99cb618934a7f2613d877..829920eb3bc93fd36ba155791a6d7edff4683722 100644 --- a/cvat-ui/src/components/task-page/task-page.tsx +++ b/cvat-ui/src/components/task-page/task-page.tsx @@ -1,4 +1,4 @@ -// Copyright (C) 2020-2021 Intel Corporation +// Copyright (C) 2020-2022 Intel Corporation // // SPDX-License-Identifier: MIT @@ -13,6 +13,7 @@ import Result from 'antd/lib/result'; import DetailsContainer from 'containers/task-page/details'; import JobListContainer from 'containers/task-page/job-list'; import ModelRunnerModal from 'components/model-runner-modal/model-runner-dialog'; +import CVATLoadingSpinner from 'components/common/loading-spinner'; import MoveTaskModal from 'components/move-task-modal/move-task-modal'; import { Task } from 'reducers/interfaces'; import TopBarComponent from './top-bar'; @@ -21,6 +22,7 @@ interface TaskPageComponentProps { task: Task | null | undefined; fetching: boolean; updating: boolean; + jobUpdating: boolean; deleteActivity: boolean | null; installedGit: boolean; getTask: () => void; @@ -37,12 +39,13 @@ class TaskPageComponent extends React.PureComponent { } } - public componentDidUpdate(): void { + public componentDidUpdate(prevProps: Props): void { const { - deleteActivity, history, task, fetching, getTask, + deleteActivity, history, task, fetching, getTask, jobUpdating, } = this.props; - if (task === null && !fetching) { + const jobUpdated = prevProps.jobUpdating && !jobUpdating; + if ((task === null && !fetching) || jobUpdated) { getTask(); } @@ -54,7 +57,7 @@ class TaskPageComponent extends React.PureComponent { public render(): JSX.Element { const { task, updating, fetching } = this.props; - if (task === null || fetching) { + if (task === null || (fetching && !updating)) { return ; } @@ -70,10 +73,9 @@ class TaskPageComponent extends React.PureComponent { } return ( - <> - { updating ? : null } +
+ { updating ? : null } { - +
); } } diff --git a/cvat-ui/src/containers/task-page/task-page.tsx b/cvat-ui/src/containers/task-page/task-page.tsx index a290eb07df33dee84d8c47aa49099ebf3d1b5569..52c8ecd57aab6fcc157ce05eb7efbe3a6692d178 100644 --- a/cvat-ui/src/containers/task-page/task-page.tsx +++ b/cvat-ui/src/containers/task-page/task-page.tsx @@ -1,4 +1,4 @@ -// Copyright (C) 2020 Intel Corporation +// Copyright (C) 2020-2022 Intel Corporation // // SPDX-License-Identifier: MIT @@ -17,6 +17,7 @@ interface StateToProps { task: Task | null | undefined; fetching: boolean; updating: boolean; + jobUpdating: boolean; deleteActivity: boolean | null; installedGit: boolean; } @@ -28,8 +29,10 @@ interface DispatchToProps { function mapStateToProps(state: CombinedState, own: Props): StateToProps { const { list } = state.plugins; const { tasks } = state; - const { gettingQuery, fetching, updating } = tasks; - const { deletes } = tasks.activities; + const { + gettingQuery, fetching, updating, + } = tasks; + const { deletes, jobUpdates } = tasks.activities; const id = +own.match.params.id; @@ -42,8 +45,13 @@ function mapStateToProps(state: CombinedState, own: Props): StateToProps { deleteActivity = deletes[id]; } + const jobIDs = task ? Object.fromEntries(task.instance.jobs.map((job:any) => [job.id])) : {}; + const updatingJobs = Object.keys(jobUpdates); + const jobUpdating = updatingJobs.some((jobID) => jobID in jobIDs); + return { task, + jobUpdating, deleteActivity, fetching, updating, diff --git a/cvat-ui/src/reducers/interfaces.ts b/cvat-ui/src/reducers/interfaces.ts index 06f3adf3199bd49ebadeed22d952dc8807d2f052..4d3bf3d4015804437ad365403c1e1a92020676e4 100644 --- a/cvat-ui/src/reducers/interfaces.ts +++ b/cvat-ui/src/reducers/interfaces.ts @@ -116,6 +116,9 @@ export interface TasksState { backups: { [tid: number]: boolean; }; + jobUpdates: { + [jid: number]: boolean, + }; }; } diff --git a/cvat-ui/src/reducers/tasks-reducer.ts b/cvat-ui/src/reducers/tasks-reducer.ts index 7a860c336df0f1c31456706151e5a287ca0b793d..c9d1d8e01ace40c46cc126117a0371add339a33a 100644 --- a/cvat-ui/src/reducers/tasks-reducer.ts +++ b/cvat-ui/src/reducers/tasks-reducer.ts @@ -8,6 +8,7 @@ import { BoundariesActionTypes } from 'actions/boundaries-actions'; import { TasksActionTypes } from 'actions/tasks-actions'; import { AuthActionTypes } from 'actions/auth-actions'; +import { AnnotationActionTypes } from 'actions/annotation-actions'; import { TasksState, Task } from './interfaces'; const defaultState: TasksState = { @@ -38,6 +39,7 @@ const defaultState: TasksState = { error: '', }, backups: {}, + jobUpdates: {}, }, importing: false, }; @@ -55,7 +57,6 @@ export default (state: TasksState = defaultState, action: AnyAction): TasksState fetching: true, hideEmpty: true, count: 0, - current: [], gettingQuery: action.payload.updateQuery ? { ...action.payload.query } : state.gettingQuery, }; case TasksActionTypes.GET_TASKS_SUCCESS: { @@ -70,6 +71,7 @@ export default (state: TasksState = defaultState, action: AnyAction): TasksState ...state, initialized: true, fetching: false, + updating: false, count: action.payload.count, current: combinedWithPreviews, }; @@ -313,25 +315,34 @@ export default (state: TasksState = defaultState, action: AnyAction): TasksState }; } case TasksActionTypes.UPDATE_JOB: { + const { jobID } = action.payload; + const { jobUpdates } = state.activities; + return { ...state, updating: true, + activities: { + ...state.activities, + jobUpdates: { + ...jobUpdates, + ...Object.fromEntries([[jobID, true]]), + }, + }, }; } - case TasksActionTypes.UPDATE_JOB_SUCCESS: { - const { jobInstance } = action.payload; - const idx = state.current.findIndex((task: Task) => task.instance.id === jobInstance.taskId); - const newCurrent = idx === -1 ? - state.current : [...(state.current.splice(idx, 1), state.current)]; + case TasksActionTypes.UPDATE_JOB_SUCCESS: + case TasksActionTypes.UPDATE_JOB_FAILED: { + const { jobID } = action.payload; + const { jobUpdates } = state.activities; + + delete jobUpdates[jobID]; return { ...state, - current: newCurrent, - gettingQuery: state.gettingQuery.id === jobInstance.taskId ? { - ...state.gettingQuery, - id: null, - } : state.gettingQuery, - updating: false, + activities: { + ...state.activities, + jobUpdates: omit(jobUpdates, [jobID]), + }, }; } case TasksActionTypes.HIDE_EMPTY_TASKS: { @@ -350,6 +361,12 @@ export default (state: TasksState = defaultState, action: AnyAction): TasksState }, }; } + case AnnotationActionTypes.CLOSE_JOB: { + return { + ...state, + updating: false, + }; + } case BoundariesActionTypes.RESET_AFTER_ERROR: case AuthActionTypes.LOGOUT_SUCCESS: { return { ...defaultState }; diff --git a/cvat-ui/src/styles.scss b/cvat-ui/src/styles.scss index a22b02c7cdd403d1ab0d910392c39e76f83b59a9..2b44d52e5dfb0e9020820efe9c5e2a5614c09ec5 100644 --- a/cvat-ui/src/styles.scss +++ b/cvat-ui/src/styles.scss @@ -17,6 +17,17 @@ hr { transform: translate(-50%, -50%); } +.cvat-spinner-container { + position: absolute; + background: $background-color-1; + opacity: 0.5; + width: 100%; + height: 100%; + z-index: 2; + top: 0; + left: 0; +} + .cvat-not-found { margin: 10% 25%; } diff --git a/tests/cypress/integration/actions_tasks/registration_involved/case_69_filters_sorting_jobs.js b/tests/cypress/integration/actions_tasks/registration_involved/case_69_filters_sorting_jobs.js index ee8e2fdb1db4d3849f598859a36f8b17eccf8db1..773330cadf12781dbc2d66f16a82082ef9749571 100644 --- a/tests/cypress/integration/actions_tasks/registration_involved/case_69_filters_sorting_jobs.js +++ b/tests/cypress/integration/actions_tasks/registration_involved/case_69_filters_sorting_jobs.js @@ -49,8 +49,8 @@ context('Filtering, sorting jobs.', () => { cy.get('.cvat-task-jobs-table') .contains('a', `Job #${$job}`) .parents('.cvat-task-jobs-table-row').within(() => { - cy.get('.cvat-job-item-stage').invoke('text').should('equal', stage); - cy.get('.cvat-job-item-state').invoke('text').should('equal', state); + cy.get('.cvat-job-item-stage .ant-select-selection-item').should('have.text', stage); + cy.get('.cvat-job-item-state').should('have.text', state); cy.get('.cvat-job-item-assignee') .find('[type="search"]') .invoke('val') diff --git a/tests/cypress/integration/actions_users/issue_2440_value_must_be_a_user_instance.js b/tests/cypress/integration/actions_users/issue_2440_value_must_be_a_user_instance.js index 70b8c3049f36156e3fbb2246d3d1625f58f4c95e..7d1e10322feee179480c17deafdad349038376cf 100644 --- a/tests/cypress/integration/actions_users/issue_2440_value_must_be_a_user_instance.js +++ b/tests/cypress/integration/actions_users/issue_2440_value_must_be_a_user_instance.js @@ -1,4 +1,4 @@ -// Copyright (C) 2020 Intel Corporation +// Copyright (C) 2020-2022 Intel Corporation // // SPDX-License-Identifier: MIT @@ -23,7 +23,7 @@ context('Value must be a user instance.', () => { .within(() => { cy.get(`.ant-select-item-option[title="${Cypress.env('user')}"]`).click(); }); - cy.get('.cvat-spinner').should('exist'); + cy.get('.cvat-spinner').should('not.exist'); }); it('Assign the task to the same user again', () => { cy.get('.cvat-task-details-user-block').within(() => { diff --git a/tests/cypress/support/commands.js b/tests/cypress/support/commands.js index 89abc1e0467627a36de43ba92b1dde0a641d2c27..b8ea022904150cd615ed024645ef02372cd676c8 100644 --- a/tests/cypress/support/commands.js +++ b/tests/cypress/support/commands.js @@ -698,6 +698,7 @@ Cypress.Commands.add('addNewLabel', (newLabelName, additionalAttrs, labelColor) } } cy.contains('button', 'Done').click(); + cy.get('.cvat-spinner').should('not.exist'); cy.get('.cvat-constructor-viewer').should('be.visible'); cy.contains('.cvat-constructor-viewer-item', new RegExp(`^${newLabelName}$`)).should('exist'); });