React UI: Added annotation menus, added shape context menu, added some confirmations before dangerous actions (#1123)

* Annotation menu, modified tasks menu

* Removed extra styles

* Context menu using side panel

* Mousewheel on draw

* Added more cursor icons

* Do not check .svg & .scss by eslint
main
Boris Sekachev 6 years ago committed by GitHub
parent 42614c28a1
commit 939de868a9
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -445,6 +445,8 @@ export class CanvasViewImpl implements CanvasView, Listener {
});
}
}
e.preventDefault();
}
if (value) {
@ -615,9 +617,10 @@ export class CanvasViewImpl implements CanvasView, Listener {
if ([1, 2].includes(event.which)) {
if ([Mode.DRAG_CANVAS, Mode.IDLE].includes(this.mode)) {
self.controller.enableDrag(event.clientX, event.clientY);
} else if (this.mode === Mode.ZOOM_CANVAS && event.which === 2) {
} else if ([Mode.ZOOM_CANVAS, Mode.DRAW].includes(this.mode) && event.which === 2) {
self.controller.enableDrag(event.clientX, event.clientY);
}
event.preventDefault();
}
});
@ -751,25 +754,37 @@ export class CanvasViewImpl implements CanvasView, Listener {
} else if (reason === UpdateReasons.DRAW) {
const data: DrawData = this.controller.drawData;
if (data.enabled) {
this.canvas.style.cursor = 'crosshair';
this.mode = Mode.DRAW;
} else {
this.canvas.style.cursor = '';
}
this.drawHandler.draw(data, this.geometry);
} else if (reason === UpdateReasons.MERGE) {
const data: MergeData = this.controller.mergeData;
if (data.enabled) {
this.canvas.style.cursor = 'copy';
this.mode = Mode.MERGE;
} else {
this.canvas.style.cursor = '';
}
this.mergeHandler.merge(data);
} else if (reason === UpdateReasons.SPLIT) {
const data: SplitData = this.controller.splitData;
if (data.enabled) {
this.canvas.style.cursor = 'copy';
this.mode = Mode.SPLIT;
} else {
this.canvas.style.cursor = '';
}
this.splitHandler.split(data);
} else if (reason === UpdateReasons.GROUP) {
const data: GroupData = this.controller.groupData;
if (data.enabled) {
this.canvas.style.cursor = 'copy';
this.mode = Mode.GROUP;
} else {
this.canvas.style.cursor = '';
}
this.groupHandler.group(data);
} else if (reason === UpdateReasons.SELECT) {

@ -27,13 +27,13 @@ export class SplitHandlerImpl implements SplitHandler {
private release(): void {
if (this.initialized) {
this.resetShape();
this.canvas.node.removeEventListener('mousemove', this.onFindObject);
this.canvas.node.removeEventListener('mousemove', this.findObject);
this.initialized = false;
}
}
private initSplitting(): void {
this.canvas.node.addEventListener('mousemove', this.onFindObject);
this.canvas.node.addEventListener('mousemove', this.findObject);
this.initialized = true;
this.splitDone = false;
}
@ -47,6 +47,11 @@ export class SplitHandlerImpl implements SplitHandler {
this.release();
}
private findObject = (e: MouseEvent): void => {
this.resetShape();
this.onFindObject(e);
};
public constructor(
onSplitDone: (object: any) => void,
onFindObject: (event: MouseEvent) => void,
@ -83,8 +88,6 @@ export class SplitHandlerImpl implements SplitHandler {
once: true,
});
}
} else {
this.resetShape();
}
}

