diff --git a/webapp/packages/plugin-object-viewer/src/ObjectPropertiesPage/ObjectPropertyTable/MENU_OBJECT_VIEWER_FOOTER.ts b/webapp/packages/plugin-object-viewer/src/ObjectPropertiesPage/ObjectPropertyTable/MENU_OBJECT_VIEWER_FOOTER.ts new file mode 100644 index 0000000000..202c5a60a3 --- /dev/null +++ b/webapp/packages/plugin-object-viewer/src/ObjectPropertiesPage/ObjectPropertyTable/MENU_OBJECT_VIEWER_FOOTER.ts @@ -0,0 +1,10 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2024 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0. + * you may not use this file except in compliance with the License. + */ +import { createMenu } from '@cloudbeaver/core-view'; + +export const MENU_OBJECT_VIEWER_FOOTER = createMenu('object-viewer-footer', 'Object viewer footer menu'); diff --git a/webapp/packages/plugin-object-viewer/src/ObjectPropertiesPage/ObjectPropertyTable/ObjectPropertyTableFooter.tsx b/webapp/packages/plugin-object-viewer/src/ObjectPropertiesPage/ObjectPropertyTable/ObjectPropertyTableFooter.tsx index c9776234ab..938ee4d204 100644 --- a/webapp/packages/plugin-object-viewer/src/ObjectPropertiesPage/ObjectPropertyTable/ObjectPropertyTableFooter.tsx +++ b/webapp/packages/plugin-object-viewer/src/ObjectPropertiesPage/ObjectPropertyTable/ObjectPropertyTableFooter.tsx @@ -7,33 +7,29 @@ */ import { observer } from 'mobx-react-lite'; -import { TableState, ToolsPanel } from '@cloudbeaver/core-blocks'; +import type { TableState } from '@cloudbeaver/core-blocks'; import { useService } from '@cloudbeaver/core-di'; +import { DATA_CONTEXT_NAV_NODES, type NavNode, NavNodeInfoResource } from '@cloudbeaver/core-navigation-tree'; +import { resourceKeyList } from '@cloudbeaver/core-resource'; +import { MenuBar } from '@cloudbeaver/core-ui'; +import { useMenu } from '@cloudbeaver/core-view'; -import { ObjectPropertyTableFooterItem } from './ObjectPropertyTableFooterItem'; -import { ObjectPropertyTableFooterService } from './ObjectPropertyTableFooterService'; +import { MENU_OBJECT_VIEWER_FOOTER } from './MENU_OBJECT_VIEWER_FOOTER'; interface Props { - nodeIds: string[]; - tableState: TableState; + state: TableState; className?: string; } -export const ObjectPropertyTableFooter = observer(function ObjectPropertyTableFooter({ nodeIds, tableState, className }) { - const service = useService(ObjectPropertyTableFooterService); +export const ObjectPropertyTableFooter = observer(function ObjectPropertyTableFooter({ state, className }) { + const navNodeInfoResource = useService(NavNodeInfoResource); + const menu = useMenu({ menu: MENU_OBJECT_VIEWER_FOOTER }); - const items = service.constructMenuWithContext(nodeIds, tableState); - const hidden = items.every(item => item.isHidden); - - if (hidden) { - return null; + function getSelected() { + return navNodeInfoResource.get(resourceKeyList(state.selectedList)).filter(Boolean) as NavNode[]; } - return ( - - {items.map((topItem, i) => ( - - ))} - - ); + menu.context.set(DATA_CONTEXT_NAV_NODES, getSelected); + + return ; }); diff --git a/webapp/packages/plugin-object-viewer/src/ObjectPropertiesPage/ObjectPropertyTable/ObjectPropertyTableFooterItem.tsx b/webapp/packages/plugin-object-viewer/src/ObjectPropertiesPage/ObjectPropertyTable/ObjectPropertyTableFooterItem.tsx deleted file mode 100644 index 55c86fca69..0000000000 --- a/webapp/packages/plugin-object-viewer/src/ObjectPropertiesPage/ObjectPropertyTable/ObjectPropertyTableFooterItem.tsx +++ /dev/null @@ -1,86 +0,0 @@ -/* - * CloudBeaver - Cloud Database Manager - * Copyright (C) 2020-2024 DBeaver Corp and others - * - * Licensed under the Apache License, Version 2.0. - * you may not use this file except in compliance with the License. - */ -import { observer } from 'mobx-react-lite'; -import type { ButtonHTMLAttributes } from 'react'; -import styled, { css, use } from 'reshadow'; - -import { IconOrImage, MenuTrigger, ToolsAction, useTranslate } from '@cloudbeaver/core-blocks'; -import type { IMenuItem } from '@cloudbeaver/core-dialogs'; - -type Props = ButtonHTMLAttributes & { - menuItem: IMenuItem; -}; - -const style = css` - Menu { - composes: theme-text-on-surface from global; - } - MenuTrigger { - composes: theme-ripple from global; - height: 100%; - padding: 0 16px; - display: flex; - align-items: center; - cursor: pointer; - &[|hidden] { - display: none; - } - } - ToolsAction[|hidden] { - display: none; - } - menu-trigger-icon IconOrImage { - display: block; - width: 24px; - } - menu-trigger-title { - display: block; - } - menu-trigger-icon + menu-trigger-title { - padding-left: 8px; - } -`; - -export const ObjectPropertyTableFooterItem = observer(function ObjectPropertyTableFooterItem({ menuItem, ...props }) { - const translate = useTranslate(); - - if (!menuItem.panel) { - return styled(style)( - menuItem.onClick?.()} - > - {translate(menuItem.title)} - , - ); - } - - return styled(style)( - - {menuItem.icon && ( - - - - )} - {menuItem.title && {translate(menuItem.title)}} - , - ); -}); diff --git a/webapp/packages/plugin-object-viewer/src/ObjectPropertiesPage/ObjectPropertyTable/ObjectPropertyTableFooterService.ts b/webapp/packages/plugin-object-viewer/src/ObjectPropertiesPage/ObjectPropertyTable/ObjectPropertyTableFooterService.ts index a0385f972b..6564997118 100644 --- a/webapp/packages/plugin-object-viewer/src/ObjectPropertiesPage/ObjectPropertyTable/ObjectPropertyTableFooterService.ts +++ b/webapp/packages/plugin-object-viewer/src/ObjectPropertiesPage/ObjectPropertyTable/ObjectPropertyTableFooterService.ts @@ -5,79 +5,75 @@ * Licensed under the Apache License, Version 2.0. * you may not use this file except in compliance with the License. */ -import type { TableState } from '@cloudbeaver/core-blocks'; import { injectable } from '@cloudbeaver/core-di'; -import { ContextMenuService, IContextMenuItem, IMenuContext, IMenuItem } from '@cloudbeaver/core-dialogs'; import { NotificationService } from '@cloudbeaver/core-events'; -import { ENodeFeature, type NavNode, NavNodeInfoResource, NavTreeResource, NavTreeSettingsService } from '@cloudbeaver/core-navigation-tree'; +import { DATA_CONTEXT_NAV_NODES, ENodeFeature, NavTreeResource, NavTreeSettingsService } from '@cloudbeaver/core-navigation-tree'; import { resourceKeyList } from '@cloudbeaver/core-resource'; +import { ACTION_DELETE, ActionService, DATA_CONTEXT_MENU, MenuService } from '@cloudbeaver/core-view'; -interface IObjectPropertyTableFooterContext { - nodeIds: string[]; - tableState: TableState; -} +import { MENU_OBJECT_VIEWER_FOOTER } from './MENU_OBJECT_VIEWER_FOOTER'; @injectable() export class ObjectPropertyTableFooterService { - static objectPropertyContextType = 'objectProperty'; - private readonly objectPropertyTableFooterToken = 'objectPropertyTableFooter'; - constructor( - private readonly contextMenuService: ContextMenuService, private readonly navTreeResource: NavTreeResource, - private readonly navNodeInfoResource: NavNodeInfoResource, private readonly notificationService: NotificationService, private readonly navTreeSettingsService: NavTreeSettingsService, - ) { - this.contextMenuService.addPanel(this.objectPropertyTableFooterToken); + private readonly menuService: MenuService, + private readonly actionService: ActionService, + ) {} - this.registerMenuItem({ - id: 'delete', - title: 'ui_delete', - tooltip: 'ui_delete', - icon: 'delete', - order: 0, - isPresent(context) { - return context.contextType === ObjectPropertyTableFooterService.objectPropertyContextType; + registerFooterActions() { + this.menuService.addCreator({ + menus: [MENU_OBJECT_VIEWER_FOOTER], + isApplicable: context => { + const selected = context.tryGet(DATA_CONTEXT_NAV_NODES); + return selected !== undefined && this.navTreeSettingsService.settings.getValue('deleting'); }, - isHidden: () => !this.navTreeSettingsService.settings.getValue('deleting'), - isDisabled: context => { - if (context.data.tableState.selectedList.length === 0) { - return true; - } + getItems: (_, items) => [...items, ACTION_DELETE], + }); - const selectedNodes = this.getSelectedNodes(context.data.tableState.selectedList); - return !selectedNodes.some(node => node.features?.includes(ENodeFeature.canDelete)) || this.navTreeResource.isLoading(); - }, - onClick: async context => { - const nodes = this.getSelectedNodes(context.data.tableState.selectedList).filter(node => node.features?.includes(ENodeFeature.canDelete)); + this.actionService.addHandler({ + id: 'object-viewer-footer-base', + isActionApplicable: (context, action) => { + const menu = context.hasValue(DATA_CONTEXT_MENU, MENU_OBJECT_VIEWER_FOOTER); - try { - await this.navTreeResource.deleteNode(resourceKeyList(nodes.map(node => node.id))); - } catch (exception: any) { - this.notificationService.logException(exception, 'Failed to delete item'); + if (!menu || !context.has(DATA_CONTEXT_NAV_NODES)) { + return false; } + + return [ACTION_DELETE].includes(action); }, - }); - } + getActionInfo: (_, action) => { + if (action === ACTION_DELETE) { + return { + ...action.info, + icon: 'delete', + }; + } - registerMenuItem(options: IContextMenuItem): void { - this.contextMenuService.addMenuItem(this.objectPropertyTableFooterToken, options); - } + return action.info; + }, + isDisabled: (context, action) => { + if (action === ACTION_DELETE) { + const selected = context.get(DATA_CONTEXT_NAV_NODES)(); + return !selected.some(node => node.features?.includes(ENodeFeature.canDelete)) || this.navTreeResource.isLoading(); + } - constructMenuWithContext(nodeIds: string[], tableState: TableState): IMenuItem[] { - const context: IMenuContext = { - menuId: this.objectPropertyTableFooterToken, - contextType: ObjectPropertyTableFooterService.objectPropertyContextType, - data: { - nodeIds, - tableState, + return true; }, - }; - return this.contextMenuService.createContextMenu(context, this.objectPropertyTableFooterToken).menuItems; - } + handler: async (context, action) => { + if (action === ACTION_DELETE) { + const selected = context.get(DATA_CONTEXT_NAV_NODES)(); + const nodes = selected.filter(node => node.features?.includes(ENodeFeature.canDelete)); - private getSelectedNodes(list: string[]) { - return this.navNodeInfoResource.get(resourceKeyList(list)).filter(Boolean) as NavNode[]; + try { + await this.navTreeResource.deleteNode(resourceKeyList(nodes.map(node => node.id))); + } catch (exception: any) { + this.notificationService.logException(exception, 'plugin_object_viewer_delete_object_fail'); + } + } + }, + }); } } diff --git a/webapp/packages/plugin-object-viewer/src/ObjectPropertiesPage/ObjectPropertyTable/Table/Table.tsx b/webapp/packages/plugin-object-viewer/src/ObjectPropertiesPage/ObjectPropertyTable/Table/Table.tsx index f667a24fae..5e6b299dbe 100644 --- a/webapp/packages/plugin-object-viewer/src/ObjectPropertiesPage/ObjectPropertyTable/Table/Table.tsx +++ b/webapp/packages/plugin-object-viewer/src/ObjectPropertiesPage/ObjectPropertyTable/Table/Table.tsx @@ -9,8 +9,9 @@ import { observer } from 'mobx-react-lite'; import { useCallback, useState } from 'react'; import styled from 'reshadow'; -import { IScrollState, Link, s, useControlledScroll, useS, useStyles, useTable, useTranslate } from '@cloudbeaver/core-blocks'; -import type { DBObject } from '@cloudbeaver/core-navigation-tree'; +import { IScrollState, Link, s, useControlledScroll, useExecutor, useS, useStyles, useTable, useTranslate } from '@cloudbeaver/core-blocks'; +import { useService } from '@cloudbeaver/core-di'; +import { type DBObject, NavTreeResource } from '@cloudbeaver/core-navigation-tree'; import type { ObjectPropertyInfo } from '@cloudbeaver/core-sdk'; import { useTabLocalState } from '@cloudbeaver/core-ui'; import { isDefined, TextTools } from '@cloudbeaver/core-utils'; @@ -76,6 +77,7 @@ const CUSTOM_COLUMNS = [ColumnSelect, ColumnIcon]; export const Table = observer(function Table({ objects, hasNextPage, loadMore }) { const styles = useS(classes); + const navTreeResource = useService(NavTreeResource); const [tableContainer, setTableContainerRef] = useState(null); const translate = useTranslate(); @@ -88,7 +90,6 @@ export const Table = observer(function Table({ objects, hasNextPage, const baseObject = objects.slice().sort((a, b) => (b.object?.properties?.length || 0) - (a.object?.properties?.length || 0)); - const nodeIds = objects.map(object => object.id); const properties = baseObject[0]?.object?.properties ?? []; const measuredCells = getMeasuredCells(properties, objects); @@ -115,6 +116,15 @@ export const Table = observer(function Table({ objects, hasNextPage, [loadMore], ); + useExecutor({ + executor: navTreeResource.onItemDelete, + handlers: [ + function handleNodeDelete(nodeId) { + tableState.unselect(nodeId); + }, + ], + }); + if (objects.length === 0) { return null; } @@ -137,7 +147,7 @@ export const Table = observer(function Table({ objects, hasNextPage, )} - + , ); diff --git a/webapp/packages/plugin-object-viewer/src/ObjectViewerBootstrap.ts b/webapp/packages/plugin-object-viewer/src/ObjectViewerBootstrap.ts index 118cc11761..8eb0931a1a 100644 --- a/webapp/packages/plugin-object-viewer/src/ObjectViewerBootstrap.ts +++ b/webapp/packages/plugin-object-viewer/src/ObjectViewerBootstrap.ts @@ -8,6 +8,7 @@ import { Bootstrap, injectable } from '@cloudbeaver/core-di'; import { ObjectPropertiesPageService } from './ObjectPropertiesPage/ObjectPropertiesPageService'; +import { ObjectPropertyTableFooterService } from './ObjectPropertiesPage/ObjectPropertyTable/ObjectPropertyTableFooterService'; import { ObjectViewerTabService } from './ObjectViewerTabService'; @injectable() @@ -15,6 +16,7 @@ export class ObjectViewerBootstrap extends Bootstrap { constructor( private readonly objectViewerTabService: ObjectViewerTabService, private readonly objectPropertiesPageService: ObjectPropertiesPageService, + private readonly objectPropertyTableFooterService: ObjectPropertyTableFooterService, ) { super(); } @@ -22,6 +24,7 @@ export class ObjectViewerBootstrap extends Bootstrap { register(): void { this.objectViewerTabService.registerTabHandler(); this.objectPropertiesPageService.registerDBObjectPage(); + this.objectPropertyTableFooterService.registerFooterActions(); } load(): void {} diff --git a/webapp/packages/plugin-object-viewer/src/locales/en.ts b/webapp/packages/plugin-object-viewer/src/locales/en.ts index 8fb5326248..51b434342b 100644 --- a/webapp/packages/plugin-object-viewer/src/locales/en.ts +++ b/webapp/packages/plugin-object-viewer/src/locales/en.ts @@ -4,4 +4,5 @@ export default [ ['plugin_object_viewer_table_name', 'Name'], ['plugin_object_viewer_table_no_items', 'There are no items to show'], ['plugin_object_viewer_error', 'Error occurred while tab loading'], + ['plugin_object_viewer_delete_object_fail', 'Failed to delete object'], ]; diff --git a/webapp/packages/plugin-object-viewer/src/locales/it.ts b/webapp/packages/plugin-object-viewer/src/locales/it.ts index 5649eb5fd4..85f331b6c0 100644 --- a/webapp/packages/plugin-object-viewer/src/locales/it.ts +++ b/webapp/packages/plugin-object-viewer/src/locales/it.ts @@ -4,4 +4,5 @@ export default [ ['plugin_object_viewer_table_name', 'Nome'], ['plugin_object_viewer_table_no_items', 'Non ci sono elementi da mostrare'], ['plugin_object_viewer_error', 'Errore durante il caricamento della tab'], + ['plugin_object_viewer_delete_object_fail', 'Failed to delete object'], ]; diff --git a/webapp/packages/plugin-object-viewer/src/locales/ru.ts b/webapp/packages/plugin-object-viewer/src/locales/ru.ts index 4974003b9c..1b272583ff 100644 --- a/webapp/packages/plugin-object-viewer/src/locales/ru.ts +++ b/webapp/packages/plugin-object-viewer/src/locales/ru.ts @@ -4,4 +4,5 @@ export default [ ['plugin_object_viewer_table_name', 'Название'], ['plugin_object_viewer_table_no_items', 'Нет объектов для показа'], ['plugin_object_viewer_error', 'Произошла ошибка при загрузке вкладки'], + ['plugin_object_viewer_delete_object_fail', 'Не удалось удалить объект'], ]; diff --git a/webapp/packages/plugin-object-viewer/src/locales/zh.ts b/webapp/packages/plugin-object-viewer/src/locales/zh.ts index eaef767334..f63584bb3a 100644 --- a/webapp/packages/plugin-object-viewer/src/locales/zh.ts +++ b/webapp/packages/plugin-object-viewer/src/locales/zh.ts @@ -4,4 +4,5 @@ export default [ ['plugin_object_viewer_table_name', '名称'], ['plugin_object_viewer_table_no_items', '没有要显示的项目'], ['plugin_object_viewer_error', '选项卡加载时出错'], + ['plugin_object_viewer_delete_object_fail', 'Failed to delete object'], ];