mirror of
https://github.com/dbeaver/cloudbeaver.git
synced 2026-09-24 16:04:36 +08:00
CB-3274 fix folders sync (#1568)
* CB-3274 fix: connection / resource folders sync * CB-3274 fix: connection folders caching * CB-3274 fix: dialog error handling * CB-3274 fix: nav tree folders mode * CB-3274 fix: nav tree performance * CB-3263 fix: new folders validation * CB-3263 fix: commit includes to alias transformation * CB-3274 chore: clear code
This commit is contained in:
@@ -50,7 +50,7 @@ export const Main = observer(function Main() {
|
||||
mode={leftBarDisabled ? 'minimize' : splitMainState.mode}
|
||||
disable={leftBarDisabled}
|
||||
>
|
||||
<Pane main>
|
||||
<Pane basis='250px' main>
|
||||
<Loader suspense>
|
||||
<SideBarPanel container={leftBarPanelService.tabsContainer} />
|
||||
</Loader>
|
||||
@@ -67,7 +67,7 @@ export const Main = observer(function Main() {
|
||||
<RightArea />
|
||||
</Pane>
|
||||
<ResizerControls />
|
||||
<Pane main>
|
||||
<Pane basis='250px' main>
|
||||
<Loader suspense>
|
||||
<SideBarPanel container={sideBarPanelService.tabsContainer} />
|
||||
</Loader>
|
||||
|
||||
@@ -10,12 +10,14 @@ import type React from 'react';
|
||||
import styled, { css, use } from 'reshadow';
|
||||
|
||||
import { ENotificationType } from '@cloudbeaver/core-events';
|
||||
import type { ComponentStyle } from '@cloudbeaver/core-theming';
|
||||
|
||||
import { AppRefreshButton } from './AppRefreshButton';
|
||||
import { NotificationMark } from './Snackbars/NotificationMark';
|
||||
import { useStyles } from './useStyles';
|
||||
|
||||
const style = css`
|
||||
container {
|
||||
error {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
@@ -25,7 +27,7 @@ const style = css`
|
||||
height: 100vh;
|
||||
}
|
||||
}
|
||||
container-inner-block {
|
||||
error-inner-block {
|
||||
display: flex;
|
||||
margin: auto;
|
||||
padding: 16px 24px;
|
||||
@@ -46,6 +48,7 @@ interface Props {
|
||||
error?: Error;
|
||||
errorInfo?: React.ErrorInfo;
|
||||
className?: string;
|
||||
styles?: ComponentStyle;
|
||||
}
|
||||
|
||||
export const DisplayError: React.FC<React.PropsWithChildren<Props>> = function DisplayError({
|
||||
@@ -54,12 +57,13 @@ export const DisplayError: React.FC<React.PropsWithChildren<Props>> = function D
|
||||
error,
|
||||
errorInfo,
|
||||
className,
|
||||
styles,
|
||||
}) {
|
||||
const stack = errorInfo?.componentStack || error?.stack;
|
||||
|
||||
return styled(style)(
|
||||
<container className={className} {...use({ root })}>
|
||||
<container-inner-block>
|
||||
return styled(useStyles(style, styles))(
|
||||
<error className={className} {...use({ root })}>
|
||||
<error-inner-block>
|
||||
<NotificationMark type={ENotificationType.Error} />
|
||||
<p>Something went wrong.</p>
|
||||
{root && <AppRefreshButton />}
|
||||
@@ -71,7 +75,7 @@ export const DisplayError: React.FC<React.PropsWithChildren<Props>> = function D
|
||||
{stack}
|
||||
</details>
|
||||
)}
|
||||
</container-inner-block>
|
||||
</container>
|
||||
</error-inner-block>
|
||||
</error>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
import React, { ErrorInfo } from 'react';
|
||||
import styled, { css } from 'reshadow';
|
||||
|
||||
import type { ComponentStyle } from '@cloudbeaver/core-theming';
|
||||
import { errorOf, LoadingError } from '@cloudbeaver/core-utils';
|
||||
|
||||
import { Button } from './Button';
|
||||
@@ -23,12 +24,14 @@ const style = css`
|
||||
`;
|
||||
|
||||
interface Props {
|
||||
onRefresh?: () => any;
|
||||
icon?: boolean;
|
||||
root?: boolean;
|
||||
inline?: boolean;
|
||||
remount?: boolean;
|
||||
className?: string;
|
||||
styles?: ComponentStyle;
|
||||
onClose?: () => any;
|
||||
onRefresh?: () => any;
|
||||
}
|
||||
|
||||
interface IErrorData {
|
||||
@@ -76,7 +79,7 @@ export class ErrorBoundary
|
||||
}
|
||||
|
||||
render(): React.ReactNode {
|
||||
const { root, inline, icon, children, className } = this.props;
|
||||
const { root, inline, icon, children, styles, className, onClose } = this.props;
|
||||
|
||||
for (const errorData of this.state.exceptions) {
|
||||
if (root) {
|
||||
@@ -85,8 +88,10 @@ export class ErrorBoundary
|
||||
className={className}
|
||||
root={root}
|
||||
error={errorData.error}
|
||||
styles={styles}
|
||||
errorInfo={errorData.errorInfo}
|
||||
>
|
||||
{onClose && <action><Button onClick={onClose}>Close</Button></action>}
|
||||
{this.canRefresh && <action><Button onClick={this.refresh}>Refresh</Button></action>}
|
||||
</DisplayError>
|
||||
);
|
||||
@@ -97,7 +102,9 @@ export class ErrorBoundary
|
||||
icon={icon}
|
||||
className={className}
|
||||
exception={errorData.error}
|
||||
styles={styles}
|
||||
onRetry={this.canRefresh ? this.refresh : undefined}
|
||||
onClose={onClose}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -9,12 +9,16 @@
|
||||
import { observer } from 'mobx-react-lite';
|
||||
import styled, { css, use } from 'reshadow';
|
||||
|
||||
import type { ComponentStyle } from '@cloudbeaver/core-theming';
|
||||
|
||||
import { Button } from './Button';
|
||||
import { Icon } from './Icon';
|
||||
import { IconOrImage } from './IconOrImage';
|
||||
import { useTranslate } from './localization/useTranslate';
|
||||
import { useErrorDetails } from './useErrorDetails';
|
||||
import { useStyles } from './useStyles';
|
||||
|
||||
const styles = css`
|
||||
const style = css`
|
||||
error {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
@@ -91,6 +95,12 @@ const styles = css`
|
||||
error[|icon] {
|
||||
padding: 0;
|
||||
}
|
||||
error-action-close {
|
||||
cursor: pointer;
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
`;
|
||||
|
||||
interface Props {
|
||||
@@ -100,11 +110,13 @@ interface Props {
|
||||
icon?: boolean;
|
||||
inline?: boolean;
|
||||
className?: string;
|
||||
styles?: ComponentStyle;
|
||||
onRetry?: () => void;
|
||||
onClose?: () => void;
|
||||
}
|
||||
|
||||
export const ExceptionMessage = observer<Props>(function ExceptionMessage({
|
||||
name, message, exception = null, icon, inline, className, onRetry,
|
||||
name, message, exception = null, icon, inline, className, styles, onRetry, onClose,
|
||||
}) {
|
||||
const translate = useTranslate();
|
||||
const error = useErrorDetails(exception);
|
||||
@@ -120,28 +132,37 @@ export const ExceptionMessage = observer<Props>(function ExceptionMessage({
|
||||
};
|
||||
}
|
||||
|
||||
return styled(styles)(
|
||||
return styled(useStyles(style, styles))(
|
||||
<error {...use({ inline, icon })} className={className}>
|
||||
<error-icon title={message}>
|
||||
<IconOrImage icon={inline ? '/icons/error_icon_sm.svg' : '/icons/error_icon.svg'} />
|
||||
</error-icon>
|
||||
{!icon && (
|
||||
<error-data>
|
||||
<error-name><span>{name}</span></error-name>
|
||||
<error-message>{message}</error-message>
|
||||
<error-actions>
|
||||
{error.hasDetails && (
|
||||
<Button type='button' mod={['outlined']} disabled={error.isOpen} onClick={error.open}>
|
||||
{translate('ui_errors_details')}
|
||||
</Button>
|
||||
)}
|
||||
{onRetry && (
|
||||
<Button type='button' mod={['unelevated']} onClick={onRetry}>
|
||||
{translate('ui_processing_retry')}
|
||||
</Button>
|
||||
)}
|
||||
</error-actions>
|
||||
</error-data>
|
||||
<>
|
||||
<error-data>
|
||||
<error-name>
|
||||
<span>{name}</span>
|
||||
</error-name>
|
||||
<error-message>{message}</error-message>
|
||||
<error-actions>
|
||||
{error.hasDetails && (
|
||||
<Button type='button' mod={['outlined']} disabled={error.isOpen} onClick={error.open}>
|
||||
{translate('ui_errors_details')}
|
||||
</Button>
|
||||
)}
|
||||
{onRetry && (
|
||||
<Button type='button' mod={['unelevated']} onClick={onRetry}>
|
||||
{translate('ui_processing_retry')}
|
||||
</Button>
|
||||
)}
|
||||
</error-actions>
|
||||
</error-data>
|
||||
{onClose && (
|
||||
<error-action-close>
|
||||
<Icon name="cross" viewBox="0 0 16 16" onClick={onClose} />
|
||||
</error-action-close>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</error>
|
||||
);
|
||||
|
||||
@@ -44,7 +44,12 @@ export const FolderExplorerPath = observer<Props>(function FolderExplorerPath({
|
||||
const path = context.state.fullPath.slice(0, i);
|
||||
const skipFolder = !canSkip || canSkip(folder);
|
||||
|
||||
if (i === 0 || i === context.state.fullPath.length - 1 || !skipFolder || context.state.fullPath.length < 5) {
|
||||
if (
|
||||
i === 0
|
||||
|| i === context.state.fullPath.length - 1
|
||||
|| !skipFolder
|
||||
|| context.state.fullPath.length < 5
|
||||
) {
|
||||
if (skip) {
|
||||
pathElements.push(
|
||||
<FolderName
|
||||
|
||||
@@ -10,8 +10,9 @@ import { css } from 'reshadow';
|
||||
|
||||
export const folderExplorerStyles = css`
|
||||
folder-explorer-path {
|
||||
display: flex;
|
||||
composes: theme-typography--caption from global;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
folder-explorer-path-element {
|
||||
@@ -23,7 +24,6 @@ export const folderExplorerStyles = css`
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
transform: rotate(-90deg);
|
||||
margin: 0 4px;
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
@@ -31,6 +31,7 @@ export const folderExplorerStyles = css`
|
||||
max-width: 150px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
padding: 0 4px;
|
||||
}
|
||||
|
||||
folder-explorer-path-element:first-child folder-explorer-path-element-arrow {
|
||||
|
||||
@@ -14,8 +14,10 @@ import type { ComponentStyle } from '@cloudbeaver/core-theming';
|
||||
|
||||
import type { ILayoutSizeProps } from '../Containers/ILayoutSizeProps';
|
||||
import { Icon } from '../Icon';
|
||||
import { Loader } from '../Loader/Loader';
|
||||
import { useTranslate } from '../localization/useTranslate';
|
||||
import { useCombinedHandler } from '../useCombinedHandler';
|
||||
import { useStateDelay } from '../useStateDelay';
|
||||
import { useStyles } from '../useStyles';
|
||||
import { baseFormControlStyles, baseInvalidFormControlStyles, baseValidFormControlStyles } from './baseFormControlStyles';
|
||||
import { FormContext } from './FormContext';
|
||||
@@ -37,14 +39,16 @@ const INPUT_FIELD_STYLES = css`
|
||||
input-container {
|
||||
position: relative;
|
||||
}
|
||||
icon-container {
|
||||
loader-container, icon-container {
|
||||
position: absolute;
|
||||
right: 8px;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
display: flex;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
display: flex;
|
||||
}
|
||||
icon-container {
|
||||
cursor: pointer;
|
||||
& Icon {
|
||||
width: 100%;
|
||||
@@ -62,6 +66,7 @@ const INPUT_FIELD_STYLES = css`
|
||||
|
||||
type BaseProps = Omit<React.InputHTMLAttributes<HTMLInputElement>, 'onChange' | 'name' | 'value' | 'style'> & ILayoutSizeProps & {
|
||||
error?: boolean;
|
||||
loading?: boolean;
|
||||
description?: string;
|
||||
labelTooltip?: string;
|
||||
mod?: 'surface';
|
||||
@@ -107,6 +112,7 @@ export const InputField: InputFieldType = observer(forwardRef(function InputFiel
|
||||
children,
|
||||
className,
|
||||
error,
|
||||
loading,
|
||||
description,
|
||||
labelTooltip,
|
||||
mod,
|
||||
@@ -130,6 +136,7 @@ export const InputField: InputFieldType = observer(forwardRef(function InputFiel
|
||||
style
|
||||
);
|
||||
const context = useContext(FormContext);
|
||||
loading = useStateDelay(loading ?? false, 300);
|
||||
|
||||
const revealPassword = useCallback(() => {
|
||||
if (rest.disabled) {
|
||||
@@ -192,6 +199,13 @@ export const InputField: InputFieldType = observer(forwardRef(function InputFiel
|
||||
{...use({ mod })}
|
||||
required={required}
|
||||
/>
|
||||
{loading && (
|
||||
<loader-container
|
||||
title={translate('ui_processing_loading')}
|
||||
>
|
||||
<Loader small />
|
||||
</loader-container>
|
||||
)}
|
||||
{showRevealPasswordButton && (
|
||||
<icon-container
|
||||
title={translate('ui_reveal_password')}
|
||||
|
||||
@@ -25,10 +25,10 @@ interface KeyWithIncludes<TKey, TIncludes> {
|
||||
}
|
||||
|
||||
type ResourceData<
|
||||
TResource extends CachedResource<any, any, any, any>,
|
||||
TResource extends CachedResource<any, any, any, any, any>,
|
||||
TKey,
|
||||
TIncludes
|
||||
> = TResource extends CachedDataResource<any, any, any>
|
||||
> = TResource extends CachedDataResource<any, any, any, any>
|
||||
? CachedResourceData<TResource>
|
||||
: CachedMapResourceLoader<
|
||||
TKey,
|
||||
@@ -39,11 +39,12 @@ type ResourceData<
|
||||
;
|
||||
|
||||
interface IActions<
|
||||
TResource extends CachedResource<any, any, any, any>,
|
||||
TResource extends CachedResource<any, any, any, any, any>,
|
||||
TKey,
|
||||
TIncludes
|
||||
> {
|
||||
active?: boolean;
|
||||
forceSuspense?: boolean;
|
||||
silent?: boolean;
|
||||
onData?: (
|
||||
data: ResourceData<TResource, TKey, TIncludes>,
|
||||
@@ -110,7 +111,7 @@ type TResult<TResource, TKey, TIncludes> = (
|
||||
);
|
||||
|
||||
export function useResource<
|
||||
TResource extends CachedResource<any, any, any, any>,
|
||||
TResource extends CachedResource<any, any, any, any, any>,
|
||||
TKeyArg extends ResourceKey<CachedResourceKey<TResource>>,
|
||||
TIncludes extends Readonly<CachedResourceContext<TResource>>
|
||||
>(
|
||||
@@ -121,7 +122,7 @@ export function useResource<
|
||||
): TResult<TResource, TKeyArg, TIncludes>;
|
||||
|
||||
export function useResource<
|
||||
TResource extends CachedResource<any, any, any, any>,
|
||||
TResource extends CachedResource<any, any, any, any, any>,
|
||||
TKeyArg extends ResourceKey<CachedResourceKey<TResource>>,
|
||||
TIncludes extends CachedResourceContext<TResource>
|
||||
>(
|
||||
@@ -468,6 +469,10 @@ export function useResource<
|
||||
}
|
||||
}, [result.canLoad]);
|
||||
|
||||
if (actions?.forceSuspense) {
|
||||
result.data;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
|
||||
@@ -82,5 +82,5 @@ export function useStyles(
|
||||
}
|
||||
|
||||
export function joinStyles(...styles: ComponentStyle[]): ComponentStyle {
|
||||
return styles.flat();
|
||||
return styles.flat(2);
|
||||
}
|
||||
|
||||
@@ -8,26 +8,30 @@
|
||||
|
||||
import { useEffect, useRef } from 'react';
|
||||
|
||||
import { UserDataService } from '@cloudbeaver/core-authentication';
|
||||
import { UserDataService, UserInfoResource } from '@cloudbeaver/core-authentication';
|
||||
import { useService } from '@cloudbeaver/core-di';
|
||||
|
||||
import { useResource } from './ResourcesHooks/useResource';
|
||||
import { useObjectRef } from './useObjectRef';
|
||||
|
||||
export function useUserData<T extends Record<any, any>>(
|
||||
key: string,
|
||||
defaultValue: () => T,
|
||||
onUpdate?: (data: T) => void,
|
||||
validate?: (data: T) => boolean
|
||||
): T {
|
||||
const optionsRef = useObjectRef({ defaultValue, onUpdate, validate });
|
||||
useResource(useUserData, UserInfoResource, undefined);
|
||||
const userDataService = useService(UserDataService);
|
||||
const ref = useRef<T | null>(null);
|
||||
const data = userDataService.getUserData(key, defaultValue, validate);
|
||||
const previous = ref.current;
|
||||
ref.current = data;
|
||||
|
||||
useEffect(() => {
|
||||
if (previous !== data) {
|
||||
onUpdate?.(data);
|
||||
if (ref.current !== data) {
|
||||
ref.current = data;
|
||||
optionsRef.onUpdate?.(data);
|
||||
}
|
||||
});
|
||||
}, [ref.current, data]);
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
@@ -11,7 +11,10 @@ import { action, makeObservable, runInAction } from 'mobx';
|
||||
import { AppAuthService } from '@cloudbeaver/core-authentication';
|
||||
import { injectable } from '@cloudbeaver/core-di';
|
||||
import { SessionDataResource } from '@cloudbeaver/core-root';
|
||||
import { GraphQLService, CachedMapResource, resourceKeyList, ResourceKeyUtils, CachedMapAllKey, ConnectionFolderInfoFragment, isResourceKeyList, isResourceKeyAlias, resourceKeyAliasFactory, ResourceKey, isResourceAlias } from '@cloudbeaver/core-sdk';
|
||||
import { GraphQLService, CachedMapResource, resourceKeyList, ResourceKeyUtils, CachedMapAllKey, ConnectionFolderInfoFragment, resourceKeyAliasFactory, ResourceKey, isResourceAlias } from '@cloudbeaver/core-sdk';
|
||||
|
||||
import { createConnectionFolderParam } from './createConnectionFolderParam';
|
||||
import { getConnectionFolderIdFromNodeId } from './NavTree/getConnectionFolderIdFromNodeId';
|
||||
|
||||
export type ConnectionFolder = ConnectionFolderInfoFragment;
|
||||
|
||||
@@ -49,13 +52,14 @@ export class ConnectionFolderResource extends CachedMapResource<IConnectionFolde
|
||||
});
|
||||
}
|
||||
|
||||
async create(key: IConnectionFolderParam, parentId?: string): Promise<ConnectionFolder> {
|
||||
async create(projectId: string, name: string, parentId?: string): Promise<ConnectionFolder> {
|
||||
const { folder } = await this.graphQLService.sdk.createConnectionFolder({
|
||||
projectId: key.projectId,
|
||||
folderName: key.folderId,
|
||||
projectId,
|
||||
folderName: name,
|
||||
parentFolderPath: parentId,
|
||||
});
|
||||
|
||||
const key = createConnectionFolderParam(projectId, folder.id);
|
||||
this.set(key, { ...folder, projectId: key.projectId });
|
||||
|
||||
return this.get(key)!;
|
||||
@@ -72,12 +76,10 @@ export class ConnectionFolderResource extends CachedMapResource<IConnectionFolde
|
||||
}
|
||||
|
||||
fromNodeId(nodeId: string): ConnectionFolder | undefined {
|
||||
const data = /^folder:\/\/(.*?)\/(.*)$/ig.exec(nodeId);
|
||||
const key = getConnectionFolderIdFromNodeId(nodeId);
|
||||
|
||||
if (data) {
|
||||
const [t, projectId, folderId] = data;
|
||||
|
||||
return this.get(createConnectionFolderParam(projectId, folderId));
|
||||
if (key) {
|
||||
return this.get(key);
|
||||
}
|
||||
|
||||
return undefined;
|
||||
@@ -88,7 +90,7 @@ export class ConnectionFolderResource extends CachedMapResource<IConnectionFolde
|
||||
): Promise<Map<IConnectionFolderParam, ConnectionFolder>> {
|
||||
const all = this.isAlias(originalKey, CachedMapAllKey);
|
||||
const isProjectFolders = this.isAlias(originalKey, ConnectionFolderProjectKey);
|
||||
const folders: ConnectionFolder[] = [];
|
||||
const folderList: ConnectionFolder[] = [];
|
||||
let projectId: string | undefined;
|
||||
let folderId: string | undefined;
|
||||
|
||||
@@ -108,21 +110,21 @@ export class ConnectionFolderResource extends CachedMapResource<IConnectionFolde
|
||||
projectId,
|
||||
path: folderId,
|
||||
});
|
||||
folders.push(...folders);
|
||||
folderList.push(...folders);
|
||||
});
|
||||
|
||||
const key = resourceKeyList(folders.map<IConnectionFolderParam>(folder => createConnectionFolderParam(
|
||||
const key = resourceKeyList(folderList.map<IConnectionFolderParam>(folder => createConnectionFolderParam(
|
||||
folder.projectId,
|
||||
folder.id,
|
||||
)));
|
||||
|
||||
runInAction(() => {
|
||||
if (all) {
|
||||
this.replace(key, folders);
|
||||
this.replace(key, folderList);
|
||||
} else {
|
||||
if (isProjectFolders) {
|
||||
const removedFolders = this.keys
|
||||
.filter(key => !folders.some(f => (
|
||||
.filter(key => !folderList.some(f => (
|
||||
key.projectId === projectId
|
||||
&& key.folderId === f.id
|
||||
)));
|
||||
@@ -130,7 +132,7 @@ export class ConnectionFolderResource extends CachedMapResource<IConnectionFolde
|
||||
this.delete(resourceKeyList(removedFolders));
|
||||
}
|
||||
|
||||
this.set(key, folders);
|
||||
this.set(key, folderList);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -152,25 +154,3 @@ export class ConnectionFolderResource extends CachedMapResource<IConnectionFolde
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export function createConnectionFolderParam(
|
||||
projectId: string,
|
||||
folder: ConnectionFolder
|
||||
): IConnectionFolderParam;
|
||||
export function createConnectionFolderParam(
|
||||
projectId: string,
|
||||
folderId: string
|
||||
): IConnectionFolderParam;
|
||||
export function createConnectionFolderParam(
|
||||
projectId: string,
|
||||
folderIdOrFolder: string | ConnectionFolder
|
||||
): IConnectionFolderParam {
|
||||
if (typeof folderIdOrFolder === 'object') {
|
||||
folderIdOrFolder = folderIdOrFolder.id;
|
||||
}
|
||||
|
||||
return {
|
||||
projectId: projectId,
|
||||
folderId: folderIdOrFolder,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -20,7 +20,7 @@ import { Connection, ConnectionInfoActiveProjectKey, ConnectionInfoResource, cre
|
||||
import { ConnectionsManagerService } from '../ConnectionsManagerService';
|
||||
import type { IConnectionInfoParams } from '../IConnectionsResource';
|
||||
import { getConnectionParentId } from './getConnectionParentId';
|
||||
import { getFolderNodeParents } from './getFolderParents';
|
||||
import { getFolderNodeParents } from './getFolderNodeParents';
|
||||
|
||||
@injectable()
|
||||
export class ConnectionNavNodeService extends Dependency {
|
||||
@@ -57,9 +57,9 @@ export class ConnectionNavNodeService extends Dependency {
|
||||
const parents = data.nodePaths.map(nodeId => {
|
||||
const parents = getFolderNodeParents(nodeId);
|
||||
|
||||
return parents[parents.length - 2];
|
||||
return parents[parents.length - 1];
|
||||
});
|
||||
this.navTreeResource.markTreeOutdated(resourceKeyList(parents));
|
||||
this.navTreeResource.markOutdated(resourceKeyList(parents));
|
||||
},
|
||||
undefined,
|
||||
this.navTreeResource
|
||||
@@ -70,10 +70,10 @@ export class ConnectionNavNodeService extends Dependency {
|
||||
const parents = data.nodePaths.map(nodeId => {
|
||||
const parents = getFolderNodeParents(nodeId);
|
||||
|
||||
return parents[parents.length - 2];
|
||||
return parents[parents.length - 1];
|
||||
});
|
||||
|
||||
this.navTreeResource.deleteInNode(resourceKeyList(parents), data.nodePaths);
|
||||
this.navTreeResource.deleteInNode(resourceKeyList(parents), data.nodePaths.map(value => [value]));
|
||||
},
|
||||
undefined,
|
||||
this.navTreeResource
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
/*
|
||||
* CloudBeaver - Cloud Database Manager
|
||||
* Copyright (C) 2020-2023 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 type { IConnectionFolderParam } from '../ConnectionFolderResource';
|
||||
import { createConnectionFolderParam } from '../createConnectionFolderParam';
|
||||
|
||||
export function getConnectionFolderIdFromNodeId(nodeId: string): IConnectionFolderParam | undefined {
|
||||
const data = /^folder:\/\/(.*?)\/(.*)$/ig.exec(nodeId);
|
||||
|
||||
if (data) {
|
||||
const [t, projectId, folderId] = data;
|
||||
return createConnectionFolderParam(projectId, folderId);
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
+1
-1
@@ -15,7 +15,7 @@ export function getFolderNodeParents(nodeId: string): string[] {
|
||||
if (isFolderNodeId(nodeId)) {
|
||||
const parents = getPathParents(nodeId);
|
||||
|
||||
return [getProjectNodeId(parents[0].replace('folder://', '')), ...parents.slice(1)];
|
||||
return [parents[0], getProjectNodeId(parents[1].replace('folder://', '')), ...parents.slice(2)];
|
||||
}
|
||||
|
||||
return [];
|
||||
@@ -0,0 +1,31 @@
|
||||
/*
|
||||
* CloudBeaver - Cloud Database Manager
|
||||
* Copyright (C) 2020-2023 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 type { ConnectionFolder, IConnectionFolderParam } from './ConnectionFolderResource';
|
||||
|
||||
export function createConnectionFolderParam(
|
||||
projectId: string,
|
||||
folder: ConnectionFolder
|
||||
): IConnectionFolderParam;
|
||||
export function createConnectionFolderParam(
|
||||
projectId: string,
|
||||
folderId: string
|
||||
): IConnectionFolderParam;
|
||||
export function createConnectionFolderParam(
|
||||
projectId: string,
|
||||
folderIdOrFolder: string | ConnectionFolder
|
||||
): IConnectionFolderParam {
|
||||
if (typeof folderIdOrFolder === 'object') {
|
||||
folderIdOrFolder = folderIdOrFolder.id;
|
||||
}
|
||||
|
||||
return {
|
||||
projectId: projectId,
|
||||
folderId: folderIdOrFolder,
|
||||
};
|
||||
}
|
||||
@@ -10,6 +10,10 @@ export * from './extensions/IObjectSchemaProvider';
|
||||
export * from './extensions/IObjectSchemaSetter';
|
||||
export * from './NavTree/ConnectionNavNodeService';
|
||||
export * from './NavTree/NavNodeExtensionsService';
|
||||
export * from './NavTree/getConnectionFolderIdFromNodeId';
|
||||
export * from './NavTree/getConnectionFolderId';
|
||||
export * from './NavTree/getConnectionParentId';
|
||||
export * from './NavTree/getFolderNodeParents';
|
||||
|
||||
export * from './extensions/IConnectionProvider';
|
||||
export * from './extensions/IConnectionSetter';
|
||||
@@ -22,6 +26,7 @@ export * from './EConnectionFeature';
|
||||
export * from './ConnectionsSettingsService';
|
||||
export * from './ContainerResource';
|
||||
export * from './ConnectionsLocaleService';
|
||||
export * from './createConnectionFolderParam';
|
||||
export * from './DatabaseAuthModelsResource';
|
||||
export * from './DatabaseConnection';
|
||||
export * from './DBDriverResource';
|
||||
|
||||
@@ -25,176 +25,181 @@ export const commonDialogThemeStyle: ThemeSelector = async theme => {
|
||||
return [styles.default];
|
||||
};
|
||||
|
||||
export const commonDialogBaseStyle = css`
|
||||
dialog {
|
||||
composes: theme-background-surface theme-text-on-surface theme-elevation-z10 from global;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
margin: 0;
|
||||
border: none;
|
||||
height: auto;
|
||||
max-height: 100%;
|
||||
max-width: 748px;
|
||||
border-radius: 0.25rem;
|
||||
export const commonDialogContainerStyles = css`
|
||||
error, dialog {
|
||||
composes: theme-background-surface theme-text-on-surface theme-elevation-z10 from global;
|
||||
border-radius: 0.25rem;
|
||||
}
|
||||
`;
|
||||
|
||||
export const commonDialogBaseStyle = [commonDialogContainerStyles, css`
|
||||
dialog {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
margin: 0;
|
||||
border: none;
|
||||
height: auto;
|
||||
max-height: 100%;
|
||||
max-width: 748px;
|
||||
padding: 0px;
|
||||
|
||||
&[|size=small] {
|
||||
min-width: 404px;
|
||||
min-height: 262px;
|
||||
max-height: max(100vh - 48px, 262px);
|
||||
|
||||
&[|fixedSize] {
|
||||
width: 404px;
|
||||
height: 262px;
|
||||
}
|
||||
&[|fixedWidth] {
|
||||
width: 404px;
|
||||
}
|
||||
}
|
||||
&[|size=medium] {
|
||||
min-width: 576px;
|
||||
min-height: 374px;
|
||||
max-height: max(100vh - 48px, 374px);
|
||||
|
||||
&[|fixedSize] {
|
||||
width: 576px;
|
||||
height: 374px;
|
||||
}
|
||||
&[|fixedWidth] {
|
||||
width: 576px;
|
||||
}
|
||||
}
|
||||
&[|size=large] {
|
||||
min-width: 720px;
|
||||
min-height: 468px;
|
||||
max-height: max(100vh - 48px, 468px);
|
||||
|
||||
&[|fixedSize] {
|
||||
width: 720px;
|
||||
height: 468px;
|
||||
}
|
||||
&[|fixedWidth] {
|
||||
width: 720px;
|
||||
}
|
||||
}
|
||||
}
|
||||
header, dialog-body, footer {
|
||||
flex-shrink: 0;
|
||||
padding: 24px;
|
||||
|
||||
&[|no-padding] {
|
||||
padding: 0px;
|
||||
|
||||
&[|size=small] {
|
||||
min-width: 404px;
|
||||
min-height: 262px;
|
||||
max-height: max(100vh - 48px, 262px);
|
||||
|
||||
&[|fixedSize] {
|
||||
width: 404px;
|
||||
height: 262px;
|
||||
}
|
||||
&[|fixedWidth] {
|
||||
width: 404px;
|
||||
}
|
||||
}
|
||||
&[|size=medium] {
|
||||
min-width: 576px;
|
||||
min-height: 374px;
|
||||
max-height: max(100vh - 48px, 374px);
|
||||
|
||||
&[|fixedSize] {
|
||||
width: 576px;
|
||||
height: 374px;
|
||||
}
|
||||
&[|fixedWidth] {
|
||||
width: 576px;
|
||||
}
|
||||
}
|
||||
&[|size=large] {
|
||||
min-width: 720px;
|
||||
min-height: 468px;
|
||||
max-height: max(100vh - 48px, 468px);
|
||||
|
||||
&[|fixedSize] {
|
||||
width: 720px;
|
||||
height: 468px;
|
||||
}
|
||||
&[|fixedWidth] {
|
||||
width: 720px;
|
||||
}
|
||||
}
|
||||
}
|
||||
header, dialog-body, footer {
|
||||
flex-shrink: 0;
|
||||
padding: 24px;
|
||||
|
||||
&[|no-padding] {
|
||||
padding: 0px;
|
||||
}
|
||||
}
|
||||
dialog-body {
|
||||
padding-top: 0px;
|
||||
padding-right: 0px;
|
||||
}
|
||||
footer {
|
||||
padding-top: 0px;
|
||||
}
|
||||
header {
|
||||
position: relative;
|
||||
display: grid;
|
||||
grid-template-columns: max-content 1fr;
|
||||
}
|
||||
header-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
position: relative;
|
||||
min-height: 24px;
|
||||
overflow: hidden;
|
||||
}
|
||||
icon-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
IconOrImage {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
margin-right: 16px;
|
||||
&[|bigIcon] {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
}
|
||||
dialog-body {
|
||||
padding-top: 0px;
|
||||
padding-right: 0px;
|
||||
}
|
||||
dialog-body {
|
||||
flex: 1;
|
||||
box-sizing: content-box;
|
||||
display: flex;
|
||||
max-height: 100%;
|
||||
overflow: auto;
|
||||
}
|
||||
dialog-body-overflow-box {
|
||||
position: relative;
|
||||
flex: 1;
|
||||
box-sizing: border-box;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: auto;
|
||||
word-break: break-word;
|
||||
white-space: pre-wrap;
|
||||
padding-right: 24px;
|
||||
}
|
||||
dialog-body-content {
|
||||
flex: 1;
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
Loader {
|
||||
height: 100%;
|
||||
}
|
||||
dialog-body[|no-padding] + footer {
|
||||
padding-top: 24px;
|
||||
}
|
||||
dialog-body[|no-padding] dialog-body-overflow-box {
|
||||
padding-right: 0;
|
||||
}
|
||||
dialog-body[|no-overflow] dialog-body-content {
|
||||
overflow: auto;
|
||||
}
|
||||
dialog-body-overflow {
|
||||
composes: branding-overflow from global;
|
||||
position: sticky;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
flex-shrink: 0;
|
||||
width: 100%;
|
||||
height: 24px;
|
||||
pointer-events: none;
|
||||
}
|
||||
h3 {
|
||||
margin: 0;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
sub-title {
|
||||
composes: theme-typography--caption from global;
|
||||
grid-column: 2;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
reject {
|
||||
cursor: pointer;
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
footer {
|
||||
display: flex;
|
||||
z-index: 0;
|
||||
box-sizing: border-box;
|
||||
|
||||
&:empty {
|
||||
display: none;
|
||||
}
|
||||
footer {
|
||||
padding-top: 0px;
|
||||
}
|
||||
header {
|
||||
position: relative;
|
||||
display: grid;
|
||||
grid-template-columns: max-content 1fr;
|
||||
}
|
||||
header-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
position: relative;
|
||||
min-height: 24px;
|
||||
overflow: hidden;
|
||||
}
|
||||
icon-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
IconOrImage {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
margin-right: 16px;
|
||||
&[|bigIcon] {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
}
|
||||
}
|
||||
dialog-body {
|
||||
flex: 1;
|
||||
box-sizing: content-box;
|
||||
display: flex;
|
||||
max-height: 100%;
|
||||
overflow: auto;
|
||||
}
|
||||
dialog-body-overflow-box {
|
||||
position: relative;
|
||||
flex: 1;
|
||||
box-sizing: border-box;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: auto;
|
||||
word-break: break-word;
|
||||
white-space: pre-wrap;
|
||||
padding-right: 24px;
|
||||
}
|
||||
dialog-body-content {
|
||||
flex: 1;
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
Loader {
|
||||
height: 100%;
|
||||
}
|
||||
dialog-body[|no-padding] + footer {
|
||||
padding-top: 24px;
|
||||
}
|
||||
dialog-body[|no-padding] dialog-body-overflow-box {
|
||||
padding-right: 0;
|
||||
}
|
||||
dialog-body[|no-overflow] dialog-body-content {
|
||||
overflow: auto;
|
||||
}
|
||||
dialog-body-overflow {
|
||||
composes: branding-overflow from global;
|
||||
position: sticky;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
flex-shrink: 0;
|
||||
width: 100%;
|
||||
height: 24px;
|
||||
pointer-events: none;
|
||||
}
|
||||
h3 {
|
||||
margin: 0;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
sub-title {
|
||||
composes: theme-typography--caption from global;
|
||||
grid-column: 2;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
reject {
|
||||
cursor: pointer;
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
footer {
|
||||
display: flex;
|
||||
z-index: 0;
|
||||
box-sizing: border-box;
|
||||
|
||||
&:empty {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
`;
|
||||
}
|
||||
`];
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
import { observable, runInAction } from 'mobx';
|
||||
|
||||
import { injectable } from '@cloudbeaver/core-di';
|
||||
import { uuid } from '@cloudbeaver/core-utils';
|
||||
|
||||
export interface DialogOptions {
|
||||
persistent?: boolean;
|
||||
@@ -33,6 +34,7 @@ export enum DialogueStateResult {
|
||||
}
|
||||
|
||||
export interface DialogInternal<TResult> {
|
||||
id: string;
|
||||
component: DialogComponent<any, any>;
|
||||
payload: any;
|
||||
options?: DialogOptions;
|
||||
@@ -59,6 +61,7 @@ export class CommonDialogService {
|
||||
});
|
||||
|
||||
const dialogInternal: DialogInternal<TResult> = {
|
||||
id: uuid(),
|
||||
component,
|
||||
payload,
|
||||
resolve: _resolve!,
|
||||
|
||||
@@ -11,10 +11,11 @@ import { useMemo } from 'react';
|
||||
import { DialogBackdrop } from 'reakit/Dialog';
|
||||
import styled from 'reshadow';
|
||||
|
||||
import { Loader, useObjectRef, useStyles } from '@cloudbeaver/core-blocks';
|
||||
import { ErrorBoundary, Loader, useObjectRef, useStyles } from '@cloudbeaver/core-blocks';
|
||||
import { useService } from '@cloudbeaver/core-di';
|
||||
|
||||
|
||||
import { commonDialogContainerStyles } from './CommonDialog/styles';
|
||||
import { CommonDialogService, DialogInternal } from './CommonDialogService';
|
||||
import { DialogContext, IDialogContext } from './DialogContext';
|
||||
import { dialogStyles } from './styles';
|
||||
@@ -54,13 +55,20 @@ export const DialogsPortal = observer(function DialogsPortal() {
|
||||
<DialogBackdrop visible={!!activeDialog} onMouseDown={state.backdropClick}>
|
||||
<inner-box>
|
||||
{commonDialogService.dialogs.map((dialog, i, arr) => (
|
||||
<NestedDialog
|
||||
key={i}
|
||||
visible={i === arr.length - 1}
|
||||
dialog={dialog}
|
||||
resolveDialog={state.resolve}
|
||||
rejectDialog={state.reject}
|
||||
/>
|
||||
<ErrorBoundary
|
||||
key={dialog.id}
|
||||
styles={commonDialogContainerStyles}
|
||||
remount
|
||||
onClose={state.reject}
|
||||
>
|
||||
<NestedDialog
|
||||
key={dialog.id}
|
||||
visible={i === arr.length - 1}
|
||||
dialog={dialog}
|
||||
resolveDialog={state.resolve}
|
||||
rejectDialog={state.reject}
|
||||
/>
|
||||
</ErrorBoundary>
|
||||
))}
|
||||
</inner-box>
|
||||
</DialogBackdrop>
|
||||
|
||||
@@ -30,6 +30,10 @@ export const dialogStyles = css`
|
||||
padding: 24px;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
|
||||
& > :not(:last-child) {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
Dialog {
|
||||
|
||||
@@ -411,10 +411,11 @@ export class NavTreeResource extends CachedMapResource<string, string[], Record<
|
||||
if (!this.navNodeInfoResource.has(nodeId) && nodeId !== ROOT_NODE_PATH) {
|
||||
await this.navNodeInfoResource.loadNodeParents(nodeId);
|
||||
}
|
||||
const preloaded = await this.preloadNodeParents(this.navNodeInfoResource.getParents(nodeId), nodeId);
|
||||
const parents = this.navNodeInfoResource.getParents(nodeId);
|
||||
const preloaded = await this.preloadNodeParents(parents, nodeId);
|
||||
|
||||
if (!preloaded) {
|
||||
const cause = new DetailsError(`Entity not found: ${nodeId}`);
|
||||
const cause = new DetailsError(`Entity not found:\n"${nodeId}"\nPath:\n${parents.map(parent => `"${parent}"`).join('\n')}`);
|
||||
const error = new ResourceError(this, key, undefined, 'Entity not found', { cause });
|
||||
ExecutorInterrupter.interrupt(contexts);
|
||||
throw this.markError(error, key);
|
||||
|
||||
@@ -20,10 +20,12 @@ import { ResourceKeyListAlias, resourceKeyListAlias } from './ResourceKeyListAli
|
||||
import { ResourceKeyUtils } from './ResourceKeyUtils';
|
||||
|
||||
export type CachedMapResourceKey<TResource> = CachedResourceKey<TResource>;
|
||||
export type CachedMapResourceValue<TResource> = TResource extends CachedResource<Map<any, infer T>, any, any, any>
|
||||
export type CachedMapResourceValue<TResource> = TResource extends CachedResource<Map<any, infer T>, any, any, any, any>
|
||||
? T
|
||||
: never;
|
||||
export type CachedMapResourceArguments<TResource> = TResource extends CachedMapResource<any, any, infer T, any>
|
||||
? T
|
||||
: never;
|
||||
export type CachedMapResourceArguments<TResource> = TResource extends CachedMapResource<any, any, infer T> ? T : never;
|
||||
|
||||
export type CachedMapResourceListGetter<
|
||||
TValue,
|
||||
|
||||
@@ -10,7 +10,7 @@ import { observable, makeObservable, action, computed, toJS } from 'mobx';
|
||||
|
||||
import { Dependency } from '@cloudbeaver/core-di';
|
||||
import { ExecutionContext, Executor, ExecutorInterrupter, IExecutor, IExecutorHandler, IExecutionContextProvider, ISyncExecutor, SyncExecutor, TaskScheduler } from '@cloudbeaver/core-executor';
|
||||
import { MetadataMap, uuid } from '@cloudbeaver/core-utils';
|
||||
import { isPrimitive, MetadataMap, uuid } from '@cloudbeaver/core-utils';
|
||||
|
||||
import { isResourceAlias, ResourceAlias, ResourceAliasFactory, ResourceAliasOptions } from './ResourceAlias';
|
||||
import { ResourceError } from './ResourceError';
|
||||
@@ -560,6 +560,16 @@ export abstract class CachedResource<
|
||||
this.commitIncludes(metadata, includes);
|
||||
}
|
||||
});
|
||||
if (isResourceAlias(param)) {
|
||||
param = this.transformToKey(param);
|
||||
|
||||
this.updateMetadata(param, metadata => {
|
||||
metadata.loaded = true;
|
||||
if (includes) {
|
||||
this.commitIncludes(metadata, includes);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
markError(exception: Error, key: ResourceKey<TKey>, include?: TInclude): ResourceError {
|
||||
@@ -744,6 +754,9 @@ export abstract class CachedResource<
|
||||
* Can be override to provide static link to complicated keys
|
||||
*/
|
||||
protected getKeyRef(key: TKey): TKey {
|
||||
if (isPrimitive(key)) {
|
||||
return key;
|
||||
}
|
||||
return Object.freeze(toJS(key));
|
||||
}
|
||||
|
||||
@@ -751,11 +764,15 @@ export abstract class CachedResource<
|
||||
* Can be override to provide static link to complicated keys
|
||||
*/
|
||||
protected getMetadataKeyRef(key: ResourceKeyFlat<TKey>): TKey {
|
||||
if (this.isAlias(key)) {
|
||||
if (isResourceAlias(key)) {
|
||||
return this.transformToAlias(key)
|
||||
.toString() as TKey;
|
||||
}
|
||||
|
||||
if (isPrimitive(key)) {
|
||||
return key;
|
||||
}
|
||||
|
||||
const ref = Array.from(this.metadata.keys())
|
||||
.find(k => this.isKeyEqual(k, key));
|
||||
|
||||
@@ -879,6 +896,9 @@ export abstract class CachedResource<
|
||||
}
|
||||
|
||||
protected markOutdatedSync(key: ResourceKey<TKey>): void {
|
||||
if (!this.hasMetadata(key)) {
|
||||
return;
|
||||
}
|
||||
this.updateMetadata(key, metadata => {
|
||||
metadata.outdated = true;
|
||||
});
|
||||
|
||||
@@ -12,7 +12,7 @@ import type { CachedResource } from './CachedResource';
|
||||
|
||||
export class ResourceError extends LoadingError {
|
||||
constructor(
|
||||
private readonly resource: CachedResource<any, any, any, any>,
|
||||
private readonly resource: CachedResource<any, any, any, any, any>,
|
||||
private readonly key: any,
|
||||
private readonly context: any,
|
||||
message?: string,
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
* you may not use this file except in compliance with the License.
|
||||
*/
|
||||
|
||||
import { action, autorun, entries, IReactionDisposer, keys, observable, ObservableMap, remove, runInAction, set, toJS } from 'mobx';
|
||||
import { entries, IReactionDisposer, keys, observable, ObservableMap, reaction, remove, runInAction, set, toJS } from 'mobx';
|
||||
|
||||
import { injectable } from '@cloudbeaver/core-di';
|
||||
import { ISyncExecutor, SyncExecutor } from '@cloudbeaver/core-executor';
|
||||
@@ -213,7 +213,14 @@ class DataStorage<T extends Record<any, any> | Map<any, any>> implements ILocalS
|
||||
|
||||
subscribe() {
|
||||
this.unsubscribe();
|
||||
this.mobxSub = autorun(() => this.saveState());
|
||||
this.mobxSub = reaction(
|
||||
() => toJS(this.store),
|
||||
() => this.saveState(),
|
||||
{
|
||||
fireImmediately: true,
|
||||
delay: 500,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
unsubscribe() {
|
||||
|
||||
@@ -12,4 +12,4 @@ export type BaseStyles = ClassCollection;
|
||||
export type ThemeSelector = (theme: string) => Promise<undefined | BaseStyles | BaseStyles[]>;
|
||||
export type Style = BaseStyles | ThemeSelector;
|
||||
export type DynamicStyle = Style | boolean | undefined;
|
||||
export type ComponentStyle = DynamicStyle | DynamicStyle[];
|
||||
export type ComponentStyle = DynamicStyle | DynamicStyle[] | DynamicStyle[][];
|
||||
@@ -6,95 +6,99 @@
|
||||
* you may not use this file except in compliance with the License.
|
||||
*/
|
||||
|
||||
import { makeObservable, observable, untracked } from 'mobx';
|
||||
import { observable, makeAutoObservable, action } from 'mobx';
|
||||
|
||||
import { TempMap } from './TempMap';
|
||||
|
||||
export type MetadataValueGetter<TKey, TValue> = (key: TKey, metadata: MetadataMap<TKey, any>) => TValue;
|
||||
export type DefaultValueGetter<TKey, TValue> = (key: TKey, metadata: MetadataMap<TKey, TValue>) => TValue;
|
||||
|
||||
export class MetadataMap<TKey, TValue> {
|
||||
private data: Map<TKey, TValue>;
|
||||
private length: number;
|
||||
|
||||
export class MetadataMap<TKey, TValue> implements Map<TKey, TValue> {
|
||||
get size(): number {
|
||||
return this.temp.size;
|
||||
}
|
||||
private syncData: Array<[TKey, TValue]> | null;
|
||||
private readonly temp: TempMap<TKey, TValue>;
|
||||
private readonly data: Map<TKey, TValue>;
|
||||
|
||||
constructor(private readonly defaultValueGetter?: DefaultValueGetter<TKey, TValue>) {
|
||||
this.data = observable(new Map());
|
||||
this.length = 0;
|
||||
this.syncData = null;
|
||||
this.data = observable(new Map());
|
||||
this.temp = new TempMap<TKey, TValue>(
|
||||
this.data,
|
||||
() => {
|
||||
this.syncData?.splice(0, this.syncData.length, ...this.data.entries());
|
||||
}
|
||||
);
|
||||
|
||||
makeObservable<this, 'data'>(this, {
|
||||
data: observable.ref,
|
||||
makeAutoObservable(this, {
|
||||
sync: action,
|
||||
});
|
||||
}
|
||||
|
||||
[Symbol.iterator]() {
|
||||
return this.data[Symbol.iterator]();
|
||||
[Symbol.iterator](): IterableIterator<[TKey, TValue]> {
|
||||
return this.temp[Symbol.iterator]();
|
||||
}
|
||||
|
||||
get [Symbol.toStringTag](): string {
|
||||
return 'MetadataMap';
|
||||
}
|
||||
|
||||
sync(entities: Array<[TKey, TValue]>): void {
|
||||
this.data = observable(new Map(entities));
|
||||
this.temp.clear();
|
||||
this.data.clear();
|
||||
for (const [key, value] of entities) {
|
||||
this.data.set(key, value);
|
||||
}
|
||||
this.syncData = entities;
|
||||
}
|
||||
|
||||
forEach(callbackfn: (value: TValue, key: TKey, map: Map<TKey, TValue>) => void, thisArg?: any): void {
|
||||
this.temp.forEach(callbackfn, thisArg);
|
||||
}
|
||||
|
||||
entries(): IterableIterator<[TKey, TValue]> {
|
||||
return this.data.entries();
|
||||
return this.temp.entries();
|
||||
}
|
||||
|
||||
keys(): IterableIterator<TKey> {
|
||||
return this.data.keys();
|
||||
return this.temp.keys();
|
||||
}
|
||||
|
||||
values(): IterableIterator<TValue> {
|
||||
return this.data.values();
|
||||
}
|
||||
|
||||
count(): number {
|
||||
return this.length;
|
||||
return this.temp.values();
|
||||
}
|
||||
|
||||
has(key: TKey): boolean {
|
||||
return this.data.has(key);
|
||||
return this.temp.has(key);
|
||||
}
|
||||
|
||||
set(key: TKey, value: TValue): void {
|
||||
this.data.set(key, value);
|
||||
this.syncData?.push([key, this.data.get(key)!]);
|
||||
set(key: TKey, value: TValue): this {
|
||||
this.temp.set(key, value);
|
||||
return this;
|
||||
}
|
||||
|
||||
get(key: TKey, defaultValue?: DefaultValueGetter<TKey, TValue>): TValue {
|
||||
if (this.data.has(key)) {
|
||||
return this.data.get(key)!;
|
||||
if (!this.temp.has(key)) {
|
||||
const provider = defaultValue || this.defaultValueGetter;
|
||||
|
||||
if (!provider) {
|
||||
throw new Error('MetadataMap: defaultValue should be provided if defaultValueGetter not set');
|
||||
}
|
||||
|
||||
const value = provider(key, this);
|
||||
this.temp.set(key, observable(value as any));
|
||||
}
|
||||
|
||||
const provider = defaultValue || this.defaultValueGetter;
|
||||
|
||||
if (!provider) {
|
||||
throw new Error('MetadataMap: defaultValue should be provided if defaultValueGetter not set');
|
||||
}
|
||||
|
||||
const value = provider(key, this);
|
||||
untracked(() => {
|
||||
this.set(key, observable(value as any));
|
||||
this.length++;
|
||||
});
|
||||
return this.data.get(key)!;
|
||||
return this.temp.get(key)!;
|
||||
}
|
||||
|
||||
delete(key: TKey): void {
|
||||
if (this.data.has(key)) {
|
||||
this.data.delete(key);
|
||||
|
||||
const removeIndex = this.syncData?.findIndex(([k]) => k === key) || -1;
|
||||
if (removeIndex > -1) {
|
||||
this.syncData?.splice(removeIndex, 1);
|
||||
}
|
||||
this.length--;
|
||||
}
|
||||
delete(key: TKey): boolean {
|
||||
return this.temp.delete(key);
|
||||
}
|
||||
|
||||
clear(): void {
|
||||
this.data.clear();
|
||||
this.length = 0;
|
||||
this.temp.clear();
|
||||
this.syncData?.splice(0, this.syncData.length);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,6 +8,8 @@
|
||||
|
||||
import { action } from 'mobx';
|
||||
|
||||
import { cacheValue, ICachedValueObject } from './cacheValue';
|
||||
|
||||
const combine = <T>(a: IterableIterator<T>, b: IterableIterator<T>): IterableIterator<T> => (
|
||||
function* () { yield* a; yield* b; }
|
||||
)();
|
||||
@@ -28,23 +30,38 @@ export class TempMap<TKey, TValue> implements Map<TKey, TValue> {
|
||||
private readonly deleted: TKey[];
|
||||
private readonly temp: Map<TKey, TValue>;
|
||||
private flushTask: NodeJS.Timeout | null;
|
||||
private readonly keysTemp: ICachedValueObject<TKey[]>;
|
||||
private readonly valuesTemp: ICachedValueObject<TValue[]>;
|
||||
private readonly entriesTemp: ICachedValueObject<[TKey, TValue][]>;
|
||||
|
||||
constructor(
|
||||
private readonly target: Map<TKey, TValue>
|
||||
private readonly target: Map<TKey, TValue>,
|
||||
private readonly onSync?: () => void,
|
||||
) {
|
||||
this.temp = new Map();
|
||||
this.flushTask = null;
|
||||
this.deleted = [];
|
||||
this.keysTemp = cacheValue();
|
||||
this.entriesTemp = cacheValue();
|
||||
this.valuesTemp = cacheValue();
|
||||
}
|
||||
|
||||
isDeleted(key: TKey): boolean {
|
||||
return this.deleted.includes(key);
|
||||
}
|
||||
|
||||
/**
|
||||
* This function will not call clear on target map
|
||||
*/
|
||||
clear(): void {
|
||||
if (this.flushTask) {
|
||||
clearTimeout(this.flushTask);
|
||||
}
|
||||
this.deleted.splice(0, this.deleted.length);
|
||||
this.temp.clear();
|
||||
this.target.clear();
|
||||
this.keysTemp.invalidate();
|
||||
this.valuesTemp.invalidate();
|
||||
this.entriesTemp.invalidate();
|
||||
}
|
||||
|
||||
delete(key: TKey): boolean {
|
||||
@@ -88,29 +105,39 @@ export class TempMap<TKey, TValue> implements Map<TKey, TValue> {
|
||||
|
||||
set(key: TKey, value: TValue): this {
|
||||
this.temp.set(key, value);
|
||||
|
||||
const indexOfDeleted = this.deleted.indexOf(key);
|
||||
if (indexOfDeleted !== -1) {
|
||||
this.deleted.splice(indexOfDeleted, 1);
|
||||
}
|
||||
|
||||
this.scheduleFlush();
|
||||
return this;
|
||||
}
|
||||
|
||||
entries(): IterableIterator<[TKey, TValue]> {
|
||||
return Array.from(this.keys())
|
||||
.map<[TKey, TValue]>(key => [key, this.get(key)!])
|
||||
return this.entriesTemp.value(() => Array.from(this.keys())
|
||||
.map<[TKey, TValue]>(key => [key, this.get(key)!]))
|
||||
.values();
|
||||
}
|
||||
|
||||
keys(): IterableIterator<TKey> {
|
||||
return Array.from(new Set(combine(this.target.keys(), this.temp.keys())))
|
||||
.filter(key => !this.isDeleted(key))
|
||||
return this.keysTemp.value(() => Array.from(new Set(combine(this.target.keys(), this.temp.keys())))
|
||||
.filter(key => !this.isDeleted(key)))
|
||||
.values();
|
||||
}
|
||||
|
||||
values(): IterableIterator<TValue> {
|
||||
return Array.from(this.keys())
|
||||
.map<TValue>(key => this.get(key)!)
|
||||
return this.valuesTemp.value(() => Array.from(this.keys())
|
||||
.map<TValue>(key => this.get(key)!))
|
||||
.values();
|
||||
}
|
||||
|
||||
private scheduleFlush(): void {
|
||||
this.keysTemp.invalidate();
|
||||
this.valuesTemp.invalidate();
|
||||
this.entriesTemp.invalidate();
|
||||
|
||||
if (this.flushTask !== null) {
|
||||
return;
|
||||
}
|
||||
@@ -124,9 +151,13 @@ export class TempMap<TKey, TValue> implements Map<TKey, TValue> {
|
||||
for (const [key, value] of this.temp) {
|
||||
this.target.set(key, value);
|
||||
}
|
||||
this.onSync?.();
|
||||
this.temp.clear();
|
||||
this.keysTemp.invalidate();
|
||||
this.valuesTemp.invalidate();
|
||||
this.entriesTemp.invalidate();
|
||||
|
||||
this.flushTask = null;
|
||||
}), 0);
|
||||
}), 300);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
/*
|
||||
* CloudBeaver - Cloud Database Manager
|
||||
* Copyright (C) 2020-2023 DBeaver Corp and others
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0.
|
||||
* you may not use this file except in compliance with the License.
|
||||
*/
|
||||
|
||||
export interface ICachedValueObject<T> {
|
||||
readonly invalid: boolean;
|
||||
value(getter: () => T): T;
|
||||
invalidate(): void;
|
||||
}
|
||||
|
||||
export function cacheValue<T>(): ICachedValueObject<T> {
|
||||
let value: T;
|
||||
let invalid = true;
|
||||
|
||||
return {
|
||||
value(getter: () => T) {
|
||||
if (invalid) {
|
||||
value = getter();
|
||||
invalid = false;
|
||||
}
|
||||
return value;
|
||||
},
|
||||
get invalid() {
|
||||
return invalid;
|
||||
},
|
||||
invalidate() {
|
||||
invalid = true;
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -10,6 +10,7 @@ export * from './underscore';
|
||||
|
||||
export * from './blobToData';
|
||||
export * from './bytesToSize';
|
||||
export * from './cacheValue';
|
||||
export * from './clsx';
|
||||
export * from './copyToClipboard';
|
||||
export * from './declensionOfNumber';
|
||||
@@ -25,6 +26,7 @@ export * from './GlobalConstants';
|
||||
export * from './ILoadableState';
|
||||
export * from './errorOf';
|
||||
export * from './isArraysEqual';
|
||||
export * from './isPrimitive';
|
||||
export * from './isPropertiesEqual';
|
||||
export * from './isValuesEqual';
|
||||
export * from './md5';
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
/*
|
||||
* CloudBeaver - Cloud Database Manager
|
||||
* Copyright (C) 2020-2023 DBeaver Corp and others
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0.
|
||||
* you may not use this file except in compliance with the License.
|
||||
*/
|
||||
|
||||
export function isPrimitive(value: any): boolean {
|
||||
return value === null || (typeof value !== 'object' && typeof value !== 'function');
|
||||
}
|
||||
@@ -10,7 +10,8 @@
|
||||
import { untracked } from 'mobx';
|
||||
|
||||
import { UserInfoResource } from '@cloudbeaver/core-authentication';
|
||||
import { ConnectionFolder, ConnectionFolderProjectKey, ConnectionFolderResource, ConnectionInfoResource, ConnectionsManagerService, CONNECTION_FOLDER_NAME_VALIDATION, createConnectionFolderParam, createConnectionParam, IConnectionInfoParams } from '@cloudbeaver/core-connections';
|
||||
import { ConnectionFolderProjectKey, ConnectionFolderResource, ConnectionInfoResource, ConnectionsManagerService, CONNECTION_FOLDER_NAME_VALIDATION, createConnectionFolderParam, createConnectionParam, getConnectionFolderIdFromNodeId, IConnectionFolderParam, IConnectionInfoParams } from '@cloudbeaver/core-connections';
|
||||
import { getConnectionFolderId } from '@cloudbeaver/core-connections/src/NavTree/getConnectionFolderId';
|
||||
import { Bootstrap, injectable } from '@cloudbeaver/core-di';
|
||||
import { CommonDialogService, ConfirmationDialogDelete, DialogueStateResult } from '@cloudbeaver/core-dialogs';
|
||||
import { NotificationService } from '@cloudbeaver/core-events';
|
||||
@@ -18,7 +19,7 @@ import { ExecutorInterrupter, IExecutionContextProvider } from '@cloudbeaver/cor
|
||||
import { LocalizationService } from '@cloudbeaver/core-localization';
|
||||
import { ENodeMoveType, getNodesFromContext, INodeMoveData, NavNode, NavNodeInfoResource, NavNodeManagerService, navNodeMoveContext, NavTreeResource, NAV_NODE_TYPE_FOLDER, nodeDeleteContext, ProjectsNavNodeService, ROOT_NODE_PATH } from '@cloudbeaver/core-navigation-tree';
|
||||
import { getProjectNodeId, NAV_NODE_TYPE_PROJECT, ProjectInfoResource } from '@cloudbeaver/core-projects';
|
||||
import { CachedMapAllKey, ResourceKey, resourceKeyList, ResourceKeySimple, ResourceKeyUtils } from '@cloudbeaver/core-sdk';
|
||||
import { CachedMapAllKey, ResourceKeyAlias, resourceKeyList, ResourceKeySimple, ResourceKeyUtils } from '@cloudbeaver/core-sdk';
|
||||
import { createPath } from '@cloudbeaver/core-utils';
|
||||
import { ActionService, ACTION_NEW_FOLDER, DATA_CONTEXT_MENU, IAction, IDataContextProvider, MenuService } from '@cloudbeaver/core-view';
|
||||
import { DATA_CONTEXT_ELEMENTS_TREE, MENU_ELEMENTS_TREE_TOOLS, type IElementsTree } from '@cloudbeaver/plugin-navigation-tree';
|
||||
@@ -36,7 +37,6 @@ interface ITargetNode {
|
||||
|
||||
@injectable()
|
||||
export class ConnectionFoldersBootstrap extends Bootstrap {
|
||||
|
||||
constructor(
|
||||
private readonly localizationService: LocalizationService,
|
||||
private readonly userInfoResource: UserInfoResource,
|
||||
@@ -256,53 +256,59 @@ export class ConnectionFoldersBootstrap extends Bootstrap {
|
||||
return;
|
||||
}
|
||||
|
||||
await this.connectionFolderResource.load(ConnectionFolderProjectKey(targetNode.projectId));
|
||||
|
||||
let parentFolder: ConnectionFolder | undefined;
|
||||
let parentFolderParam: IConnectionFolderParam | undefined;
|
||||
|
||||
if (targetNode.folderId) {
|
||||
parentFolder = this.connectionFolderResource.fromNodeId(targetNode.folderId);
|
||||
|
||||
if (!parentFolder) {
|
||||
this.notificationService.logError({ title:'Can\'t create folder', message: 'Folder not found' });
|
||||
return;
|
||||
}
|
||||
parentFolderParam = getConnectionFolderIdFromNodeId(targetNode.folderId);
|
||||
}
|
||||
|
||||
const result = await this.commonDialogService.open(FolderDialog, {
|
||||
value: this.localizationService.translate('ui_folder_new'),
|
||||
projectId: targetNode.projectId,
|
||||
folder: parentFolderParam?.folderId,
|
||||
title: 'core_view_action_new_folder',
|
||||
subTitle: parentFolder?.id,
|
||||
icon: '/icons/folder.svg#root',
|
||||
create: true,
|
||||
selectProject: targetNode.selectProject,
|
||||
validation: async ({ folder, projectId }, setMessage) => {
|
||||
const trimmed = folder.trim();
|
||||
validation: async ({ name, folder, projectId }, setMessage) => {
|
||||
const trimmed = name.trim();
|
||||
|
||||
if (trimmed.length === 0 || !folder.match(CONNECTION_FOLDER_NAME_VALIDATION)) {
|
||||
if (trimmed.length === 0 || !trimmed.match(CONNECTION_FOLDER_NAME_VALIDATION)) {
|
||||
setMessage('connections_connection_folder_validation');
|
||||
return false;
|
||||
}
|
||||
|
||||
await this.connectionFolderResource.load(ConnectionFolderProjectKey(projectId));
|
||||
let parentKey: ResourceKeyAlias<any, {
|
||||
projectId: string;
|
||||
}> | IConnectionFolderParam = ConnectionFolderProjectKey(projectId);
|
||||
|
||||
return !this.connectionFolderResource.has(createConnectionFolderParam(
|
||||
projectId,
|
||||
createPath(parentFolder?.id, trimmed)
|
||||
));
|
||||
if (folder) {
|
||||
parentKey = createConnectionFolderParam(
|
||||
projectId,
|
||||
folder
|
||||
);
|
||||
}
|
||||
|
||||
try {
|
||||
await this.connectionFolderResource.load(parentKey);
|
||||
|
||||
return !this.connectionFolderResource.has(createConnectionFolderParam(
|
||||
projectId,
|
||||
createPath(folder, trimmed)
|
||||
));
|
||||
} catch (exception: any) {
|
||||
setMessage('connections_connection_folder_validation');
|
||||
return false;
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
if (result !== DialogueStateResult.Rejected && result !== DialogueStateResult.Resolved) {
|
||||
try {
|
||||
await this.connectionFolderResource.create(createConnectionFolderParam(
|
||||
result.projectId,
|
||||
result.folder
|
||||
), parentFolder?.id);
|
||||
await this.connectionFolderResource.create(result.projectId, result.name, result.folder);
|
||||
this.navTreeResource.markOutdated(
|
||||
targetNode.folderId !== undefined
|
||||
? targetNode.folderId
|
||||
result.folder
|
||||
? getConnectionFolderId(createConnectionFolderParam(result.projectId, result.folder))
|
||||
: getProjectNodeId(result.projectId)
|
||||
);
|
||||
} catch (exception: any) {
|
||||
|
||||
@@ -9,10 +9,12 @@
|
||||
import { makeObservable, observable } from 'mobx';
|
||||
|
||||
import { injectable } from '@cloudbeaver/core-di';
|
||||
import { ServerConfigResource } from '@cloudbeaver/core-root';
|
||||
import { LocalStorageSaveService } from '@cloudbeaver/core-settings';
|
||||
|
||||
interface IDevToolsSettings {
|
||||
enabled: boolean;
|
||||
distributed: boolean;
|
||||
}
|
||||
|
||||
const DEVTOOLS = 'devtools';
|
||||
@@ -23,9 +25,14 @@ export class DevToolsService {
|
||||
return this.settings.enabled;
|
||||
}
|
||||
|
||||
get isDistributed(): boolean {
|
||||
return this.settings.distributed;
|
||||
}
|
||||
|
||||
private readonly settings: IDevToolsSettings;
|
||||
|
||||
constructor(
|
||||
private readonly serverConfigResource: ServerConfigResource,
|
||||
private readonly autoSaveService: LocalStorageSaveService,
|
||||
) {
|
||||
this.settings = getDefaultDevToolsSettings();
|
||||
@@ -34,15 +41,31 @@ export class DevToolsService {
|
||||
settings: observable,
|
||||
});
|
||||
this.autoSaveService.withAutoSave(DEVTOOLS, this.settings, getDefaultDevToolsSettings);
|
||||
this.serverConfigResource.onDataUpdate.addHandler(this.syncDistributedMode.bind(this));
|
||||
}
|
||||
|
||||
switch() {
|
||||
this.settings.enabled = !this.settings.enabled;
|
||||
this.syncDistributedMode();
|
||||
}
|
||||
|
||||
setDistributedMode(distributed: boolean) {
|
||||
this.settings.distributed = distributed;
|
||||
this.syncDistributedMode();
|
||||
}
|
||||
|
||||
private syncDistributedMode() {
|
||||
if (this.isEnabled) {
|
||||
if (this.serverConfigResource.data) {
|
||||
this.serverConfigResource.data.distributed = this.isDistributed;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function getDefaultDevToolsSettings(): IDevToolsSettings {
|
||||
return {
|
||||
enabled: false,
|
||||
distributed: false,
|
||||
};
|
||||
}
|
||||
@@ -15,6 +15,7 @@ import { TOP_NAV_BAR_SETTINGS_MENU } from '@cloudbeaver/plugin-settings-menu';
|
||||
import { MENU_USER_PROFILE } from '@cloudbeaver/plugin-user-profile';
|
||||
|
||||
import { ACTION_DEVTOOLS } from './actions/ACTION_DEVTOOLS';
|
||||
import { ACTION_DEVTOOLS_MODE_DISTRIBUTED } from './actions/ACTION_DEVTOOLS_MODE_DISTRIBUTED';
|
||||
import { DATA_CONTEXT_MENU_SEARCH } from './ContextMenu/DATA_CONTEXT_MENU_SEARCH';
|
||||
import { SearchResourceMenuItem } from './ContextMenu/SearchResourceMenuItem';
|
||||
import { DevToolsService } from './DevToolsService';
|
||||
@@ -109,12 +110,22 @@ export class PluginBootstrap extends Bootstrap {
|
||||
|
||||
return [
|
||||
new SearchResourceMenuItem(),
|
||||
ACTION_DEVTOOLS_MODE_DISTRIBUTED,
|
||||
MENU_PLUGINS,
|
||||
...items,
|
||||
];
|
||||
},
|
||||
});
|
||||
|
||||
this.actionService.addHandler({
|
||||
id: 'devtools-mode-distributed',
|
||||
isActionApplicable: (context, action) => action === ACTION_DEVTOOLS_MODE_DISTRIBUTED,
|
||||
isChecked: () => this.devToolsService.isDistributed,
|
||||
handler: () => {
|
||||
this.devToolsService.setDistributedMode(!this.devToolsService.isDistributed);
|
||||
},
|
||||
});
|
||||
|
||||
this.menuService.addCreator({
|
||||
isApplicable: context => context.get(DATA_CONTEXT_MENU) === MENU_PLUGINS,
|
||||
getItems: (context, items) => [
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
/*
|
||||
* CloudBeaver - Cloud Database Manager
|
||||
* Copyright (C) 2020-2022 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 { createAction } from '@cloudbeaver/core-view';
|
||||
|
||||
export const ACTION_DEVTOOLS_MODE_DISTRIBUTED = createAction('devtools-mode-distributed', {
|
||||
type: 'checkbox',
|
||||
label: 'Distributed mode',
|
||||
tooltip: 'Enable distributed mode',
|
||||
});
|
||||
@@ -131,7 +131,7 @@ export class NavigationTabsService extends View<ITab> {
|
||||
this.onTabClose = new SyncExecutor();
|
||||
this.onInit = new SyncExecutor();
|
||||
this.onStateUpdate = new SyncExecutor();
|
||||
this.metadata = new MetadataMap(() => ({ restored: false }));
|
||||
this.metadata = new MetadataMap<string, ITabMetadata>(() => ({ restored: false }));
|
||||
|
||||
this.navigationTabContext = (): ITabNavigationContext => new TabNavigationContext(this, this.userInfoResource);
|
||||
this.registerAction(ACTION_OPEN_IN_TAB);
|
||||
|
||||
+6
-87
@@ -14,7 +14,6 @@ import { EventTreeNodeClickFlag, EventTreeNodeExpandFlag, EventTreeNodeSelectFla
|
||||
import { useService } from '@cloudbeaver/core-di';
|
||||
import { EventContext, EventStopPropagationFlag } from '@cloudbeaver/core-events';
|
||||
import { type NavNode, ROOT_NODE_PATH, NavTreeResource, NavNodeInfoResource, EObjectFeature } from '@cloudbeaver/core-navigation-tree';
|
||||
import { resourceKeyList } from '@cloudbeaver/core-sdk';
|
||||
import type { ComponentStyle } from '@cloudbeaver/core-theming';
|
||||
|
||||
import { useNavTreeDropBox } from '../useNavTreeDropBox';
|
||||
@@ -31,6 +30,7 @@ import { elementsTreeLimitFilter } from './NavTreeLimitFilter/elementsTreeLimitF
|
||||
import { elementsTreeLimitRenderer } from './NavTreeLimitFilter/elementsTreeLimitRenderer';
|
||||
import { useDropOutside } from './useDropOutside';
|
||||
import { IElementsTreeOptions, useElementsTree } from './useElementsTree';
|
||||
import { useElementsTreeFolderExplorer } from './useElementsTreeFolderExplorer';
|
||||
|
||||
const styles = css`
|
||||
box {
|
||||
@@ -65,7 +65,7 @@ const styles = css`
|
||||
}
|
||||
|
||||
FolderExplorerPath {
|
||||
padding: 0 12px 8px 12px;
|
||||
padding: 0 4px 8px 4px;
|
||||
}
|
||||
|
||||
drop-outside {
|
||||
@@ -133,61 +133,13 @@ export const ElementsTree = observer<ElementsTreeProps>(function ElementsTree({
|
||||
onSelect,
|
||||
onFilter,
|
||||
}) {
|
||||
const folderExplorer = useFolderExplorer(baseRoot, {
|
||||
saveState: settings?.saveExpanded,
|
||||
});
|
||||
const navTreeResource = useService(NavTreeResource);
|
||||
const navNodeInfoResource = useService(NavNodeInfoResource);
|
||||
const ref = useObjectRef({ settings, getChildren, loadChildren });
|
||||
const treeRootRef = useRef<HTMLDivElement>(null);
|
||||
const folderExplorer = useElementsTreeFolderExplorer(baseRoot, settings);
|
||||
|
||||
const root = folderExplorer.state.folder;
|
||||
|
||||
function exitFolders(path: string[]) {
|
||||
path = path.filter(nodeId => ref.getChildren(nodeId) !== undefined);
|
||||
if (path.length > 0) {
|
||||
folderExplorer.open(path.slice(0, path.length - 1), path[path.length - 1]);
|
||||
}
|
||||
}
|
||||
|
||||
const autoOpenFolders = useCallback(async function autoOpenFolders(nodeId: string, path: string[]) {
|
||||
path = [...path];
|
||||
|
||||
if (!ref.settings?.foldersTree && !folderExplorer.options.expandFoldersWithSingleElement) {
|
||||
return;
|
||||
}
|
||||
|
||||
while (folderExplorer.options.expandFoldersWithSingleElement) {
|
||||
const children = ref.getChildren(nodeId);
|
||||
|
||||
if (children?.length === 1) {
|
||||
const nextNodeId = children[0];
|
||||
const loaded = await ref.loadChildren(nextNodeId, false);
|
||||
|
||||
if (!loaded) {
|
||||
break;
|
||||
}
|
||||
|
||||
path.push(nodeId);
|
||||
nodeId = nextNodeId;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
folderExplorer.open(path, nodeId);
|
||||
|
||||
}, [folderExplorer]);
|
||||
|
||||
useResource(ElementsTree, navTreeResource, resourceKeyList(folderExplorer.state.fullPath), {
|
||||
onError: () => {
|
||||
exitFolders(folderExplorer.state.fullPath);
|
||||
},
|
||||
onData: () => {
|
||||
autoOpenFolders(folderExplorer.state.folder, folderExplorer.state.path);
|
||||
},
|
||||
});
|
||||
|
||||
const limitFilter = useMemo(() => elementsTreeLimitFilter(
|
||||
navTreeResource,
|
||||
limit
|
||||
@@ -221,6 +173,8 @@ export const ElementsTree = observer<ElementsTreeProps>(function ElementsTree({
|
||||
customSelect,
|
||||
onExpand,
|
||||
onSelect,
|
||||
onOpen,
|
||||
onClick,
|
||||
});
|
||||
|
||||
const context = useMemo<IElementsTreeContext>(
|
||||
@@ -230,24 +184,8 @@ export const ElementsTree = observer<ElementsTreeProps>(function ElementsTree({
|
||||
selectionTree,
|
||||
control,
|
||||
getTreeRoot: () => treeRootRef.current,
|
||||
onOpen: async (node, path, leaf) => {
|
||||
const folder = !leaf && tree.settings?.foldersTree || false;
|
||||
|
||||
await onOpen?.(node, folder);
|
||||
|
||||
if (!leaf && tree.settings?.foldersTree) {
|
||||
const nodeId = node.id;
|
||||
const loaded = await ref.loadChildren(nodeId, true);
|
||||
|
||||
if (loaded) {
|
||||
await autoOpenFolders(nodeId, path);
|
||||
tree.setFilter('');
|
||||
}
|
||||
}
|
||||
},
|
||||
onClick,
|
||||
}),
|
||||
[control, folderExplorer, selectionTree, onOpen, onClick, folderExplorer]
|
||||
[tree, folderExplorer, selectionTree, control]
|
||||
);
|
||||
|
||||
const getName = useCallback(
|
||||
@@ -281,22 +219,6 @@ export const ElementsTree = observer<ElementsTreeProps>(function ElementsTree({
|
||||
tree.resetSelection();
|
||||
}
|
||||
|
||||
const foldersTree = settings?.foldersTree; // mobx subscription
|
||||
const filter = settings?.filter;
|
||||
|
||||
useEffect(() => {
|
||||
if (!foldersTree && folderExplorer.state.folder !== baseRoot) {
|
||||
folderExplorer.open([], baseRoot);
|
||||
}
|
||||
if (!filter && tree.filtering) {
|
||||
tree.setFilter('');
|
||||
}
|
||||
});
|
||||
|
||||
const children = tree.getNodeChildren(root);
|
||||
const hasChildren = children.length > 0;
|
||||
const loaderAvailable = !foldersTree || context.folderExplorer.root === root;
|
||||
|
||||
return styled(useStyles(TREE_NODE_STYLES, styles, style))(
|
||||
<>
|
||||
<ElementsTreeTools tree={tree} settingsElements={settingsElements} style={style} />
|
||||
@@ -319,11 +241,9 @@ export const ElementsTree = observer<ElementsTreeProps>(function ElementsTree({
|
||||
</TreeNodeNested>
|
||||
</drop-outside>
|
||||
<ElementsTreeContentLoader
|
||||
root={root}
|
||||
context={context}
|
||||
emptyPlaceholder={emptyPlaceholder}
|
||||
childrenState={tree}
|
||||
hasChildren={hasChildren}
|
||||
>
|
||||
<tree-elements>
|
||||
<NavigationNodeNested
|
||||
@@ -334,7 +254,6 @@ export const ElementsTree = observer<ElementsTreeProps>(function ElementsTree({
|
||||
root
|
||||
/>
|
||||
</tree-elements>
|
||||
{loaderAvailable && <Loader state={tree} overlay={hasChildren} />}
|
||||
</ElementsTreeContentLoader>
|
||||
</tree>
|
||||
</FolderExplorer>
|
||||
|
||||
+11
-33
@@ -7,58 +7,36 @@
|
||||
*/
|
||||
|
||||
import { observer } from 'mobx-react-lite';
|
||||
import styled, { css } from 'reshadow';
|
||||
import styled from 'reshadow';
|
||||
|
||||
import { Loader, Translate, TreeNodeNestedMessage, TREE_NODE_STYLES } from '@cloudbeaver/core-blocks';
|
||||
import { getComputed, Loader, Translate, TreeNodeNestedMessage, TREE_NODE_STYLES } from '@cloudbeaver/core-blocks';
|
||||
import type { ILoadableState } from '@cloudbeaver/core-utils';
|
||||
|
||||
import type { IElementsTreeContext } from './ElementsTreeContext';
|
||||
|
||||
const styles = css`
|
||||
center {
|
||||
display: flex;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
min-width: 240px;
|
||||
}
|
||||
`;
|
||||
|
||||
interface Props {
|
||||
root: string;
|
||||
context: IElementsTreeContext;
|
||||
childrenState: ILoadableState;
|
||||
emptyPlaceholder?: React.FC;
|
||||
hasChildren?: boolean;
|
||||
}
|
||||
|
||||
export const ElementsTreeContentLoader = observer<React.PropsWithChildren<Props>>(function ElementsTreeContentLoader({
|
||||
root,
|
||||
context,
|
||||
childrenState,
|
||||
emptyPlaceholder: Placeholder,
|
||||
hasChildren,
|
||||
children,
|
||||
}) {
|
||||
let loading = childrenState.isLoading() || context.tree.loading;
|
||||
const hasChildren = getComputed(() => context.tree.getNodeChildren(context.tree.root).length > 0);
|
||||
|
||||
if (context.tree.settings?.foldersTree && context.folderExplorer.root !== root) {
|
||||
loading = false;
|
||||
}
|
||||
|
||||
if (context.tree.settings?.saveExpanded && context.tree.loading) {
|
||||
return styled(styles)(
|
||||
<center>
|
||||
<Loader />
|
||||
</center>
|
||||
);
|
||||
}
|
||||
const loading = getComputed(() => (
|
||||
childrenState.isLoading()
|
||||
|| context.tree.loading
|
||||
) && !context.tree.isLoaded());
|
||||
|
||||
if (!hasChildren) {
|
||||
if (loading) {
|
||||
return styled(styles)(
|
||||
<center>
|
||||
<Loader />
|
||||
</center>
|
||||
return (
|
||||
<Loader />
|
||||
);
|
||||
} else if (context.tree.filtering) {
|
||||
return styled(TREE_NODE_STYLES)(
|
||||
@@ -72,8 +50,8 @@ export const ElementsTreeContentLoader = observer<React.PropsWithChildren<Props>
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Loader suspense>
|
||||
{children}
|
||||
</>
|
||||
</Loader>
|
||||
);
|
||||
});
|
||||
|
||||
-3
@@ -9,7 +9,6 @@
|
||||
import { createContext } from 'react';
|
||||
|
||||
import type { IFolderExplorerContext } from '@cloudbeaver/core-blocks';
|
||||
import type { NavNode } from '@cloudbeaver/core-navigation-tree';
|
||||
|
||||
import type { NavTreeControlComponent } from './NavigationNodeComponent';
|
||||
import type { IElementsTree } from './useElementsTree';
|
||||
@@ -20,8 +19,6 @@ export interface IElementsTreeContext {
|
||||
selectionTree: boolean;
|
||||
control?: NavTreeControlComponent;
|
||||
getTreeRoot: () => HTMLDivElement | null;
|
||||
onOpen?: (node: NavNode, path: string[], leaf: boolean) => Promise<void> | void;
|
||||
onClick?: (node: NavNode, path: string[], leaf: boolean) => Promise<void> | void;
|
||||
}
|
||||
|
||||
export const ElementsTreeContext = createContext<IElementsTreeContext | null>(null);
|
||||
|
||||
+1
-3
@@ -11,7 +11,6 @@ import React, { useState } from 'react';
|
||||
import styled, { css, use } from 'reshadow';
|
||||
|
||||
import { ACTION_ICON_BUTTON_STYLES, IconButton, PlaceholderElement, useResource, useStyles, useTranslate } from '@cloudbeaver/core-blocks';
|
||||
import { NavTreeResource } from '@cloudbeaver/core-navigation-tree';
|
||||
import type { ComponentStyle } from '@cloudbeaver/core-theming';
|
||||
import { useCaptureViewContext } from '@cloudbeaver/core-view';
|
||||
|
||||
@@ -80,14 +79,13 @@ export const ElementsTreeTools = observer<React.PropsWithChildren<Props>>(functi
|
||||
const translate = useTranslate();
|
||||
const [opened, setOpen] = useState(false);
|
||||
const styles = useStyles(ACTION_ICON_BUTTON_STYLES, toolsStyles, style);
|
||||
const rootNode = useResource(ElementsTreeTools, NavTreeResource, root);
|
||||
|
||||
useCaptureViewContext(context => {
|
||||
context?.set(DATA_CONTEXT_NAV_TREE_ROOT, tree.baseRoot);
|
||||
context?.set(DATA_CONTEXT_ELEMENTS_TREE, tree);
|
||||
});
|
||||
|
||||
const loading = rootNode.isLoading();
|
||||
const loading = tree.isLoading();
|
||||
|
||||
return styled(styles)(
|
||||
<tools>
|
||||
|
||||
-1
@@ -45,7 +45,6 @@ export const NavigationNodeNested = observer(forwardRef<HTMLDivElement, Props>(f
|
||||
!!root && treeContext?.folderExplorer.state.folder !== treeContext?.folderExplorer.root
|
||||
));
|
||||
|
||||
|
||||
if (!rootFolder) {
|
||||
children = getComputed(
|
||||
() => treeContext?.tree.getNodeChildren(nodeId) || [],
|
||||
|
||||
+2
-2
@@ -73,8 +73,8 @@ export function useNavigationNode(node: NavNode, path: string[]): INavigationNod
|
||||
() => contextRef.context?.tree.isNodeIndeterminateSelected(node.id) || false
|
||||
);
|
||||
|
||||
const handleClick = async (leaf: boolean) => await contextRef.context?.onClick?.(node, path, leaf);
|
||||
const handleOpen = async (leaf: boolean) => await contextRef.context?.onOpen?.(node, path, leaf);
|
||||
const handleClick = async (leaf: boolean) => await contextRef.context?.tree.click(node, path, leaf);
|
||||
const handleOpen = async (leaf: boolean) => await contextRef.context?.tree.open(node, path, leaf);
|
||||
const handleExpand = async () => await contextRef.context?.tree.expand(node, !expanded);
|
||||
const handleSelect = async (
|
||||
multiple = false,
|
||||
|
||||
+147
-71
@@ -9,7 +9,7 @@
|
||||
import { action, computed, observable, runInAction } from 'mobx';
|
||||
import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
|
||||
import { IFolderExplorerContext, useExecutor, useResource, useObjectRef, useObservableRef, useUserData } from '@cloudbeaver/core-blocks';
|
||||
import { IFolderExplorerContext, useExecutor, useResource, useObjectRef, useObservableRef, useUserData, getComputed } from '@cloudbeaver/core-blocks';
|
||||
import { ConnectionInfoActiveProjectKey, ConnectionInfoResource } from '@cloudbeaver/core-connections';
|
||||
import { useService } from '@cloudbeaver/core-di';
|
||||
import { NotificationService } from '@cloudbeaver/core-events';
|
||||
@@ -78,6 +78,8 @@ export interface IElementsTreeOptions {
|
||||
onFilter?: (value: string) => Promise<void> | void;
|
||||
onSelect?: (node: NavNode, state: boolean) => Promise<void> | void;
|
||||
onExpand?: (node: NavNode, state: boolean) => Promise<void> | void;
|
||||
onOpen?: (node: NavNode, folder: boolean) => Promise<void> | void;
|
||||
onClick?: (node: NavNode) => Promise<void> | void;
|
||||
isGroup?: (node: NavNode) => boolean;
|
||||
customSelect?: (node: NavNode, multiple: boolean, nested: boolean) => Promise<void> | void;
|
||||
beforeSelect?: (node: NavNode, multiple: boolean, nested: boolean) => Promise<void> | void;
|
||||
@@ -117,10 +119,13 @@ export interface IElementsTree extends ILoadableState {
|
||||
select: (node: NavNode, multiple: boolean, nested: boolean) => Promise<void>;
|
||||
resetSelection(): Promise<void>;
|
||||
setDnDData: (data: IDNDData, dragging: boolean) => void;
|
||||
open: (node: NavNode, path: string[], leaf: boolean) => Promise<void>;
|
||||
click: (node: NavNode, path: string[], leaf: boolean) => Promise<void>;
|
||||
expand: (node: NavNode, state: boolean) => Promise<void>;
|
||||
show: (nodeId: string, parents: string[]) => Promise<void>;
|
||||
refresh: (nodeId: string) => Promise<void>;
|
||||
collapse: () => void;
|
||||
loadPath: (path: string[], lastNode?: string) => Promise<string | undefined>;
|
||||
}
|
||||
|
||||
export function useElementsTree(options: IOptions): IElementsTree {
|
||||
@@ -145,52 +150,96 @@ export function useElementsTree(options: IOptions): IElementsTree {
|
||||
|
||||
const functionsRef = useObjectRef({
|
||||
async loadTree(nodeId: string) {
|
||||
await connectionInfoResource.load(ConnectionInfoActiveProjectKey);
|
||||
const preloaded = await navTreeResource.preloadNodeParents(options.folderExplorer.state.fullPath);
|
||||
elementsTree.loading = true;
|
||||
try {
|
||||
await projectInfoResource.load();
|
||||
await connectionInfoResource.load(ConnectionInfoActiveProjectKey);
|
||||
const preloadedRoot = await elementsTree.loadPath(options.folderExplorer.state.fullPath);
|
||||
|
||||
if (!preloaded) {
|
||||
return;
|
||||
}
|
||||
if (preloadedRoot !== options.folderExplorer.state.folder) {
|
||||
if (preloadedRoot === undefined) {
|
||||
options.folderExplorer.open([], options.baseRoot);
|
||||
} else {
|
||||
this.exitNodeFolder(preloadedRoot);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
let children = [nodeId];
|
||||
let children = [nodeId];
|
||||
|
||||
while (children.length > 0) {
|
||||
const nextChildren: string[] = [];
|
||||
while (children.length > 0) {
|
||||
const nextChildren: string[] = [];
|
||||
|
||||
await Promise.all(children.map(async child => {
|
||||
await navTreeResource.waitLoad();
|
||||
await navNodeInfoResource.waitLoad();
|
||||
await Promise.all(children.map(async child => {
|
||||
await projectInfoResource.waitLoad();
|
||||
await connectionInfoResource.waitLoad();
|
||||
await navTreeResource.waitLoad();
|
||||
await navNodeInfoResource.waitLoad();
|
||||
|
||||
const expanded = elementsTree.isNodeExpanded(child, true);
|
||||
if (!expanded && child !== options.root) {
|
||||
if (navNodeInfoResource.isOutdated(child)) {
|
||||
const expanded = elementsTree.isNodeExpanded(child, true);
|
||||
if (!expanded && child !== options.root) {
|
||||
if (navNodeInfoResource.isOutdated(child)) {
|
||||
const node = navNodeInfoResource.get(child);
|
||||
|
||||
if (node?.parentId !== undefined && !navTreeResource.isOutdated(node.parentId)) {
|
||||
await navNodeInfoResource.load(child);
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const loaded = await options.loadChildren(child, false);
|
||||
|
||||
if (!loaded) {
|
||||
const node = navNodeInfoResource.get(child);
|
||||
|
||||
if (node?.parentId !== undefined && !navTreeResource.isOutdated(node.parentId)) {
|
||||
await navNodeInfoResource.load(child);
|
||||
if (node) {
|
||||
await elementsTree.expand(node, false);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (
|
||||
elementsTree.settings?.foldersTree
|
||||
&& options.folderExplorer.options.expandFoldersWithSingleElement
|
||||
&& child === options.root
|
||||
&& elementsTree.getNodeChildren(child).length === 1
|
||||
) {
|
||||
const nextNode = elementsTree.getNodeChildren(child)[0];
|
||||
|
||||
if (elementsTree.isNodeExpandable(nextNode) || elementsTree.isNodeExpanded(nextNode)) {
|
||||
options.folderExplorer.open(navNodeInfoResource.getParents(nextNode), nextNode);
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
nextChildren.push(...(navTreeResource.get(child) || []));
|
||||
}));
|
||||
|
||||
const loaded = await options.loadChildren(child, false);
|
||||
|
||||
if (!loaded) {
|
||||
const node = navNodeInfoResource.get(child);
|
||||
|
||||
if (node) {
|
||||
await elementsTree.expand(node, false);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
nextChildren.push(...(navTreeResource.get(child) || []));
|
||||
}));
|
||||
|
||||
children = nextChildren;
|
||||
children = nextChildren;
|
||||
}
|
||||
} finally {
|
||||
elementsTree.loading = false;
|
||||
}
|
||||
},
|
||||
|
||||
exitNodeFolder(nodeId: string) {
|
||||
runInAction(() => {
|
||||
const folderExplorer = options.folderExplorer;
|
||||
|
||||
if (folderExplorer.state.fullPath.length === 1 || nodeId === options.baseRoot) {
|
||||
return;
|
||||
}
|
||||
|
||||
const pathIndex = folderExplorer.state.fullPath.indexOf(nodeId);
|
||||
|
||||
if (pathIndex >= 0) {
|
||||
folderExplorer.open(
|
||||
folderExplorer.state.fullPath.slice(0, pathIndex - 1),
|
||||
folderExplorer.state.fullPath[pathIndex - 1]
|
||||
);
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
getNestedChildren(nodeId: string): string[] {
|
||||
const nestedChildren: string[] = [];
|
||||
const prevChildren = elementsTree.getNodeChildren(nodeId);
|
||||
@@ -292,12 +341,7 @@ export function useElementsTree(options: IOptions): IElementsTree {
|
||||
|
||||
state.sync(data.nodeState);
|
||||
|
||||
elementsTree.loading = true;
|
||||
try {
|
||||
await functionsRef.loadTree(options.root);
|
||||
} finally {
|
||||
elementsTree.loading = false;
|
||||
}
|
||||
await functionsRef.loadTree(options.root);
|
||||
},
|
||||
data => (
|
||||
typeof data === 'object'
|
||||
@@ -314,14 +358,14 @@ export function useElementsTree(options: IOptions): IElementsTree {
|
||||
get filter(): string {
|
||||
return this.userData.filter;
|
||||
},
|
||||
get filtering() {
|
||||
get filtering(): boolean {
|
||||
return this.filter !== '';
|
||||
},
|
||||
isLoading() {
|
||||
isLoading(): boolean {
|
||||
return this.loading;
|
||||
},
|
||||
isLoaded() {
|
||||
return true;
|
||||
isLoaded(): boolean {
|
||||
return navNodeInfoResource.isLoaded(this.root);
|
||||
},
|
||||
getNodeState(nodeId: string) {
|
||||
return this.state.get(nodeId);
|
||||
@@ -407,6 +451,7 @@ export function useElementsTree(options: IOptions): IElementsTree {
|
||||
},
|
||||
getNodeChildren(nodeId: string): string[] {
|
||||
const node = navNodeInfoResource.get(nodeId);
|
||||
const children = options.getChildren(nodeId) || [];
|
||||
|
||||
if (!node) {
|
||||
return []; // Maybe filter should accept nodeId, so we be able to apply filters to empty node
|
||||
@@ -415,7 +460,7 @@ export function useElementsTree(options: IOptions): IElementsTree {
|
||||
return (options.filters || [])
|
||||
.reduce(
|
||||
(children, filter) => filter(elementsTree, elementsTree.filter, node, children, this.state),
|
||||
options.getChildren(node.id) || []
|
||||
children
|
||||
);
|
||||
},
|
||||
async setFilter(value: string) {
|
||||
@@ -443,16 +488,14 @@ export function useElementsTree(options: IOptions): IElementsTree {
|
||||
}
|
||||
},
|
||||
async show(nodeId: string, path: string[]): Promise<void> {
|
||||
const preloaded = await navTreeResource.preloadNodeParents(path, nodeId);
|
||||
const preloaded = await this.loadPath(path, nodeId);
|
||||
|
||||
if (!preloaded) {
|
||||
if (preloaded !== nodeId) {
|
||||
notificationService.logError({
|
||||
title: 'app_navigationTree_node_not_found',
|
||||
message: nodeId,
|
||||
});
|
||||
}
|
||||
|
||||
if (preloaded) {
|
||||
} else {
|
||||
runInAction(() => {
|
||||
for (const parent of path) {
|
||||
const state = this.getNodeState(parent);
|
||||
@@ -470,6 +513,24 @@ export function useElementsTree(options: IOptions): IElementsTree {
|
||||
}
|
||||
}
|
||||
},
|
||||
async click(node: NavNode, path: string[], leaf: boolean) {
|
||||
await options.onClick?.(node);
|
||||
},
|
||||
async open(node: NavNode, path: string[], leaf: boolean) {
|
||||
const expandableOrExpanded = this.isNodeExpandable(node.id) || this.isNodeExpanded(node.id);
|
||||
if (!leaf && this.settings?.foldersTree && expandableOrExpanded) {
|
||||
const nodeId = node.id;
|
||||
|
||||
const loaded = await options.loadChildren(node.id, true);
|
||||
if (loaded) {
|
||||
this.setFilter('');
|
||||
options.folderExplorer.open(path, nodeId);
|
||||
}
|
||||
}
|
||||
|
||||
const folder = !leaf && this.settings?.foldersTree || false;
|
||||
await options.onOpen?.(node, folder);
|
||||
},
|
||||
async expand(node: NavNode, state: boolean) {
|
||||
if (!this.isNodeExpandable(node.id)) {
|
||||
return;
|
||||
@@ -538,6 +599,26 @@ export function useElementsTree(options: IOptions): IElementsTree {
|
||||
}
|
||||
await functionsRef.resetSelection();
|
||||
},
|
||||
async loadPath(path: string[], lastNode?: string): Promise<string | undefined> {
|
||||
let lastLoadedNode: string | undefined;
|
||||
for (const nodeId of path) {
|
||||
const loaded = await options.loadChildren(nodeId, false);
|
||||
|
||||
if (!loaded) {
|
||||
return lastLoadedNode;
|
||||
}
|
||||
lastLoadedNode = nodeId;
|
||||
}
|
||||
|
||||
if (
|
||||
lastNode !== undefined
|
||||
&& lastLoadedNode !== undefined
|
||||
&& options.getChildren(lastLoadedNode)?.includes(lastNode)) {
|
||||
return lastNode;
|
||||
}
|
||||
|
||||
return lastLoadedNode;
|
||||
},
|
||||
setDnDData(data: IDNDData, dragging: boolean) {
|
||||
if (dragging) {
|
||||
if (!this.activeDnDData.includes(data)) {
|
||||
@@ -570,25 +651,6 @@ export function useElementsTree(options: IOptions): IElementsTree {
|
||||
userData,
|
||||
}, ['isLoading', 'isLoaded']);
|
||||
|
||||
function exitNodeFolder(nodeId: string) {
|
||||
runInAction(() => {
|
||||
const folderExplorer = options.folderExplorer;
|
||||
|
||||
if (folderExplorer.state.fullPath.length === 1 || nodeId === options.baseRoot) {
|
||||
return;
|
||||
}
|
||||
|
||||
const pathIndex = folderExplorer.state.fullPath.indexOf(nodeId);
|
||||
|
||||
if (pathIndex >= 0) {
|
||||
folderExplorer.open(
|
||||
folderExplorer.state.fullPath.slice(0, pathIndex - 1),
|
||||
folderExplorer.state.fullPath[pathIndex - 1]
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
functionsRef.loadTree(options.root);
|
||||
}, [options.root]);
|
||||
@@ -597,6 +659,10 @@ export function useElementsTree(options: IOptions): IElementsTree {
|
||||
functionsRef.loadTree(options.root);
|
||||
}, 100), []);
|
||||
|
||||
useResource(useElementsTree, navTreeResource, options.baseRoot, {
|
||||
onData: () => loadTreeThreshold(),
|
||||
});
|
||||
|
||||
useResource(useElementsTree, ProjectInfoResource, CachedMapAllKey, {
|
||||
onData: () => {
|
||||
loadTreeThreshold();
|
||||
@@ -631,7 +697,7 @@ export function useElementsTree(options: IOptions): IElementsTree {
|
||||
const children = navTreeResource.get(key);
|
||||
|
||||
if (!children) {
|
||||
exitNodeFolder(key);
|
||||
functionsRef.exitNodeFolder(key);
|
||||
}
|
||||
});
|
||||
}],
|
||||
@@ -657,11 +723,21 @@ export function useElementsTree(options: IOptions): IElementsTree {
|
||||
if (node) {
|
||||
await elementsTree.expand(node, false);
|
||||
|
||||
exitNodeFolder(key);
|
||||
functionsRef.exitNodeFolder(key);
|
||||
}
|
||||
});
|
||||
}],
|
||||
});
|
||||
|
||||
|
||||
// sync settings
|
||||
const filterDisabled = getComputed(() => !options.settings?.filter && elementsTree.filtering);
|
||||
|
||||
useEffect(() => {
|
||||
if (filterDisabled) {
|
||||
elementsTree.setFilter('');
|
||||
}
|
||||
});
|
||||
|
||||
return elementsTree;
|
||||
}
|
||||
|
||||
+32
@@ -0,0 +1,32 @@
|
||||
/*
|
||||
* CloudBeaver - Cloud Database Manager
|
||||
* Copyright (C) 2020-2023 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 { useEffect } from 'react';
|
||||
|
||||
import { getComputed, useFolderExplorer } from '@cloudbeaver/core-blocks';
|
||||
|
||||
import type { IElementsTreeSettings } from './useElementsTree';
|
||||
|
||||
export function useElementsTreeFolderExplorer(
|
||||
baseRoot: string,
|
||||
settings: IElementsTreeSettings | undefined
|
||||
) {
|
||||
const folderExplorer = useFolderExplorer(baseRoot, {
|
||||
saveState: settings?.saveExpanded,
|
||||
});
|
||||
|
||||
const foldersTreeDisabled = getComputed(() => !settings?.foldersTree && folderExplorer.state.folder !== baseRoot);
|
||||
|
||||
useEffect(() => {
|
||||
if (foldersTreeDisabled) {
|
||||
folderExplorer.open([], baseRoot);
|
||||
}
|
||||
});
|
||||
|
||||
return folderExplorer;
|
||||
}
|
||||
@@ -84,7 +84,6 @@ export const NavigationTree = observer(function NavigationTree() {
|
||||
const navNodeViewService = useService(NavNodeViewService);
|
||||
|
||||
const root = ROOT_NODE_PATH;
|
||||
const { authenticated } = useService(AppAuthService);
|
||||
const { handleOpen, handleSelect, handleSelectReset } = useNavigationTree();
|
||||
|
||||
const connectionGroupFilter = useMemo(() => navigationTreeConnectionGroupFilter(
|
||||
@@ -114,10 +113,6 @@ export const NavigationTree = observer(function NavigationTree() {
|
||||
|
||||
const settingsElements = useMemo(() => ([ProjectsSettingsPlaceholderElement]), []);
|
||||
|
||||
if (!authenticated) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return styled(navigationTreeStyles)(
|
||||
<CaptureView view={navTreeService}>
|
||||
<ElementsTree
|
||||
|
||||
@@ -12,7 +12,7 @@ import { ConnectionInfoResource, ConnectionsManagerService, createConnectionPara
|
||||
import { injectable } from '@cloudbeaver/core-di';
|
||||
import { NotificationService } from '@cloudbeaver/core-events';
|
||||
import { ISyncExecutor, SyncExecutor } from '@cloudbeaver/core-executor';
|
||||
import { NavNodeManagerService, NavTreeResource, ROOT_NODE_PATH, EObjectFeature } from '@cloudbeaver/core-navigation-tree';
|
||||
import { NavNodeManagerService, NavTreeResource, ROOT_NODE_PATH, EObjectFeature, NavNodeInfoResource } from '@cloudbeaver/core-navigation-tree';
|
||||
import { ResourceKey, resourceKeyList } from '@cloudbeaver/core-sdk';
|
||||
import { MetadataMap } from '@cloudbeaver/core-utils';
|
||||
import { ACTION_COLLAPSE_ALL, ACTION_FILTER, IActiveView, View } from '@cloudbeaver/core-view';
|
||||
@@ -36,11 +36,12 @@ export class NavigationTreeService extends View<string> {
|
||||
private readonly connectionsManagerService: ConnectionsManagerService,
|
||||
private readonly connectionInfoResource: ConnectionInfoResource,
|
||||
private readonly navNodeExtensionsService: NavNodeExtensionsService,
|
||||
private readonly navNodeInfoResource: NavNodeInfoResource,
|
||||
private readonly navTreeResource: NavTreeResource
|
||||
) {
|
||||
super();
|
||||
|
||||
this.treeState = new MetadataMap(() => ({
|
||||
this.treeState = new MetadataMap<string, ITreeNodeState>(() => ({
|
||||
showInFilter: false,
|
||||
expanded: false,
|
||||
selected: false,
|
||||
@@ -99,6 +100,12 @@ export class NavigationTreeService extends View<string> {
|
||||
this.navTreeResource.markOutdated(id);
|
||||
}
|
||||
|
||||
const parents = this.navNodeInfoResource.getParents(id);
|
||||
|
||||
if (parents.length > 0 && !this.navNodeInfoResource.has(id)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
await this.navTreeResource.load(id);
|
||||
return true;
|
||||
} catch (exception: any) {
|
||||
|
||||
+5
@@ -92,6 +92,10 @@ export const NavigationNodeProjectControl: NavTreeControlComponent = observer<Na
|
||||
treeNodeContext.select(event.ctrlKey || event.metaKey);
|
||||
}
|
||||
|
||||
function onDbClickHandler(event: React.MouseEvent<HTMLDivElement>) {
|
||||
elementsTreeContext?.tree.open(node, navNodeInfoResource.getParents(node.id), false);
|
||||
}
|
||||
|
||||
if (elementsTreeContext?.tree.settings?.projects === false && !isDragging) {
|
||||
return null;
|
||||
}
|
||||
@@ -100,6 +104,7 @@ export const NavigationNodeProjectControl: NavTreeControlComponent = observer<Na
|
||||
<TreeNodeControl
|
||||
ref={ref}
|
||||
onClick={onClickHandler}
|
||||
onDoubleClick={onDbClickHandler}
|
||||
{...use({ outdated, editing, dragging: dndElement })}
|
||||
className={className}
|
||||
>
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
|
||||
import { observable, makeObservable } from 'mobx';
|
||||
|
||||
import type { DynamicStyle } from '@cloudbeaver/core-theming';
|
||||
import type { ComponentStyle } from '@cloudbeaver/core-theming';
|
||||
import type { ITab } from '@cloudbeaver/plugin-navigation-tabs';
|
||||
|
||||
import type { IObjectViewerTabState } from '../IObjectViewerTabState';
|
||||
@@ -17,7 +17,7 @@ export interface ObjectPageTabProps<T = unknown> {
|
||||
tab: ITab<IObjectViewerTabState>;
|
||||
page: ObjectPage<T>;
|
||||
onSelect: () => void;
|
||||
style: DynamicStyle | DynamicStyle[];
|
||||
style: ComponentStyle;
|
||||
}
|
||||
export type ObjectPageTabComponent<T = unknown> = React.FunctionComponent<ObjectPageTabProps<T>>;
|
||||
|
||||
|
||||
@@ -11,9 +11,10 @@ import { observer } from 'mobx-react-lite';
|
||||
import { useEffect } from 'react';
|
||||
import styled, { css } from 'reshadow';
|
||||
|
||||
import { BASE_CONTAINERS_STYLES, Button, Container, InputField, SubmittingForm, Translate, useFocus, useObservableRef, useTranslate } from '@cloudbeaver/core-blocks';
|
||||
import { BASE_CONTAINERS_STYLES, Button, Container, InputField, SubmittingForm, Translate, useFocus, useObservableRef, useResource, useTranslate } from '@cloudbeaver/core-blocks';
|
||||
import { CommonDialogBody, CommonDialogFooter, CommonDialogHeader, CommonDialogWrapper, DialogComponent } from '@cloudbeaver/core-dialogs';
|
||||
import { throttleAsync } from '@cloudbeaver/core-utils';
|
||||
import { ProjectInfoResource } from '@cloudbeaver/core-projects';
|
||||
import { createPath, throttleAsync } from '@cloudbeaver/core-utils';
|
||||
import { ProjectSelect } from '@cloudbeaver/plugin-projects';
|
||||
|
||||
const style = css`
|
||||
@@ -29,26 +30,30 @@ const style = css`
|
||||
interface IFolderDialogState {
|
||||
value: string;
|
||||
projectId: string;
|
||||
folder?: string;
|
||||
message: string | undefined;
|
||||
valid: boolean;
|
||||
payload: FolderDialogPayload;
|
||||
validationInProgress: boolean;
|
||||
validate: () => Promise<void>;
|
||||
setMessage: (message: string) => void;
|
||||
setProjectId: (projectId: string) => void;
|
||||
}
|
||||
|
||||
export interface IFolderDialogResult {
|
||||
folder: string;
|
||||
folder?: string;
|
||||
name: string;
|
||||
projectId: string;
|
||||
}
|
||||
|
||||
export interface FolderDialogPayload {
|
||||
value: string;
|
||||
projectId: string;
|
||||
folder?: string;
|
||||
|
||||
selectProject: boolean;
|
||||
objectName?: string;
|
||||
icon?: string;
|
||||
subTitle?: string;
|
||||
bigIcon?: boolean;
|
||||
viewBox?: string;
|
||||
confirmActionText?: string;
|
||||
@@ -68,7 +73,7 @@ export const FolderDialog: DialogComponent<FolderDialogPayload, IFolderDialogRes
|
||||
|
||||
const {
|
||||
icon,
|
||||
subTitle,
|
||||
folder,
|
||||
bigIcon,
|
||||
viewBox,
|
||||
value,
|
||||
@@ -93,34 +98,55 @@ export const FolderDialog: DialogComponent<FolderDialogPayload, IFolderDialogRes
|
||||
const state = useObservableRef<IFolderDialogState>(() => ({
|
||||
value,
|
||||
projectId,
|
||||
folder,
|
||||
message: undefined,
|
||||
valid: true,
|
||||
validationInProgress: false,
|
||||
validate: throttleAsync(async () => {
|
||||
const { folder, value, projectId } = state;
|
||||
state.message = undefined;
|
||||
state.valid = (await state.payload.validation?.(
|
||||
{ folder:state.value, projectId: state.projectId },
|
||||
state.setMessage.bind(state)
|
||||
)) ?? true;
|
||||
state.validationInProgress = true;
|
||||
let valid: boolean | undefined;
|
||||
try {
|
||||
valid = await state.payload.validation?.(
|
||||
{ folder, name: value, projectId },
|
||||
(message: string) => {
|
||||
if (state.folder === folder && state.value === value && state.projectId === projectId) {
|
||||
state.setMessage(message);
|
||||
}
|
||||
}
|
||||
);
|
||||
} catch {}
|
||||
|
||||
if (state.folder === folder && state.value === value && state.projectId === projectId) {
|
||||
state.valid = valid ?? true;
|
||||
state.validationInProgress = false;
|
||||
}
|
||||
}, 300),
|
||||
setMessage(message) {
|
||||
this.message = message;
|
||||
},
|
||||
setProjectId(projectId) {
|
||||
this.projectId = projectId;
|
||||
this.folder = undefined;
|
||||
},
|
||||
}), {
|
||||
value: observable.ref,
|
||||
projectId: observable.ref,
|
||||
validationInProgress: observable.ref,
|
||||
folder: observable.ref,
|
||||
valid: observable.ref,
|
||||
message: observable.ref,
|
||||
}, {
|
||||
payload,
|
||||
});
|
||||
|
||||
const projectInfoLoader = useResource(FolderDialog, ProjectInfoResource, state.projectId);
|
||||
|
||||
async function resolveHandler() {
|
||||
await state.validate();
|
||||
if (state.valid) {
|
||||
resolveDialog({ folder: state.value, projectId: state.projectId });
|
||||
resolveDialog({ folder: state.folder, name: state.value, projectId: state.projectId });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -129,6 +155,7 @@ export const FolderDialog: DialogComponent<FolderDialogPayload, IFolderDialogRes
|
||||
}, [state.value, state.projectId]);
|
||||
|
||||
const errorMessage = state.valid ? ' ' : translate(state.message ?? 'ui_rename_taken_or_invalid');
|
||||
const subTitle = createPath(projectInfoLoader.data?.name ?? state.projectId, state.folder);
|
||||
|
||||
return styled(style, BASE_CONTAINERS_STYLES)(
|
||||
<CommonDialogWrapper size='small' className={className} fixedWidth>
|
||||
@@ -154,6 +181,7 @@ export const FolderDialog: DialogComponent<FolderDialogPayload, IFolderDialogRes
|
||||
state={state}
|
||||
error={!state.valid}
|
||||
description={errorMessage}
|
||||
loading={state.validationInProgress}
|
||||
onChange={() => state.validate()}
|
||||
>
|
||||
{translate('ui_name') + ':'}
|
||||
|
||||
@@ -11,13 +11,13 @@ import { CONNECTION_FOLDER_NAME_VALIDATION } from '@cloudbeaver/core-connections
|
||||
import { Bootstrap, injectable } from '@cloudbeaver/core-di';
|
||||
import { DialogueStateResult, CommonDialogService } from '@cloudbeaver/core-dialogs';
|
||||
import { NotificationService } from '@cloudbeaver/core-events';
|
||||
import { executorHandlerFilter, IExecutionContextProvider } from '@cloudbeaver/core-executor';
|
||||
import type { IExecutionContextProvider } from '@cloudbeaver/core-executor';
|
||||
import { LocalizationService } from '@cloudbeaver/core-localization';
|
||||
import { NavTreeResource, NavNodeManagerService, NavNodeInfoResource, type INodeMoveData, navNodeMoveContext, getNodesFromContext, ENodeMoveType, ProjectsNavNodeService } from '@cloudbeaver/core-navigation-tree';
|
||||
import { ProjectInfo, ProjectInfoResource, ProjectsService } from '@cloudbeaver/core-projects';
|
||||
import { getRmResourceKey, getRmResourcePath, NAV_NODE_TYPE_RM_PROJECT, NAV_NODE_TYPE_RM_RESOURCE, ResourceManagerResource, RESOURCES_NODE_PATH } from '@cloudbeaver/core-resource-manager';
|
||||
import { CachedMapAllKey, CachedTreeChildrenKey, getCachedMapResourceLoaderState, resourceKeyList, ResourceKeyUtils } from '@cloudbeaver/core-sdk';
|
||||
import { createPath, getPathParent, isDefined } from '@cloudbeaver/core-utils';
|
||||
import { createPath, getPathParent } from '@cloudbeaver/core-utils';
|
||||
import { ActionService, MenuService, ACTION_NEW_FOLDER, DATA_CONTEXT_MENU, IAction, IDataContextProvider, DATA_CONTEXT_LOADABLE_STATE } from '@cloudbeaver/core-view';
|
||||
import { DATA_CONTEXT_ELEMENTS_TREE, MENU_ELEMENTS_TREE_TOOLS } from '@cloudbeaver/plugin-navigation-tree';
|
||||
import { FolderDialog } from '@cloudbeaver/plugin-projects';
|
||||
@@ -164,18 +164,13 @@ export class ResourceFoldersBootstrap extends Bootstrap {
|
||||
|
||||
let path: string | undefined;
|
||||
|
||||
if (!targetNode.folderId) {
|
||||
const project = this.projectInfoResource.get(targetNode.projectId);
|
||||
targetNode.folderId = this.getResourceTypeFolder(project!, resourceTypeId);
|
||||
}
|
||||
|
||||
if (targetNode.folderId) {
|
||||
const resourceKey = getResourceKeyFromNodeId(targetNode.folderId);
|
||||
|
||||
if (resourceKey !== undefined) {
|
||||
const key = getRmResourceKey(resourceKey);
|
||||
if (key.path) {
|
||||
path = resourceKey;
|
||||
path = key.path;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -183,32 +178,39 @@ export class ResourceFoldersBootstrap extends Bootstrap {
|
||||
const result = await this.commonDialogService.open(FolderDialog, {
|
||||
value: this.localizationService.translate('ui_folder_new'),
|
||||
projectId: targetNode.projectId,
|
||||
folder: path,
|
||||
title: 'core_view_action_new_folder',
|
||||
subTitle: path,
|
||||
icon: '/icons/folder.svg#root',
|
||||
create: true,
|
||||
selectProject: targetNode.selectProject,
|
||||
validation: async ({ folder, projectId }, setMessage) => {
|
||||
const trimmed = folder.trim();
|
||||
validation: async ({ name, folder, projectId }, setMessage) => {
|
||||
const trimmed = name.trim();
|
||||
|
||||
if (trimmed.length === 0 || !folder.match(CONNECTION_FOLDER_NAME_VALIDATION)) {
|
||||
if (trimmed.length === 0 || !name.match(CONNECTION_FOLDER_NAME_VALIDATION)) {
|
||||
setMessage('connections_connection_folder_validation');
|
||||
return false;
|
||||
}
|
||||
|
||||
const key = path !== undefined ? path : getRmResourcePath(projectId);
|
||||
const root = this.getResourceTypeFolder(projectId, resourceTypeId);
|
||||
const key = getRmResourcePath(projectId, folder ?? root);
|
||||
|
||||
await this.resourceManagerResource.load(CachedTreeChildrenKey(key));
|
||||
try {
|
||||
await this.resourceManagerResource.load(CachedTreeChildrenKey(key));
|
||||
|
||||
return !this.resourceManagerResource.has(createPath(key, trimmed));
|
||||
return !this.resourceManagerResource.has(createPath(key, trimmed));
|
||||
} catch (exception: any) {
|
||||
setMessage('connections_connection_folder_validation');
|
||||
return false;
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
if (result !== DialogueStateResult.Rejected && result !== DialogueStateResult.Resolved) {
|
||||
try {
|
||||
const key = path !== undefined ? path : getRmResourcePath(result.projectId);
|
||||
const root = this.getResourceTypeFolder(result.projectId, resourceTypeId);
|
||||
const key = getRmResourcePath(result.projectId, result.folder ?? root);
|
||||
await this.resourceManagerResource.create(
|
||||
createPath(key, result.folder),
|
||||
createPath(key, result.name),
|
||||
true
|
||||
);
|
||||
|
||||
@@ -276,18 +278,22 @@ export class ResourceFoldersBootstrap extends Bootstrap {
|
||||
};
|
||||
}
|
||||
|
||||
private getResourceTypeFolder(project: ProjectInfo, resourceTypeId: string | undefined): string | undefined {
|
||||
private getResourceTypeFolder(projectId: string, resourceTypeId: string | undefined): string | undefined {
|
||||
if (!resourceTypeId) {
|
||||
return undefined;
|
||||
}
|
||||
const project = this.projectInfoResource.get(projectId);
|
||||
|
||||
if (!project) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const resourceFolder = this.resourceManagerService.getRootFolder(project, resourceTypeId);
|
||||
return createPath(RESOURCES_NODE_PATH, project.id, resourceFolder);
|
||||
return resourceFolder;
|
||||
}
|
||||
|
||||
private syncNavTree() {
|
||||
this.navNodeManagerService.onMove.addHandler(this.moveResourceToFolder.bind(this));
|
||||
let syncOutdate = true;
|
||||
|
||||
// this.navNodeInfoResource.onItemUpdate.addHandler(executorHandlerFilter(
|
||||
// () => syncOutdate,
|
||||
@@ -310,31 +316,29 @@ export class ResourceFoldersBootstrap extends Bootstrap {
|
||||
// }
|
||||
// ));
|
||||
|
||||
this.navNodeInfoResource.onItemDelete.addHandler(executorHandlerFilter(
|
||||
() => true,
|
||||
key => {
|
||||
const resources = ResourceKeyUtils
|
||||
.mapArray(key, getResourceKeyFromNodeId)
|
||||
.filter(isDefined);
|
||||
// this.navNodeInfoResource.onItemDelete.addHandler(executorHandlerFilter(
|
||||
// () => true,
|
||||
// key => {
|
||||
// const resources = ResourceKeyUtils
|
||||
// .mapArray(key, getResourceKeyFromNodeId)
|
||||
// .filter(isDefined);
|
||||
|
||||
this.resourceManagerResource.delete(resourceKeyList(resources));
|
||||
}
|
||||
));
|
||||
// this.resourceManagerResource.delete(resourceKeyList(resources));
|
||||
// }
|
||||
// ));
|
||||
|
||||
this.resourceManagerResource.onItemUpdate.addHandler(executorHandlerFilter(
|
||||
() => syncOutdate,
|
||||
key => {
|
||||
syncOutdate = false;
|
||||
try {
|
||||
const updated = resourceKeyList([...new Set(ResourceKeyUtils.mapArray(key, getResourceNodeId)
|
||||
.map(getPathParent))]);
|
||||
if (!this.navTreeResource.isOutdated(updated)) {
|
||||
this.navTreeResource.markTreeOutdated(updated);
|
||||
}
|
||||
} finally {
|
||||
syncOutdate = true;
|
||||
}
|
||||
this.resourceManagerResource.onItemUpdate.addHandler(key => {
|
||||
const updated = resourceKeyList([...new Set(ResourceKeyUtils.mapArray(key, getResourceNodeId)
|
||||
.map(getPathParent))]);
|
||||
if (!this.navTreeResource.isOutdated(updated)) {
|
||||
this.navTreeResource.markTreeOutdated(updated);
|
||||
}
|
||||
));
|
||||
});
|
||||
|
||||
this.resourceManagerResource.onItemDelete.addHandler(key => {
|
||||
const updated = resourceKeyList([...new Set(ResourceKeyUtils.mapArray(key, getResourceNodeId)
|
||||
.map(getPathParent))]);
|
||||
this.navTreeResource.deleteInNode(updated, ResourceKeyUtils.toArray(key).map(value => [value]));
|
||||
});
|
||||
}
|
||||
}
|
||||
+12
-11
@@ -1,25 +1,26 @@
|
||||
### Steps to run
|
||||
# Steps to run
|
||||
## Install Dependencies
|
||||
|
||||
1. install ```yarn``` globaly
|
||||
2. execute ```yarn``` in this folder
|
||||
3. execute ```lerna bootstrap```
|
||||
3. execute ```lerna run build --stream --scope=@cloudbeaver/product-default```
|
||||
4. open ```packages/product-default/lib/index.html``` or run ```npx serve packages/product-default/lib```
|
||||
```sh
|
||||
yarn
|
||||
yarn lerna bootstrap
|
||||
```
|
||||
|
||||
## Build
|
||||
|
||||
```sh
|
||||
yarn lerna run build --stream --scope=@cloudbeaver/product-default
|
||||
```
|
||||
lerna run build --stream --scope=@cloudbeaver/product-default
|
||||
```
|
||||
open ```packages/product-default/lib/index.html``` or run ```npx serve packages/product-default/lib```
|
||||
|
||||
## Dev
|
||||
|
||||
```
|
||||
lerna run dev --stream --scope=@cloudbeaver/product-default
|
||||
```sh
|
||||
yarn lerna run dev --stream --scope=@cloudbeaver/product-default
|
||||
```
|
||||
|
||||
## Build GQL SDK
|
||||
```sh
|
||||
lerna run gql:gen --stream
|
||||
yarn lerna run gql:gen --stream
|
||||
```
|
||||
|
||||
|
||||
Reference in New Issue
Block a user