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 eslintmain
parent
42614c28a1
commit
939de868a9
@ -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>
|
||||
);
|
||||
}
|
||||
@ -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,
|
||||
);
|
||||
}
|
||||
@ -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>
|
||||
);
|
||||
}
|
||||
@ -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);
|
||||
@ -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),
|
||||
);
|
||||
Loading…
Reference in New Issue