Label deleting (#2881)

* Added label deleting

* Added label deletion for server mock cvat-core tests

* vscode settings adjustments

* Added server tests

* Removed unused import

* Added CHANGELOG and increased npm version

* Added ingoring npm scripts for non-project directories

* Added dummy no labels wrapper

* Added handling no labels jobs

* Fixed PR comments

* Added generic usage to the hook

Co-authored-by: Boris Sekachev <boris.sekachev@intel.com>
main
Dmitry Kalinin 5 years ago committed by GitHub
parent 5b46b516dd
commit 16bc9fb3d0
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -1,6 +1,5 @@
{ {
"python.pythonPath": ".env/bin/python", "python.pythonPath": ".env/bin/python",
"eslint.enable": true,
"eslint.probe": [ "eslint.probe": [
"javascript", "javascript",
"typescript", "typescript",
@ -19,8 +18,10 @@
"!cwd": true "!cwd": true
} }
], ],
"npm.exclude": "**/.env/**",
"python.linting.enabled": true,
"python.linting.pylintEnabled": true, "python.linting.pylintEnabled": true,
"python.testing.unittestEnabled": true, "python.linting.pycodestyleEnabled": false,
"licenser.license": "Custom", "licenser.license": "Custom",
"licenser.customHeader": "Copyright (C) @YEAR@ Intel Corporation\n\nSPDX-License-Identifier: MIT", "licenser.customHeader": "Copyright (C) @YEAR@ Intel Corporation\n\nSPDX-License-Identifier: MIT",
"files.trimTrailingWhitespace": true "files.trimTrailingWhitespace": true

@ -24,6 +24,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- [WiderFace](http://shuoyang1213.me/WIDERFACE/) format support (<https://github.com/openvinotoolkit/cvat/pull/2864>) - [WiderFace](http://shuoyang1213.me/WIDERFACE/) format support (<https://github.com/openvinotoolkit/cvat/pull/2864>)
- [VGGFace2](https://github.com/ox-vgg/vgg_face2) format support (<https://github.com/openvinotoolkit/cvat/pull/2865>) - [VGGFace2](https://github.com/ox-vgg/vgg_face2) format support (<https://github.com/openvinotoolkit/cvat/pull/2865>)
- [Backup/Restore guide](cvat/apps/documentation/backup_guide.md) (<https://github.com/openvinotoolkit/cvat/pull/2964>) - [Backup/Restore guide](cvat/apps/documentation/backup_guide.md) (<https://github.com/openvinotoolkit/cvat/pull/2964>)
- Label deletion from tasks and projects (<https://github.com/openvinotoolkit/cvat/pull/2881>)
### Changed ### Changed

@ -133,6 +133,7 @@
id: undefined, id: undefined,
name: undefined, name: undefined,
color: undefined, color: undefined,
deleted: false,
}; };
for (const key in data) { for (const key in data) {
@ -208,6 +209,12 @@
attributes: { attributes: {
get: () => [...data.attributes], get: () => [...data.attributes],
}, },
deleted: {
get: () => data.deleted,
set: (value) => {
data.deleted = value;
},
},
}), }),
); );
} }
@ -223,6 +230,10 @@
object.id = this.id; object.id = this.id;
} }
if (this.deleted) {
object.deleted = this.deleted;
}
return object; return object;
} }
} }

@ -186,7 +186,13 @@
); );
} }
data.labels = [...labels]; const IDs = labels.map((_label) => _label.id);
const deletedLabels = data.labels.filter((_label) => !IDs.includes(_label.id));
deletedLabels.forEach((_label) => {
_label.deleted = true;
});
data.labels = [...deletedLabels, ...labels];
}, },
}, },
/** /**
@ -211,6 +217,9 @@
subsets: { subsets: {
get: () => [...data.task_subsets], get: () => [...data.task_subsets],
}, },
_internalData: {
get: () => data,
},
}), }),
); );
} }
@ -257,7 +266,7 @@
name: this.name, name: this.name,
assignee_id: this.assignee ? this.assignee.id : null, assignee_id: this.assignee ? this.assignee.id : null,
bug_tracker: this.bugTracker, bug_tracker: this.bugTracker,
labels: [...this.labels.map((el) => el.toJSON())], labels: [...this._internalData.labels.map((el) => el.toJSON())],
}; };
await serverProxy.projects.save(this.id, projectData); await serverProxy.projects.save(this.id, projectData);

@ -1304,7 +1304,7 @@
* @throws {module:API.cvat.exceptions.ArgumentError} * @throws {module:API.cvat.exceptions.ArgumentError}
*/ */
labels: { labels: {
get: () => [...data.labels], get: () => data.labels.filter((_label) => !_label.deleted),
set: (labels) => { set: (labels) => {
if (!Array.isArray(labels)) { if (!Array.isArray(labels)) {
throw new ArgumentError('Value must be an array of Labels'); throw new ArgumentError('Value must be an array of Labels');
@ -1318,8 +1318,14 @@
} }
} }
const IDs = labels.map((_label) => _label.id);
const deletedLabels = data.labels.filter((_label) => !IDs.includes(_label.id));
deletedLabels.forEach((_label) => {
_label.deleted = true;
});
updatedFields.labels = true; updatedFields.labels = true;
data.labels = [...labels]; data.labels = [...deletedLabels, ...labels];
}, },
}, },
/** /**
@ -1485,12 +1491,6 @@
dataChunkType: { dataChunkType: {
get: () => data.data_compressed_chunk_type, get: () => data.data_compressed_chunk_type,
}, },
__updatedFields: {
get: () => updatedFields,
set: (fields) => {
updatedFields = fields;
},
},
dimension: { dimension: {
/** /**
* @name enabled * @name enabled
@ -1501,6 +1501,15 @@
*/ */
get: () => data.dimension, get: () => data.dimension,
}, },
_internalData: {
get: () => data,
},
__updatedFields: {
get: () => updatedFields,
set: (fields) => {
updatedFields = fields;
},
},
}), }),
); );
@ -1920,7 +1929,7 @@
taskData.subset = this.subset; taskData.subset = this.subset;
break; break;
case 'labels': case 'labels':
taskData.labels = [...this.labels.map((el) => el.toJSON())]; taskData.labels = [...this._internalData.labels.map((el) => el.toJSON())];
break; break;
default: default:
break; break;

@ -1,4 +1,4 @@
// Copyright (C) 2020 Intel Corporation // Copyright (C) 2020-2021 Intel Corporation
// //
// SPDX-License-Identifier: MIT // SPDX-License-Identifier: MIT
@ -97,7 +97,11 @@ class ServerProxy {
Object.prototype.hasOwnProperty.call(projectData, prop) Object.prototype.hasOwnProperty.call(projectData, prop)
&& Object.prototype.hasOwnProperty.call(object, prop) && Object.prototype.hasOwnProperty.call(object, prop)
) { ) {
object[prop] = projectData[prop]; if (prop === 'labels') {
object[prop] = projectData[prop].filter((label) => !label.deleted);
} else {
object[prop] = projectData[prop];
}
} }
} }
} }
@ -156,7 +160,11 @@ class ServerProxy {
Object.prototype.hasOwnProperty.call(taskData, prop) Object.prototype.hasOwnProperty.call(taskData, prop)
&& Object.prototype.hasOwnProperty.call(object, prop) && Object.prototype.hasOwnProperty.call(object, prop)
) { ) {
object[prop] = taskData[prop]; if (prop === 'labels') {
object[prop] = taskData[prop].filter((label) => !label.deleted);
} else {
object[prop] = taskData[prop];
}
} }
} }
} }