@ -17,6 +17,7 @@ module.exports = {
'@typescript-eslint',
'import',
],
'ignorePatterns': ['*.svg', '*.scss'],
'extends': [
'plugin:@typescript-eslint/recommended',
'airbnb-typescript',

@ -68,6 +68,105 @@ export enum AnnotationActionTypes {
CHANGE_JOB_STATUS = 'CHANGE_JOB_STATUS',
CHANGE_JOB_STATUS_SUCCESS = 'CHANGE_JOB_STATUS_SUCCESS',
CHANGE_JOB_STATUS_FAILED = 'CHANGE_JOB_STATUS_FAILED',
UPLOAD_JOB_ANNOTATIONS = 'UPLOAD_JOB_ANNOTATIONS',
UPLOAD_JOB_ANNOTATIONS_SUCCESS = 'UPLOAD_JOB_ANNOTATIONS_SUCCESS',
UPLOAD_JOB_ANNOTATIONS_FAILED = 'UPLOAD_JOB_ANNOTATIONS_FAILED',
REMOVE_JOB_ANNOTATIONS_SUCCESS = 'REMOVE_JOB_ANNOTATIONS_SUCCESS',
REMOVE_JOB_ANNOTATIONS_FAILED = 'REMOVE_JOB_ANNOTATIONS_FAILED',
UPDATE_CANVAS_CONTEXT_MENU = 'UPDATE_CANVAS_CONTEXT_MENU',
}
export function updateCanvasContextMenu(visible: boolean, left: number, top: number): AnyAction {
return {
type: AnnotationActionTypes.UPDATE_CANVAS_CONTEXT_MENU,
payload: {
visible,
left,
top,
},
};
}
export function removeAnnotationsAsync(sessionInstance: any):
ThunkAction<Promise<void>, {}, {}, AnyAction> {
return async (dispatch: ActionCreator<Dispatch>): Promise<void> => {
try {
sessionInstance.annotations.clear();
dispatch({
type: AnnotationActionTypes.REMOVE_JOB_ANNOTATIONS_SUCCESS,
payload: {
sessionInstance,
},
});
} catch (error) {
dispatch({
type: AnnotationActionTypes.REMOVE_JOB_ANNOTATIONS_FAILED,
payload: {
error,
},
});
}
};
}
export function uploadJobAnnotationsAsync(job: any, loader: any, file: File):
ThunkAction<Promise<void>, {}, {}, AnyAction> {
return async (dispatch: ActionCreator<Dispatch>): Promise<void> => {
try {
const store = getCVATStore();
const state: CombinedState = store.getState();
if (state.tasks.activities.loads[job.task.id]) {
throw Error('Annotations is being uploaded for the task');
}
if (state.annotation.activities.loads[job.id]) {
throw Error('Only one uploading of annotations for a job allowed at the same time');
}
dispatch({
type: AnnotationActionTypes.UPLOAD_JOB_ANNOTATIONS,
payload: {
job,
loader,
},
});
const frame = state.annotation.player.frame.number;
await job.annotations.upload(file, loader);
// One more update to escape some problems
// in canvas when shape with the same
// clientID has different type (polygon, rectangle) for example
dispatch({
type: AnnotationActionTypes.UPLOAD_JOB_ANNOTATIONS_SUCCESS,
payload: {
job,
states: [],
},
});
await job.annotations.clear(true);
const states = await job.annotations.get(frame);
setTimeout(() => {
dispatch({
type: AnnotationActionTypes.UPLOAD_JOB_ANNOTATIONS_SUCCESS,
payload: {
job,
states,
},
});
});
} catch (error) {
dispatch({
type: AnnotationActionTypes.UPLOAD_JOB_ANNOTATIONS_FAILED,
payload: {
job,
error,
},
});
}
};
}
export function changeJobStatusAsync(jobInstance: any, status: string):

@ -1,6 +1,10 @@
import { AnyAction, Dispatch, ActionCreator } from 'redux';
import { ThunkAction } from 'redux-thunk';
import { TasksQuery } from 'reducers/interfaces';
import {
TasksQuery,
CombinedState,
} from 'reducers/interfaces';
import { getCVATStore } from 'cvat-store';
import getCore from 'cvat-core';
import { getInferenceStatusAsync } from './models-actions';
@ -213,6 +217,11 @@ export function loadAnnotationsAsync(task: any, loader: any, file: File):
ThunkAction<Promise<void>, {}, {}, AnyAction> {
return async (dispatch: ActionCreator<Dispatch>): Promise<void> => {
try {
const store = getCVATStore();
const state: CombinedState = store.getState();
if (state.tasks.activities.loads[task.id]) {
throw Error('Only one loading of annotations for a task allowed at the same time');
}
dispatch(loadAnnotations(task, loader));
await task.annotations.upload(file, loader);
} catch (error) {

@ -8,135 +8,151 @@ import {
import { ClickParam } from 'antd/lib/menu/index';
import LoaderItemComponent from './loader-item';
import DumperItemComponent from './dumper-item';
import ExportItemComponent from './export-item';
import DumpSubmenu from './dump-submenu';
import LoadSubmenu from './load-submenu';
import ExportSubmenu from './export-submenu';
interface ActionsMenuComponentProps {
taskInstance: any;
loaders: any[];
dumpers: any[];
exporters: any[];
interface Props {
taskID: number;
taskMode: string;
bugTracker: string;
loaders: string[];
dumpers: string[];
exporters: string[];
loadActivity: string | null;
dumpActivities: string[] | null;
exportActivities: string[] | null;
installedTFAnnotation: boolean;
installedTFSegmentation: boolean;
installedAutoAnnotation: boolean;
inferenceIsActive: boolean;
onLoadAnnotation: (taskInstance: any, loader: any, file: File) => void;
onDumpAnnotation: (taskInstance: any, dumper: any) => void;
onExportDataset: (taskInstance: any, exporter: any) => void;
onDeleteTask: (taskInstance: any) => void;
onOpenRunWindow: (taskInstance: any) => void;
}
interface MinActionsMenuProps {
taskInstance: any;
onDeleteTask: (task: any) => void;
onOpenRunWindow: (taskInstance: any) => void;
onClickMenu: (params: ClickParam, file?: File) => void;
}
export function handleMenuClick(props: MinActionsMenuProps, params: ClickParam): void {
const { taskInstance } = props;
const tracker = taskInstance.bugTracker;
if (params.keyPath.length !== 2) {
switch (params.key) {
case 'tracker': {
// false positive eslint(security/detect-non-literal-fs-filename)
// eslint-disable-next-line
window.open(`${tracker}`, '_blank');
return;
} case 'auto_annotation': {
props.onOpenRunWindow(taskInstance);
return;
} case 'delete': {
const taskID = taskInstance.id;
Modal.confirm({
title: `The task ${taskID} will be deleted`,
content: 'All related data (images, annotations) will be lost. Continue?',
onOk: () => {
props.onDeleteTask(taskInstance);
},
});
break;
} default: {
// do nothing
}
}
}
export enum Actions {
DUMP_TASK_ANNO = 'dump_task_anno',
LOAD_TASK_ANNO = 'load_task_anno',
EXPORT_TASK_DATASET = 'export_task_dataset',
DELETE_TASK = 'delete_task',
RUN_AUTO_ANNOTATION = 'run_auto_annotation',
OPEN_BUG_TRACKER = 'open_bug_tracker',
}
export default function ActionsMenuComponent(props: ActionsMenuComponentProps): JSX.Element {
export default function ActionsMenuComponent(props: Props): JSX.Element {
const {
taskInstance,
taskID,
taskMode,
bugTracker,
installedAutoAnnotation,
installedTFAnnotation,
installedTFSegmentation,
inferenceIsActive,
dumpers,
loaders,
exporters,
inferenceIsActive,
onClickMenu,
dumpActivities,
exportActivities,
loadActivity,
} = props;
const tracker = taskInstance.bugTracker;
const renderModelRunner = installedAutoAnnotation
|| installedTFAnnotation || installedTFSegmentation;
let latestParams: ClickParam | null = null;
function onClickMenuWrapper(params: ClickParam | null, file?: File): void {
const copyParams = params || latestParams;
if (!copyParams) {
return;
}
latestParams = copyParams;
if (copyParams.keyPath.length === 2) {
const [, action] = copyParams.keyPath;
if (action === Actions.LOAD_TASK_ANNO) {
if (file) {
Modal.confirm({
title: 'Current annotation will be lost',
content: 'You are going to upload new annotations to this task. Continue?',
onOk: () => {
onClickMenu(copyParams, file);
},
okButtonProps: {
type: 'danger',
},
okText: 'Update',
});
}
} else {
onClickMenu(copyParams);
}
} else if (copyParams.key === Actions.DELETE_TASK) {
Modal.confirm({
title: `The task ${taskID} will be deleted`,
content: 'All related data (images, annotations) will be lost. Continue?',
onOk: () => {
onClickMenu(copyParams);
},
okButtonProps: {
type: 'danger',
},
okText: 'Delete',
});
} else {
onClickMenu(copyParams);
}
}
return (
<Menu
selectable={false}
className='cvat-actions-menu'
onClick={
(params: ClickParam): void => handleMenuClick(props, params)
}
onClick={onClickMenuWrapper}
>
<Menu.SubMenu key='dump' title='Dump annotations'>
{
dumpers.map((dumper): JSX.Element => DumperItemComponent({
dumper,
taskInstance: props.taskInstance,
dumpActivity: (props.dumpActivities || [])
.filter((_dumper: string) => _dumper === dumper.name)[0] || null,
onDumpAnnotation: props.onDumpAnnotation,
}))
}
</Menu.SubMenu>
<Menu.SubMenu key='load' title='Upload annotations'>
{
loaders.map((loader): JSX.Element => LoaderItemComponent({
loader,
taskInstance: props.taskInstance,
loadActivity: props.loadActivity,
onLoadAnnotation: props.onLoadAnnotation,
}))
}
</Menu.SubMenu>
<Menu.SubMenu key='export' title='Export as a dataset'>
{
exporters.map((exporter): JSX.Element => ExportItemComponent({
exporter,
taskInstance: props.taskInstance,
exportActivity: (props.exportActivities || [])
.filter((_exporter: string) => _exporter === exporter.name)[0] || null,
onExportDataset: props.onExportDataset,
}))
}
</Menu.SubMenu>
{tracker && <Menu.Item key='tracker'>Open bug tracker</Menu.Item>}
{
DumpSubmenu({
taskMode,
dumpers,
dumpActivities,
menuKey: Actions.DUMP_TASK_ANNO,
})
}
{
LoadSubmenu({
loaders,
loadActivity,
onFileUpload: (file: File): void => {
onClickMenuWrapper(null, file);
},
menuKey: Actions.LOAD_TASK_ANNO,
})
}
{
ExportSubmenu({
exporters,
exportActivities,
menuKey: Actions.EXPORT_TASK_DATASET,
})
}
{!!bugTracker && <Menu.Item key={Actions.OPEN_BUG_TRACKER}>Open bug tracker</Menu.Item>}
{
renderModelRunner
&& (
<Menu.Item
disabled={inferenceIsActive}
key='auto_annotation'
key={Actions.RUN_AUTO_ANNOTATION}
>
Automatic annotation
</Menu.Item>
)
}
<hr />
<Menu.Item key='delete'>Delete</Menu.Item>
<Menu.Item key={Actions.DELETE_TASK}>Delete</Menu.Item>
</Menu>
);
}

@ -0,0 +1,51 @@
import React from 'react';
import {
Menu,
Icon,
} from 'antd';
import Text from 'antd/lib/typography/Text';
function isDefaultFormat(dumperName: string, taskMode: string): boolean {
return (dumperName === 'CVAT XML 1.1 for videos' && taskMode === 'interpolation')
|| (dumperName === 'CVAT XML 1.1 for images' && taskMode === 'annotation');
}
interface Props {
taskMode: string;
menuKey: string;
dumpers: string[];
dumpActivities: string[] | null;
}
export default function DumpSubmenu(props: Props): JSX.Element {
const {
taskMode,
menuKey,
dumpers,
dumpActivities,
} = props;
return (
<Menu.SubMenu key={menuKey} title='Dump annotations'>
{
dumpers.map((dumper: string): JSX.Element => {
const pending = (dumpActivities || []).includes(dumper);
const isDefault = isDefaultFormat(dumper, taskMode);
return (
<Menu.Item
key={dumper}
disabled={pending}
className='cvat-menu-dump-submenu-item'
>
<Icon type='download' />
<Text strong={isDefault}>{dumper}</Text>
{pending && <Icon style={{ marginLeft: 10 }} type='loading' />}
</Menu.Item>
);
})
}
</Menu.SubMenu>
);
}

@ -1,50 +0,0 @@
import React from 'react';
import {
Menu,
Button,
Icon,
} from 'antd';
import Text from 'antd/lib/typography/Text';
interface DumperItemComponentProps {
taskInstance: any;
dumper: any;
dumpActivity: string | null;
onDumpAnnotation: (task: any, dumper: any) => void;
}
function isDefaultFormat(dumperName: string, taskMode: string): boolean {
return (dumperName === 'CVAT XML 1.1 for videos' && taskMode === 'interpolation')
|| (dumperName === 'CVAT XML 1.1 for images' && taskMode === 'annotation');
}
export default function DumperItemComponent(props: DumperItemComponentProps): JSX.Element {
const {
taskInstance,
dumpActivity,
} = props;
const { mode } = taskInstance;
const { dumper } = props;
const pending = !!dumpActivity;
return (
<Menu.Item className='cvat-actions-menu-dump-submenu-item' key={dumper.name}>
<Button
block
type='link'
disabled={pending}
onClick={(): void => {
props.onDumpAnnotation(taskInstance, dumper);
}}
>
<Icon type='download' />
<Text strong={isDefaultFormat(dumper.name, mode)}>
{dumper.name}
</Text>
{pending && <Icon type='loading' />}
</Button>
</Menu.Item>
);
}

@ -1,45 +0,0 @@
import React from 'react';
import {
Menu,
Button,
Icon,
} from 'antd';
import Text from 'antd/lib/typography/Text';
interface DumperItemComponentProps {
taskInstance: any;
exporter: any;
exportActivity: string | null;
onExportDataset: (task: any, exporter: any) => void;
}
export default function DumperItemComponent(props: DumperItemComponentProps): JSX.Element {
const {
taskInstance,
exporter,
exportActivity,
} = props;
const pending = !!exportActivity;
return (
<Menu.Item className='cvat-actions-menu-export-submenu-item' key={exporter.name}>
<Button
block
type='link'
disabled={pending}
onClick={(): void => {
props.onExportDataset(taskInstance, exporter);
}}
>
<Icon type='export' />
<Text strong={exporter.is_default}>
{exporter.name}
</Text>
{pending && <Icon type='loading' />}
</Button>
</Menu.Item>
);
}

@ -0,0 +1,43 @@
import React from 'react';
import {
Menu,
Icon,
} from 'antd';
import Text from 'antd/lib/typography/Text';
interface Props {
menuKey: string;
exporters: string[];
exportActivities: string[] | null;
}
export default function ExportSubmenu(props: Props): JSX.Element {
const {
menuKey,
exporters,
exportActivities,
} = props;
return (
<Menu.SubMenu key={menuKey} title='Export as a dataset'>
{
exporters.map((exporter: string): JSX.Element => {
const pending = (exportActivities || []).includes(exporter);
return (
<Menu.Item
key={exporter}
disabled={pending}
className='cvat-menu-export-submenu-item'
>
<Icon type='export' />
<Text>{exporter}</Text>
{pending && <Icon style={{ marginLeft: 10 }} type='loading' />}
</Menu.Item>
);
})
}
</Menu.SubMenu>
);
}

@ -0,0 +1,61 @@
import React from 'react';
import {
Menu,
Icon,
Upload,
Button,
} from 'antd';
import Text from 'antd/lib/typography/Text';
interface Props {
menuKey: string;
loaders: string[];
loadActivity: string | null;
onFileUpload(file: File): void;
}
export default function LoadSubmenu(props: Props): JSX.Element {
const {
menuKey,
loaders,
loadActivity,
onFileUpload,
} = props;
return (
<Menu.SubMenu key={menuKey} title='Upload annotations'>
{
loaders.map((_loader: string): JSX.Element => {
const [loader, accept] = _loader.split('::');
const pending = loadActivity === loader;
return (
<Menu.Item
key={loader}
disabled={!!loadActivity}
className='cvat-menu-load-submenu-item'
>
<Upload
accept={accept}
multiple={false}
showUploadList={false}
beforeUpload={(file: File): boolean => {
onFileUpload(file);
return false;
}}
>
<Button block type='link' disabled={!!loadActivity}>
<Icon type='upload' />
<Text>{loader}</Text>
{pending && <Icon style={{ marginLeft: 10 }} type='loading' />}
</Button>
</Upload>
</Menu.Item>
);
})
}
</Menu.SubMenu>
);
}

@ -1,56 +0,0 @@
import React from 'react';
import {
Menu,
Button,
Icon,
Upload,
} from 'antd';
import { RcFile } from 'antd/lib/upload';
import Text from 'antd/lib/typography/Text';
interface LoaderItemComponentProps {
taskInstance: any;
loader: any;
loadActivity: string | null;
onLoadAnnotation: (taskInstance: any, loader: any, file: File) => void;
}
export default function LoaderItemComponent(props: LoaderItemComponentProps): JSX.Element {
const {
loader,
loadActivity,
} = props;
const loadingWithThisLoader = loadActivity
&& loadActivity === loader.name
? loadActivity : null;
const pending = !!loadingWithThisLoader;
return (
<Menu.Item className='cvat-actions-menu-load-submenu-item' key={loader.name}>
<Upload
accept={`.${loader.format}`}
multiple={false}
showUploadList={false}
beforeUpload={(file: RcFile): boolean => {
props.onLoadAnnotation(
props.taskInstance,
loader,
file as File,
);
return false;
}}
>
<Button block type='link' disabled={!!loadActivity}>
<Icon type='upload' />
<Text>{loader.name}</Text>
{pending && <Icon type='loading' />}
</Button>
</Upload>
</Menu.Item>
);
}

@ -7,23 +7,37 @@
background-color: $hover-menu-color;
}
.ant-menu-submenu-arrow {
width: 0px;
.ant-menu-submenu-title {
margin: 0px;
width: 13em;
}
}
.cvat-actions-menu-load-submenu-item,
.cvat-actions-menu-dump-submenu-item,
.cvat-actions-menu-export-submenu-item {
.cvat-menu-load-submenu-item,
.cvat-menu-dump-submenu-item,
.cvat-menu-export-submenu-item {
> i {
color: $info-icon-color;
}
&:hover {
background-color: $hover-menu-color;
}
}
.cvat-actions-menu-dump-submenu-item,
.cvat-actions-menu-export-submenu-item {
> button {
text-align: left;
.ant-menu-item.cvat-menu-load-submenu-item {
margin: 0px;
padding: 0px;
> span > .ant-upload {
width: 100%;
height: 100%;
> span > button {
width: 100%;
height: 100%;
text-align: left;
}
}
}

@ -0,0 +1,31 @@
import React from 'react';
import ReactDOM from 'react-dom';
import ObjectItemContainer from 'containers/annotation-page/standard-workspace/objects-side-bar/object-item';
interface Props {
activatedStateID: number | null;
visible: boolean;
left: number;
top: number;
}
export default function CanvasContextMenu(props: Props): JSX.Element | null {
const {
activatedStateID,
visible,
left,
top,
} = props;
if (!visible || activatedStateID === null) {
return null;
}
return ReactDOM.createPortal(
<div className='cvat-canvas-context-menu' style={{ top, left }}>
<ObjectItemContainer clientID={activatedStateID} />
</div>,
window.document.body,
);
}

@ -53,8 +53,9 @@ interface Props {
onMergeAnnotations(sessionInstance: any, frame: number, states: any[]): void;
onGroupAnnotations(sessionInstance: any, frame: number, states: any[]): void;
onSplitAnnotations(sessionInstance: any, frame: number, state: any): void;
onActivateObject: (activatedStateID: number | null) => void;
onSelectObjects: (selectedStatesID: number[]) => void;
onActivateObject(activatedStateID: number | null): void;
onSelectObjects(selectedStatesID: number[]): void;
onUpdateContextMenu(visible: boolean, left: number, top: number): void;
}
export default class CanvasWrapperComponent extends React.PureComponent<Props> {
@ -322,6 +323,7 @@ export default class CanvasWrapperComponent extends React.PureComponent<Props> {
onZoomCanvas,
onResetCanvas,
onActivateObject,
onUpdateContextMenu,
onEditShape,
} = this.props;
@ -342,12 +344,24 @@ export default class CanvasWrapperComponent extends React.PureComponent<Props> {
canvasInstance.grid(gridSize, gridSize);
// Events
canvasInstance.html().addEventListener('click', (e: MouseEvent): void => {
if ((e.target as HTMLElement).tagName === 'svg') {
canvasInstance.html().addEventListener('mousedown', (e: MouseEvent): void => {
const {
activatedStateID,
} = this.props;
if ((e.target as HTMLElement).tagName === 'svg' && activatedStateID !== null) {
onActivateObject(null);
}
});
canvasInstance.html().addEventListener('contextmenu', (e: MouseEvent): void => {
const {
activatedStateID,
} = this.props;
onUpdateContextMenu(activatedStateID !== null, e.clientX, e.clientY);
});
canvasInstance.html().addEventListener('canvas.editstart', (): void => {
onActivateObject(null);
onEditShape(true);

@ -9,6 +9,7 @@ import CanvasWrapperContainer from 'containers/annotation-page/standard-workspac
import ControlsSideBarContainer from 'containers/annotation-page/standard-workspace/controls-side-bar/controls-side-bar';
import ObjectSideBarContainer from 'containers/annotation-page/standard-workspace/objects-side-bar/objects-side-bar';
import PropagateConfirmContainer from 'containers/annotation-page/standard-workspace/propagate-confirm';
import CanvasContextMenuContainer from 'containers/annotation-page/standard-workspace/canvas-context-menu';
export default function StandardWorkspaceComponent(): JSX.Element {
return (
@ -17,6 +18,7 @@ export default function StandardWorkspaceComponent(): JSX.Element {
<CanvasWrapperContainer />
<ObjectSideBarContainer />
<PropagateConfirmContainer />
<CanvasContextMenuContainer />
</Layout>
);
}

@ -110,4 +110,17 @@
width: 70px;
margin: 0px 5px;
}
}
.cvat-canvas-context-menu {
opacity: 0.6;
position: fixed;
width: 300px;
z-index: 10;
max-height: 50%;
overflow-y: auto;
&:hover {
opacity: 1;
}
}

@ -181,4 +181,17 @@
}
}
}
}
}
.ant-menu.cvat-annotation-menu {
box-shadow: 0 0 17px rgba(0,0,0,0.2);
> li:hover {
background-color: $hover-menu-color;
}
.ant-menu-submenu-title {
margin: 0px;
width: 15em;
}
}

@ -0,0 +1,125 @@
import React from 'react';
import {
Menu, Modal,
} from 'antd';
import { ClickParam } from 'antd/lib/menu/index';
import DumpSubmenu from 'components/actions-menu/dump-submenu';
import LoadSubmenu from 'components/actions-menu/load-submenu';
import ExportSubmenu from 'components/actions-menu/export-submenu';
interface Props {
taskMode: string;
loaders: string[];
dumpers: string[];
exporters: string[];
loadActivity: string | null;
dumpActivities: string[] | null;
exportActivities: string[] | null;
onClickMenu(params: ClickParam, file?: File): void;
}
export enum Actions {
DUMP_TASK_ANNO = 'dump_task_anno',
LOAD_JOB_ANNO = 'load_job_anno',
EXPORT_TASK_DATASET = 'export_task_dataset',
REMOVE_ANNO = 'remove_anno',
OPEN_TASK = 'open_task',
}
export default function AnnotationMenuComponent(props: Props): JSX.Element {
const {
taskMode,
loaders,
dumpers,
exporters,
onClickMenu,
loadActivity,
dumpActivities,
exportActivities,
} = props;
let latestParams: ClickParam | null = null;
function onClickMenuWrapper(params: ClickParam | null, file?: File): void {
const copyParams = params || latestParams;
if (!copyParams) {
return;
}
latestParams = params;
if (copyParams.keyPath.length === 2) {
const [, action] = copyParams.keyPath;
if (action === Actions.LOAD_JOB_ANNO) {
if (file) {
Modal.confirm({
title: 'Current annotation will be lost',
content: 'You are going to upload new annotations to this job. Continue?',
onOk: () => {
onClickMenu(copyParams, file);
},
okButtonProps: {
type: 'danger',
},
okText: 'Update',
});
}
} else {
onClickMenu(copyParams);
}
} else if (copyParams.key === Actions.REMOVE_ANNO) {
Modal.confirm({
title: 'All annotations will be removed',
content: 'You are goung to remove all annotations from the client. '
+ 'It will stay on the server till you save a job. Continue?',
onOk: () => {
onClickMenu(copyParams);
},
okButtonProps: {
type: 'danger',
},
okText: 'Delete',
});
} else {
onClickMenu(copyParams);
}
}
return (
<Menu onClick={onClickMenuWrapper} className='cvat-annotation-menu' selectable={false}>
{
DumpSubmenu({
taskMode,
dumpers,
dumpActivities,
menuKey: Actions.DUMP_TASK_ANNO,
})
}
{
LoadSubmenu({
loaders,
loadActivity,
onFileUpload: (file: File): void => {
onClickMenuWrapper(null, file);
},
menuKey: Actions.LOAD_JOB_ANNO,
})
}
{
ExportSubmenu({
exporters,
exportActivities,
menuKey: Actions.EXPORT_TASK_DATASET,
})
}
<Menu.Item key={Actions.REMOVE_ANNO}>
Remove annotations
</Menu.Item>
<Menu.Item key={Actions.OPEN_TASK}>
Open the task
</Menu.Item>
</Menu>
);
}

@ -6,8 +6,11 @@ import {
Modal,
Button,
Timeline,
Dropdown,
} from 'antd';
import AnnotationMenuContainer from 'containers/annotation-page/top-bar/annotation-menu';
import {
MainMenuIcon,
SaveIcon,
@ -30,10 +33,12 @@ function LeftGroup(props: Props): JSX.Element {
return (
<Col className='cvat-annotation-header-left-group'>
<Button type='link' className='cvat-annotation-header-button'>
<Icon component={MainMenuIcon} />
Menu
</Button>
<Dropdown overlay={<AnnotationMenuContainer />}>
<Button type='link' className='cvat-annotation-header-button'>
<Icon component={MainMenuIcon} />
Menu
</Button>
</Dropdown>
<Button
onClick={saving ? undefined : onSaveAnnotation}
type='link'

@ -1,7 +1,7 @@
import React from 'react';
import { connect } from 'react-redux';
import ActionsMenuComponent from 'components/actions-menu/actions-menu';
import ActionsMenuComponent, { Actions } from 'components/actions-menu/actions-menu';
import {
CombinedState,
} from 'reducers/interfaces';
@ -13,14 +13,14 @@ import {
exportDatasetAsync,
deleteTaskAsync,
} from 'actions/tasks-actions';
import { ClickParam } from 'antd/lib/menu';
interface OwnProps {
taskInstance: any;
}
interface StateToProps {
loaders: any[];
dumpers: any[];
annotationFormats: any[];
exporters: any[];
loadActivity: string | null;
dumpActivities: string[] | null;
@ -32,61 +32,156 @@ interface StateToProps {
}
interface DispatchToProps {
onLoadAnnotation: (taskInstance: any, loader: any, file: File) => void;
onDumpAnnotation: (taskInstance: any, dumper: any) => void;
onExportDataset: (taskInstance: any, exporter: any) => void;
onDeleteTask: (taskInstance: any) => void;
onOpenRunWindow: (taskInstance: any) => void;
loadAnnotations: (taskInstance: any, loader: any, file: File) => void;
dumpAnnotations: (taskInstance: any, dumper: any) => void;
exportDataset: (taskInstance: any, exporter: any) => void;
deleteTask: (taskInstance: any) => void;
openRunModelWindow: (taskInstance: any) => void;
}
function mapStateToProps(state: CombinedState, own: OwnProps): StateToProps {
const { formats } = state;
const { activities } = state.tasks;
const { dumps } = activities;
const { loads } = activities;
const activeExports = activities.exports;
const { list } = state.plugins;
const { id } = own.taskInstance;
const {
taskInstance: {
id: tid,
},
} = own;
const {
formats: {
annotationFormats,
datasetFormats,
},
plugins: {
list: {
TF_ANNOTATION: installedTFAnnotation,
TF_SEGMENTATION: installedTFSegmentation,
AUTO_ANNOTATION: installedAutoAnnotation,
},
},
tasks: {
activities: {
dumps,
loads,
exports: activeExports,
},
},
} = state;
return {
installedTFAnnotation: list.TF_ANNOTATION,
installedTFSegmentation: list.TF_SEGMENTATION,
installedAutoAnnotation: list.AUTO_ANNOTATION,
dumpActivities: dumps.byTask[id] ? dumps.byTask[id] : null,
exportActivities: activeExports.byTask[id] ? activeExports.byTask[id] : null,
loadActivity: loads.byTask[id] ? loads.byTask[id] : null,
loaders: formats.annotationFormats
.map((format: any): any[] => format.loaders).flat(),
dumpers: formats.annotationFormats
.map((format: any): any[] => format.dumpers).flat(),
exporters: formats.datasetFormats,
inferenceIsActive: id in state.models.inferences,
installedTFAnnotation,
installedTFSegmentation,
installedAutoAnnotation,
dumpActivities: tid in dumps ? dumps[tid] : null,
exportActivities: tid in activeExports ? activeExports[tid] : null,
loadActivity: tid in loads ? loads[tid] : null,
annotationFormats,
exporters: datasetFormats,
inferenceIsActive: tid in state.models.inferences,
};
}
function mapDispatchToProps(dispatch: any): DispatchToProps {
return {
onLoadAnnotation: (taskInstance: any, loader: any, file: File): void => {
loadAnnotations: (taskInstance: any, loader: any, file: File): void => {
dispatch(loadAnnotationsAsync(taskInstance, loader, file));
},
onDumpAnnotation: (taskInstance: any, dumper: any): void => {
dumpAnnotations: (taskInstance: any, dumper: any): void => {
dispatch(dumpAnnotationsAsync(taskInstance, dumper));
},
onExportDataset: (taskInstance: any, exporter: any): void => {
exportDataset: (taskInstance: any, exporter: any): void => {
dispatch(exportDatasetAsync(taskInstance, exporter));
},
onDeleteTask: (taskInstance: any): void => {
deleteTask: (taskInstance: any): void => {
dispatch(deleteTaskAsync(taskInstance));
},
onOpenRunWindow: (taskInstance: any): void => {
openRunModelWindow: (taskInstance: any): void => {
dispatch(showRunModelDialog(taskInstance));
},
};
}
function ActionsMenuContainer(props: OwnProps & StateToProps & DispatchToProps): JSX.Element {
const {
taskInstance,
annotationFormats,
exporters,
loadActivity,
dumpActivities,
exportActivities,
inferenceIsActive,
installedAutoAnnotation,
installedTFAnnotation,
installedTFSegmentation,
loadAnnotations,
dumpAnnotations,
exportDataset,
deleteTask,
openRunModelWindow,
} = props;
const loaders = annotationFormats
.map((format: any): any[] => format.loaders).flat();
const dumpers = annotationFormats
.map((format: any): any[] => format.dumpers).flat();
function onClickMenu(params: ClickParam, file?: File): void {
if (params.keyPath.length > 1) {
const [additionalKey, action] = params.keyPath;
if (action === Actions.DUMP_TASK_ANNO) {
const format = additionalKey;
const [dumper] = dumpers
.filter((_dumper: any): boolean => _dumper.name === format);
if (dumper) {
dumpAnnotations(taskInstance, dumper);
}
} else if (action === Actions.LOAD_TASK_ANNO) {
const [format] = additionalKey.split('::');
const [loader] = loaders
.filter((_loader: any): boolean => _loader.name === format);
if (loader && file) {
loadAnnotations(taskInstance, loader, file);
}
} else if (action === Actions.EXPORT_TASK_DATASET) {
const format = additionalKey;
const [exporter] = exporters
.filter((_exporter: any): boolean => _exporter.name === format);
if (exporter) {
exportDataset(taskInstance, exporter);
}
}
} else {
const [action] = params.keyPath;
if (action === Actions.DELETE_TASK) {
deleteTask(taskInstance);
} else if (action === Actions.OPEN_BUG_TRACKER) {
// eslint-disable-next-line
window.open(`${taskInstance.bugTracker}`, '_blank');
} else if (action === Actions.RUN_AUTO_ANNOTATION) {
openRunModelWindow(taskInstance);
}
}
}
return (
<ActionsMenuComponent {...props} />
<ActionsMenuComponent
taskID={taskInstance.id}
taskMode={taskInstance.mode}
bugTracker={taskInstance.bugTracker}
loaders={loaders.map((loader: any): string => `${loader.name}::${loader.format}`)}
dumpers={dumpers.map((dumper: any): string => dumper.name)}
exporters={exporters.map((exporter: any): string => exporter.name)}
loadActivity={loadActivity}
dumpActivities={dumpActivities}
exportActivities={exportActivities}
inferenceIsActive={inferenceIsActive}
installedAutoAnnotation={installedAutoAnnotation}
installedTFAnnotation={installedTFAnnotation}
installedTFSegmentation={installedTFSegmentation}
onClickMenu={onClickMenu}
/>
);
}

@ -0,0 +1,189 @@
import React from 'react';
import { connect } from 'react-redux';
import { CombinedState } from 'reducers/interfaces';
import CanvasContextMenuComponent from 'components/annotation-page/standard-workspace/canvas-context-menu';
interface StateToProps {
activatedStateID: number | null;
visible: boolean;
top: number;
left: number;
collapsed: boolean | undefined;
}
function mapStateToProps(state: CombinedState): StateToProps {
const {
annotation: {
annotations: {
activatedStateID,
collapsed,
},
canvas: {
contextMenu: {
visible,
top,
left,
},
},
},
} = state;
return {
activatedStateID,
collapsed: activatedStateID !== null ? collapsed[activatedStateID] : undefined,
visible,
left,
top,
};
}
type Props = StateToProps;
interface State {
latestLeft: number;
latestTop: number;
left: number;
top: number;
}
class CanvasContextMenuContainer extends React.PureComponent<Props, State> {
private initialized: HTMLDivElement | null;
private dragging: boolean;
private dragInitPosX: number;
private dragInitPosY: number;
public constructor(props: Props) {
super(props);
this.initialized = null;
this.dragging = false;
this.dragInitPosX = 0;
this.dragInitPosY = 0;
this.state = {
latestLeft: 0,
latestTop: 0,
left: 0,
top: 0,
};
}
static getDerivedStateFromProps(props: Props, state: State): State | null {
if (props.left === state.latestLeft
&& props.top === state.latestTop) {
return null;
}
return {
...state,
latestLeft: props.left,
latestTop: props.top,
top: props.top,
left: props.left,
};
}
public componentDidMount(): void {
this.updatePositionIfOutOfScreen();
window.addEventListener('mousemove', this.moveContextMenu);
}
public componentDidUpdate(prevProps: Props): void {
const { collapsed } = this.props;
const [element] = window.document.getElementsByClassName('cvat-canvas-context-menu');
if (collapsed !== prevProps.collapsed && element) {
element.addEventListener('transitionend', () => {
this.updatePositionIfOutOfScreen();
}, { once: true });
} else if (element) {
this.updatePositionIfOutOfScreen();
}
if (element && (!this.initialized || this.initialized !== element)) {
this.initialized = element as HTMLDivElement;
this.initialized.addEventListener('mousedown', (e: MouseEvent): any => {
this.dragging = true;
this.dragInitPosX = e.clientX;
this.dragInitPosY = e.clientY;
});
this.initialized.addEventListener('mouseup', () => {
this.dragging = false;
});
}
}
public componentWillUnmount(): void {
window.removeEventListener('mousemove', this.moveContextMenu);
}
private moveContextMenu = (e: MouseEvent): void => {
if (this.dragging) {
this.setState((state) => {
const value = {
left: state.left + e.clientX - this.dragInitPosX,
top: state.top + e.clientY - this.dragInitPosY,
};
this.dragInitPosX = e.clientX;
this.dragInitPosY = e.clientY;
return value;
});
e.preventDefault();
}
};
private updatePositionIfOutOfScreen(): void {
const {
top,
left,
} = this.state;
const {
innerWidth,
innerHeight,
} = window;
const [element] = window.document.getElementsByClassName('cvat-canvas-context-menu');
if (element) {
const height = element.clientHeight;
const width = element.clientWidth;
if (top + height > innerHeight || left + width > innerWidth) {
this.setState({
top: top - Math.max(top + height - innerHeight, 0),
left: left - Math.max(left + width - innerWidth, 0),
});
}
}
}
public render(): JSX.Element {
const {
left,
top,
} = this.state;
const {
visible,
activatedStateID,
} = this.props;
return (
<CanvasContextMenuComponent
left={left}
top={top}
visible={visible}
activatedStateID={activatedStateID}
/>
);
}
}
export default connect(
mapStateToProps,
)(CanvasContextMenuContainer);

@ -20,6 +20,7 @@ import {
splitAnnotationsAsync,
activateObject,
selectObjects,
updateCanvasContextMenu,
} from 'actions/annotation-actions';
import {
ColorBy,
@ -68,6 +69,7 @@ interface DispatchToProps {
onSplitAnnotations(sessionInstance: any, frame: number, state: any): void;
onActivateObject: (activatedStateID: number | null) => void;
onSelectObjects: (selectedStatesID: number[]) => void;
onUpdateContextMenu(visible: boolean, left: number, top: number): void;
}
function mapStateToProps(state: CombinedState): StateToProps {
@ -179,11 +181,18 @@ function mapDispatchToProps(dispatch: any): DispatchToProps {
dispatch(splitAnnotationsAsync(sessionInstance, frame, state));
},
onActivateObject(activatedStateID: number | null): void {
if (activatedStateID === null) {
dispatch(updateCanvasContextMenu(false, 0, 0));
}
dispatch(activateObject(activatedStateID));
},
onSelectObjects(selectedStatesID: number[]): void {
dispatch(selectObjects(selectedStatesID));
},
onUpdateContextMenu(visible: boolean, left: number, top: number): void {
dispatch(updateCanvasContextMenu(visible, left, top));
},
};
}

@ -0,0 +1,166 @@
import React from 'react';
import { withRouter, RouteComponentProps } from 'react-router';
import { connect } from 'react-redux';
import { ClickParam } from 'antd/lib/menu/index';
import { CombinedState } from 'reducers/interfaces';
import AnnotationMenuComponent, { Actions } from 'components/annotation-page/top-bar/annotation-menu';
import {
dumpAnnotationsAsync,
exportDatasetAsync,
} from 'actions/tasks-actions';
import {
uploadJobAnnotationsAsync,
removeAnnotationsAsync,
} from 'actions/annotation-actions';
interface StateToProps {
annotationFormats: any[];
exporters: any[];
jobInstance: any;
loadActivity: string | null;
dumpActivities: string[] | null;
exportActivities: string[] | null;
}
interface DispatchToProps {
loadAnnotations(job: any, loader: any, file: File): void;
dumpAnnotations(task: any, dumper: any): void;
exportDataset(task: any, exporter: any): void;
removeAnnotations(sessionInstance: any): void;
}
function mapStateToProps(state: CombinedState): StateToProps {
const {
annotation: {
activities: {
loads: jobLoads,
},
job: {
instance: jobInstance,
},
},
formats: {
annotationFormats,
datasetFormats: exporters,
},
tasks: {
activities: {
dumps,
loads,
exports: activeExports,
},
},
} = state;
const taskID = jobInstance.task.id;
const jobID = jobInstance.id;
return {
dumpActivities: taskID in dumps ? dumps[taskID] : null,
exportActivities: taskID in activeExports ? activeExports[taskID] : null,
loadActivity: taskID in loads || jobID in jobLoads
? loads[taskID] || jobLoads[jobID] : null,
jobInstance,
annotationFormats,
exporters,
};
}
function mapDispatchToProps(dispatch: any): DispatchToProps {
return {
loadAnnotations(job: any, loader: any, file: File): void {
dispatch(uploadJobAnnotationsAsync(job, loader, file));
},
dumpAnnotations(task: any, dumper: any): void {
dispatch(dumpAnnotationsAsync(task, dumper));
},
exportDataset(task: any, exporter: any): void {
dispatch(exportDatasetAsync(task, exporter));
},
removeAnnotations(sessionInstance: any): void {
dispatch(removeAnnotationsAsync(sessionInstance));
},
};
}
type Props = StateToProps & DispatchToProps & RouteComponentProps;
function AnnotationMenuContainer(props: Props): JSX.Element {
const {
jobInstance,
annotationFormats,
exporters,
loadAnnotations,
dumpAnnotations,
exportDataset,
removeAnnotations,
history,
loadActivity,
dumpActivities,
exportActivities,
} = props;
const loaders = annotationFormats
.map((format: any): any[] => format.loaders).flat();
const dumpers = annotationFormats
.map((format: any): any[] => format.dumpers).flat();
const onClickMenu = (params: ClickParam, file?: File): void => {
if (params.keyPath.length > 1) {
const [additionalKey, action] = params.keyPath;
if (action === Actions.DUMP_TASK_ANNO) {
const format = additionalKey;
const [dumper] = dumpers
.filter((_dumper: any): boolean => _dumper.name === format);
if (dumper) {
dumpAnnotations(jobInstance.task, dumper);
}
} else if (action === Actions.LOAD_JOB_ANNO) {
const [format] = additionalKey.split('::');
const [loader] = loaders
.filter((_loader: any): boolean => _loader.name === format);
if (loader && file) {
loadAnnotations(jobInstance, loader, file);
}
} else if (action === Actions.EXPORT_TASK_DATASET) {
const format = additionalKey;
const [exporter] = exporters
.filter((_exporter: any): boolean => _exporter.name === format);
if (exporter) {
exportDataset(jobInstance.task, exporter);
}
}
} else {
const [action] = params.keyPath;
if (action === Actions.REMOVE_ANNO) {
removeAnnotations(jobInstance);
} else if (action === Actions.OPEN_TASK) {
history.push(`/tasks/${jobInstance.task.id}`);
}
}
};
return (
<AnnotationMenuComponent
taskMode={jobInstance.task.mode}
loaders={loaders.map((loader: any): string => loader.name)}
dumpers={dumpers.map((dumper: any): string => dumper.name)}
exporters={exporters.map((exporter: any): string => exporter.name)}
loadActivity={loadActivity}
dumpActivities={dumpActivities}
exportActivities={exportActivities}
onClickMenu={onClickMenu}
/>
);
}
export default withRouter(
connect(
mapStateToProps,
mapDispatchToProps,
)(AnnotationMenuContainer),
);

@ -39,8 +39,8 @@ function mapStateToProps(state: CombinedState, own: Props): StateToProps {
? undefined : null);
let deleteActivity = null;
if (task && id in deletes.byTask) {
deleteActivity = deletes.byTask[id];
if (task && id in deletes) {
deleteActivity = deletes[id];
}
return {

@ -37,7 +37,7 @@ function mapStateToProps(state: CombinedState, own: OwnProps): StateToProps {
return {
hidden: state.tasks.hideEmpty && task.instance.jobs.length === 0,
deleted: deletes.byTask[id] ? deletes.byTask[id] === true : false,
deleted: id in deletes ? deletes[id] === true : false,
previewImage: task.preview,
taskInstance: task.instance,
activeInference: state.models.inferences[id] || null,

@ -11,7 +11,15 @@ import {
} from './interfaces';
const defaultState: AnnotationState = {
activities: {
loads: {},
},
canvas: {
contextMenu: {
visible: false,
left: 0,
top: 0,
},
instance: new Canvas(),
ready: false,
activeControl: ActiveControl.CURSOR,
@ -694,6 +702,95 @@ export default (state = defaultState, action: AnyAction): AnnotationState => {
},
};
}
case AnnotationActionTypes.UPLOAD_JOB_ANNOTATIONS: {
const {
job,
loader,
} = action.payload;
const { loads } = state.activities;
loads[job.id] = job.id in loads ? loads[job.id] : loader.name;
return {
...state,
activities: {
...state.activities,
loads: {
...loads,
},
},
};
}
case AnnotationActionTypes.UPLOAD_JOB_ANNOTATIONS_FAILED: {
const { job } = action.payload;
const { loads } = state.activities;
delete loads[job.id];
return {
...state,
activities: {
...state.activities,
loads: {
...loads,
},
},
};
}
case AnnotationActionTypes.UPLOAD_JOB_ANNOTATIONS_SUCCESS: {
const { states, job } = action.payload;
const { loads } = state.activities;
delete loads[job.id];
return {
...state,
activities: {
...state.activities,
loads: {
...loads,
},
},
annotations: {
...state.annotations,
states,
selectedStatesID: [],
activatedStateID: null,
collapsed: {},
},
};
}
case AnnotationActionTypes.REMOVE_JOB_ANNOTATIONS_SUCCESS: {
return {
...state,
annotations: {
...state.annotations,
selectedStatesID: [],
activatedStateID: null,
collapsed: {},
states: [],
},
};
}
case AnnotationActionTypes.UPDATE_CANVAS_CONTEXT_MENU: {
const {
visible,
left,
top,
} = action.payload;
return {
...state,
canvas: {
...state.canvas,
contextMenu: {
...state.canvas.contextMenu,
visible,
left,
top,
},
},
};
}
case AnnotationActionTypes.RESET_CANVAS: {
return {
...state,

@ -36,27 +36,19 @@ export interface TasksState {
current: Task[];
activities: {
dumps: {
byTask: {
// dumps in different formats at the same time
[tid: number]: string[]; // dumper names
};
// dumps in different formats at the same time
[tid: number]: string[]; // dumper names
};
exports: {
byTask: {
// exports in different formats at the same time
[tid: number]: string[]; // dumper names
};
// exports in different formats at the same time
[tid: number]: string[]; // dumper names
};
loads: {
byTask: {
// only one loading simultaneously
[tid: number]: string; // loader name
};
// only one loading simultaneously
[tid: number]: string; // loader name
};
deletes: {
byTask: {
[tid: number]: boolean; // deleted (deleting if in dictionary)
};
[tid: number]: boolean; // deleted (deleting if in dictionary)
};
creates: {
status: string;
@ -215,6 +207,8 @@ export interface NotificationsState {
propagating: null | ErrorState;
collectingStatistics: null | ErrorState;
savingJob: null | ErrorState;
uploadAnnotations: null | ErrorState;
removeAnnotations: null | ErrorState;
};
[index: string]: any;
@ -264,8 +258,24 @@ export enum StatesOrdering {
UPDATED = 'Updated time',
}
export enum ContextMenuType {
CANVAS = 'canvas',
CANVAS_SHAPE = 'canvas_shape',
}
export interface AnnotationState {
activities: {
loads: {
// only one loading simultaneously
[jid: number]: string; // loader name
};
};
canvas: {
contextMenu: {
visible: boolean;
top: number;
left: number;
};
instance: Canvas;
ready: boolean;
activeControl: ActiveControl;

@ -63,6 +63,8 @@ const defaultState: NotificationsState = {
propagating: null,
collectingStatistics: null,
savingJob: null,
uploadAnnotations: null,
removeAnnotations: null,
},
},
messages: {
@ -635,6 +637,49 @@ export default function (state = defaultState, action: AnyAction): Notifications
},
};
}
case AnnotationActionTypes.UPLOAD_JOB_ANNOTATIONS_FAILED: {
const {
job,
error,
} = action.payload;
const {
id: jobID,
task: {
id: taskID,
},
} = job;
return {
...state,
errors: {
...state.errors,
annotation: {
...state.errors.annotation,
uploadAnnotations: {
message: 'Could not upload annotations for the '
+ `<a href="/tasks/${taskID}/jobs/${jobID}" target="_blank">job ${taskID}</a>`,
reason: error.toString(),
},
},
},
};
}
case AnnotationActionTypes.REMOVE_JOB_ANNOTATIONS_FAILED: {
return {
...state,
errors: {
...state.errors,
annotation: {
...state.errors.annotation,
removeAnnotations: {
message: 'Could not remove annotations',
reason: action.payload.error.toString(),
},
},
},
};
}
case NotificationsActionType.RESET_ERRORS: {
return {
...state,

@ -21,18 +21,10 @@ const defaultState: TasksState = {
mode: null,
},
activities: {
dumps: {
byTask: {},
},
exports: {
byTask: {},
},
loads: {
byTask: {},
},
deletes: {
byTask: {},
},
dumps: {},
exports: {},
loads: {},
deletes: {},
creates: {
status: '',
},
@ -46,9 +38,7 @@ export default (state: TasksState = defaultState, action: AnyAction): TasksState
...state,
activities: {
...state.activities,
deletes: {
byTask: {},
},
deletes: {},
},
initialized: false,
fetching: true,
@ -82,243 +72,156 @@ export default (state: TasksState = defaultState, action: AnyAction): TasksState
case TasksActionTypes.DUMP_ANNOTATIONS: {
const { task } = action.payload;
const { dumper } = action.payload;
const { dumps } = state.activities;
const tasksDumpingActivities = {
...state.activities.dumps,
};
const theTaskDumpingActivities = [...tasksDumpingActivities.byTask[task.id] || []];
if (!theTaskDumpingActivities.includes(dumper.name)) {
theTaskDumpingActivities.push(dumper.name);
}
tasksDumpingActivities.byTask[task.id] = theTaskDumpingActivities;
dumps[task.id] = task.id in dumps && !dumps[task.id].includes(dumper.name)
? [...dumps[task.id], dumper.name] : dumps[task.id] || [dumper.name];
return {
...state,
activities: {
...state.activities,
dumps: tasksDumpingActivities,
dumps: {
...dumps,
},
},
};
}
case TasksActionTypes.DUMP_ANNOTATIONS_FAILED:
case TasksActionTypes.DUMP_ANNOTATIONS_SUCCESS: {
const { task } = action.payload;
const { dumper } = action.payload;
const { dumps } = state.activities;
const tasksDumpingActivities = {
...state.activities.dumps,
};
const theTaskDumpingActivities = tasksDumpingActivities.byTask[task.id]
.filter((dumperName: string): boolean => dumperName !== dumper.name);
tasksDumpingActivities.byTask[task.id] = theTaskDumpingActivities;
return {
...state,
activities: {
...state.activities,
dumps: tasksDumpingActivities,
},
};
}
case TasksActionTypes.DUMP_ANNOTATIONS_FAILED: {
const { task } = action.payload;
const { dumper } = action.payload;
const tasksDumpingActivities = {
...state.activities.dumps,
};
const theTaskDumpingActivities = tasksDumpingActivities.byTask[task.id]
dumps[task.id] = dumps[task.id]
.filter((dumperName: string): boolean => dumperName !== dumper.name);
tasksDumpingActivities.byTask[task.id] = theTaskDumpingActivities;
return {
...state,
activities: {
...state.activities,
dumps: tasksDumpingActivities,
dumps: {
...dumps,
},
},
};
}
case TasksActionTypes.EXPORT_DATASET: {
const { task } = action.payload;
const { exporter } = action.payload;
const { exports: activeExports } = state.activities;
const tasksExportingActivities = {
...state.activities.exports,
};
const theTaskDumpingActivities = [...tasksExportingActivities.byTask[task.id] || []];
if (!theTaskDumpingActivities.includes(exporter.name)) {
theTaskDumpingActivities.push(exporter.name);
}
tasksExportingActivities.byTask[task.id] = theTaskDumpingActivities;
activeExports[task.id] = task.id in activeExports && !activeExports[task.id]
.includes(exporter.name) ? [...activeExports[task.id], exporter.name]
: activeExports[task.id] || [exporter.name];
return {
...state,
activities: {
...state.activities,
exports: tasksExportingActivities,
exports: {
...activeExports,
},
},
};
}
case TasksActionTypes.EXPORT_DATASET_FAILED:
case TasksActionTypes.EXPORT_DATASET_SUCCESS: {
const { task } = action.payload;
const { exporter } = action.payload;
const { exports: activeExports } = state.activities;
const tasksExportingActivities = {
...state.activities.exports,
};
const theTaskExportingActivities = tasksExportingActivities.byTask[task.id]
activeExports[task.id] = activeExports[task.id]
.filter((exporterName: string): boolean => exporterName !== exporter.name);
tasksExportingActivities.byTask[task.id] = theTaskExportingActivities;
return {
...state,
activities: {
...state.activities,
exports: tasksExportingActivities,
},
};
}
case TasksActionTypes.EXPORT_DATASET_FAILED: {
const { task } = action.payload;
const { exporter } = action.payload;
const tasksExportingActivities = {
...state.activities.exports,
};
const theTaskExportingActivities = tasksExportingActivities.byTask[task.id]
.filter((exporterName: string): boolean => exporterName !== exporter.name);
tasksExportingActivities.byTask[task.id] = theTaskExportingActivities;
return {
...state,
activities: {
...state.activities,
exports: tasksExportingActivities,
exports: {
...activeExports,
},
},
};
}
case TasksActionTypes.LOAD_ANNOTATIONS: {
const { task } = action.payload;
const { loader } = action.payload;
const { loads } = state.activities;
const tasksLoadingActivity = {
...state.activities.loads,
};
if (task.id in tasksLoadingActivity.byTask) {
throw Error('Load for this task has been already started');
}
tasksLoadingActivity.byTask[task.id] = loader.name;
return {
...state,
activities: {
...state.activities,
loads: tasksLoadingActivity,
},
};
}
case TasksActionTypes.LOAD_ANNOTATIONS_SUCCESS: {
const { task } = action.payload;
const tasksLoadingActivity = {
...state.activities.loads,
};
delete tasksLoadingActivity.byTask[task.id];
loads[task.id] = task.id in loads ? loads[task.id] : loader.name;
return {
...state,
activities: {
...state.activities,
loads: {
...tasksLoadingActivity,
...loads,
},
},
};
}
case TasksActionTypes.LOAD_ANNOTATIONS_FAILED: {
case TasksActionTypes.LOAD_ANNOTATIONS_FAILED:
case TasksActionTypes.LOAD_ANNOTATIONS_SUCCESS: {
const { task } = action.payload;
const { loads } = state.activities;
const tasksLoadingActivity = {
...state.activities.loads,
};
delete tasksLoadingActivity.byTask[task.id];
delete loads[task.id];
return {
...state,
activities: {
...state.activities,
loads: {
...tasksLoadingActivity,
...loads,
},
},
};
}
case TasksActionTypes.DELETE_TASK: {
const { taskID } = action.payload;
const { deletes } = state.activities;
const deletesActivities = state.activities.deletes;
const activities = { ...state.activities };
activities.deletes = { ...activities.deletes };
activities.deletes.byTask[taskID] = false;
deletes[taskID] = false;
return {
...state,
activities: {
...state.activities,
deletes: deletesActivities,
deletes: {
...deletes,
},
},
};
}
case TasksActionTypes.DELETE_TASK_SUCCESS: {
const { taskID } = action.payload;
const { deletes } = state.activities;
const deletesActivities = state.activities.deletes;
const activities = { ...state.activities };
activities.deletes = { ...activities.deletes };
activities.deletes.byTask[taskID] = true;
deletes[taskID] = true;
return {
...state,
activities: {
...state.activities,
deletes: deletesActivities,
deletes: {
...deletes,
},
},
};
}
case TasksActionTypes.DELETE_TASK_FAILED: {
const { taskID } = action.payload;
const { deletes } = state.activities;
const deletesActivities = state.activities.deletes;
const activities = { ...state.activities };
activities.deletes = { ...activities.deletes };
delete activities.deletes.byTask[taskID];
delete deletes[taskID];
return {
...state,
activities: {
...state.activities,
deletes: {
...deletesActivities,
...deletes,
},
},
};

Loading…
Cancel
Save