diff --git a/webapp/packages/core-app/src/NavigationTree/navigationTreeDuplicateIdFilter.ts b/webapp/packages/core-app/src/NavigationTree/navigationTreeDuplicateIdFilter.ts index e5b7fc96c9..5f451a5742 100644 --- a/webapp/packages/core-app/src/NavigationTree/navigationTreeDuplicateIdFilter.ts +++ b/webapp/packages/core-app/src/NavigationTree/navigationTreeDuplicateIdFilter.ts @@ -16,24 +16,12 @@ export function navigationTreeDuplicateFilter( navNodeViewService: NavNodeViewService, ): IElementsTreeFilter { return (node: NavNode, children: string[]) => { - const nextChildren: string[] = []; - const duplicates: string[] = []; - - for (const child of children) { - if (nextChildren.includes(child)) { - if (!duplicates.includes(child)) { - duplicates.push(child); - nextChildren.splice(nextChildren.indexOf(child), 1); - } - } else { - nextChildren.push(child); - } - } + const { nodes, duplicates } = navNodeViewService.filterDuplicates(children); untracked(() => { navNodeViewService.logDuplicates(node.id, duplicates); }); - return nextChildren; + return nodes; }; } diff --git a/webapp/packages/core-app/src/shared/NavigationTabs/NavigationTabsService.ts b/webapp/packages/core-app/src/shared/NavigationTabs/NavigationTabsService.ts index decc68355d..d0a0e9870c 100644 --- a/webapp/packages/core-app/src/shared/NavigationTabs/NavigationTabsService.ts +++ b/webapp/packages/core-app/src/shared/NavigationTabs/NavigationTabsService.ts @@ -168,6 +168,16 @@ export class NavigationTabsService extends View> { const tab = this.tabsMap.get(tabId); if (tab && !skipHandlers) { + const handler = this.handlers.get(tab.handlerId); + + if (handler) { + const state = await handler.canClose?.(tab); + + if (state === false) { + return; + } + } + await this.callHandlerCallback(tab, handler => handler.onClose); } diff --git a/webapp/packages/core-app/src/shared/NavigationTabs/TabHandler.ts b/webapp/packages/core-app/src/shared/NavigationTabs/TabHandler.ts index ec1ab6a512..31409672bd 100644 --- a/webapp/packages/core-app/src/shared/NavigationTabs/TabHandler.ts +++ b/webapp/packages/core-app/src/shared/NavigationTabs/TabHandler.ts @@ -26,6 +26,7 @@ export interface TabHandlerPanelProps { } export type TabHandlerPanelComponent = React.FunctionComponent>; +export type TabHandlerCloseEvent = (tab: ITab) => Promise | boolean; export type TabHandlerEvent = (tab: ITab) => Promise | void; export type TabRestoreEvent = (tab: ITab) => Promise | boolean; @@ -34,6 +35,7 @@ export interface TabHandlerOptions { getTabComponent: () => TabHandlerTabComponent; getPanelComponent: () => TabHandlerPanelComponent; onSelect?: TabHandlerEvent; + canClose?: TabHandlerCloseEvent; onClose?: TabHandlerEvent; onRestore?: TabRestoreEvent; extensions?: Array>>; @@ -45,6 +47,7 @@ export class TabHandler { getPanelComponent: () => TabHandlerPanelComponent; onSelect?: TabHandlerEvent; onClose?: TabHandlerEvent; + canClose?: TabHandlerCloseEvent; onRestore?: TabRestoreEvent; extensions?: Array>>; @@ -53,6 +56,7 @@ export class TabHandler { this.getTabComponent = options.getTabComponent; this.getPanelComponent = options.getPanelComponent; this.onSelect = options.onSelect; + this.canClose = options.canClose; this.onClose = options.onClose; this.onRestore = options.onRestore; this.extensions = options.extensions; diff --git a/webapp/packages/core-app/src/shared/NodesManager/NavNodeView/NavNodeViewService.ts b/webapp/packages/core-app/src/shared/NodesManager/NavNodeView/NavNodeViewService.ts index 4d13142920..9e9741b11c 100644 --- a/webapp/packages/core-app/src/shared/NodesManager/NavNodeView/NavNodeViewService.ts +++ b/webapp/packages/core-app/src/shared/NodesManager/NavNodeView/NavNodeViewService.ts @@ -14,6 +14,11 @@ import { NotificationService } from '@cloudbeaver/core-events'; import { NavTreeResource } from '../NavTreeResource'; import type { NavNodeTransformView, INavNodeFolderTransform, NavNodeFolderTransformFn } from './IFolderTransform'; +export interface INodeDuplicateList { + nodes: string[]; + duplicates: string[]; +} + @injectable() export class NavNodeViewService { get tabs(): NavNodeTransformView[] { @@ -53,25 +58,13 @@ export class NavNodeViewService { return children; } - const nextChildren: string[] = []; - const duplicates: string[] = []; - - for (const child of children) { - if (nextChildren.includes(child)) { - if (!duplicates.includes(child)) { - duplicates.push(child); - nextChildren.splice(nextChildren.indexOf(child), 1); - } - } else { - nextChildren.push(child); - } - } + const { nodes, duplicates } = this.filterDuplicates(children); untracked(() => { this.logDuplicates(nodeId, duplicates); }); - return nextChildren; + return nodes; }, }); } @@ -89,6 +82,27 @@ export class NavNodeViewService { this.transformers.push(transform); } + filterDuplicates(nodes: string[]): INodeDuplicateList { + const nextChildren: string[] = []; + const duplicates: string[] = []; + + for (const child of nodes) { + if (nextChildren.includes(child)) { + if (!duplicates.includes(child)) { + duplicates.push(child); + nextChildren.splice(nextChildren.indexOf(child), 1); + } + } else { + nextChildren.push(child); + } + } + + return { + nodes: nextChildren, + duplicates, + }; + } + logDuplicates(nodeId: string, duplicates: string[]) { if (duplicates.length > 0 && !this.duplicationNotify.has(nodeId)) { this.duplicationNotify.add(nodeId); diff --git a/webapp/packages/plugin-data-viewer/src/DataViewerTabService.ts b/webapp/packages/plugin-data-viewer/src/DataViewerTabService.ts index 60002284a1..0e55375980 100644 --- a/webapp/packages/plugin-data-viewer/src/DataViewerTabService.ts +++ b/webapp/packages/plugin-data-viewer/src/DataViewerTabService.ts @@ -40,6 +40,7 @@ export class DataViewerTabService { getPanelComponent: () => DataViewerPanel, onSelect: this.handleTabSelect.bind(this), onRestore: this.handleTabRestore.bind(this), + canClose: this.handleTabCanClose.bind(this), onClose: this.handleTabClose.bind(this), }); } @@ -133,6 +134,23 @@ export class DataViewerTabService { return true; } + private async handleTabCanClose(tab: ITab): Promise { + const model = this.dataViewerTableService.get(tab.handlerState.tableId || ''); + + if (model) { + let canClose = false; + try { + await model.requestDataAction(() => { + canClose = true; + }); + } catch {} + + return canClose; + } + + return true; + } + private handleTabClose(tab: ITab) { if (tab.handlerState.tableId) { this.dataViewerTableService.removeTableModel(tab.handlerState.tableId); diff --git a/webapp/packages/plugin-object-viewer/src/ObjectPage/DBObjectPageService.ts b/webapp/packages/plugin-object-viewer/src/ObjectPage/DBObjectPageService.ts index 125567aa91..e3400edf15 100644 --- a/webapp/packages/plugin-object-viewer/src/ObjectPage/DBObjectPageService.ts +++ b/webapp/packages/plugin-object-viewer/src/ObjectPage/DBObjectPageService.ts @@ -83,6 +83,18 @@ export class DBObjectPageService { return true; } + async canClosePages(tab: ITab): Promise { + for (const page of this.pages.values()) { + const state = await page.canClose?.(tab, this.getPageState(tab, page)); + + if (state === false) { + return false; + } + } + + return true; + } + async closePages(tab: ITab) { await this.callHandlerCallback(tab, page => page.onClose); } diff --git a/webapp/packages/plugin-object-viewer/src/ObjectPage/ObjectPage.ts b/webapp/packages/plugin-object-viewer/src/ObjectPage/ObjectPage.ts index e3ac450f9b..9aaada4b22 100644 --- a/webapp/packages/plugin-object-viewer/src/ObjectPage/ObjectPage.ts +++ b/webapp/packages/plugin-object-viewer/src/ObjectPage/ObjectPage.ts @@ -27,6 +27,7 @@ export interface ObjectPagePanelProps { } export type ObjectPagePanelComponent = React.FunctionComponent>; +export type ObjectPageCloseCallback = (tab: ITab, pageState: T) => Promise | boolean; export type ObjectPageCallback = (tab: ITab, pageState: T) => Promise | void; export type ObjectPageRestoreCallback = ( tab: ITab, @@ -39,6 +40,7 @@ export interface ObjectPageOptions { order?: number; getTabComponent: () => ObjectPageTabComponent; getPanelComponent: () => ObjectPagePanelComponent; + canClose?: ObjectPageCloseCallback; onSelect?: ObjectPageCallback; onClose?: ObjectPageCallback; onRestore?: ObjectPageRestoreCallback; @@ -51,6 +53,7 @@ export class ObjectPage { getTabComponent: () => ObjectPageTabComponent; getPanelComponent: () => ObjectPagePanelComponent; onSelect?: ObjectPageCallback; + canClose?: ObjectPageCloseCallback; onClose?: ObjectPageCallback; onRestore?: ObjectPageRestoreCallback; @@ -65,6 +68,7 @@ export class ObjectPage { this.getTabComponent = options.getTabComponent; this.getPanelComponent = options.getPanelComponent; this.onSelect = options.onSelect; + this.canClose = options.canClose; this.onClose = options.onClose; this.onRestore = options.onRestore; } diff --git a/webapp/packages/plugin-object-viewer/src/ObjectPropertiesPage/ObjectPropertyTable/ObjectChildrenPropertyTable.tsx b/webapp/packages/plugin-object-viewer/src/ObjectPropertiesPage/ObjectPropertyTable/ObjectChildrenPropertyTable.tsx index bf2bbc7629..ef2ba8cd86 100644 --- a/webapp/packages/plugin-object-viewer/src/ObjectPropertiesPage/ObjectPropertyTable/ObjectChildrenPropertyTable.tsx +++ b/webapp/packages/plugin-object-viewer/src/ObjectPropertiesPage/ObjectPropertyTable/ObjectChildrenPropertyTable.tsx @@ -11,7 +11,7 @@ import { useState } from 'react'; import styled, { css } from 'reshadow'; import type { DBObject } from '@cloudbeaver/core-app'; -import { TableHeader, TableBody, Table, useTable, getComputed, useTabLocalState, useControlledScroll, IScrollState } from '@cloudbeaver/core-blocks'; +import { TableHeader, TableBody, Table, useTable, useTabLocalState, useControlledScroll, IScrollState } from '@cloudbeaver/core-blocks'; import { composes, useStyles } from '@cloudbeaver/core-theming'; import { Header } from './Header'; @@ -67,13 +67,11 @@ export const ObjectChildrenPropertyTable = observer(function ObjectProper return null; } - const baseObject = getComputed(() => ( - objects - .slice() - .sort((a, b) => (a.object?.properties?.length || 0) - (b.object?.properties?.length || 0)) - )); + const baseObject = objects + .slice() + .sort((a, b) => (a.object?.properties?.length || 0) - (b.object?.properties?.length || 0)); - const nodeIds = getComputed(() => objects.map(object => object.id)); + const nodeIds = objects.map(object => object.id); const properties = baseObject[0].object?.properties || []; return styled(styles)( diff --git a/webapp/packages/plugin-object-viewer/src/ObjectPropertiesPage/ObjectPropertyTable/ObjectPropertyTable.tsx b/webapp/packages/plugin-object-viewer/src/ObjectPropertiesPage/ObjectPropertyTable/ObjectPropertyTable.tsx index 03d71222ee..f23d0d2b0d 100644 --- a/webapp/packages/plugin-object-viewer/src/ObjectPropertiesPage/ObjectPropertyTable/ObjectPropertyTable.tsx +++ b/webapp/packages/plugin-object-viewer/src/ObjectPropertiesPage/ObjectPropertyTable/ObjectPropertyTable.tsx @@ -6,11 +6,14 @@ * you may not use this file except in compliance with the License. */ +import { untracked } from 'mobx'; import { observer } from 'mobx-react-lite'; +import { useEffect } from 'react'; import styled, { css } from 'reshadow'; -import { DBObject, DBObjectResource, NavTreeResource } from '@cloudbeaver/core-app'; +import { DBObject, DBObjectResource, NavNodeViewService, NavTreeResource } from '@cloudbeaver/core-app'; import { Loader, TextPlaceholder, useMapResource } from '@cloudbeaver/core-blocks'; +import { useService } from '@cloudbeaver/core-di'; import { useTranslate } from '@cloudbeaver/core-localization'; import { resourceKeyList } from '@cloudbeaver/core-sdk'; @@ -39,10 +42,14 @@ export const ObjectPropertyTable = observer(function O }) { parents = [...parents, parentId]; const translate = useTranslate(); + const navNodeViewService = useService(NavNodeViewService); const tree = useMapResource(ObjectPropertyTable, NavTreeResource, objectId, { onLoad: async resource => !(await resource.preloadNodeParents(parents, objectId)), }); - const key = resourceKeyList(tree.data || []); + + const { nodes, duplicates } = navNodeViewService.filterDuplicates(tree.data || []); + + const key = resourceKeyList(nodes); const dbObject = useMapResource(ObjectPropertyTable, DBObjectResource, key, { async onLoad(resource: DBObjectResource) { const preloaded = await tree.resource.preloadNodeParents(parents, objectId); @@ -57,12 +64,18 @@ export const ObjectPropertyTable = observer(function O preload: [tree], }); + useEffect(() => { + untracked(() => { + navNodeViewService.logDuplicates(objectId, duplicates); + }); + }); + const objects = dbObject.data as DBObject[]; return styled(styles)( {() => styled(styles)( <> - {!tree.data || tree.data.length === 0 ? ( + {nodes.length === 0 ? ( {translate('plugin_object_viewer_table_no_items')} ) : (
diff --git a/webapp/packages/plugin-object-viewer/src/ObjectViewerTabService.ts b/webapp/packages/plugin-object-viewer/src/ObjectViewerTabService.ts index d135daa204..62cc078fe1 100644 --- a/webapp/packages/plugin-object-viewer/src/ObjectViewerTabService.ts +++ b/webapp/packages/plugin-object-viewer/src/ObjectViewerTabService.ts @@ -48,6 +48,7 @@ export class ObjectViewerTabService { onRestore: this.restoreObjectTab.bind(this), onSelect: this.selectObjectTab.bind(this), onClose: this.closeObjectTab.bind(this), + canClose: this.canCloseObjectTab.bind(this), extensions: [ connectionProvider(this.getConnection.bind(this)), @@ -330,6 +331,10 @@ export class ObjectViewerTabService { return false; } + private async canCloseObjectTab(tab: ITab): Promise { + return await this.dbObjectPageService.canClosePages(tab); + } + private async closeObjectTab(tab: ITab) { await this.dbObjectPageService.closePages(tab); } diff --git a/webapp/packages/plugin-sql-editor-navigation-tab/src/SqlEditorNavigatorService.ts b/webapp/packages/plugin-sql-editor-navigation-tab/src/SqlEditorNavigatorService.ts index b63326a583..8c0b85172a 100644 --- a/webapp/packages/plugin-sql-editor-navigation-tab/src/SqlEditorNavigatorService.ts +++ b/webapp/packages/plugin-sql-editor-navigation-tab/src/SqlEditorNavigatorService.ts @@ -112,7 +112,11 @@ export class SqlEditorNavigatorService { if (data.type === SQLEditorNavigationAction.select) { this.sqlResultTabsService.selectResultTab(tab.handlerState, data.resultId); } else if (data.type === SQLEditorNavigationAction.close) { - this.sqlResultTabsService.removeResultTab(tab.handlerState, data.resultId); + const canClose = await this.sqlResultTabsService.canCloseResultTab(tab.handlerState, data.resultId); + + if (canClose) { + this.sqlResultTabsService.removeResultTab(tab.handlerState, data.resultId); + } } this.navigationTabsService.selectTab(tab.id); } catch (exception) { diff --git a/webapp/packages/plugin-sql-editor-navigation-tab/src/SqlEditorTabService.ts b/webapp/packages/plugin-sql-editor-navigation-tab/src/SqlEditorTabService.ts index d9f720ebce..30709d72b1 100644 --- a/webapp/packages/plugin-sql-editor-navigation-tab/src/SqlEditorTabService.ts +++ b/webapp/packages/plugin-sql-editor-navigation-tab/src/SqlEditorTabService.ts @@ -55,6 +55,7 @@ export class SqlEditorTabService extends Bootstrap { getPanelComponent: () => SqlEditorPanel, onRestore: this.handleTabRestore.bind(this), onClose: this.handleTabClose.bind(this), + canClose: this.handleCanTabClose.bind(this), extensions: [ connectionProvider(this.getConnectionId.bind(this)), objectCatalogProvider(this.getObjectCatalogId.bind(this)), @@ -250,13 +251,16 @@ export class SqlEditorTabService extends Bootstrap { } } + private async handleCanTabClose(editorTab: ITab) { + return await this.sqlResultTabsService.canCloseResultTabs(editorTab.handlerState); + } + private async handleTabClose(editorTab: ITab) { if (editorTab.handlerState.executionContext) { await this.destroyContext(editorTab.handlerState.executionContext); } - for (const tab of editorTab.handlerState.tabs) { - await this.sqlResultTabsService.removeResultTab(editorTab.handlerState, tab.id); - } + + this.sqlResultTabsService.removeResultTabs(editorTab.handlerState); } private async destroyContext(contextInfo: IConnectionExecutionContextInfo) { diff --git a/webapp/packages/plugin-sql-editor/src/SqlEditor/SqlEditorController.tsx b/webapp/packages/plugin-sql-editor/src/SqlEditor/SqlEditorController.tsx index 160eca6017..3b10440ace 100644 --- a/webapp/packages/plugin-sql-editor/src/SqlEditor/SqlEditorController.tsx +++ b/webapp/packages/plugin-sql-editor/src/SqlEditor/SqlEditorController.tsx @@ -241,6 +241,12 @@ export class SqlEditorController implements IInitializableController, IDestructi }); if (result === DialogueStateResult.Resolved) { + const state = await this.sqlResultTabsService.canCloseResultTabs(this.state); + + if (!state) { + return; + } + this.sqlResultTabsService.removeResultTabs(this.state); } else if (result === DialogueStateResult.Rejected) { return; diff --git a/webapp/packages/plugin-sql-editor/src/SqlResultTabs/SqlQueryResultService.ts b/webapp/packages/plugin-sql-editor/src/SqlResultTabs/SqlQueryResultService.ts index d7f677b21b..8928752e82 100644 --- a/webapp/packages/plugin-sql-editor/src/SqlResultTabs/SqlQueryResultService.ts +++ b/webapp/packages/plugin-sql-editor/src/SqlResultTabs/SqlQueryResultService.ts @@ -136,6 +136,27 @@ export class SqlQueryResultService { this.tableViewerStorageService.remove(group.modelId); } + async canCloseResultTab(state: ISqlEditorTabState, tabId: string): Promise { + const resultTab = state.resultTabs.find(resultTab => resultTab.tabId === tabId); + const group = state.resultGroups.find(group => group.groupId === resultTab?.groupId); + + if (resultTab && group) { + const model = this.tableViewerStorageService.get(group.modelId); + + if (model) { + let canClose = false; + + try { + await model.requestDataAction(() => { + canClose = true; + }); + } catch {} + return canClose; + } + } + return true; + } + removeResultTab(state: ISqlEditorTabState, tabId: string): void { const resultTab = state.resultTabs.find(resultTab => resultTab.tabId === tabId); const group = state.resultGroups.find(group => group.groupId === resultTab?.groupId); diff --git a/webapp/packages/plugin-sql-editor/src/SqlResultTabs/SqlResultTabs.tsx b/webapp/packages/plugin-sql-editor/src/SqlResultTabs/SqlResultTabs.tsx index be144226b4..3fdff2a9eb 100644 --- a/webapp/packages/plugin-sql-editor/src/SqlResultTabs/SqlResultTabs.tsx +++ b/webapp/packages/plugin-sql-editor/src/SqlResultTabs/SqlResultTabs.tsx @@ -79,9 +79,13 @@ export const SqlResultTabs = observer(function SqlDataResult({ state, onT onTabSelect?.(tab.tabId); } - function handleClose(tab: ITabData) { - sqlResultTabsService.removeResultTab(state, tab.tabId); - onTabClose?.(tab.tabId); + async function handleClose(tab: ITabData) { + const canClose = await sqlResultTabsService.canCloseResultTab(state, tab.tabId); + + if (canClose) { + sqlResultTabsService.removeResultTab(state, tab.tabId); + onTabClose?.(tab.tabId); + } } if (!state.tabs.length) { diff --git a/webapp/packages/plugin-sql-editor/src/SqlResultTabs/SqlResultTabsService.ts b/webapp/packages/plugin-sql-editor/src/SqlResultTabs/SqlResultTabsService.ts index d97350731b..18ed2b1b7c 100644 --- a/webapp/packages/plugin-sql-editor/src/SqlResultTabs/SqlResultTabsService.ts +++ b/webapp/packages/plugin-sql-editor/src/SqlResultTabs/SqlResultTabsService.ts @@ -27,6 +27,16 @@ export class SqlResultTabsService { }); } + async canCloseResultTab(state: ISqlEditorTabState, tabId: string): Promise { + const tab = state.tabs.find(tab => tab.id === tabId); + + if (tab) { + return await this.sqlQueryResultService.canCloseResultTab(state, tab.id); + } + + return true; + } + selectResultTab(state: ISqlEditorTabState, resultId: string): void { state.currentTabId = resultId; } @@ -41,6 +51,18 @@ export class SqlResultTabsService { } } + async canCloseResultTabs(state: ISqlEditorTabState): Promise { + for (const tab of state.tabs) { + const canClose = await this.sqlQueryResultService.canCloseResultTab(state, tab.id); + + if (!canClose) { + return false; + } + } + + return true; + } + removeResultTabs(state: ISqlEditorTabState): void { for (const tab of state.tabs.slice()) { this.removeTab(state, tab);