diff --git a/CHANGELOG.md b/CHANGELOG.md index 25a2c93ce243..d401b709468a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,6 +17,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Player option: Smooth image when zoom-in, enabled by default () - Google Cloud Storage support in UI () - Add project tasks paginations () +- Add remove issue button () ### Changed - TDB diff --git a/cvat-core/src/issue.js b/cvat-core/src/issue.js index e18ae3ed3d06..c6fe6e447166 100644 --- a/cvat-core/src/issue.js +++ b/cvat-core/src/issue.js @@ -1,4 +1,4 @@ -// Copyright (C) 2020 Intel Corporation +// Copyright (C) 2020-2021 Intel Corporation // // SPDX-License-Identifier: MIT @@ -241,6 +241,21 @@ class Issue { return result; } + /** + * The method deletes the issue + * Deletes local or server-saved issues + * @method delete + * @memberof module:API.cvat.classes.Issue + * @readonly + * @instance + * @async + * @throws {module:API.cvat.exceptions.ServerError} + * @throws {module:API.cvat.exceptions.PluginError} + */ + async delete() { + await PluginRegistry.apiWrapper.call(this, Issue.prototype.delete); + } + serialize() { const { comments } = this; const data = { @@ -332,4 +347,11 @@ Issue.prototype.reopen.implementation = async function () { } }; +Issue.prototype.delete.implementation = async function () { + const { id } = this; + if (id >= 0) { + await serverProxy.issues.delete(id); + } +}; + module.exports = Issue; diff --git a/cvat-core/src/review.js b/cvat-core/src/review.js index db9491e4f8fb..320fc200511a 100644 --- a/cvat-core/src/review.js +++ b/cvat-core/src/review.js @@ -1,4 +1,4 @@ -// Copyright (C) 2020 Intel Corporation +// Copyright (C) 2020-2021 Intel Corporation // // SPDX-License-Identifier: MIT @@ -250,6 +250,10 @@ class Review { return result; } + async deleteIssue(issueId) { + await PluginRegistry.apiWrapper.call(this, Review.prototype.deleteIssue, issueId); + } + /** * Method submits local review to the server * @method submit @@ -394,4 +398,8 @@ Review.prototype.submit.implementation = async function () { } }; +Review.prototype.deleteIssue.implementation = function (issueId) { + this.__internal.issue_set = this.__internal.issue_set.filter((issue) => issue.id !== issueId); +}; + module.exports = Review; diff --git a/cvat-core/src/server-proxy.js b/cvat-core/src/server-proxy.js index b9c59d299446..124826767edc 100644 --- a/cvat-core/src/server-proxy.js +++ b/cvat-core/src/server-proxy.js @@ -757,6 +757,16 @@ return response.data; } + async function deleteIssue(issueID) { + const { backendAPI } = config; + + try { + await Axios.delete(`${backendAPI}/issues/${issueID}`); + } catch (errorData) { + throw generateError(errorData); + } + } + async function saveJob(id, jobData) { const { backendAPI } = config; @@ -1413,6 +1423,7 @@ issues: { value: Object.freeze({ update: updateIssue, + delete: deleteIssue, }), writable: false, }, diff --git a/cvat-ui/package-lock.json b/cvat-ui/package-lock.json index 8744ae45aa90..027dd93062eb 100644 --- a/cvat-ui/package-lock.json +++ b/cvat-ui/package-lock.json @@ -1,12 +1,12 @@ { "name": "cvat-ui", - "version": "1.28.0", + "version": "1.28.1", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "cvat-ui", - "version": "1.28.0", + "version": "1.28.1", "license": "MIT", "dependencies": { "@ant-design/icons": "^4.6.3", diff --git a/cvat-ui/package.json b/cvat-ui/package.json index 93bc59238667..2ef52978fbb4 100644 --- a/cvat-ui/package.json +++ b/cvat-ui/package.json @@ -1,6 +1,6 @@ { "name": "cvat-ui", - "version": "1.28.0", + "version": "1.28.1", "description": "CVAT single-page application", "main": "src/index.tsx", "scripts": { diff --git a/cvat-ui/src/actions/review-actions.ts b/cvat-ui/src/actions/review-actions.ts index 2f90d6720599..00e95fc21f02 100644 --- a/cvat-ui/src/actions/review-actions.ts +++ b/cvat-ui/src/actions/review-actions.ts @@ -25,10 +25,13 @@ export enum ReviewActionTypes { COMMENT_ISSUE = 'COMMENT_ISSUE', COMMENT_ISSUE_SUCCESS = 'COMMENT_ISSUE_SUCCESS', COMMENT_ISSUE_FAILED = 'COMMENT_ISSUE_FAILED', + REMOVE_ISSUE_SUCCESS = 'REMOVE_ISSUE_SUCCESS', + REMOVE_ISSUE_FAILED = 'REMOVE_ISSUE_FAILED', SUBMIT_REVIEW = 'SUBMIT_REVIEW', SUBMIT_REVIEW_SUCCESS = 'SUBMIT_REVIEW_SUCCESS', SUBMIT_REVIEW_FAILED = 'SUBMIT_REVIEW_FAILED', SWITCH_ISSUES_HIDDEN_FLAG = 'SWITCH_ISSUES_HIDDEN_FLAG', + SWITCH_RESOLVED_ISSUES_HIDDEN_FLAG = 'SWITCH_RESOLVED_ISSUES_HIDDEN_FLAG', } export const reviewActions = { @@ -57,7 +60,14 @@ export const reviewActions = { submitReview: (reviewId: number) => createAction(ReviewActionTypes.SUBMIT_REVIEW, { reviewId }), submitReviewSuccess: () => createAction(ReviewActionTypes.SUBMIT_REVIEW_SUCCESS), submitReviewFailed: (error: any) => createAction(ReviewActionTypes.SUBMIT_REVIEW_FAILED, { error }), + removeIssueSuccess: (issueId: number, frame: number) => ( + createAction(ReviewActionTypes.REMOVE_ISSUE_SUCCESS, { issueId, frame }) + ), + removeIssueFailed: (error: any) => createAction(ReviewActionTypes.REMOVE_ISSUE_FAILED, { error }), switchIssuesHiddenFlag: (hidden: boolean) => createAction(ReviewActionTypes.SWITCH_ISSUES_HIDDEN_FLAG, { hidden }), + switchIssuesHiddenResolvedFlag: (hidden: boolean) => ( + createAction(ReviewActionTypes.SWITCH_RESOLVED_ISSUES_HIDDEN_FLAG, { hidden }) + ), }; export type ReviewActions = ActionUnion; @@ -204,3 +214,27 @@ export const submitReviewAsync = (review: any): ThunkAction => async (dispatch, dispatch(reviewActions.submitReviewFailed(error)); } }; + +export const deleteIssueAsync = (id: number): ThunkAction => async (dispatch, getState) => { + const state = getState(); + const { + review: { frameIssues, activeReview }, + annotation: { + player: { + frame: { number: frameNumber }, + }, + }, + } = state; + + try { + const [issue] = frameIssues.filter((_issue: any): boolean => _issue.id === id); + await issue.delete(); + if (activeReview !== null) { + await activeReview.deleteIssue(id); + await activeReview.toLocalStorage(); + } + dispatch(reviewActions.removeIssueSuccess(id, frameNumber)); + } catch (error) { + dispatch(reviewActions.removeIssueFailed(error)); + } +}; diff --git a/cvat-ui/src/components/annotation-page/review/issue-dialog.tsx b/cvat-ui/src/components/annotation-page/review/issue-dialog.tsx index 27b89772ced7..c4b9537569b0 100644 --- a/cvat-ui/src/components/annotation-page/review/issue-dialog.tsx +++ b/cvat-ui/src/components/annotation-page/review/issue-dialog.tsx @@ -2,8 +2,15 @@ // // SPDX-License-Identifier: MIT -import React, { useState, useEffect, useRef } from 'react'; +import React, { + useState, + useEffect, + useRef, + useCallback, +} from 'react'; import ReactDOM from 'react-dom'; +import { useDispatch } from 'react-redux'; +import Modal from 'antd/lib/modal'; import { Row, Col } from 'antd/lib/grid'; import { CloseOutlined } from '@ant-design/icons'; import Comment from 'antd/lib/comment'; @@ -13,6 +20,7 @@ import Button from 'antd/lib/button'; import Input from 'antd/lib/input'; import moment from 'moment'; import CVATTooltip from 'components/common/cvat-tooltip'; +import { deleteIssueAsync } from 'actions/review-actions'; interface Props { id: number; @@ -32,6 +40,7 @@ interface Props { export default function IssueDialog(props: Props): JSX.Element { const ref = useRef(null); const [currentText, setCurrentText] = useState(''); + const dispatch = useDispatch(); const { comments, id, @@ -55,6 +64,22 @@ export default function IssueDialog(props: Props): JSX.Element { } }, [resolved]); + const onDeleteIssue = useCallback((): void => { + Modal.confirm({ + title: `The issue${id >= 0 ? ` #${id}` : ''} will be deleted.`, + className: 'cvat-modal-confirm-remove-issue', + onOk: () => { + collapse(); + dispatch(deleteIssueAsync(id)); + }, + okButtonProps: { + type: 'primary', + danger: true, + }, + okText: 'Delete', + }); + }, []); + const lines = comments.map( (_comment: any): JSX.Element => { const created = _comment.createdDate ? moment(_comment.createdDate) : moment(moment.now()); @@ -118,7 +143,12 @@ export default function IssueDialog(props: Props): JSX.Element { /> - + + + + {currentText.length ? (