mirror of
https://github.com/dbeaver/cloudbeaver.git
synced 2026-09-24 16:04:36 +08:00
Merge remote-tracking branch 'origin/devel' into devel
This commit is contained in:
@@ -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;
|
||||
};
|
||||
}
|
||||
|
||||
@@ -168,6 +168,16 @@ export class NavigationTabsService extends View<ITab<any>> {
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
|
||||
@@ -26,6 +26,7 @@ export interface TabHandlerPanelProps<T = any> {
|
||||
}
|
||||
export type TabHandlerPanelComponent<T = any> = React.FunctionComponent<TabHandlerPanelProps<T>>;
|
||||
|
||||
export type TabHandlerCloseEvent<T = any> = (tab: ITab<T>) => Promise<boolean> | boolean;
|
||||
export type TabHandlerEvent<T = any> = (tab: ITab<T>) => Promise<void> | void;
|
||||
export type TabRestoreEvent<T = any> = (tab: ITab<T>) => Promise<boolean> | boolean;
|
||||
|
||||
@@ -34,6 +35,7 @@ export interface TabHandlerOptions<TState = any> {
|
||||
getTabComponent: () => TabHandlerTabComponent<TState>;
|
||||
getPanelComponent: () => TabHandlerPanelComponent<TState>;
|
||||
onSelect?: TabHandlerEvent<TState>;
|
||||
canClose?: TabHandlerCloseEvent<TState>;
|
||||
onClose?: TabHandlerEvent<TState>;
|
||||
onRestore?: TabRestoreEvent<TState>;
|
||||
extensions?: Array<IExtension<ITab<TState>>>;
|
||||
@@ -45,6 +47,7 @@ export class TabHandler<TState = any> {
|
||||
getPanelComponent: () => TabHandlerPanelComponent<TState>;
|
||||
onSelect?: TabHandlerEvent<TState>;
|
||||
onClose?: TabHandlerEvent<TState>;
|
||||
canClose?: TabHandlerCloseEvent<TState>;
|
||||
onRestore?: TabRestoreEvent<TState>;
|
||||
extensions?: Array<IExtension<ITab<TState>>>;
|
||||
|
||||
@@ -53,6 +56,7 @@ export class TabHandler<TState = any> {
|
||||
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;
|
||||
|
||||
+28
-14
@@ -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);
|
||||
|
||||
@@ -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<IObjectViewerTabState>): Promise<boolean> {
|
||||
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<IObjectViewerTabState>) {
|
||||
if (tab.handlerState.tableId) {
|
||||
this.dataViewerTableService.removeTableModel(tab.handlerState.tableId);
|
||||
|
||||
@@ -83,6 +83,18 @@ export class DBObjectPageService {
|
||||
return true;
|
||||
}
|
||||
|
||||
async canClosePages(tab: ITab<IObjectViewerTabState>): Promise<boolean> {
|
||||
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<IObjectViewerTabState>) {
|
||||
await this.callHandlerCallback(tab, page => page.onClose);
|
||||
}
|
||||
|
||||
@@ -27,6 +27,7 @@ export interface ObjectPagePanelProps<T = unknown> {
|
||||
}
|
||||
export type ObjectPagePanelComponent<T = unknown> = React.FunctionComponent<ObjectPagePanelProps<T>>;
|
||||
|
||||
export type ObjectPageCloseCallback<T> = (tab: ITab<IObjectViewerTabState>, pageState: T) => Promise<boolean> | boolean;
|
||||
export type ObjectPageCallback<T> = (tab: ITab<IObjectViewerTabState>, pageState: T) => Promise<void> | void;
|
||||
export type ObjectPageRestoreCallback<T> = (
|
||||
tab: ITab<IObjectViewerTabState>,
|
||||
@@ -39,6 +40,7 @@ export interface ObjectPageOptions<T = unknown> {
|
||||
order?: number;
|
||||
getTabComponent: () => ObjectPageTabComponent<T>;
|
||||
getPanelComponent: () => ObjectPagePanelComponent<T>;
|
||||
canClose?: ObjectPageCloseCallback<T>;
|
||||
onSelect?: ObjectPageCallback<T>;
|
||||
onClose?: ObjectPageCallback<T>;
|
||||
onRestore?: ObjectPageRestoreCallback<T>;
|
||||
@@ -51,6 +53,7 @@ export class ObjectPage<T = unknown> {
|
||||
getTabComponent: () => ObjectPageTabComponent<T>;
|
||||
getPanelComponent: () => ObjectPagePanelComponent<T>;
|
||||
onSelect?: ObjectPageCallback<T>;
|
||||
canClose?: ObjectPageCloseCallback<T>;
|
||||
onClose?: ObjectPageCallback<T>;
|
||||
onRestore?: ObjectPageRestoreCallback<T>;
|
||||
|
||||
@@ -65,6 +68,7 @@ export class ObjectPage<T = unknown> {
|
||||
this.getTabComponent = options.getTabComponent;
|
||||
this.getPanelComponent = options.getPanelComponent;
|
||||
this.onSelect = options.onSelect;
|
||||
this.canClose = options.canClose;
|
||||
this.onClose = options.onClose;
|
||||
this.onRestore = options.onRestore;
|
||||
}
|
||||
|
||||
+5
-7
@@ -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<Props>(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)(
|
||||
|
||||
+16
-3
@@ -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<ObjectPropertyTableProps>(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<ObjectPropertyTableProps>(function O
|
||||
preload: [tree],
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
untracked(() => {
|
||||
navNodeViewService.logDuplicates(objectId, duplicates);
|
||||
});
|
||||
});
|
||||
|
||||
const objects = dbObject.data as DBObject[];
|
||||
|
||||
return styled(styles)(
|
||||
<Loader state={[tree, dbObject]} style={styles}>{() => styled(styles)(
|
||||
<>
|
||||
{!tree.data || tree.data.length === 0 ? (
|
||||
{nodes.length === 0 ? (
|
||||
<TextPlaceholder>{translate('plugin_object_viewer_table_no_items')}</TextPlaceholder>
|
||||
) : (
|
||||
<div>
|
||||
|
||||
@@ -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<IObjectViewerTabState>): Promise<boolean> {
|
||||
return await this.dbObjectPageService.canClosePages(tab);
|
||||
}
|
||||
|
||||
private async closeObjectTab(tab: ITab<IObjectViewerTabState>) {
|
||||
await this.dbObjectPageService.closePages(tab);
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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<ISqlEditorTabState>) {
|
||||
return await this.sqlResultTabsService.canCloseResultTabs(editorTab.handlerState);
|
||||
}
|
||||
|
||||
private async handleTabClose(editorTab: ITab<ISqlEditorTabState>) {
|
||||
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) {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -136,6 +136,27 @@ export class SqlQueryResultService {
|
||||
this.tableViewerStorageService.remove(group.modelId);
|
||||
}
|
||||
|
||||
async canCloseResultTab(state: ISqlEditorTabState, tabId: string): Promise<boolean> {
|
||||
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);
|
||||
|
||||
@@ -79,9 +79,13 @@ export const SqlResultTabs = observer<Props>(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) {
|
||||
|
||||
@@ -27,6 +27,16 @@ export class SqlResultTabsService {
|
||||
});
|
||||
}
|
||||
|
||||
async canCloseResultTab(state: ISqlEditorTabState, tabId: string): Promise<boolean> {
|
||||
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<boolean> {
|
||||
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);
|
||||
|
||||
Reference in New Issue
Block a user