Issue deleting (#3952)

main
Dmitry Kalinin 4 years ago committed by GitHub
parent 439c6d5fb9
commit 1d952acee8
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -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 (<https://github.com/openvinotoolkit/cvat/pull/3933>) - Player option: Smooth image when zoom-in, enabled by default (<https://github.com/openvinotoolkit/cvat/pull/3933>)
- Google Cloud Storage support in UI (<https://github.com/openvinotoolkit/cvat/pull/3919>) - Google Cloud Storage support in UI (<https://github.com/openvinotoolkit/cvat/pull/3919>)
- Add project tasks paginations (<https://github.com/openvinotoolkit/cvat/pull/3910>) - Add project tasks paginations (<https://github.com/openvinotoolkit/cvat/pull/3910>)
- Add remove issue button (<https://github.com/openvinotoolkit/cvat/pull/3952>)
### Changed ### Changed
- TDB - TDB

@ -1,4 +1,4 @@
// Copyright (C) 2020 Intel Corporation // Copyright (C) 2020-2021 Intel Corporation
// //
// SPDX-License-Identifier: MIT // SPDX-License-Identifier: MIT
@ -241,6 +241,21 @@ class Issue {
return result; 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() { serialize() {
const { comments } = this; const { comments } = this;
const data = { 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; module.exports = Issue;

@ -1,4 +1,4 @@
// Copyright (C) 2020 Intel Corporation // Copyright (C) 2020-2021 Intel Corporation
// //
// SPDX-License-Identifier: MIT // SPDX-License-Identifier: MIT
@ -250,6 +250,10 @@ class Review {
return result; return result;
} }
async deleteIssue(issueId) {
await PluginRegistry.apiWrapper.call(this, Review.prototype.deleteIssue, issueId);
}
/** /**
* Method submits local review to the server * Method submits local review to the server
* @method submit * @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; module.exports = Review;

@ -757,6 +757,16 @@
return response.data; 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) { async function saveJob(id, jobData) {
const { backendAPI } = config; const { backendAPI } = config;
@ -1413,6 +1423,7 @@
issues: { issues: {
value: Object.freeze({ value: Object.freeze({
update: updateIssue, update: updateIssue,
delete: deleteIssue,
}), }),
writable: false, writable: false,
}, },

@ -1,12 +1,12 @@
{ {
"name": "cvat-ui", "name": "cvat-ui",
"version": "1.28.0", "version": "1.28.1",
"lockfileVersion": 2, "lockfileVersion": 2,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "cvat-ui", "name": "cvat-ui",
"version": "1.28.0", "version": "1.28.1",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@ant-design/icons": "^4.6.3", "@ant-design/icons": "^4.6.3",

@ -1,6 +1,6 @@
{ {
"name": "cvat-ui", "name": "cvat-ui",
"version": "1.28.0", "version": "1.28.1",
"description": "CVAT single-page application", "description": "CVAT single-page application",
"main": "src/index.tsx", "main": "src/index.tsx",
"scripts": { "scripts": {

@ -25,10 +25,13 @@ export enum ReviewActionTypes {
COMMENT_ISSUE = 'COMMENT_ISSUE', COMMENT_ISSUE = 'COMMENT_ISSUE',
COMMENT_ISSUE_SUCCESS = 'COMMENT_ISSUE_SUCCESS', COMMENT_ISSUE_SUCCESS = 'COMMENT_ISSUE_SUCCESS',
COMMENT_ISSUE_FAILED = 'COMMENT_ISSUE_FAILED', COMMENT_ISSUE_FAILED = 'COMMENT_ISSUE_FAILED',
REMOVE_ISSUE_SUCCESS = 'REMOVE_ISSUE_SUCCESS',
REMOVE_ISSUE_FAILED = 'REMOVE_ISSUE_FAILED',
SUBMIT_REVIEW = 'SUBMIT_REVIEW', SUBMIT_REVIEW = 'SUBMIT_REVIEW',
SUBMIT_REVIEW_SUCCESS = 'SUBMIT_REVIEW_SUCCESS', SUBMIT_REVIEW_SUCCESS = 'SUBMIT_REVIEW_SUCCESS',
SUBMIT_REVIEW_FAILED = 'SUBMIT_REVIEW_FAILED', SUBMIT_REVIEW_FAILED = 'SUBMIT_REVIEW_FAILED',
SWITCH_ISSUES_HIDDEN_FLAG = 'SWITCH_ISSUES_HIDDEN_FLAG', SWITCH_ISSUES_HIDDEN_FLAG = 'SWITCH_ISSUES_HIDDEN_FLAG',
SWITCH_RESOLVED_ISSUES_HIDDEN_FLAG = 'SWITCH_RESOLVED_ISSUES_HIDDEN_FLAG',
} }
export const reviewActions = { export const reviewActions = {
@ -57,7 +60,14 @@ export const reviewActions = {
submitReview: (reviewId: number) => createAction(ReviewActionTypes.SUBMIT_REVIEW, { reviewId }), submitReview: (reviewId: number) => createAction(ReviewActionTypes.SUBMIT_REVIEW, { reviewId }),
submitReviewSuccess: () => createAction(ReviewActionTypes.SUBMIT_REVIEW_SUCCESS), submitReviewSuccess: () => createAction(ReviewActionTypes.SUBMIT_REVIEW_SUCCESS),
submitReviewFailed: (error: any) => createAction(ReviewActionTypes.SUBMIT_REVIEW_FAILED, { error }), 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 }), 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<typeof reviewActions>; export type ReviewActions = ActionUnion<typeof reviewActions>;
@ -204,3 +214,27 @@ export const submitReviewAsync = (review: any): ThunkAction => async (dispatch,
dispatch(reviewActions.submitReviewFailed(error)); 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));
}
};

@ -2,8 +2,15 @@
// //
// SPDX-License-Identifier: MIT // 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 ReactDOM from 'react-dom';
import { useDispatch } from 'react-redux';
import Modal from 'antd/lib/modal';
import { Row, Col } from 'antd/lib/grid'; import { Row, Col } from 'antd/lib/grid';
import { CloseOutlined } from '@ant-design/icons'; import { CloseOutlined } from '@ant-design/icons';
import Comment from 'antd/lib/comment'; import Comment from 'antd/lib/comment';
@ -13,6 +20,7 @@ import Button from 'antd/lib/button';
import Input from 'antd/lib/input'; import Input from 'antd/lib/input';
import moment from 'moment'; import moment from 'moment';
import CVATTooltip from 'components/common/cvat-tooltip'; import CVATTooltip from 'components/common/cvat-tooltip';
import { deleteIssueAsync } from 'actions/review-actions';
interface Props { interface Props {
id: number; id: number;
@ -32,6 +40,7 @@ interface Props {
export default function IssueDialog(props: Props): JSX.Element { export default function IssueDialog(props: Props): JSX.Element {
const ref = useRef<HTMLDivElement>(null); const ref = useRef<HTMLDivElement>(null);
const [currentText, setCurrentText] = useState<string>(''); const [currentText, setCurrentText] = useState<string>('');
const dispatch = useDispatch();
const { const {
comments, comments,
id, id,
@ -55,6 +64,22 @@ export default function IssueDialog(props: Props): JSX.Element {
} }
}, [resolved]); }, [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( const lines = comments.map(
(_comment: any): JSX.Element => { (_comment: any): JSX.Element => {
const created = _comment.createdDate ? moment(_comment.createdDate) : moment(moment.now()); const created = _comment.createdDate ? moment(_comment.createdDate) : moment(moment.now());
@ -118,7 +143,12 @@ export default function IssueDialog(props: Props): JSX.Element {
/> />
</Col> </Col>
</Row> </Row>
<Row className='cvat-issue-dialog-footer' justify='end'> <Row className='cvat-issue-dialog-footer' justify='space-between'>
<Col>
<Button type='link' danger onClick={onDeleteIssue}>
Remove
</Button>
</Col>
<Col> <Col>
{currentText.length ? ( {currentText.length ? (
<Button <Button

@ -1,4 +1,4 @@
// Copyright (C) 2020 Intel Corporation // Copyright (C) 2020-2021 Intel Corporation
// //
// SPDX-License-Identifier: MIT // SPDX-License-Identifier: MIT
@ -29,14 +29,17 @@ export default function IssueAggregatorComponent(): JSX.Element | null {
const dispatch = useDispatch(); const dispatch = useDispatch();
const [expandedIssue, setExpandedIssue] = useState<number | null>(null); const [expandedIssue, setExpandedIssue] = useState<number | null>(null);
const frameIssues = useSelector((state: CombinedState): any[] => state.review.frameIssues); const frameIssues = useSelector((state: CombinedState): any[] => state.review.frameIssues);
const canvasInstance = useSelector((state: CombinedState): Canvas => state.annotation.canvas.instance); const canvasInstance = useSelector((state: CombinedState) => state.annotation.canvas.instance);
const canvasIsReady = useSelector((state: CombinedState): boolean => state.annotation.canvas.ready); const canvasIsReady = useSelector((state: CombinedState): boolean => state.annotation.canvas.ready);
const newIssuePosition = useSelector((state: CombinedState): number[] | null => state.review.newIssuePosition); const newIssuePosition = useSelector((state: CombinedState): number[] | null => state.review.newIssuePosition);
const issuesHidden = useSelector((state: CombinedState): any => state.review.issuesHidden); const issuesHidden = useSelector((state: CombinedState): any => state.review.issuesHidden);
const issuesResolvedHidden = useSelector((state: CombinedState): any => state.review.issuesResolvedHidden);
const issueFetching = useSelector((state: CombinedState): number | null => state.review.fetching.issueId); const issueFetching = useSelector((state: CombinedState): number | null => state.review.fetching.issueId);
const issueLabels: JSX.Element[] = []; const issueLabels: JSX.Element[] = [];
const issueDialogs: JSX.Element[] = []; const issueDialogs: JSX.Element[] = [];
if (!(canvasInstance instanceof Canvas)) return null;
useEffect(() => { useEffect(() => {
scaleHandler(canvasInstance); scaleHandler(canvasInstance);
}); });
@ -81,6 +84,7 @@ export default function IssueAggregatorComponent(): JSX.Element | null {
const { geometry } = canvasInstance; const { geometry } = canvasInstance;
for (const issue of frameIssues) { for (const issue of frameIssues) {
if (issuesHidden) break; if (issuesHidden) break;
if (issuesResolvedHidden && !!issue.resolvedDate) continue;
const issueResolved = !!issue.resolver; const issueResolved = !!issue.resolver;
const offset = 15; const offset = 15;
const translated = issue.position.map((coord: number): number => coord + geometry.offset); const translated = issue.position.map((coord: number): number => coord + geometry.offset);

@ -6,6 +6,7 @@ import React from 'react';
import { useSelector, useDispatch } from 'react-redux'; import { useSelector, useDispatch } from 'react-redux';
import { import {
LeftOutlined, RightOutlined, EyeInvisibleFilled, EyeOutlined, LeftOutlined, RightOutlined, EyeInvisibleFilled, EyeOutlined,
CheckCircleFilled, CheckCircleOutlined,
} from '@ant-design/icons'; } from '@ant-design/icons';
import Alert from 'antd/lib/alert'; import Alert from 'antd/lib/alert';
import { Row, Col } from 'antd/lib/grid'; import { Row, Col } from 'antd/lib/grid';
@ -22,6 +23,7 @@ export default function LabelsListComponent(): JSX.Element {
const issues = useSelector((state: CombinedState): any[] => state.review.issues); const issues = useSelector((state: CombinedState): any[] => state.review.issues);
const activeReview = useSelector((state: CombinedState): any => state.review.activeReview); const activeReview = useSelector((state: CombinedState): any => state.review.activeReview);
const issuesHidden = useSelector((state: CombinedState): any => state.review.issuesHidden); const issuesHidden = useSelector((state: CombinedState): any => state.review.issuesHidden);
const issuesResolvedHidden = useSelector((state: CombinedState): any => state.review.issuesResolvedHidden);
const combinedIssues = activeReview ? issues.concat(activeReview.issues) : issues; const combinedIssues = activeReview ? issues.concat(activeReview.issues) : issues;
const frames = combinedIssues.map((issue: any): number => issue.frame).sort((a: number, b: number) => +a - +b); const frames = combinedIssues.map((issue: any): number => issue.frame).sort((a: number, b: number) => +a - +b);
const nearestLeft = frames.filter((_frame: number): boolean => _frame < frame).reverse()[0]; const nearestLeft = frames.filter((_frame: number): boolean => _frame < frame).reverse()[0];
@ -62,8 +64,8 @@ export default function LabelsListComponent(): JSX.Element {
<RightOutlined className='cvat-issues-sidebar-next-frame' {...dinamicRightProps} /> <RightOutlined className='cvat-issues-sidebar-next-frame' {...dinamicRightProps} />
</CVATTooltip> </CVATTooltip>
</Col> </Col>
<Col offset={3}> <Col offset={2}>
<CVATTooltip title='Show/hide all the issues'> <CVATTooltip title='Show/hide all issues'>
{issuesHidden ? ( {issuesHidden ? (
<EyeInvisibleFilled <EyeInvisibleFilled
className='cvat-issues-sidebar-hidden-issues' className='cvat-issues-sidebar-hidden-issues'
@ -77,6 +79,22 @@ export default function LabelsListComponent(): JSX.Element {
)} )}
</CVATTooltip> </CVATTooltip>
</Col> </Col>
<Col offset={2}>
<CVATTooltip title='Show/hide resolved issues'>
{ issuesResolvedHidden ? (
<CheckCircleFilled
className='cvat-issues-sidebar-hidden-resolved-status'
onClick={() => dispatch(reviewActions.switchIssuesHiddenResolvedFlag(false))}
/>
) : (
<CheckCircleOutlined
className='cvat-issues-sidebar-hidden-resolved-status'
onClick={() => dispatch(reviewActions.switchIssuesHiddenResolvedFlag(true))}
/>
)}
</CVATTooltip>
</Col>
</Row> </Row>
</div> </div>
<div className='cvat-objects-sidebar-issues-list'> <div className='cvat-objects-sidebar-issues-list'>

@ -169,16 +169,20 @@ function mapStateToProps(state: CombinedState): StateToProps {
opacity, colorBy, selectedOpacity, outlined, outlineColor, showBitmap, showProjections, opacity, colorBy, selectedOpacity, outlined, outlineColor, showBitmap, showProjections,
}, },
}, },
review: { frameIssues, issuesHidden }, review: { frameIssues, issuesHidden, issuesResolvedHidden },
shortcuts: { keyMap }, shortcuts: { keyMap },
} = state; } = state;
const issues = frameIssues.filter((issue) => (
!issuesHidden && [Workspace.REVIEW_WORKSPACE, Workspace.STANDARD].includes(workspace) &&
!(!!issue.resolvedDate && issuesResolvedHidden)
));
return { return {
sidebarCollapsed, sidebarCollapsed,
canvasInstance, canvasInstance,
jobInstance, jobInstance,
frameIssues: frameIssues: issues,
issuesHidden || ![Workspace.REVIEW_WORKSPACE, Workspace.STANDARD].includes(workspace) ? null : frameIssues,
frameData, frameData,
frameAngle: frameAngles[frame - jobInstance.startFrame], frameAngle: frameAngles[frame - jobInstance.startFrame],
frameFetching, frameFetching,

@ -391,6 +391,7 @@ export interface NotificationsState {
reopeningIssue: null | ErrorState; reopeningIssue: null | ErrorState;
commentingIssue: null | ErrorState; commentingIssue: null | ErrorState;
submittingReview: null | ErrorState; submittingReview: null | ErrorState;
deletingIssue: null | ErrorState;
}; };
predictor: { predictor: {
prediction: null | ErrorState; prediction: null | ErrorState;
@ -678,6 +679,7 @@ export interface ReviewState {
activeReview: any | null; activeReview: any | null;
newIssuePosition: number[] | null; newIssuePosition: number[] | null;
issuesHidden: boolean; issuesHidden: boolean;
issuesResolvedHidden: boolean;
fetching: { fetching: {
reviewId: number | null; reviewId: number | null;
issueId: number | null; issueId: number | null;

@ -110,6 +110,7 @@ const defaultState: NotificationsState = {
reopeningIssue: null, reopeningIssue: null,
resolvingIssue: null, resolvingIssue: null,
submittingReview: null, submittingReview: null,
deletingIssue: null,
}, },
predictor: { predictor: {
prediction: null, prediction: null,
@ -1136,6 +1137,21 @@ export default function (state = defaultState, action: AnyAction): Notifications
}, },
}; };
} }
case ReviewActionTypes.REMOVE_ISSUE_FAILED: {
return {
...state,
errors: {
...state.errors,
review: {
...state.errors.review,
deletingIssue: {
message: 'Could not remove issue from the server',
reason: action.payload.error.toString(),
},
},
},
};
}
case NotificationsActionType.RESET_ERRORS: { case NotificationsActionType.RESET_ERRORS: {
return { return {
...state, ...state,

@ -16,6 +16,7 @@ const defaultState: ReviewState = {
activeReview: null, // not saved on the server activeReview: null, // not saved on the server
newIssuePosition: null, newIssuePosition: null,
issuesHidden: false, issuesHidden: false,
issuesResolvedHidden: false,
fetching: { fetching: {
reviewId: null, reviewId: null,
issueId: null, issueId: null,
@ -175,6 +176,23 @@ export default function (state: ReviewState = defaultState, action: any): Review
issuesHidden: hidden, issuesHidden: hidden,
}; };
} }
case ReviewActionTypes.SWITCH_RESOLVED_ISSUES_HIDDEN_FLAG: {
const { hidden } = action.payload;
return {
...state,
issuesResolvedHidden: hidden,
};
}
case ReviewActionTypes.REMOVE_ISSUE_SUCCESS: {
const { issueId, frame } = action.payload;
const issues = state.issues.filter((issue: any) => issue.id !== issueId);
const frameIssues = computeFrameIssues(issues, state.activeReview, frame);
return {
...state,
issues,
frameIssues,
};
}
case AnnotationActionTypes.CLOSE_JOB: case AnnotationActionTypes.CLOSE_JOB:
case AuthActionTypes.LOGOUT_SUCCESS: { case AuthActionTypes.LOGOUT_SUCCESS: {
return { ...defaultState }; return { ...defaultState };

Loading…
Cancel
Save