未验证 提交 1d952ace 编写于 作者: D Dmitry Kalinin 提交者: GitHub

Issue deleting (#3952)

上级 439c6d5f
......@@ -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>)
- 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 remove issue button (<https://github.com/openvinotoolkit/cvat/pull/3952>)
### Changed
- TDB
......
// 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;
// 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;
......@@ -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,
},
......
{
"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",
......
{
"name": "cvat-ui",
"version": "1.28.0",
"version": "1.28.1",
"description": "CVAT single-page application",
"main": "src/index.tsx",
"scripts": {
......
......@@ -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<typeof reviewActions>;
......@@ -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));
}
};
......@@ -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<HTMLDivElement>(null);
const [currentText, setCurrentText] = useState<string>('');
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 {
/>
</Col>
</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>
{currentText.length ? (
<Button
......
// Copyright (C) 2020 Intel Corporation
// Copyright (C) 2020-2021 Intel Corporation
//
// SPDX-License-Identifier: MIT
......@@ -29,14 +29,17 @@ export default function IssueAggregatorComponent(): JSX.Element | null {
const dispatch = useDispatch();
const [expandedIssue, setExpandedIssue] = useState<number | null>(null);
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 newIssuePosition = useSelector((state: CombinedState): number[] | null => state.review.newIssuePosition);
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 issueLabels: JSX.Element[] = [];
const issueDialogs: JSX.Element[] = [];
if (!(canvasInstance instanceof Canvas)) return null;
useEffect(() => {
scaleHandler(canvasInstance);
});
......@@ -81,6 +84,7 @@ export default function IssueAggregatorComponent(): JSX.Element | null {
const { geometry } = canvasInstance;
for (const issue of frameIssues) {
if (issuesHidden) break;
if (issuesResolvedHidden && !!issue.resolvedDate) continue;
const issueResolved = !!issue.resolver;
const offset = 15;
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 {
LeftOutlined, RightOutlined, EyeInvisibleFilled, EyeOutlined,
CheckCircleFilled, CheckCircleOutlined,
} from '@ant-design/icons';
import Alert from 'antd/lib/alert';
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 activeReview = useSelector((state: CombinedState): any => state.review.activeReview);
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 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];
......@@ -62,8 +64,8 @@ export default function LabelsListComponent(): JSX.Element {
<RightOutlined className='cvat-issues-sidebar-next-frame' {...dinamicRightProps} />
</CVATTooltip>
</Col>
<Col offset={3}>
<CVATTooltip title='Show/hide all the issues'>
<Col offset={2}>
<CVATTooltip title='Show/hide all issues'>
{issuesHidden ? (
<EyeInvisibleFilled
className='cvat-issues-sidebar-hidden-issues'
......@@ -77,6 +79,22 @@ export default function LabelsListComponent(): JSX.Element {
)}
</CVATTooltip>
</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>
</div>
<div className='cvat-objects-sidebar-issues-list'>
......
......@@ -169,16 +169,20 @@ function mapStateToProps(state: CombinedState): StateToProps {
opacity, colorBy, selectedOpacity, outlined, outlineColor, showBitmap, showProjections,
},
},
review: { frameIssues, issuesHidden },
review: { frameIssues, issuesHidden, issuesResolvedHidden },
shortcuts: { keyMap },
} = state;
const issues = frameIssues.filter((issue) => (
!issuesHidden && [Workspace.REVIEW_WORKSPACE, Workspace.STANDARD].includes(workspace) &&
!(!!issue.resolvedDate && issuesResolvedHidden)
));
return {
sidebarCollapsed,
canvasInstance,
jobInstance,
frameIssues:
issuesHidden || ![Workspace.REVIEW_WORKSPACE, Workspace.STANDARD].includes(workspace) ? null : frameIssues,
frameIssues: issues,
frameData,
frameAngle: frameAngles[frame - jobInstance.startFrame],
frameFetching,
......
......@@ -391,6 +391,7 @@ export interface NotificationsState {
reopeningIssue: null | ErrorState;
commentingIssue: null | ErrorState;
submittingReview: null | ErrorState;
deletingIssue: null | ErrorState;
};
predictor: {
prediction: null | ErrorState;
......@@ -678,6 +679,7 @@ export interface ReviewState {
activeReview: any | null;
newIssuePosition: number[] | null;
issuesHidden: boolean;
issuesResolvedHidden: boolean;
fetching: {
reviewId: number | null;
issueId: number | null;
......
......@@ -110,6 +110,7 @@ const defaultState: NotificationsState = {
reopeningIssue: null,
resolvingIssue: null,
submittingReview: null,
deletingIssue: null,
},
predictor: {
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: {
return {
...state,
......
......@@ -16,6 +16,7 @@ const defaultState: ReviewState = {
activeReview: null, // not saved on the server
newIssuePosition: null,
issuesHidden: false,
issuesResolvedHidden: false,
fetching: {
reviewId: null,
issueId: null,
......@@ -175,6 +176,23 @@ export default function (state: ReviewState = defaultState, action: any): Review
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 AuthActionTypes.LOGOUT_SUCCESS: {
return { ...defaultState };
......
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册