@ -2,6 +2,7 @@
// //
// SPDX-License-Identifier: MIT // SPDX-License-Identifier: MIT
import { MutableRefObject } from 'react';
import { import {
AnyAction, Dispatch, ActionCreator, Store, AnyAction, Dispatch, ActionCreator, Store,
} from 'redux'; } from 'redux';
@ -26,7 +27,6 @@ import getCore from 'cvat-core-wrapper';
import logger, { LogType } from 'cvat-logger'; import logger, { LogType } from 'cvat-logger';
import { RectDrawingMethod } from 'cvat-canvas-wrapper'; import { RectDrawingMethod } from 'cvat-canvas-wrapper';
import { getCVATStore } from 'cvat-store'; import { getCVATStore } from 'cvat-store';
import { MutableRefObject } from 'react';
interface AnnotationsParameters { interface AnnotationsParameters {
filters: string[]; filters: string[];
@ -919,10 +919,6 @@ export function getJobAsync(tid: number, jid: number, initialFrame: number, init
throw new Error(`Task ${tid} doesn't contain the job ${jid}`); throw new Error(`Task ${tid} doesn't contain the job ${jid}`);
} }
if (!task.labels.length && task.projectId) {
throw new Error(`Project ${task.projectId} does not contain any label`);
}
const frameNumber = Math.max(Math.min(job.stopFrame, initialFrame), job.startFrame); const frameNumber = Math.max(Math.min(job.stopFrame, initialFrame), job.startFrame);
const frameData = await job.frames.get(frameNumber); const frameData = await job.frames.get(frameNumber);
// call first getting of frame data before rendering interface // call first getting of frame data before rendering interface

@ -8,8 +8,10 @@ import { useHistory } from 'react-router';
import Layout from 'antd/lib/layout'; import Layout from 'antd/lib/layout';
import Spin from 'antd/lib/spin'; import Spin from 'antd/lib/spin';
import Result from 'antd/lib/result'; import Result from 'antd/lib/result';
import notification from 'antd/lib/notification';
import { Workspace } from 'reducers/interfaces'; import { Workspace } from 'reducers/interfaces';
import { usePrevious } from 'utils/hooks';
import AnnotationTopBarContainer from 'containers/annotation-page/top-bar/top-bar'; import AnnotationTopBarContainer from 'containers/annotation-page/top-bar/top-bar';
import StatisticsModalContainer from 'containers/annotation-page/top-bar/statistics-modal'; import StatisticsModalContainer from 'containers/annotation-page/top-bar/statistics-modal';
import StandardWorkspaceComponent from 'components/annotation-page/standard-workspace/standard-workspace'; import StandardWorkspaceComponent from 'components/annotation-page/standard-workspace/standard-workspace';
@ -33,6 +35,8 @@ export default function AnnotationPageComponent(props: Props): JSX.Element {
const { const {
job, fetching, getJob, closeJob, saveLogs, workspace, job, fetching, getJob, closeJob, saveLogs, workspace,
} = props; } = props;
const prevJob = usePrevious(job);
const prevFetching = usePrevious(fetching);
const history = useHistory(); const history = useHistory();
useEffect(() => { useEffect(() => {
@ -60,6 +64,26 @@ export default function AnnotationPageComponent(props: Props): JSX.Element {
} }
}, [job, fetching]); }, [job, fetching]);
useEffect(() => {
if (prevFetching && !fetching && !prevJob && job && !job.task.labels.length) {
notification.warning({
message: 'No labels',
description: (
<span>
{`${job.task.projectId ? 'Project' : 'Task'} ${
job.task.projectId || job.task.id
} does not contain any label. `}
<a href={`/${job.task.projectId ? 'projects' : 'tasks'}/${job.task.projectId || job.task.id}/`}>
Add
</a>
{' the first one for editing annotation.'}
</span>
),
placement: 'topRight',
});
}
}, [job, fetching, prevJob, prevFetching]);
if (job === null) { if (job === null) {
return <Spin size='large' className='cvat-spinner' />; return <Spin size='large' className='cvat-spinner' />;
} }

@ -3,8 +3,8 @@
// SPDX-License-Identifier: MIT // SPDX-License-Identifier: MIT
import React from 'react'; import React from 'react';
import GlobalHotKeys, { KeyMap } from 'utils/mousetrap-react';
import Layout from 'antd/lib/layout'; import Layout from 'antd/lib/layout';
import GlobalHotKeys, { KeyMap } from 'utils/mousetrap-react';
import { ActiveControl, Rotation } from 'reducers/interfaces'; import { ActiveControl, Rotation } from 'reducers/interfaces';
import { Canvas } from 'cvat-canvas-wrapper'; import { Canvas } from 'cvat-canvas-wrapper';
@ -32,6 +32,7 @@ interface Props {
activeControl: ActiveControl; activeControl: ActiveControl;
keyMap: KeyMap; keyMap: KeyMap;
normalizedKeyMap: Record<string, string>; normalizedKeyMap: Record<string, string>;
labels: any[];
mergeObjects(enabled: boolean): void; mergeObjects(enabled: boolean): void;
groupObjects(enabled: boolean): void; groupObjects(enabled: boolean): void;
@ -64,10 +65,11 @@ const ObservedSplitControl = ControlVisibilityObserver<SplitControlProps>(SplitC
export default function ControlsSideBarComponent(props: Props): JSX.Element { export default function ControlsSideBarComponent(props: Props): JSX.Element {
const { const {
canvasInstance,
activeControl, activeControl,
canvasInstance,
normalizedKeyMap, normalizedKeyMap,
keyMap, keyMap,
labels,
mergeObjects, mergeObjects,
groupObjects, groupObjects,
splitTrack, splitTrack,
@ -84,93 +86,13 @@ export default function ControlsSideBarComponent(props: Props): JSX.Element {
} }
}; };
const subKeyMap = { let subKeyMap: any = {
PASTE_SHAPE: keyMap.PASTE_SHAPE,
SWITCH_DRAW_MODE: keyMap.SWITCH_DRAW_MODE,
SWITCH_MERGE_MODE: keyMap.SWITCH_MERGE_MODE,
SWITCH_SPLIT_MODE: keyMap.SWITCH_SPLIT_MODE,
SWITCH_GROUP_MODE: keyMap.SWITCH_GROUP_MODE,
RESET_GROUP: keyMap.RESET_GROUP,
CANCEL: keyMap.CANCEL, CANCEL: keyMap.CANCEL,
CLOCKWISE_ROTATION: keyMap.CLOCKWISE_ROTATION, CLOCKWISE_ROTATION: keyMap.CLOCKWISE_ROTATION,
ANTICLOCKWISE_ROTATION: keyMap.ANTICLOCKWISE_ROTATION, ANTICLOCKWISE_ROTATION: keyMap.ANTICLOCKWISE_ROTATION,
}; };
const handlers = { let handlers: any = {
PASTE_SHAPE: (event: KeyboardEvent | undefined) => {
preventDefault(event);
canvasInstance.cancel();
pasteShape();
},
SWITCH_DRAW_MODE: (event: KeyboardEvent | undefined) => {
preventDefault(event);
const drawing = [
ActiveControl.DRAW_POINTS,
ActiveControl.DRAW_POLYGON,
ActiveControl.DRAW_POLYLINE,
ActiveControl.DRAW_RECTANGLE,
ActiveControl.DRAW_CUBOID,
ActiveControl.AI_TOOLS,
ActiveControl.OPENCV_TOOLS,
].includes(activeControl);
if (!drawing) {
canvasInstance.cancel();
// repeateDrawShapes gets all the latest parameters
// and calls canvasInstance.draw() with them
if (event && event.shiftKey) {
redrawShape();
} else {
repeatDrawShape();
}
} else {
if ([ActiveControl.AI_TOOLS, ActiveControl.OPENCV_TOOLS].includes(activeControl)) {
// separated API method
canvasInstance.interact({ enabled: false });
return;
}
canvasInstance.draw({ enabled: false });
}
},
SWITCH_MERGE_MODE: (event: KeyboardEvent | undefined) => {
preventDefault(event);
const merging = activeControl === ActiveControl.MERGE;
if (!merging) {
canvasInstance.cancel();
}
canvasInstance.merge({ enabled: !merging });
mergeObjects(!merging);
},
SWITCH_SPLIT_MODE: (event: KeyboardEvent | undefined) => {
preventDefault(event);
const splitting = activeControl === ActiveControl.SPLIT;
if (!splitting) {
canvasInstance.cancel();
}
canvasInstance.split({ enabled: !splitting });
splitTrack(!splitting);
},
SWITCH_GROUP_MODE: (event: KeyboardEvent | undefined) => {
preventDefault(event);
const grouping = activeControl === ActiveControl.GROUP;
if (!grouping) {
canvasInstance.cancel();
}
canvasInstance.group({ enabled: !grouping });
groupObjects(!grouping);
},
RESET_GROUP: (event: KeyboardEvent | undefined) => {
preventDefault(event);
const grouping = activeControl === ActiveControl.GROUP;
if (!grouping) {
return;
}
resetGroup();
canvasInstance.group({ enabled: false });
groupObjects(false);
},
CANCEL: (event: KeyboardEvent | undefined) => { CANCEL: (event: KeyboardEvent | undefined) => {
preventDefault(event); preventDefault(event);
if (activeControl !== ActiveControl.CURSOR) { if (activeControl !== ActiveControl.CURSOR) {
@ -187,6 +109,95 @@ export default function ControlsSideBarComponent(props: Props): JSX.Element {
}, },
}; };
if (labels.length) {
handlers = {
...handlers,
PASTE_SHAPE: (event: KeyboardEvent | undefined) => {
preventDefault(event);
canvasInstance.cancel();
pasteShape();
},
SWITCH_DRAW_MODE: (event: KeyboardEvent | undefined) => {
preventDefault(event);
const drawing = [
ActiveControl.DRAW_POINTS,
ActiveControl.DRAW_POLYGON,
ActiveControl.DRAW_POLYLINE,
ActiveControl.DRAW_RECTANGLE,
ActiveControl.DRAW_CUBOID,
ActiveControl.AI_TOOLS,
ActiveControl.OPENCV_TOOLS,
].includes(activeControl);
if (!drawing) {
canvasInstance.cancel();
// repeateDrawShapes gets all the latest parameters
// and calls canvasInstance.draw() with them
if (event && event.shiftKey) {
redrawShape();
} else {
repeatDrawShape();
}
} else {
if ([ActiveControl.AI_TOOLS, ActiveControl.OPENCV_TOOLS].includes(activeControl)) {
// separated API method
canvasInstance.interact({ enabled: false });
return;
}
canvasInstance.draw({ enabled: false });
}
},
SWITCH_MERGE_MODE: (event: KeyboardEvent | undefined) => {
preventDefault(event);
const merging = activeControl === ActiveControl.MERGE;
if (!merging) {
canvasInstance.cancel();
}
canvasInstance.merge({ enabled: !merging });
mergeObjects(!merging);
},
SWITCH_SPLIT_MODE: (event: KeyboardEvent | undefined) => {
preventDefault(event);
const splitting = activeControl === ActiveControl.SPLIT;
if (!splitting) {
canvasInstance.cancel();
}
canvasInstance.split({ enabled: !splitting });
splitTrack(!splitting);
},
SWITCH_GROUP_MODE: (event: KeyboardEvent | undefined) => {
preventDefault(event);
const grouping = activeControl === ActiveControl.GROUP;
if (!grouping) {
canvasInstance.cancel();
}
canvasInstance.group({ enabled: !grouping });
groupObjects(!grouping);
},
RESET_GROUP: (event: KeyboardEvent | undefined) => {
preventDefault(event);
const grouping = activeControl === ActiveControl.GROUP;
if (!grouping) {
return;
}
resetGroup();
canvasInstance.group({ enabled: false });
groupObjects(false);
},
};
subKeyMap = {
...subKeyMap,
PASTE_SHAPE: keyMap.PASTE_SHAPE,
SWITCH_DRAW_MODE: keyMap.SWITCH_DRAW_MODE,
SWITCH_MERGE_MODE: keyMap.SWITCH_MERGE_MODE,
SWITCH_SPLIT_MODE: keyMap.SWITCH_SPLIT_MODE,
SWITCH_GROUP_MODE: keyMap.SWITCH_GROUP_MODE,
RESET_GROUP: keyMap.RESET_GROUP,
};
}
return ( return (
<Layout.Sider className='cvat-canvas-controls-sidebar' theme='light' width={44}> <Layout.Sider className='cvat-canvas-controls-sidebar' theme='light' width={44}>
<GlobalHotKeys keyMap={subKeyMap} handlers={handlers} /> <GlobalHotKeys keyMap={subKeyMap} handlers={handlers} />
@ -213,24 +224,29 @@ export default function ControlsSideBarComponent(props: Props): JSX.Element {
<ObservedDrawRectangleControl <ObservedDrawRectangleControl
canvasInstance={canvasInstance} canvasInstance={canvasInstance}
isDrawing={activeControl === ActiveControl.DRAW_RECTANGLE} isDrawing={activeControl === ActiveControl.DRAW_RECTANGLE}
disabled={!labels.length}
/> />
<ObservedDrawPolygonControl <ObservedDrawPolygonControl
canvasInstance={canvasInstance} canvasInstance={canvasInstance}
isDrawing={activeControl === ActiveControl.DRAW_POLYGON} isDrawing={activeControl === ActiveControl.DRAW_POLYGON}
disabled={!labels.length}
/> />
<ObservedDrawPolylineControl <ObservedDrawPolylineControl
canvasInstance={canvasInstance} canvasInstance={canvasInstance}
isDrawing={activeControl === ActiveControl.DRAW_POLYLINE} isDrawing={activeControl === ActiveControl.DRAW_POLYLINE}
disabled={!labels.length}
/> />
<ObservedDrawPointsControl <ObservedDrawPointsControl
canvasInstance={canvasInstance} canvasInstance={canvasInstance}
isDrawing={activeControl === ActiveControl.DRAW_POINTS} isDrawing={activeControl === ActiveControl.DRAW_POINTS}
disabled={!labels.length}
/> />
<ObservedDrawCuboidControl <ObservedDrawCuboidControl
canvasInstance={canvasInstance} canvasInstance={canvasInstance}
isDrawing={activeControl === ActiveControl.DRAW_CUBOID} isDrawing={activeControl === ActiveControl.DRAW_CUBOID}
disabled={!labels.length}
/> />
<ObservedSetupTagControl canvasInstance={canvasInstance} isDrawing={false} /> <ObservedSetupTagControl canvasInstance={canvasInstance} isDrawing={false} disabled={!labels.length} />
<hr /> <hr />
@ -239,6 +255,7 @@ export default function ControlsSideBarComponent(props: Props): JSX.Element {
canvasInstance={canvasInstance} canvasInstance={canvasInstance}
activeControl={activeControl} activeControl={activeControl}
mergeObjects={mergeObjects} mergeObjects={mergeObjects}
disabled={!labels.length}
/> />
<ObservedGroupControl <ObservedGroupControl
switchGroupShortcut={normalizedKeyMap.SWITCH_GROUP_MODE} switchGroupShortcut={normalizedKeyMap.SWITCH_GROUP_MODE}
@ -246,12 +263,14 @@ export default function ControlsSideBarComponent(props: Props): JSX.Element {
canvasInstance={canvasInstance} canvasInstance={canvasInstance}
activeControl={activeControl} activeControl={activeControl}
groupObjects={groupObjects} groupObjects={groupObjects}
disabled={!labels.length}
/> />
<ObservedSplitControl <ObservedSplitControl
canvasInstance={canvasInstance} canvasInstance={canvasInstance}
switchSplitShortcut={normalizedKeyMap.SWITCH_SPLIT_MODE} switchSplitShortcut={normalizedKeyMap.SWITCH_SPLIT_MODE}
activeControl={activeControl} activeControl={activeControl}
splitTrack={splitTrack} splitTrack={splitTrack}
disabled={!labels.length}
/> />
<ExtraControlsControl /> <ExtraControlsControl />

@ -17,11 +17,12 @@ import withVisibilityHandling from './handle-popover-visibility';
export interface Props { export interface Props {
canvasInstance: Canvas; canvasInstance: Canvas;
isDrawing: boolean; isDrawing: boolean;
disabled?: boolean;
} }
const CustomPopover = withVisibilityHandling(Popover, 'draw-cuboid'); const CustomPopover = withVisibilityHandling(Popover, 'draw-cuboid');
function DrawPolygonControl(props: Props): JSX.Element { function DrawPolygonControl(props: Props): JSX.Element {
const { canvasInstance, isDrawing } = props; const { canvasInstance, isDrawing, disabled } = props;
const dynamcPopoverPros = isDrawing ? const dynamcPopoverPros = isDrawing ?
{ {
overlayStyle: { overlayStyle: {
@ -41,7 +42,9 @@ function DrawPolygonControl(props: Props): JSX.Element {
className: 'cvat-draw-cuboid-control', className: 'cvat-draw-cuboid-control',
}; };
return ( return disabled ? (
<Icon className='cvat-draw-cuboid-control cvat-disabled-canvas-control' component={CubeIcon} />
) : (
<CustomPopover <CustomPopover
{...dynamcPopoverPros} {...dynamcPopoverPros}
overlayClassName='cvat-draw-shape-popover' overlayClassName='cvat-draw-shape-popover'

@ -16,11 +16,12 @@ import withVisibilityHandling from './handle-popover-visibility';
export interface Props { export interface Props {
canvasInstance: Canvas; canvasInstance: Canvas;
isDrawing: boolean; isDrawing: boolean;
disabled?: boolean;
} }
const CustomPopover = withVisibilityHandling(Popover, 'draw-points'); const CustomPopover = withVisibilityHandling(Popover, 'draw-points');
function DrawPointsControl(props: Props): JSX.Element { function DrawPointsControl(props: Props): JSX.Element {
const { canvasInstance, isDrawing } = props; const { canvasInstance, isDrawing, disabled } = props;
const dynamcPopoverPros = isDrawing ? const dynamcPopoverPros = isDrawing ?
{ {
overlayStyle: { overlayStyle: {
@ -40,7 +41,9 @@ function DrawPointsControl(props: Props): JSX.Element {
className: 'cvat-draw-points-control', className: 'cvat-draw-points-control',
}; };
return ( return disabled ? (
<Icon className='cvat-draw-points-control cvat-disabled-canvas-control' component={PointIcon} />
) : (
<CustomPopover <CustomPopover
{...dynamcPopoverPros} {...dynamcPopoverPros}
overlayClassName='cvat-draw-shape-popover' overlayClassName='cvat-draw-shape-popover'

@ -16,11 +16,12 @@ import withVisibilityHandling from './handle-popover-visibility';
export interface Props { export interface Props {
canvasInstance: Canvas; canvasInstance: Canvas;
isDrawing: boolean; isDrawing: boolean;
disabled?: boolean;
} }
const CustomPopover = withVisibilityHandling(Popover, 'draw-polygon'); const CustomPopover = withVisibilityHandling(Popover, 'draw-polygon');
function DrawPolygonControl(props: Props): JSX.Element { function DrawPolygonControl(props: Props): JSX.Element {
const { canvasInstance, isDrawing } = props; const { canvasInstance, isDrawing, disabled } = props;
const dynamcPopoverPros = isDrawing ? const dynamcPopoverPros = isDrawing ?
{ {
overlayStyle: { overlayStyle: {
@ -40,7 +41,9 @@ function DrawPolygonControl(props: Props): JSX.Element {
className: 'cvat-draw-polygon-control', className: 'cvat-draw-polygon-control',
}; };
return ( return disabled ? (
<Icon className='cvat-draw-polygon-control cvat-disabled-canvas-control' component={PolygonIcon} />
) : (
<CustomPopover <CustomPopover
{...dynamcPopoverPros} {...dynamcPopoverPros}
overlayClassName='cvat-draw-shape-popover' overlayClassName='cvat-draw-shape-popover'

@ -16,11 +16,12 @@ import withVisibilityHandling from './handle-popover-visibility';
export interface Props { export interface Props {
canvasInstance: Canvas; canvasInstance: Canvas;
isDrawing: boolean; isDrawing: boolean;
disabled?: boolean;
} }
const CustomPopover = withVisibilityHandling(Popover, 'draw-polyline'); const CustomPopover = withVisibilityHandling(Popover, 'draw-polyline');
function DrawPolylineControl(props: Props): JSX.Element { function DrawPolylineControl(props: Props): JSX.Element {
const { canvasInstance, isDrawing } = props; const { canvasInstance, isDrawing, disabled } = props;
const dynamcPopoverPros = isDrawing ? const dynamcPopoverPros = isDrawing ?
{ {
overlayStyle: { overlayStyle: {
@ -40,7 +41,9 @@ function DrawPolylineControl(props: Props): JSX.Element {
className: 'cvat-draw-polyline-control', className: 'cvat-draw-polyline-control',
}; };
return ( return disabled ? (
<Icon className='cvat-draw-polyline-control cvat-disabled-canvas-control' component={PolylineIcon} />
) : (
<CustomPopover <CustomPopover
{...dynamcPopoverPros} {...dynamcPopoverPros}
overlayClassName='cvat-draw-shape-popover' overlayClassName='cvat-draw-shape-popover'

@ -16,11 +16,12 @@ import withVisibilityHandling from './handle-popover-visibility';
export interface Props { export interface Props {
canvasInstance: Canvas; canvasInstance: Canvas;
isDrawing: boolean; isDrawing: boolean;
disabled?: boolean;
} }
const CustomPopover = withVisibilityHandling(Popover, 'draw-rectangle'); const CustomPopover = withVisibilityHandling(Popover, 'draw-rectangle');
function DrawRectangleControl(props: Props): JSX.Element { function DrawRectangleControl(props: Props): JSX.Element {
const { canvasInstance, isDrawing } = props; const { canvasInstance, isDrawing, disabled } = props;
const dynamcPopoverPros = isDrawing ? const dynamcPopoverPros = isDrawing ?
{ {
overlayStyle: { overlayStyle: {
@ -40,7 +41,9 @@ function DrawRectangleControl(props: Props): JSX.Element {
className: 'cvat-draw-rectangle-control', className: 'cvat-draw-rectangle-control',
}; };
return ( return disabled ? (
<Icon className='cvat-draw-rectangle-control cvat-disabled-canvas-control' component={RectangleIcon} />
) : (
<CustomPopover <CustomPopover
{...dynamcPopoverPros} {...dynamcPopoverPros}
overlayClassName='cvat-draw-shape-popover' overlayClassName='cvat-draw-shape-popover'

@ -15,12 +15,13 @@ export interface Props {
activeControl: ActiveControl; activeControl: ActiveControl;
switchGroupShortcut: string; switchGroupShortcut: string;
resetGroupShortcut: string; resetGroupShortcut: string;
disabled?: boolean;
groupObjects(enabled: boolean): void; groupObjects(enabled: boolean): void;
} }
function GroupControl(props: Props): JSX.Element { function GroupControl(props: Props): JSX.Element {
const { const {
switchGroupShortcut, resetGroupShortcut, activeControl, canvasInstance, groupObjects, switchGroupShortcut, resetGroupShortcut, activeControl, canvasInstance, groupObjects, disabled,
} = props; } = props;
const dynamicIconProps = const dynamicIconProps =
@ -46,7 +47,9 @@ function GroupControl(props: Props): JSX.Element {
`Select and press ${resetGroupShortcut} to reset a group.`, `Select and press ${resetGroupShortcut} to reset a group.`,
].join(' '); ].join(' ');
return ( return disabled ? (
<Icon className='cvat-group-control cvat-disabled-canvas-control' component={GroupIcon} />
) : (
<CVATTooltip title={title} placement='right'> <CVATTooltip title={title} placement='right'>
<Icon {...dynamicIconProps} component={GroupIcon} /> <Icon {...dynamicIconProps} component={GroupIcon} />
</CVATTooltip> </CVATTooltip>

@ -14,12 +14,13 @@ export interface Props {
canvasInstance: Canvas; canvasInstance: Canvas;
activeControl: ActiveControl; activeControl: ActiveControl;
switchMergeShortcut: string; switchMergeShortcut: string;
disabled?: boolean;
mergeObjects(enabled: boolean): void; mergeObjects(enabled: boolean): void;
} }
function MergeControl(props: Props): JSX.Element { function MergeControl(props: Props): JSX.Element {
const { const {
switchMergeShortcut, activeControl, canvasInstance, mergeObjects, switchMergeShortcut, activeControl, canvasInstance, mergeObjects, disabled,
} = props; } = props;
const dynamicIconProps = const dynamicIconProps =
@ -40,7 +41,9 @@ function MergeControl(props: Props): JSX.Element {
}, },
}; };
return ( return disabled ? (
<Icon className='cvat-merge-control cvat-disabled-canvas-control' component={MergeIcon} />
) : (
<CVATTooltip title={`Merge shapes/tracks ${switchMergeShortcut}`} placement='right'> <CVATTooltip title={`Merge shapes/tracks ${switchMergeShortcut}`} placement='right'>
<Icon {...dynamicIconProps} component={MergeIcon} /> <Icon {...dynamicIconProps} component={MergeIcon} />
</CVATTooltip> </CVATTooltip>

@ -108,7 +108,7 @@ class OpenCVControlComponent extends React.PureComponent<Props & DispatchToProps
libraryInitialized: openCVWrapper.isInitialized, libraryInitialized: openCVWrapper.isInitialized,
initializationError: false, initializationError: false,
initializationProgress: -1, initializationProgress: -1,
activeLabelID: labels[0].id, activeLabelID: labels.length ? labels[0].id : null,
}; };
} }
@ -383,7 +383,7 @@ class OpenCVControlComponent extends React.PureComponent<Props & DispatchToProps
} }
public render(): JSX.Element { public render(): JSX.Element {
const { isActivated, canvasInstance } = this.props; const { isActivated, canvasInstance, labels } = this.props;
const dynamcPopoverPros = isActivated ? const dynamcPopoverPros = isActivated ?
{ {
overlayStyle: { overlayStyle: {
@ -394,7 +394,7 @@ class OpenCVControlComponent extends React.PureComponent<Props & DispatchToProps
const dynamicIconProps = isActivated ? const dynamicIconProps = isActivated ?
{ {
className: 'cvat-active-canvas-control cvat-opencv-control', className: 'cvat-opencv-control cvat-active-canvas-control',
onClick: (): void => { onClick: (): void => {
canvasInstance.interact({ enabled: false }); canvasInstance.interact({ enabled: false });
}, },
@ -403,7 +403,9 @@ class OpenCVControlComponent extends React.PureComponent<Props & DispatchToProps
className: 'cvat-tools-control', className: 'cvat-tools-control',
}; };
return ( return !labels.length ? (
<Icon className='cvat-opencv-control cvat-disabled-canvas-control' component={OpenCVIcon} />
) : (
<CustomPopover <CustomPopover
{...dynamcPopoverPros} {...dynamcPopoverPros}
placement='right' placement='right'

@ -15,11 +15,12 @@ import withVisibilityHandling from './handle-popover-visibility';
export interface Props { export interface Props {
canvasInstance: Canvas; canvasInstance: Canvas;
isDrawing: boolean; isDrawing: boolean;
disabled?: boolean;
} }
const CustomPopover = withVisibilityHandling(Popover, 'setup-tag'); const CustomPopover = withVisibilityHandling(Popover, 'setup-tag');
function SetupTagControl(props: Props): JSX.Element { function SetupTagControl(props: Props): JSX.Element {
const { isDrawing } = props; const { isDrawing, disabled } = props;
const dynamcPopoverPros = isDrawing ? const dynamcPopoverPros = isDrawing ?
{ {
overlayStyle: { overlayStyle: {
@ -28,7 +29,9 @@ function SetupTagControl(props: Props): JSX.Element {
} : } :
{}; {};
return ( return disabled ? (
<Icon className='cvat-setup-tag-control cvat-disabled-canvas-control' component={TagIcon} />
) : (
<CustomPopover {...dynamcPopoverPros} placement='right' content={<SetupTagPopoverContainer />}> <CustomPopover {...dynamcPopoverPros} placement='right' content={<SetupTagPopoverContainer />}>
<Icon className='cvat-setup-tag-control' component={TagIcon} /> <Icon className='cvat-setup-tag-control' component={TagIcon} />
</CustomPopover> </CustomPopover>

@ -14,12 +14,13 @@ export interface Props {
canvasInstance: Canvas; canvasInstance: Canvas;
activeControl: ActiveControl; activeControl: ActiveControl;
switchSplitShortcut: string; switchSplitShortcut: string;
disabled?: boolean;
splitTrack(enabled: boolean): void; splitTrack(enabled: boolean): void;
} }
function SplitControl(props: Props): JSX.Element { function SplitControl(props: Props): JSX.Element {
const { const {
switchSplitShortcut, activeControl, canvasInstance, splitTrack, switchSplitShortcut, activeControl, canvasInstance, splitTrack, disabled,
} = props; } = props;
const dynamicIconProps = const dynamicIconProps =
@ -40,7 +41,9 @@ function SplitControl(props: Props): JSX.Element {
}, },
}; };
return ( return disabled ? (
<Icon className='cvat-split-track-control cvat-disabled-canvas-control' component={SplitIcon} />
) : (
<CVATTooltip title={`Split a track ${switchSplitShortcut}`} placement='right'> <CVATTooltip title={`Split a track ${switchSplitShortcut}`} placement='right'>
<Icon {...dynamicIconProps} component={SplitIcon} /> <Icon {...dynamicIconProps} component={SplitIcon} />
</CVATTooltip> </CVATTooltip>

@ -111,7 +111,7 @@ export class ToolsControlComponent extends React.PureComponent<Props, State> {
this.state = { this.state = {
activeInteractor: props.interactors.length ? props.interactors[0] : null, activeInteractor: props.interactors.length ? props.interactors[0] : null,
activeTracker: props.trackers.length ? props.trackers[0] : null, activeTracker: props.trackers.length ? props.trackers[0] : null,
activeLabelID: props.labels[0].id, activeLabelID: props.labels.length ? props.labels[0].id : null,
interactiveStateID: null, interactiveStateID: null,
trackingProgress: null, trackingProgress: null,
trackingFrames: 10, trackingFrames: 10,
@ -239,7 +239,7 @@ export class ToolsControlComponent extends React.PureComponent<Props, State> {
const object = new core.classes.ObjectState({ const object = new core.classes.ObjectState({
frame, frame,
objectType: ObjectType.SHAPE, objectType: ObjectType.SHAPE,
label: labels.filter((label: any) => label.id === activeLabelID)[0], label: labels.length ? labels.filter((label: any) => label.id === activeLabelID)[0] : null,
shapeType: ShapeType.POLYGON, shapeType: ShapeType.POLYGON,
points: result.flat(), points: result.flat(),
occluded: false, occluded: false,
@ -257,7 +257,7 @@ export class ToolsControlComponent extends React.PureComponent<Props, State> {
const object = new core.classes.ObjectState({ const object = new core.classes.ObjectState({
frame, frame,
objectType: ObjectType.SHAPE, objectType: ObjectType.SHAPE,
label: labels.filter((label: any) => label.id === activeLabelID)[0], label: labels.length ? labels.filter((label: any) => label.id === activeLabelID)[0] : null,
shapeType: ShapeType.POLYGON, shapeType: ShapeType.POLYGON,
points: result.flat(), points: result.flat(),
occluded: false, occluded: false,
@ -716,7 +716,7 @@ export class ToolsControlComponent extends React.PureComponent<Props, State> {
public render(): JSX.Element | null { public render(): JSX.Element | null {
const { const {
interactors, detectors, trackers, isActivated, canvasInstance, interactors, detectors, trackers, isActivated, canvasInstance, labels,
} = this.props; } = this.props;
const { fetching, trackingProgress } = this.state; const { fetching, trackingProgress } = this.state;
@ -732,7 +732,7 @@ export class ToolsControlComponent extends React.PureComponent<Props, State> {
const dynamicIconProps = isActivated ? const dynamicIconProps = isActivated ?
{ {
className: 'cvat-active-canvas-control cvat-tools-control', className: 'cvat-tools-control cvat-active-canvas-control',
onClick: (): void => { onClick: (): void => {
canvasInstance.interact({ enabled: false }); canvasInstance.interact({ enabled: false });
}, },
@ -741,7 +741,9 @@ export class ToolsControlComponent extends React.PureComponent<Props, State> {
className: 'cvat-tools-control', className: 'cvat-tools-control',
}; };
return ( return !labels.length ? (
<Icon className=' cvat-tools-control cvat-disabled-canvas-control' component={AIToolsIcon} />
) : (
<> <>
<Modal <Modal
title='Making a server request' title='Making a server request'

@ -61,12 +61,12 @@
transform: scale(0.65); transform: scale(0.65);
padding: 2px; padding: 2px;
&:hover { &:hover:not(.cvat-disabled-canvas-control) {
background: $header-color; background: $header-color;
transform: scale(0.75); transform: scale(0.75);
} }
&:active { &:active:not(.cvat-disabled-canvas-control) {
transform: scale(0.65); transform: scale(0.65);
} }
@ -87,6 +87,10 @@
transform: scale(0.75); transform: scale(0.75);
} }
.cvat-disabled-canvas-control > svg {
filter: opacity(0.45);
}
.cvat-rotate-canvas-controls-left, .cvat-rotate-canvas-controls-left,
.cvat-rotate-canvas-controls-right { .cvat-rotate-canvas-controls-right {
transform: scale(0.65); transform: scale(0.65);

@ -45,3 +45,7 @@
padding: 5px 10px; padding: 5px 10px;
} }
} }
.labels-tag-annotation-sidebar-not-found-wrapper {
margin-top: $grid-unit-size * 4;
}

@ -4,7 +4,6 @@
import React, { useState, useEffect } from 'react'; import React, { useState, useEffect } from 'react';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import GlobalHotKeys, { KeyMap } from 'utils/mousetrap-react';
import { Action } from 'redux'; import { Action } from 'redux';
import { ThunkDispatch } from 'redux-thunk'; import { ThunkDispatch } from 'redux-thunk';
import { Row, Col } from 'antd/lib/grid'; import { Row, Col } from 'antd/lib/grid';
@ -21,6 +20,7 @@ import {
changeFrameAsync, changeFrameAsync,
rememberObject, rememberObject,
} from 'actions/annotation-actions'; } from 'actions/annotation-actions';
import GlobalHotKeys, { KeyMap } from 'utils/mousetrap-react';
import { Canvas } from 'cvat-canvas-wrapper'; import { Canvas } from 'cvat-canvas-wrapper';
import { CombinedState, ObjectType } from 'reducers/interfaces'; import { CombinedState, ObjectType } from 'reducers/interfaces';
import LabelSelector from 'components/label-selector/label-selector'; import LabelSelector from 'components/label-selector/label-selector';
@ -107,7 +107,7 @@ function TagAnnotationSidebar(props: StateToProps & DispatchToProps): JSX.Elemen
} }
}; };
const defaultLabelID = labels[0].id; const defaultLabelID = labels.length ? labels[0].id : null;
const [sidebarCollapsed, setSidebarCollapsed] = useState(false); const [sidebarCollapsed, setSidebarCollapsed] = useState(false);
const [frameTags, setFrameTags] = useState([] as any[]); const [frameTags, setFrameTags] = useState([] as any[]);
@ -196,7 +196,24 @@ function TagAnnotationSidebar(props: StateToProps & DispatchToProps): JSX.Elemen
}, },
}; };
return ( return !labels.length ? (
<Layout.Sider {...siderProps}>
{/* eslint-disable-next-line */}
<span
className={`cvat-objects-sidebar-sider
ant-layout-sider-zero-width-trigger
ant-layout-sider-zero-width-trigger-left`}
onClick={() => setSidebarCollapsed(!sidebarCollapsed)}
>
{sidebarCollapsed ? <MenuFoldOutlined title='Show' /> : <MenuUnfoldOutlined title='Hide' />}
</span>
<Row justify='center' className='labels-tag-annotation-sidebar-not-found-wrapper'>
<Col>
<Text strong>No labels are available.</Text>
</Col>
</Row>
</Layout.Sider>
) : (
<> <>
<GlobalHotKeys keyMap={subKeyMap} handlers={handlers} /> <GlobalHotKeys keyMap={subKeyMap} handlers={handlers} />
<Layout.Sider {...siderProps}> <Layout.Sider {...siderProps}>

@ -35,18 +35,16 @@ export default function ConstructorViewerItem(props: ConstructorViewerItemProps)
<EditOutlined /> <EditOutlined />
</span> </span>
</CVATTooltip> </CVATTooltip>
{label.id < 0 && ( <CVATTooltip title='Delete label'>
<CVATTooltip title='Delete label'> <span
<span role='button'
role='button' tabIndex={0}
tabIndex={0} onClick={(): void => onDelete(label)}
onClick={(): void => onDelete(label)} onKeyPress={(): boolean => false}
onKeyPress={(): boolean => false} >
> <CloseOutlined />
<CloseOutlined /> </span>
</span> </CVATTooltip>
</CVATTooltip>
)}
</div> </div>
); );
} }

@ -6,10 +6,12 @@ import './styles.scss';
import React from 'react'; import React from 'react';
import Tabs from 'antd/lib/tabs'; import Tabs from 'antd/lib/tabs';
import Button from 'antd/lib/button'; import Button from 'antd/lib/button';
import notification from 'antd/lib/notification';
import Text from 'antd/lib/typography/Text'; import Text from 'antd/lib/typography/Text';
import ModalConfirm from 'antd/lib/modal/confirm';
import copy from 'copy-to-clipboard'; import copy from 'copy-to-clipboard';
import { CopyOutlined, EditOutlined, BuildOutlined } from '@ant-design/icons'; import {
CopyOutlined, EditOutlined, BuildOutlined, ExclamationCircleOutlined,
} from '@ant-design/icons';
import CVATTooltip from 'components/common/cvat-tooltip'; import CVATTooltip from 'components/common/cvat-tooltip';
import RawViewer from './raw-viewer'; import RawViewer from './raw-viewer';
@ -144,20 +146,30 @@ export default class LabelsEditor extends React.PureComponent<LabelsEditortProps
}; };
private handleDelete = (label: Label): void => { private handleDelete = (label: Label): void => {
// the label is saved on the server, cannot delete it const deleteLabel = (): void => {
if (typeof label.id !== 'undefined' && label.id >= 0) { const { unsavedLabels, savedLabels } = this.state;
notification.error({
message: 'Could not delete the label',
description: 'It has been already saved on the server',
});
}
const { unsavedLabels, savedLabels } = this.state; const filteredUnsavedLabels = unsavedLabels.filter((_label: Label): boolean => _label.id !== label.id);
const filteredSavedLabels = savedLabels.filter((_label: Label): boolean => _label.id !== label.id);
const filteredUnsavedLabels = unsavedLabels.filter((_label: Label): boolean => _label.id !== label.id); this.setState({ savedLabels: filteredSavedLabels, unsavedLabels: filteredUnsavedLabels });
this.handleSubmit(filteredSavedLabels, filteredUnsavedLabels);
};
this.setState({ unsavedLabels: filteredUnsavedLabels }); if (typeof label.id !== 'undefined' && label.id >= 0) {
this.handleSubmit(savedLabels, filteredUnsavedLabels); ModalConfirm({
title: `Do you want to delete "${label.name}" label?`,
icon: <ExclamationCircleOutlined />,
content: 'This action is irreversible. Annotation corresponding with this label will be deleted.',
type: 'warning',
okType: 'danger',
onOk() {
deleteLabel();
},
});
} else {
deleteLabel();
}
}; };
private handleSubmit(savedLabels: Label[], unsavedLabels: Label[]): void { private handleSubmit(savedLabels: Label[], unsavedLabels: Label[]): void {

@ -2,7 +2,6 @@
// //
// SPDX-License-Identifier: MIT // SPDX-License-Identifier: MIT
import { KeyMap } from 'utils/mousetrap-react';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { Canvas } from 'cvat-canvas-wrapper'; import { Canvas } from 'cvat-canvas-wrapper';
@ -18,6 +17,7 @@ import {
} from 'actions/annotation-actions'; } from 'actions/annotation-actions';
import ControlsSideBarComponent from 'components/annotation-page/standard-workspace/controls-side-bar/controls-side-bar'; import ControlsSideBarComponent from 'components/annotation-page/standard-workspace/controls-side-bar/controls-side-bar';
import { ActiveControl, CombinedState, Rotation } from 'reducers/interfaces'; import { ActiveControl, CombinedState, Rotation } from 'reducers/interfaces';
import { KeyMap } from 'utils/mousetrap-react';
interface StateToProps { interface StateToProps {
canvasInstance: Canvas; canvasInstance: Canvas;
@ -25,6 +25,7 @@ interface StateToProps {
activeControl: ActiveControl; activeControl: ActiveControl;
keyMap: KeyMap; keyMap: KeyMap;
normalizedKeyMap: Record<string, string>; normalizedKeyMap: Record<string, string>;
labels: any[];
} }
interface DispatchToProps { interface DispatchToProps {
@ -42,6 +43,7 @@ function mapStateToProps(state: CombinedState): StateToProps {
const { const {
annotation: { annotation: {
canvas: { instance: canvasInstance, activeControl }, canvas: { instance: canvasInstance, activeControl },
job: { labels },
}, },
settings: { settings: {
player: { rotateAll }, player: { rotateAll },
@ -53,6 +55,7 @@ function mapStateToProps(state: CombinedState): StateToProps {
rotateAll, rotateAll,
canvasInstance, canvasInstance,
activeControl, activeControl,
labels,
normalizedKeyMap, normalizedKeyMap,
keyMap, keyMap,
}; };

@ -1,4 +1,4 @@
// Copyright (C) 2020 Intel Corporation // Copyright (C) 2020-2021 Intel Corporation
// //
// SPDX-License-Identifier: MIT // SPDX-License-Identifier: MIT
@ -79,7 +79,7 @@ class DrawShapePopoverContainer extends React.PureComponent<Props, State> {
super(props); super(props);
const { shapeType } = props; const { shapeType } = props;
const defaultLabelID = props.labels[0].id; const defaultLabelID = props.labels.length ? props.labels[0].id : null;
const defaultRectDrawingMethod = RectDrawingMethod.CLASSIC; const defaultRectDrawingMethod = RectDrawingMethod.CLASSIC;
const defaultCuboidDrawingMethod = CuboidDrawingMethod.CLASSIC; const defaultCuboidDrawingMethod = CuboidDrawingMethod.CLASSIC;
this.state = { this.state = {

@ -182,7 +182,7 @@ export default (state = defaultState, action: AnyAction): AnnotationState => {
}, },
drawing: { drawing: {
...state.drawing, ...state.drawing,
activeLabelID: job.task.labels[0].id, activeLabelID: job.task.labels.length ? job.task.labels[0].id : null,
activeObjectType: job.task.mode === 'interpolation' ? ObjectType.TRACK : ObjectType.SHAPE, activeObjectType: job.task.mode === 'interpolation' ? ObjectType.TRACK : ObjectType.SHAPE,
}, },
canvas: { canvas: {

@ -0,0 +1,13 @@
// Copyright (C) 2021 Intel Corporation
//
// SPDX-License-Identifier: MIT
import { useRef, useEffect } from 'react';
// eslint-disable-next-line import/prefer-default-export
export function usePrevious<T>(value: T): T | undefined {
const ref = useRef<T>();
useEffect(() => {
ref.current = value;
});
return ref.current;
}

@ -70,10 +70,17 @@ class LabelSerializer(serializers.ModelSerializer):
attributes = AttributeSerializer(many=True, source='attributespec_set', attributes = AttributeSerializer(many=True, source='attributespec_set',
default=[]) default=[])
color = serializers.CharField(allow_blank=True, required=False) color = serializers.CharField(allow_blank=True, required=False)
deleted = serializers.BooleanField(required=False)
class Meta: class Meta:
model = models.Label model = models.Label
fields = ('id', 'name', 'color', 'attributes') fields = ('id', 'name', 'color', 'attributes', 'deleted')
def validate(self, attrs):
if attrs.get('deleted') == True and attrs.get('id') is None:
raise serializers.ValidationError('Deleted label must have an ID')
return attrs
@staticmethod @staticmethod
def update_instance(validated_data, parent_instance): def update_instance(validated_data, parent_instance):
@ -96,6 +103,9 @@ class LabelSerializer(serializers.ModelSerializer):
else: else:
db_label = models.Label.objects.create(name=validated_data.get('name'), **instance) db_label = models.Label.objects.create(name=validated_data.get('name'), **instance)
logger.info("New {} label was created".format(db_label.name)) logger.info("New {} label was created".format(db_label.name))
if validated_data.get('deleted') == True:
db_label.delete()
return
if not validated_data.get('color', None): if not validated_data.get('color', None):
label_names = [l.name for l in label_names = [l.name for l in
instance[tuple(instance.keys())[0]].label_set.exclude(id=db_label.id).order_by('id') instance[tuple(instance.keys())[0]].label_set.exclude(id=db_label.id).order_by('id')

@ -28,7 +28,7 @@ from pycocotools import coco as coco_loader
from rest_framework import status from rest_framework import status
from rest_framework.test import APIClient, APITestCase from rest_framework.test import APIClient, APITestCase
from cvat.apps.engine.models import (AttributeType, Data, Job, Project, from cvat.apps.engine.models import (AttributeSpec, AttributeType, Data, Job, Project,
Segment, StatusChoice, Task, Label, StorageMethodChoice, StorageChoice) Segment, StatusChoice, Task, Label, StorageMethodChoice, StorageChoice)
from cvat.apps.engine.prepare import prepare_meta, prepare_meta_for_upload from cvat.apps.engine.prepare import prepare_meta, prepare_meta_for_upload
from cvat.apps.engine.media_extractors import ValidateDimension from cvat.apps.engine.media_extractors import ValidateDimension
@ -72,6 +72,7 @@ def create_db_task(data):
os.makedirs(db_data.get_data_dirname()) os.makedirs(db_data.get_data_dirname())
os.makedirs(db_data.get_upload_dirname()) os.makedirs(db_data.get_upload_dirname())
labels = data.pop('labels', None)
db_task = Task.objects.create(**data) db_task = Task.objects.create(**data)
shutil.rmtree(db_task.get_task_dirname(), ignore_errors=True) shutil.rmtree(db_task.get_task_dirname(), ignore_errors=True)
os.makedirs(db_task.get_task_dirname()) os.makedirs(db_task.get_task_dirname())
@ -80,6 +81,17 @@ def create_db_task(data):
db_task.data = db_data db_task.data = db_data
db_task.save() db_task.save()
if not labels is None:
for label_data in labels:
attributes = label_data.pop('attributes', None)
db_label = Label(task=db_task, **label_data)
db_label.save()
if not attributes is None:
for attribute_data in attributes:
db_attribute = AttributeSpec(label=db_label, **attribute_data)
db_attribute.save()
for x in range(0, db_task.data.size, db_task.segment_size): for x in range(0, db_task.data.size, db_task.segment_size):
start_frame = x start_frame = x
stop_frame = min(x + db_task.segment_size - 1, db_task.data.size - 1) stop_frame = min(x + db_task.segment_size - 1, db_task.data.size - 1)
@ -96,6 +108,26 @@ def create_db_task(data):
return db_task return db_task
def create_db_project(data):
labels = data.pop('labels', None)
db_project = Project.objects.create(**data)
shutil.rmtree(db_project.get_project_dirname(), ignore_errors=True)
os.makedirs(db_project.get_project_dirname())
os.makedirs(db_project.get_project_logs_dirname())
if not labels is None:
for label_data in labels:
attributes = label_data.pop('attributes', None)
db_label = Label(project=db_project, **label_data)
db_label.save()
if not attributes is None:
for attribute_data in attributes:
db_attribute = AttributeSpec(label=db_label, **attribute_data)
db_attribute.save()
return db_project
def create_dummy_db_tasks(obj, project=None): def create_dummy_db_tasks(obj, project=None):
tasks = [] tasks = []
@ -159,14 +191,14 @@ def create_dummy_db_projects(obj):
"owner": obj.owner, "owner": obj.owner,
"assignee": obj.assignee, "assignee": obj.assignee,
} }
db_project = Project.objects.create(**data) db_project = create_db_project(data)
projects.append(db_project) projects.append(db_project)
data = { data = {
"name": "my project without assignee", "name": "my project without assignee",
"owner": obj.user, "owner": obj.user,
} }
db_project = Project.objects.create(**data) db_project = create_db_project(data)
create_dummy_db_tasks(obj, db_project) create_dummy_db_tasks(obj, db_project)
projects.append(db_project) projects.append(db_project)
@ -175,14 +207,14 @@ def create_dummy_db_projects(obj):
"owner": obj.owner, "owner": obj.owner,
"assignee": obj.assignee, "assignee": obj.assignee,
} }
db_project = Project.objects.create(**data) db_project = create_db_project(data)
create_dummy_db_tasks(obj, db_project) create_dummy_db_tasks(obj, db_project)
projects.append(db_project) projects.append(db_project)
data = { data = {
"name": "public project", "name": "public project",
} }
db_project = Project.objects.create(**data) db_project = create_db_project(data)
create_dummy_db_tasks(obj, db_project) create_dummy_db_tasks(obj, db_project)
projects.append(db_project) projects.append(db_project)
@ -191,7 +223,7 @@ def create_dummy_db_projects(obj):
"owner": obj.admin, "owner": obj.admin,
"assignee": obj.assignee, "assignee": obj.assignee,
} }
db_project = Project.objects.create(**data) db_project = create_db_project(data)
create_dummy_db_tasks(obj, db_project) create_dummy_db_tasks(obj, db_project)
projects.append(db_project) projects.append(db_project)
@ -1157,7 +1189,7 @@ class ProjectPartialUpdateAPITestCase(APITestCase):
def _check_response(self, response, db_project, data): def _check_response(self, response, db_project, data):
self.assertEqual(response.status_code, status.HTTP_200_OK) self.assertEqual(response.status_code, status.HTTP_200_OK)
name = data.get("name", data.get("name", db_project.name)) name = data.get("name", db_project.name)
self.assertEqual(response.data["name"], name) self.assertEqual(response.data["name"], name)
response_owner = response.data["owner"]["id"] if response.data["owner"] else None response_owner = response.data["owner"]["id"] if response.data["owner"] else None
db_owner = db_project.owner.id if db_project.owner else None db_owner = db_project.owner.id if db_project.owner else None
@ -1167,6 +1199,16 @@ class ProjectPartialUpdateAPITestCase(APITestCase):
self.assertEqual(response_assignee, data.get("assignee_id", db_assignee)) self.assertEqual(response_assignee, data.get("assignee_id", db_assignee))
self.assertEqual(response.data["status"], data.get("status", db_project.status)) self.assertEqual(response.data["status"], data.get("status", db_project.status))
self.assertEqual(response.data["bug_tracker"], data.get("bug_tracker", db_project.bug_tracker)) self.assertEqual(response.data["bug_tracker"], data.get("bug_tracker", db_project.bug_tracker))
if data.get("labels"):
self.assertListEqual(
[label["name"] for label in data.get("labels") if not label.get("deleted", False)],
[label["name"] for label in response.data["labels"]]
)
else:
self.assertListEqual(
[label.name for label in db_project.label_set.all()],
[label["name"] for label in response.data["labels"]]
)
def _check_api_v1_projects_id(self, user, data): def _check_api_v1_projects_id(self, user, data):
for db_project in self.projects: for db_project in self.projects:
@ -1180,9 +1222,13 @@ class ProjectPartialUpdateAPITestCase(APITestCase):
def test_api_v1_projects_id_admin(self): def test_api_v1_projects_id_admin(self):
data = { data = {
"name": "new name for the project", "name": "project with some labels",
"owner_id": self.owner.id, "owner_id": self.owner.id,
"bug_tracker": "https://new.bug.tracker", "bug_tracker": "https://new.bug.tracker",
"labels": [
{"name": "car"},
{"name": "person"}
],
} }
self._check_api_v1_projects_id(self.admin, data) self._check_api_v1_projects_id(self.admin, data)
@ -1205,6 +1251,103 @@ class ProjectPartialUpdateAPITestCase(APITestCase):
} }
self._check_api_v1_projects_id(None, data) self._check_api_v1_projects_id(None, data)
class UpdateLabelsAPITestCase(APITestCase):
def setUp(self):
self.client = APIClient()
def assertLabelsEqual(self, label1, label2):
self.assertEqual(label1.get("name", label2.get("name")), label2.get("name"))
self.assertEqual(label1.get("color", label2.get("color")), label2.get("color"))
def _check_response(self, response, db_object, data):
self.assertEqual(response.status_code, status.HTTP_200_OK)
db_labels = db_object.label_set.all()
response_labels = response.data["labels"]
for label in data["labels"]:
if label.get("id", None) is None:
self.assertLabelsEqual(
label,
[l for l in response_labels if label.get("name") == l.get("name")][0],
)
db_labels = [l for l in db_labels if label.get("name") != l.name]
response_labels = [l for l in response_labels if label.get("name") != l.get("name")]
else:
if not label.get("deleted", False):
self.assertLabelsEqual(
label,
[l for l in response_labels if label.get("id") == l.get("id")][0],
)
response_labels = [l for l in response_labels if label.get("id") != l.get("id")]
db_labels = [l for l in db_labels if label.get("id") != l.id]
else:
self.assertEqual(
len([l for l in response_labels if label.get("id") == l.get("id")]), 0
)
self.assertEqual(len(response_labels), len(db_labels))
class ProjectUpdateLabelsAPITestCase(UpdateLabelsAPITestCase):
@classmethod
def setUpTestData(cls):
project_data = {
"name": "Project with labels",
"bug_tracker": "https://new.bug.tracker",
"labels": [{
"name": "car",
"color": "#ff00ff",
"attributes": [{
"name": "bool_attribute",
"mutable": True,
"input_type": AttributeType.CHECKBOX,
"default_value": "true"
}],
}, {
"name": "person",
}]
}
create_db_users(cls)
db_project = create_db_project(project_data)
create_dummy_db_tasks(cls, db_project)
cls.project = db_project
def _check_api_v1_project(self, data):
response = self._run_api_v1_project_id(self.project.id, self.admin, data)
self._check_response(response, self.project, data)
def _run_api_v1_project_id(self, pid, user, data):
with ForceLogin(user, self.client):
response = self.client.patch('/api/v1/projects/{}'.format(pid),
data=data, format="json")
return response
def test_api_v1_projects_create_label(self):
data = {
"labels": [{
"name": "new label",
}],
}
self._check_api_v1_project(data)
def test_api_v1_projects_edit_label(self):
data = {
"labels": [{
"id": 1,
"name": "New name for label",
"color": "#fefefe",
}],
}
self._check_api_v1_project(data)
def test_api_v1_projects_delete_label(self):
data = {
"labels": [{
"id": 2,
"name": "Label for deletion",
"deleted": True
}]
}
self._check_api_v1_project(data)
class ProjectListOfTasksAPITestCase(APITestCase): class ProjectListOfTasksAPITestCase(APITestCase):
def setUp(self): def setUp(self):
self.client = APIClient() self.client = APIClient()
@ -1566,6 +1709,73 @@ class TaskPartialUpdateAPITestCase(TaskUpdateAPITestCase):
} }
self._check_api_v1_tasks_id(None, data) self._check_api_v1_tasks_id(None, data)
class TaskUpdateLabelsAPITestCase(UpdateLabelsAPITestCase):
@classmethod
def setUpTestData(cls):
task_data = {
"name": "Project with labels",
"bug_tracker": "https://new.bug.tracker",
"overlap": 0,
"segment_size": 100,
"image_quality": 75,
"size": 100,
"labels": [{
"name": "car",
"color": "#ff00ff",
"attributes": [{
"name": "bool_attribute",
"mutable": True,
"input_type": AttributeType.CHECKBOX,
"default_value": "true"
}],
}, {
"name": "person",
}]
}
create_db_users(cls)
db_task = create_db_task(task_data)
cls.task = db_task
def _check_api_v1_task(self, data):
response = self._run_api_v1_task_id(self.task.id, self.admin, data)
self._check_response(response, self.task, data)
def _run_api_v1_task_id(self, tid, user, data):
with ForceLogin(user, self.client):
response = self.client.patch('/api/v1/tasks/{}'.format(tid),
data=data, format="json")
return response
def test_api_v1_tasks_create_label(self):
data = {
"labels": [{
"name": "new label",
}],
}
self._check_api_v1_task(data)
def test_api_v1_tasks_edit_label(self):
data = {
"labels": [{
"id": 1,
"name": "New name for label",
"color": "#fefefe",
}],
}
self._check_api_v1_task(data)
def test_api_v1_tasks_delete_label(self):
data = {
"labels": [{
"id": 2,
"name": "Label for deletion",
"deleted": True
}]
}
self._check_api_v1_task(data)
class TaskCreateAPITestCase(APITestCase): class TaskCreateAPITestCase(APITestCase):
def setUp(self): def setUp(self):
self.client = APIClient() self.client = APIClient()

@ -19,6 +19,9 @@ os.makedirs(MEDIA_DATA_ROOT, exist_ok=True)
TASKS_ROOT = os.path.join(DATA_ROOT, 'tasks') TASKS_ROOT = os.path.join(DATA_ROOT, 'tasks')
os.makedirs(TASKS_ROOT, exist_ok=True) os.makedirs(TASKS_ROOT, exist_ok=True)
PROJECTS_ROOT = os.path.join(DATA_ROOT, 'projects')
os.makedirs(PROJECTS_ROOT, exist_ok=True)
MODELS_ROOT = os.path.join(DATA_ROOT, 'models') MODELS_ROOT = os.path.join(DATA_ROOT, 'models')
os.makedirs(MODELS_ROOT, exist_ok=True) os.makedirs(MODELS_ROOT, exist_ok=True)

Loading…
Cancel
Save