From 322cda080f964904e04f6200f92332af44c304eb Mon Sep 17 00:00:00 2001 From: Boris Sekachev Date: Wed, 5 Feb 2020 10:09:00 +0300 Subject: [PATCH 1/6] Annotation menu, modified tasks menu --- cvat-ui/src/actions/annotation-actions.ts | 87 ++++++++ cvat-ui/src/actions/tasks-actions.ts | 11 +- .../components/actions-menu/actions-menu.tsx | 194 +++++++++-------- .../components/actions-menu/dump-submenu.tsx | 51 +++++ .../components/actions-menu/dumper-item.tsx | 50 ----- .../components/actions-menu/export-item.tsx | 45 ---- .../actions-menu/export-submenu.tsx | 43 ++++ .../components/actions-menu/load-submenu.tsx | 61 ++++++ .../components/actions-menu/loader-item.tsx | 56 ----- .../src/components/actions-menu/styles.scss | 32 ++- .../components/annotation-page/styles.scss | 25 +++ .../top-bar/annotation-menu.tsx | 125 +++++++++++ .../annotation-page/top-bar/left-group.tsx | 13 +- .../annotation-page/top-bar/menu.tsx | 0 .../containers/actions-menu/actions-menu.tsx | 161 +++++++++++--- .../top-bar/annotation-menu.tsx | 166 +++++++++++++++ .../src/containers/task-page/task-page.tsx | 4 +- .../src/containers/tasks-page/task-item.tsx | 2 +- cvat-ui/src/reducers/annotation-reducer.ts | 72 +++++++ cvat-ui/src/reducers/interfaces.ts | 30 +-- cvat-ui/src/reducers/notifications-reducer.ts | 45 ++++ cvat-ui/src/reducers/tasks-reducer.ts | 199 +++++------------- 22 files changed, 1019 insertions(+), 453 deletions(-) create mode 100644 cvat-ui/src/components/actions-menu/dump-submenu.tsx delete mode 100644 cvat-ui/src/components/actions-menu/dumper-item.tsx delete mode 100644 cvat-ui/src/components/actions-menu/export-item.tsx create mode 100644 cvat-ui/src/components/actions-menu/export-submenu.tsx create mode 100644 cvat-ui/src/components/actions-menu/load-submenu.tsx delete mode 100644 cvat-ui/src/components/actions-menu/loader-item.tsx create mode 100644 cvat-ui/src/components/annotation-page/top-bar/annotation-menu.tsx delete mode 100644 cvat-ui/src/components/annotation-page/top-bar/menu.tsx create mode 100644 cvat-ui/src/containers/annotation-page/top-bar/annotation-menu.tsx diff --git a/cvat-ui/src/actions/annotation-actions.ts b/cvat-ui/src/actions/annotation-actions.ts index 0bf029ba7cc9..a81b1db35c17 100644 --- a/cvat-ui/src/actions/annotation-actions.ts +++ b/cvat-ui/src/actions/annotation-actions.ts @@ -68,6 +68,93 @@ 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', +} + +export function removeAnnotationsAsync(sessionInstance: any): +ThunkAction, {}, {}, AnyAction> { + return async (dispatch: ActionCreator): Promise => { + 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, {}, {}, AnyAction> { + return async (dispatch: ActionCreator): Promise => { + 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): diff --git a/cvat-ui/src/actions/tasks-actions.ts b/cvat-ui/src/actions/tasks-actions.ts index fa9c3a5851f2..6f7f1a52e485 100644 --- a/cvat-ui/src/actions/tasks-actions.ts +++ b/cvat-ui/src/actions/tasks-actions.ts @@ -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, {}, {}, AnyAction> { return async (dispatch: ActionCreator): Promise => { 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) { diff --git a/cvat-ui/src/components/actions-menu/actions-menu.tsx b/cvat-ui/src/components/actions-menu/actions-menu.tsx index 61b2cc112ae4..d1087d2f0bda 100644 --- a/cvat-ui/src/components/actions-menu/actions-menu.tsx +++ b/cvat-ui/src/components/actions-menu/actions-menu.tsx @@ -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 ( handleMenuClick(props, params) - } + onClick={onClickMenuWrapper} > - - { - dumpers.map((dumper): JSX.Element => DumperItemComponent({ - dumper, - taskInstance: props.taskInstance, - dumpActivity: (props.dumpActivities || []) - .filter((_dumper: string) => _dumper === dumper.name)[0] || null, - onDumpAnnotation: props.onDumpAnnotation, - })) - } - - - { - loaders.map((loader): JSX.Element => LoaderItemComponent({ - loader, - taskInstance: props.taskInstance, - loadActivity: props.loadActivity, - onLoadAnnotation: props.onLoadAnnotation, - })) - } - - - { - exporters.map((exporter): JSX.Element => ExportItemComponent({ - exporter, - taskInstance: props.taskInstance, - exportActivity: (props.exportActivities || []) - .filter((_exporter: string) => _exporter === exporter.name)[0] || null, - onExportDataset: props.onExportDataset, - })) - } - - {tracker && Open bug tracker} + { + 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 && Open bug tracker} { renderModelRunner && ( Automatic annotation ) }
- Delete + Delete
); } diff --git a/cvat-ui/src/components/actions-menu/dump-submenu.tsx b/cvat-ui/src/components/actions-menu/dump-submenu.tsx new file mode 100644 index 000000000000..a3fa5bdee8e0 --- /dev/null +++ b/cvat-ui/src/components/actions-menu/dump-submenu.tsx @@ -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 ( + + { + dumpers.map((dumper: string): JSX.Element => { + const pending = (dumpActivities || []).includes(dumper); + const isDefault = isDefaultFormat(dumper, taskMode); + return ( + + + {dumper} + {pending && } + + ); + }) + } + + ); +} diff --git a/cvat-ui/src/components/actions-menu/dumper-item.tsx b/cvat-ui/src/components/actions-menu/dumper-item.tsx deleted file mode 100644 index 37db1a6c7f8a..000000000000 --- a/cvat-ui/src/components/actions-menu/dumper-item.tsx +++ /dev/null @@ -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 ( - - - - ); -} diff --git a/cvat-ui/src/components/actions-menu/export-item.tsx b/cvat-ui/src/components/actions-menu/export-item.tsx deleted file mode 100644 index 9dba300bdfaa..000000000000 --- a/cvat-ui/src/components/actions-menu/export-item.tsx +++ /dev/null @@ -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 ( - - - - ); -} diff --git a/cvat-ui/src/components/actions-menu/export-submenu.tsx b/cvat-ui/src/components/actions-menu/export-submenu.tsx new file mode 100644 index 000000000000..24a525be1148 --- /dev/null +++ b/cvat-ui/src/components/actions-menu/export-submenu.tsx @@ -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 ( + + { + exporters.map((exporter: string): JSX.Element => { + const pending = (exportActivities || []).includes(exporter); + return ( + + + {exporter} + {pending && } + + ); + }) + } + + ); +} diff --git a/cvat-ui/src/components/actions-menu/load-submenu.tsx b/cvat-ui/src/components/actions-menu/load-submenu.tsx new file mode 100644 index 000000000000..1b1a387e3c8e --- /dev/null +++ b/cvat-ui/src/components/actions-menu/load-submenu.tsx @@ -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 ( + + { + loaders.map((_loader: string): JSX.Element => { + const [loader, accept] = _loader.split('::'); + const pending = loadActivity === loader; + return ( + + { + onFileUpload(file); + return false; + }} + > + + + + + ); + }) + } + + ); +} diff --git a/cvat-ui/src/components/actions-menu/loader-item.tsx b/cvat-ui/src/components/actions-menu/loader-item.tsx deleted file mode 100644 index 5f736c7e8702..000000000000 --- a/cvat-ui/src/components/actions-menu/loader-item.tsx +++ /dev/null @@ -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 ( - - { - props.onLoadAnnotation( - props.taskInstance, - loader, - file as File, - ); - - return false; - }} - > - - - - ); -} diff --git a/cvat-ui/src/components/actions-menu/styles.scss b/cvat-ui/src/components/actions-menu/styles.scss index 9a348dde1366..a84dd35af760 100644 --- a/cvat-ui/src/components/actions-menu/styles.scss +++ b/cvat-ui/src/components/actions-menu/styles.scss @@ -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; + } } } diff --git a/cvat-ui/src/components/annotation-page/styles.scss b/cvat-ui/src/components/annotation-page/styles.scss index 3d77640a070f..fd5698661421 100644 --- a/cvat-ui/src/components/annotation-page/styles.scss +++ b/cvat-ui/src/components/annotation-page/styles.scss @@ -181,4 +181,29 @@ } } } +} + +.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; + } +} + +.cvat-annotation-menu-load-submenu-item, +.cvat-annotation-menu-dump-submenu-item, +.cvat-annotation-menu-export-submenu-item { + > i { + color: $info-icon-color; + } + + &:hover { + background-color: $hover-menu-color; + } } \ No newline at end of file diff --git a/cvat-ui/src/components/annotation-page/top-bar/annotation-menu.tsx b/cvat-ui/src/components/annotation-page/top-bar/annotation-menu.tsx new file mode 100644 index 000000000000..ece7d2d215a8 --- /dev/null +++ b/cvat-ui/src/components/annotation-page/top-bar/annotation-menu.tsx @@ -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 ( + + { + 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, + }) + } + + + Remove annotations + + + Open the task + + + ); +} diff --git a/cvat-ui/src/components/annotation-page/top-bar/left-group.tsx b/cvat-ui/src/components/annotation-page/top-bar/left-group.tsx index 486e286fd362..c76cf232c767 100644 --- a/cvat-ui/src/components/annotation-page/top-bar/left-group.tsx +++ b/cvat-ui/src/components/annotation-page/top-bar/left-group.tsx @@ -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 ( - + }> + +