Merge remote-tracking branch 'origin/devel' into devel

This commit is contained in:
Serge Rider
2021-12-10 17:54:06 +03:00
16 changed files with 172 additions and 45 deletions
@@ -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;
@@ -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;
}
@@ -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)(
@@ -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);