diff --git a/CHANGELOG.md b/CHANGELOG.md index bfb2e6ff..afd20a1e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Added documentation on how to mount cloud starage(AWS S3 bucket, Azure container, Google Drive) as FUSE () - Added ability to work with share files without copying inside () - Tooltips in label selectors () +- Page redirect after login using `next` query parameter () ### Changed @@ -41,10 +42,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Projects view layout fix () - Fixed the tasks view (infinite loading) when it is impossible to get a preview of the task () - Empty frames navigation () -- Disabled position editing in AAM () - TypeError: Cannot read property 'toString' of undefined () - Extra shapes are drawn after Esc, or G pressed while drawing a region in grouping () - Reset state (reviews, issues) after logout or changing a job () +- TypeError: Cannot read property 'id' of undefined when updating a task () ### Security diff --git a/cvat-ui/package-lock.json b/cvat-ui/package-lock.json index 1e30306b..1cd51dc7 100644 --- a/cvat-ui/package-lock.json +++ b/cvat-ui/package-lock.json @@ -1,6 +1,6 @@ { "name": "cvat-ui", - "version": "1.12.0", + "version": "1.13.0", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/cvat-ui/package.json b/cvat-ui/package.json index 81e171c0..29ab1770 100644 --- a/cvat-ui/package.json +++ b/cvat-ui/package.json @@ -1,6 +1,6 @@ { "name": "cvat-ui", - "version": "1.12.0", + "version": "1.13.0", "description": "CVAT single-page application", "main": "src/index.tsx", "scripts": { diff --git a/cvat-ui/src/actions/projects-actions.ts b/cvat-ui/src/actions/projects-actions.ts index 2aa385db..d2794db3 100644 --- a/cvat-ui/src/actions/projects-actions.ts +++ b/cvat-ui/src/actions/projects-actions.ts @@ -141,9 +141,11 @@ export function updateProjectAsync(projectInstance: any): ThunkAction { 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)); + dispatch(updateTaskSuccess(task, task.id)); }); } catch (error) { let project = null; diff --git a/cvat-ui/src/actions/review-actions.ts b/cvat-ui/src/actions/review-actions.ts index 6cca02ec..b433d968 100644 --- a/cvat-ui/src/actions/review-actions.ts +++ b/cvat-ui/src/actions/review-actions.ts @@ -52,13 +52,7 @@ export const reviewActions = { reopenIssueSuccess: () => createAction(ReviewActionTypes.REOPEN_ISSUE_SUCCESS), reopenIssueFailed: (error: any) => createAction(ReviewActionTypes.REOPEN_ISSUE_FAILED, { error }), submitReview: (reviewId: number) => createAction(ReviewActionTypes.SUBMIT_REVIEW, { reviewId }), - submitReviewSuccess: (activeReview: any, reviews: any[], issues: any[], frame: number) => - createAction(ReviewActionTypes.SUBMIT_REVIEW_SUCCESS, { - activeReview, - reviews, - issues, - frame, - }), + submitReviewSuccess: () => createAction(ReviewActionTypes.SUBMIT_REVIEW_SUCCESS), submitReviewFailed: (error: any) => createAction(ReviewActionTypes.SUBMIT_REVIEW_FAILED, { error }), switchIssuesHiddenFlag: (hidden: boolean) => createAction(ReviewActionTypes.SWITCH_ISSUES_HIDDEN_FLAG, { hidden }), }; @@ -193,9 +187,6 @@ export const submitReviewAsync = (review: any): ThunkAction => async (dispatch, const { annotation: { job: { instance: jobInstance }, - player: { - frame: { number: frame }, - }, }, } = state; @@ -204,13 +195,8 @@ export const submitReviewAsync = (review: any): ThunkAction => async (dispatch, await review.submit(jobInstance.id); const [task] = await cvat.tasks.get({ id: jobInstance.task.id }); - dispatch(updateTaskSuccess(task)); - - const reviews = await jobInstance.reviews(); - const issues = await jobInstance.issues(); - const reviewInstance = new cvat.classes.Review({ job: jobInstance.id }); - - dispatch(reviewActions.submitReviewSuccess(reviewInstance, reviews, issues, frame)); + dispatch(updateTaskSuccess(task, jobInstance.task.id)); + dispatch(reviewActions.submitReviewSuccess()); } catch (error) { dispatch(reviewActions.submitReviewFailed(error)); } diff --git a/cvat-ui/src/actions/tasks-actions.ts b/cvat-ui/src/actions/tasks-actions.ts index 2be26d82..bd0fd450 100644 --- a/cvat-ui/src/actions/tasks-actions.ts +++ b/cvat-ui/src/actions/tasks-actions.ts @@ -437,10 +437,10 @@ function updateTask(): AnyAction { return action; } -export function updateTaskSuccess(task: any): AnyAction { +export function updateTaskSuccess(task: any, taskID: number): AnyAction { const action = { type: TasksActionTypes.UPDATE_TASK_SUCCESS, - payload: { task }, + payload: { task, taskID }, }; return action; @@ -465,7 +465,7 @@ export function updateTaskAsync(taskInstance: any): ThunkAction, C const userFetching = getState().auth.fetching; if (!userFetching && nextUser && currentUser.username === nextUser.username) { const [task] = await cvat.tasks.get({ id: taskInstance.id }); - dispatch(updateTaskSuccess(task)); + dispatch(updateTaskSuccess(task, taskInstance.id)); } } catch (error) { // try abort all changes @@ -490,7 +490,7 @@ export function updateJobAsync(jobInstance: any): ThunkAction, {}, dispatch(updateTask()); await jobInstance.save(); const [task] = await cvat.tasks.get({ id: jobInstance.task.id }); - dispatch(updateTaskSuccess(task)); + dispatch(updateTaskSuccess(task, jobInstance.task.id)); } catch (error) { // try abort all changes let task = null; diff --git a/cvat-ui/src/components/annotation-page/annotation-page.tsx b/cvat-ui/src/components/annotation-page/annotation-page.tsx index 2ef039fa..062f83b6 100644 --- a/cvat-ui/src/components/annotation-page/annotation-page.tsx +++ b/cvat-ui/src/components/annotation-page/annotation-page.tsx @@ -53,11 +53,13 @@ export default function AnnotationPageComponent(props: Props): JSX.Element { }; }, []); - if (job === null) { - if (!fetching) { + useEffect(() => { + if (job === null && !fetching) { getJob(); } + }, [job, fetching]); + if (job === null) { return ; } diff --git a/cvat-ui/src/components/annotation-page/canvas/canvas-wrapper.tsx b/cvat-ui/src/components/annotation-page/canvas/canvas-wrapper.tsx index 15b2b48b..3c5d03c2 100644 --- a/cvat-ui/src/components/annotation-page/canvas/canvas-wrapper.tsx +++ b/cvat-ui/src/components/annotation-page/canvas/canvas-wrapper.tsx @@ -109,7 +109,7 @@ export default class CanvasWrapperComponent extends React.PureComponent { autoborders: automaticBordering, undefinedAttrValue: consts.UNDEFINED_ATTRIBUTE_VALUE, displayAllText: showObjectsTextAlways, - forceDisableEditing: [Workspace.ATTRIBUTE_ANNOTATION, Workspace.REVIEW_WORKSPACE].includes(workspace), + forceDisableEditing: workspace === Workspace.REVIEW_WORKSPACE, }); this.initialSetup(); @@ -260,11 +260,11 @@ export default class CanvasWrapperComponent extends React.PureComponent { } if (prevProps.workspace !== workspace) { - if ([Workspace.ATTRIBUTE_ANNOTATION, Workspace.REVIEW_WORKSPACE].includes(workspace)) { + if (workspace === Workspace.REVIEW_WORKSPACE) { canvasInstance.configure({ forceDisableEditing: true, }); - } else if ([Workspace.ATTRIBUTE_ANNOTATION, Workspace.REVIEW_WORKSPACE].includes(prevProps.workspace)) { + } else if (prevProps.workspace === Workspace.REVIEW_WORKSPACE) { canvasInstance.configure({ forceDisableEditing: false, }); diff --git a/cvat-ui/src/components/cvat-app.tsx b/cvat-ui/src/components/cvat-app.tsx index 7e361bdf..a7c5079e 100644 --- a/cvat-ui/src/components/cvat-app.tsx +++ b/cvat-ui/src/components/cvat-app.tsx @@ -233,16 +233,22 @@ class CVATApplication extends React.PureComponent )} - + {/* eslint-disable-next-line */} @@ -339,7 +348,9 @@ class CVATApplication extends React.PureComponent - + 1 ? `/auth/login/?next=${location.pathname}` : '/auth/login'} + /> ); diff --git a/cvat-ui/src/components/login-with-token/login-with-token.tsx b/cvat-ui/src/components/login-with-token/login-with-token.tsx index cadb0542..6d896c0d 100644 --- a/cvat-ui/src/components/login-with-token/login-with-token.tsx +++ b/cvat-ui/src/components/login-with-token/login-with-token.tsx @@ -3,15 +3,17 @@ // SPDX-License-Identifier: MIT import React, { useEffect } from 'react'; -import { Redirect, useParams } from 'react-router'; +import { Redirect, useParams, useLocation } from 'react-router'; import { useCookies } from 'react-cookie'; export default function LoginWithTokenComponent(): JSX.Element { - const { sessionId, token } = useParams(); + const location = useLocation(); + const { sessionId, token } = useParams<{ sessionId: string; token: string }>(); const [cookies, setCookie] = useCookies(['sessionid', 'csrftoken']); const expires1y = new Date(new Date().setFullYear(new Date().getFullYear() + 1)); const expires2w = new Date(new Date().setDate(new Date().getDate() + 13)); + const search = new URLSearchParams(location.search); setCookie('sessionid', sessionId, { path: '/', expires: expires2w }); setCookie('csrftoken', token, { path: '/', expires: expires1y }); @@ -24,7 +26,7 @@ export default function LoginWithTokenComponent(): JSX.Element { ); if (cookies.sessionid && cookies.csrftoken) { - return ; + return ; } return <>; } diff --git a/cvat-ui/src/components/projects-page/project-list.tsx b/cvat-ui/src/components/projects-page/project-list.tsx index 282ba785..5ff8b2ff 100644 --- a/cvat-ui/src/components/projects-page/project-list.tsx +++ b/cvat-ui/src/components/projects-page/project-list.tsx @@ -42,7 +42,7 @@ export default function ProjectListComponent(): JSX.Element { {projectInstances.map( (row: any[]): JSX.Element => ( - + {row.map((instance: any) => ( diff --git a/cvat-ui/src/components/task-page/task-page.tsx b/cvat-ui/src/components/task-page/task-page.tsx index 9944df41..e4811a23 100644 --- a/cvat-ui/src/components/task-page/task-page.tsx +++ b/cvat-ui/src/components/task-page/task-page.tsx @@ -28,6 +28,14 @@ interface TaskPageComponentProps { type Props = TaskPageComponentProps & RouteComponentProps<{ id: string }>; class TaskPageComponent extends React.PureComponent { + public componentDidMount(): void { + const { task, fetching, getTask } = this.props; + + if (task === null && !fetching) { + getTask(); + } + } + public componentDidUpdate(): void { const { deleteActivity, history } = this.props; @@ -37,15 +45,9 @@ class TaskPageComponent extends React.PureComponent { } public render(): JSX.Element { - const { - task, fetching, updating, getTask, - } = this.props; + const { task, updating } = this.props; if (task === null || updating) { - if (task === null && !fetching) { - getTask(); - } - return ; } diff --git a/cvat-ui/src/reducers/annotation-reducer.ts b/cvat-ui/src/reducers/annotation-reducer.ts index 7780eb9d..5b5b89cb 100644 --- a/cvat-ui/src/reducers/annotation-reducer.ts +++ b/cvat-ui/src/reducers/annotation-reducer.ts @@ -202,7 +202,7 @@ export default (state = defaultState, action: AnyAction): AnnotationState => { fetching: false, }, }, - } + }; } case AnnotationActionTypes.CHANGE_FRAME: { return { diff --git a/cvat-ui/src/reducers/auth-reducer.ts b/cvat-ui/src/reducers/auth-reducer.ts index eac8449b..54af7c75 100644 --- a/cvat-ui/src/reducers/auth-reducer.ts +++ b/cvat-ui/src/reducers/auth-reducer.ts @@ -94,9 +94,9 @@ export default function (state = defaultState, action: AuthActions | BoundariesA return { ...state, showChangePasswordDialog: - typeof action.payload.showChangePasswordDialog === 'undefined' - ? !state.showChangePasswordDialog - : action.payload.showChangePasswordDialog, + typeof action.payload.showChangePasswordDialog === 'undefined' ? + !state.showChangePasswordDialog : + action.payload.showChangePasswordDialog, }; case AuthActionTypes.REQUEST_PASSWORD_RESET: return { diff --git a/cvat-ui/src/reducers/review-reducer.ts b/cvat-ui/src/reducers/review-reducer.ts index a8c6d32e..919ea7ba 100644 --- a/cvat-ui/src/reducers/review-reducer.ts +++ b/cvat-ui/src/reducers/review-reducer.ts @@ -61,17 +61,8 @@ export default function (state: ReviewState = defaultState, action: any): Review }; } case ReviewActionTypes.SUBMIT_REVIEW_SUCCESS: { - const { - activeReview, reviews, issues, frame, - } = action.payload; - const frameIssues = computeFrameIssues(issues, activeReview, frame); - return { ...state, - activeReview, - reviews, - issues, - frameIssues, fetching: { ...state.fetching, reviewId: null, diff --git a/cvat-ui/src/reducers/tasks-reducer.ts b/cvat-ui/src/reducers/tasks-reducer.ts index 148d0a0e..2cc8db82 100644 --- a/cvat-ui/src/reducers/tasks-reducer.ts +++ b/cvat-ui/src/reducers/tasks-reducer.ts @@ -84,9 +84,9 @@ export default (state: TasksState = defaultState, action: AnyAction): TasksState const { dumps } = state.activities; dumps[task.id] = - task.id in dumps && !dumps[task.id].includes(dumper.name) - ? [...dumps[task.id], dumper.name] - : dumps[task.id] || [dumper.name]; + task.id in dumps && !dumps[task.id].includes(dumper.name) ? + [...dumps[task.id], dumper.name] : + dumps[task.id] || [dumper.name]; return { ...state, @@ -122,9 +122,9 @@ export default (state: TasksState = defaultState, action: AnyAction): TasksState const { exports: activeExports } = state.activities; activeExports[task.id] = - task.id in activeExports && !activeExports[task.id].includes(exporter.name) - ? [...activeExports[task.id], exporter.name] - : activeExports[task.id] || [exporter.name]; + task.id in activeExports && !activeExports[task.id].includes(exporter.name) ? + [...activeExports[task.id], exporter.name] : + activeExports[task.id] || [exporter.name]; return { ...state, @@ -299,19 +299,30 @@ export default (state: TasksState = defaultState, action: AnyAction): TasksState }; } case TasksActionTypes.UPDATE_TASK_SUCCESS: { + // a task will be undefined after updating when a user doesn't have access to the task anymore + const { task, taskID } = action.payload; + + if (typeof task === 'undefined') { + return { + ...state, + updating: false, + current: state.current.filter((_task: Task): boolean => _task.instance.id !== taskID), + }; + } + return { ...state, updating: false, current: state.current.map( - (task): Task => { - if (task.instance.id === action.payload.task.id) { + (_task): Task => { + if (_task.instance.id === task.id) { return { - ...task, - instance: action.payload.task, + ..._task, + instance: task, }; } - return task; + return _task; }, ), }; diff --git a/tests/cypress/integration/actions_tasks_objects/issue_2486_not_edit_object_aam.js b/tests/cypress/integration/actions_tasks_objects/issue_2486_not_edit_object_aam.js index ace8fa9a..e2faee99 100644 --- a/tests/cypress/integration/actions_tasks_objects/issue_2486_not_edit_object_aam.js +++ b/tests/cypress/integration/actions_tasks_objects/issue_2486_not_edit_object_aam.js @@ -25,7 +25,7 @@ context("Object can't be draggable/resizable in AAM", () => { }); describe(`Testing issue "${issueId}"`, () => { - it('Create, acttivate a object', () => { + it.skip('Create, acttivate a object', () => { cy.createRectangle(createRectangleShape2Points); cy.get('#cvat_canvas_shape_1') .should('not.have.class', 'cvat_canvas_shape_activated') @@ -33,7 +33,7 @@ context("Object can't be draggable/resizable in AAM", () => { .should('have.class', 'cvat_canvas_shape_activated'); }); - it('Go to AAM', () => { + it.skip('Go to AAM', () => { cy.changeWorkspace('Attribute annotation', labelName); cy.get('#cvat_canvas_shape_1') .then((shape) => { @@ -50,7 +50,7 @@ context("Object can't be draggable/resizable in AAM", () => { }); }); - it('Try to move/resize the object', () => { + it.skip('Try to move/resize the object', () => { cy.get('.cvat-canvas-container') .trigger('mousedown', { button: 0 }) .trigger('mousemove', 550, 251) diff --git a/tests/cypress/support/commands.js b/tests/cypress/support/commands.js index 556fb08f..da144486 100644 --- a/tests/cypress/support/commands.js +++ b/tests/cypress/support/commands.js @@ -23,6 +23,7 @@ Cypress.Commands.add('logout', (username = Cypress.env('user')) => { }); cy.get('span[aria-label="logout"]').click(); cy.url().should('include', '/auth/login'); + cy.visit('/auth/login'); // clear query parameter "next" }); Cypress.Commands.add('userRegistration', (firstName, lastName, userName, emailAddr, password) => {