mirror of
https://github.com/dbeaver/cloudbeaver.git
synced 2026-09-24 16:04:36 +08:00
Merge branch 'devel' into feat/context-sort
This commit is contained in:
+9
@@ -118,6 +118,15 @@ public class WebNavigatorNodeInfo {
|
||||
if (node instanceof DBNContainer) {
|
||||
features.add("container");
|
||||
}
|
||||
boolean isShared = false;
|
||||
if (node instanceof DBNDatabaseNode) {
|
||||
isShared = !((DBNDatabaseNode) node).getDataSourceContainer().isManageable();
|
||||
} else if (node instanceof DBNLocalFolder) {
|
||||
//isShared = ((DBNLocalFolder) node).getDataSourceRegistry().is
|
||||
}
|
||||
if (isShared) {
|
||||
features.add("shared");
|
||||
}
|
||||
return features.toArray(new String[0]);
|
||||
}
|
||||
|
||||
|
||||
+5
-6
@@ -141,11 +141,10 @@ public class WebServiceAuthImpl implements DBWServiceAuth {
|
||||
userId);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// We may need to associate new credentials with active user
|
||||
if (linkWithActiveUser) {
|
||||
securityController.setUserCredentials(userId, authProvider, userCredentials);
|
||||
}
|
||||
}
|
||||
// We may need to associate new credentials with active user
|
||||
if (linkWithActiveUser) {
|
||||
securityController.setUserCredentials(userId, authProvider, userCredentials);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -153,7 +152,7 @@ public class WebServiceAuthImpl implements DBWServiceAuth {
|
||||
throw new DBCException("Invalid user credentials");
|
||||
}
|
||||
}
|
||||
if (curUser != null && !curUser.getUserId().equals(userId)) {
|
||||
if (linkWithActiveUser && curUser != null && !curUser.getUserId().equals(userId)) {
|
||||
log.debug("Attempt to authorize user '" + userId + "' while user '" + curUser.getUserId() + "' already authorized");
|
||||
throw new DBCException("You cannot authorize with different users credentials");
|
||||
}
|
||||
|
||||
@@ -7,18 +7,22 @@
|
||||
*/
|
||||
|
||||
import { observer } from 'mobx-react-lite';
|
||||
import { useCallback, useMemo, useState } from 'react';
|
||||
import { useMemo } from 'react';
|
||||
import styled, { css } from 'reshadow';
|
||||
|
||||
import { Loader, useMapResource } from '@cloudbeaver/core-blocks';
|
||||
import { MetadataMap } from '@cloudbeaver/core-utils';
|
||||
import { useService } from '@cloudbeaver/core-di';
|
||||
import type { MetadataMap } from '@cloudbeaver/core-utils';
|
||||
|
||||
import type { NavNode } from '../shared/NodesManager/EntityTypes';
|
||||
import { ROOT_NODE_PATH } from '../shared/NodesManager/NavNodeInfoResource';
|
||||
import { NavNodeInfoResource, ROOT_NODE_PATH } from '../shared/NodesManager/NavNodeInfoResource';
|
||||
import { NavTreeResource } from '../shared/NodesManager/NavTreeResource';
|
||||
import { useChildren } from '../shared/useChildren';
|
||||
import { elementsTreeNameFilter } from './elementsTreeNameFilter';
|
||||
import { NavigationNodeNested } from './NavigationTreeNode/NavigationNode/NavigationNodeNested';
|
||||
import { NavigationNodeElement } from './NavigationTreeNode/NavigationNodeElement';
|
||||
import { ITreeContext, TreeContext } from './TreeContext';
|
||||
import { IElementsTreeCustomRenderer, IElementsTreeFilter, ITreeNodeState, useElementsTree } from './useElementsTree';
|
||||
|
||||
const styles = css`
|
||||
tree {
|
||||
@@ -34,10 +38,6 @@ const styles = css`
|
||||
}
|
||||
`;
|
||||
|
||||
export interface ITreeNodeState {
|
||||
filter: string;
|
||||
}
|
||||
|
||||
interface Props {
|
||||
root?: string;
|
||||
selectionTree?: boolean;
|
||||
@@ -47,61 +47,66 @@ interface Props {
|
||||
}>;
|
||||
emptyPlaceholder: React.FC;
|
||||
className?: string;
|
||||
filters?: IElementsTreeFilter[];
|
||||
renderers?: IElementsTreeCustomRenderer[];
|
||||
customSelect?: (node: NavNode, multiple: boolean) => void;
|
||||
isGroup?: (node: NavNode) => boolean;
|
||||
onExpand?: (node: NavNode, state: boolean) => Promise<void> | void;
|
||||
onOpen?: (node: NavNode) => Promise<void> | void;
|
||||
onSelect?: (node: NavNode, multiple: boolean) => void;
|
||||
isSelected?: (node: NavNode) => boolean;
|
||||
onSelect?: (node: NavNode, state: boolean) => void;
|
||||
onFilter?: (node: NavNode, value: string) => void;
|
||||
}
|
||||
|
||||
export const ElementsTree: React.FC<Props> = observer(function ElementsTree({
|
||||
root,
|
||||
root = ROOT_NODE_PATH,
|
||||
control,
|
||||
localState,
|
||||
selectionTree = false,
|
||||
emptyPlaceholder,
|
||||
filters,
|
||||
renderers,
|
||||
className,
|
||||
isGroup,
|
||||
customSelect,
|
||||
onExpand,
|
||||
onOpen,
|
||||
onSelect,
|
||||
isSelected,
|
||||
onFilter,
|
||||
}) {
|
||||
const nodeChildren = useChildren(root);
|
||||
const Placeholder = emptyPlaceholder;
|
||||
useMapResource(NavTreeResource, root || ROOT_NODE_PATH);
|
||||
const [localTreeNodesState] = useState(() => new MetadataMap<string, ITreeNodeState>(() => ({ filter: '' })));
|
||||
const navNodeInfoResource = useService(NavNodeInfoResource);
|
||||
|
||||
const treeNodesState = localState || localTreeNodesState;
|
||||
useMapResource(NavTreeResource, root);
|
||||
|
||||
const getTreeNodeState = useCallback((node: NavNode) =>
|
||||
treeNodesState.get(node.id), [treeNodesState]);
|
||||
const nameFilter = useMemo(() => elementsTreeNameFilter(navNodeInfoResource), [navNodeInfoResource]);
|
||||
|
||||
const onFilterHandler = useCallback((node: NavNode, value: string) => {
|
||||
const treeNodeState = treeNodesState.get(node.id);
|
||||
treeNodeState.filter = value;
|
||||
|
||||
if (onFilter) {
|
||||
onFilter(node, value);
|
||||
}
|
||||
}, [treeNodesState, onFilter]);
|
||||
const tree = useElementsTree({
|
||||
root,
|
||||
localState,
|
||||
filters: [nameFilter, ...(filters || [])],
|
||||
renderers,
|
||||
isGroup,
|
||||
onFilter,
|
||||
customSelect,
|
||||
onExpand,
|
||||
onSelect,
|
||||
});
|
||||
|
||||
const context = useMemo<ITreeContext>(
|
||||
() => ({
|
||||
treeNodesState,
|
||||
getTreeNodeState,
|
||||
tree,
|
||||
selectionTree,
|
||||
control,
|
||||
onOpen,
|
||||
onSelect,
|
||||
isSelected,
|
||||
onFilter: onFilterHandler,
|
||||
}),
|
||||
[control, selectionTree, onOpen, onSelect, isSelected, onFilterHandler, treeNodesState, getTreeNodeState]
|
||||
[control, selectionTree, onOpen]
|
||||
);
|
||||
|
||||
if (!nodeChildren.children || nodeChildren.children.length === 0) {
|
||||
if (nodeChildren.isLoading()) {
|
||||
return styled(styles)(
|
||||
<center as="div">
|
||||
<center>
|
||||
<Loader />
|
||||
</center>
|
||||
);
|
||||
@@ -112,10 +117,8 @@ export const ElementsTree: React.FC<Props> = observer(function ElementsTree({
|
||||
|
||||
return styled(styles)(
|
||||
<TreeContext.Provider value={context}>
|
||||
<tree as="div" className={className}>
|
||||
{nodeChildren.children.map(id => (
|
||||
<NavigationNodeElement key={id} nodeId={id} />
|
||||
))}
|
||||
<tree className={className}>
|
||||
<NavigationNodeNested nodeId={root} component={NavigationNodeElement} root />
|
||||
<Loader loading={nodeChildren.isLoading()} overlay />
|
||||
</tree>
|
||||
</TreeContext.Provider>
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
/*
|
||||
* CloudBeaver - Cloud Database Manager
|
||||
* Copyright (C) 2020-2021 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 React from 'react';
|
||||
|
||||
import type { NavNode } from '../shared/NodesManager/EntityTypes';
|
||||
|
||||
interface Props {
|
||||
node: NavNode;
|
||||
component: React.FC<{
|
||||
nodeId: string;
|
||||
}>;
|
||||
}
|
||||
|
||||
export type NavigationNodeComponent = React.FC<Props>;
|
||||
@@ -7,15 +7,19 @@
|
||||
*/
|
||||
|
||||
import { observer } from 'mobx-react-lite';
|
||||
import { useMemo } from 'react';
|
||||
import styled, { css } from 'reshadow';
|
||||
|
||||
import { useFocus } from '@cloudbeaver/core-blocks';
|
||||
import { ConnectionInfoResource } from '@cloudbeaver/core-connections';
|
||||
import { useService } from '@cloudbeaver/core-di';
|
||||
import { usePermission, EPermission } from '@cloudbeaver/core-root';
|
||||
import { useActiveView } from '@cloudbeaver/core-view';
|
||||
|
||||
import { ROOT_NODE_PATH } from '../shared/NodesManager/NavNodeInfoResource';
|
||||
import { NavNodeInfoResource, ROOT_NODE_PATH } from '../shared/NodesManager/NavNodeInfoResource';
|
||||
import { ElementsTree } from './ElementsTree';
|
||||
import { navigationTreeConnectionGroupFilter } from './navigationTreeConnectionGroupFilter';
|
||||
import { navigationTreeConnectionGroupRenderer } from './navigationTreeConnectionGroupRenderer';
|
||||
import { NavigationTreeService } from './NavigationTreeService';
|
||||
import { useNavigationTree } from './useNavigationTree';
|
||||
|
||||
@@ -55,10 +59,18 @@ const navigationTreeStyles = css`
|
||||
|
||||
export const NavigationTree = observer(function NavigationTree() {
|
||||
const navTreeService = useService(NavigationTreeService);
|
||||
const navNodeInfoResource = useService(NavNodeInfoResource);
|
||||
const connectionInfoResource = useService(ConnectionInfoResource);
|
||||
|
||||
const [onFocus, onBlur] = useActiveView(navTreeService.getView);
|
||||
const [ref] = useFocus<HTMLDivElement>({ onFocus, onBlur });
|
||||
const isEnabled = usePermission(EPermission.public);
|
||||
const { isSelected, handleOpen, handleSelect } = useNavigationTree();
|
||||
const { handleOpen, handleSelect } = useNavigationTree();
|
||||
|
||||
const connectionGroupFilter = useMemo(() => navigationTreeConnectionGroupFilter(
|
||||
connectionInfoResource,
|
||||
navNodeInfoResource
|
||||
), [connectionInfoResource, navNodeInfoResource]);
|
||||
|
||||
if (!isEnabled) {
|
||||
return null;
|
||||
@@ -68,6 +80,9 @@ export const NavigationTree = observer(function NavigationTree() {
|
||||
<inside-box ref={ref} as='div' tabIndex={0}>
|
||||
<ElementsTree
|
||||
root={ROOT_NODE_PATH}
|
||||
localState={navTreeService.treeState}
|
||||
filters={[connectionGroupFilter]}
|
||||
renderers={[navigationTreeConnectionGroupRenderer]}
|
||||
emptyPlaceholder={() => styled(navigationTreeStyles)(
|
||||
<center as="div">
|
||||
<message as="div">
|
||||
@@ -76,9 +91,8 @@ export const NavigationTree = observer(function NavigationTree() {
|
||||
</message>
|
||||
</center>
|
||||
)}
|
||||
isSelected={isSelected}
|
||||
customSelect={handleSelect}
|
||||
onOpen={handleOpen}
|
||||
onSelect={handleSelect}
|
||||
/>
|
||||
</inside-box>
|
||||
);
|
||||
|
||||
@@ -7,28 +7,18 @@
|
||||
*/
|
||||
|
||||
import { observer } from 'mobx-react-lite';
|
||||
import { useContext } from 'react';
|
||||
|
||||
import { TreeNode, TreeNodeContext } from '@cloudbeaver/core-blocks';
|
||||
import { TreeNode } from '@cloudbeaver/core-blocks';
|
||||
|
||||
import type { NavNode } from '../../shared/NodesManager/EntityTypes';
|
||||
import type { NavigationNodeComponent } from '../NavigationNodeComponent';
|
||||
import { NavigationNodeControl } from './NavigationNode/NavigationNodeControl';
|
||||
import { NavigationNodeNested } from './NavigationNode/NavigationNodeNested';
|
||||
import { useNavigationNode } from './useNavigationNode';
|
||||
|
||||
interface Props {
|
||||
node: NavNode;
|
||||
component: React.FC<{
|
||||
nodeId: string;
|
||||
}>;
|
||||
}
|
||||
|
||||
export const NavigationNode: React.FC<Props> = observer(function NavigationNode({
|
||||
export const NavigationNode: NavigationNodeComponent = observer(function NavigationNode({
|
||||
node,
|
||||
component,
|
||||
}) {
|
||||
const context = useContext(TreeNodeContext);
|
||||
|
||||
const {
|
||||
control,
|
||||
selected,
|
||||
@@ -57,7 +47,7 @@ export const NavigationNode: React.FC<Props> = observer(function NavigationNode(
|
||||
onFilter={handleFilter}
|
||||
>
|
||||
<Control node={node} />
|
||||
<NavigationNodeNested nodeId={node.id} component={component} />
|
||||
{expanded && <NavigationNodeNested nodeId={node.id} component={component} />}
|
||||
</TreeNode>
|
||||
);
|
||||
});
|
||||
|
||||
+2
-2
@@ -79,10 +79,10 @@ export const NavigationNodeControl: React.FC<Props> = observer(function Navigati
|
||||
<TreeNodeControl onClick={onClickHandler}>
|
||||
<TreeNodeExpand />
|
||||
<TreeNodeIcon icon={node.icon}>
|
||||
{connected && <status as='div' />}
|
||||
{connected && <status />}
|
||||
</TreeNodeIcon>
|
||||
<TreeNodeName>{node.name}</TreeNodeName>
|
||||
<portal as="div">
|
||||
<portal>
|
||||
<TreeNodeMenu node={node} selected={context?.selected} />
|
||||
</portal>
|
||||
</TreeNodeControl>
|
||||
|
||||
+19
-27
@@ -11,57 +11,49 @@ import { observer } from 'mobx-react-lite';
|
||||
import { useContext, useMemo } from 'react';
|
||||
import styled from 'reshadow';
|
||||
|
||||
import { TreeNodeContext, TreeNodeNested, TREE_NODE_STYLES } from '@cloudbeaver/core-blocks';
|
||||
import { useService } from '@cloudbeaver/core-di';
|
||||
import { resourceKeyList } from '@cloudbeaver/core-sdk';
|
||||
import { TreeNodeNested, TREE_NODE_STYLES } from '@cloudbeaver/core-blocks';
|
||||
import { useStyles } from '@cloudbeaver/core-theming';
|
||||
|
||||
import { NavNodeInfoResource } from '../../../shared/NodesManager/NavNodeInfoResource';
|
||||
import { useChildren } from '../../../shared/useChildren';
|
||||
import { TreeContext } from '../../TreeContext';
|
||||
|
||||
interface Props {
|
||||
nodeId: string;
|
||||
component: React.FC<{
|
||||
nodeId: string;
|
||||
}>;
|
||||
}
|
||||
|
||||
function isDefined<T>(val: T | undefined | null): val is T {
|
||||
return val !== undefined && val !== null;
|
||||
root?: boolean;
|
||||
}
|
||||
|
||||
export const NavigationNodeNested: React.FC<Props> = observer(function NavigationNodeNested({
|
||||
nodeId,
|
||||
component,
|
||||
root,
|
||||
}) {
|
||||
const styles = useStyles(TREE_NODE_STYLES);
|
||||
const context = useContext(TreeNodeContext);
|
||||
const navNodeInfo = useService(NavNodeInfoResource);
|
||||
const childrenInfo = useChildren(nodeId);
|
||||
const treeContext = useContext(TreeContext);
|
||||
|
||||
const children = useMemo(() => computed(() => {
|
||||
if (!childrenInfo?.children) {
|
||||
return [];
|
||||
}
|
||||
const children = useMemo(
|
||||
() => computed(() => treeContext?.tree.getNodeChildren(nodeId) || []),
|
||||
[nodeId, treeContext?.tree]
|
||||
).get();
|
||||
|
||||
const childrenEntities = navNodeInfo.get(resourceKeyList(childrenInfo.children)).filter(isDefined);
|
||||
|
||||
if (!context?.filterValue) {
|
||||
return childrenEntities;
|
||||
}
|
||||
|
||||
return childrenEntities.filter(child => child.name?.toLowerCase().includes(context.filterValue.toLowerCase()));
|
||||
}), [childrenInfo.children, context?.filterValue, navNodeInfo]).get();
|
||||
|
||||
if (!children.length || !context?.expanded) {
|
||||
if (children.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const NavigationNode = component;
|
||||
|
||||
if (root) {
|
||||
return styled(styles)(
|
||||
<>
|
||||
{children.map(child => <NavigationNode key={child} nodeId={child} />)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
return styled(styles)(
|
||||
<TreeNodeNested>
|
||||
{children.map(child => <NavigationNode key={child.id} nodeId={child.id} />)}
|
||||
{children.map(child => <NavigationNode key={child} nodeId={child} />)}
|
||||
</TreeNodeNested>
|
||||
);
|
||||
});
|
||||
|
||||
+22
-2
@@ -7,8 +7,12 @@
|
||||
*/
|
||||
|
||||
import { observer } from 'mobx-react-lite';
|
||||
import { useContext } from 'react';
|
||||
|
||||
import { useNode } from '../../shared/NodesManager/useNode';
|
||||
import { useService } from '@cloudbeaver/core-di';
|
||||
|
||||
import { NavNodeInfoResource } from '../../shared/NodesManager/NavNodeInfoResource';
|
||||
import { TreeContext } from '../TreeContext';
|
||||
import { NavigationNode } from './NavigationNode';
|
||||
|
||||
interface NavigationTreeNodeProps {
|
||||
@@ -18,11 +22,27 @@ interface NavigationTreeNodeProps {
|
||||
export const NavigationNodeElement = observer(function NavigationNodeElement({
|
||||
nodeId,
|
||||
}: NavigationTreeNodeProps) {
|
||||
const { node } = useNode(nodeId);
|
||||
const context = useContext(TreeContext);
|
||||
const navNodeInfoResource = useService(NavNodeInfoResource);
|
||||
|
||||
if (context?.tree.renderers) {
|
||||
for (const renderer of context.tree.renderers) {
|
||||
const CustomRenderer = renderer(nodeId);
|
||||
|
||||
if (CustomRenderer === undefined) {
|
||||
continue;
|
||||
}
|
||||
|
||||
return <CustomRenderer nodeId={nodeId} component={NavigationNodeElement} />;
|
||||
}
|
||||
}
|
||||
|
||||
const node = navNodeInfoResource.get(nodeId);
|
||||
|
||||
if (!node) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// TODO: after node update reference can be lost and NavigationNode skip update
|
||||
return <NavigationNode node={node} component={NavigationNodeElement} />;
|
||||
});
|
||||
|
||||
+1
-1
@@ -12,7 +12,7 @@ export const treeNodeMenuStyles = css`
|
||||
MenuTrigger {
|
||||
margin-left: 16px;
|
||||
padding: 0;
|
||||
height: 20px;
|
||||
height: 16px;
|
||||
|
||||
&:before {
|
||||
display: none;
|
||||
|
||||
+41
-78
@@ -6,15 +6,14 @@
|
||||
* you may not use this file except in compliance with the License.
|
||||
*/
|
||||
|
||||
import { useContext, useEffect, useState } from 'react';
|
||||
import { useContext, useEffect } from 'react';
|
||||
|
||||
import { useService } from '@cloudbeaver/core-di';
|
||||
import { useObjectRef } from '@cloudbeaver/core-blocks';
|
||||
|
||||
import type { NavNode } from '../../shared/NodesManager/EntityTypes';
|
||||
import { EObjectFeature } from '../../shared/NodesManager/EObjectFeature';
|
||||
import { useNode } from '../../shared/NodesManager/useNode';
|
||||
import { useChildren } from '../../shared/useChildren';
|
||||
import { NavigationTreeService } from '../NavigationTreeService';
|
||||
import { TreeContext } from '../TreeContext';
|
||||
|
||||
interface INavigationNode {
|
||||
@@ -25,98 +24,62 @@ interface INavigationNode {
|
||||
loading: boolean;
|
||||
expanded: boolean;
|
||||
leaf: boolean;
|
||||
handleExpand: () => void;
|
||||
handleOpen: () => void;
|
||||
handleSelect: (isMultiple?: boolean) => void;
|
||||
handleFilter: (value: string) => void;
|
||||
handleExpand: () => Promise<void>;
|
||||
handleOpen: () => Promise<void>;
|
||||
handleSelect: (isMultiple?: boolean, nested?: boolean) => Promise<void>;
|
||||
handleFilter: (value: string) => Promise<void>;
|
||||
filterValue: string;
|
||||
}
|
||||
|
||||
export function useNavigationNode(node: NavNode): INavigationNode {
|
||||
const context = useContext(TreeContext);
|
||||
const navigationTreeService = useService(NavigationTreeService);
|
||||
const [processing, setProcessing] = useState(false);
|
||||
const { isLoading, isOutdated } = useNode(node.id);
|
||||
const children = useChildren(node.id);
|
||||
const loading = isLoading() || children.isLoading() || processing;
|
||||
export function useNavigationNode({ id }: NavNode): INavigationNode {
|
||||
const contextRef = useObjectRef({
|
||||
context: useContext(TreeContext),
|
||||
});
|
||||
const { node, isLoading } = useNode(id);
|
||||
|
||||
const isExpanded = navigationTreeService.isNodeExpanded(node.id);
|
||||
let leaf = isLeaf(node) || (children.children?.length === 0 && !children.isOutdated());
|
||||
// TODO: hack to provide actual node information
|
||||
if (!node) {
|
||||
throw new Error('Node should exists');
|
||||
}
|
||||
|
||||
const children = useChildren(node.id);
|
||||
const loading = isLoading() || children.isLoading();
|
||||
|
||||
const state = contextRef.context?.tree.getNodeState(node.id);
|
||||
const isExpanded = state?.expanded || false;
|
||||
|
||||
let leaf = isLeaf(node);
|
||||
let expanded = isExpanded && !leaf;
|
||||
|
||||
if (
|
||||
node.objectFeatures.includes(EObjectFeature.dataSource)
|
||||
&& (
|
||||
!node.objectFeatures.includes(EObjectFeature.dataSourceConnected)
|
||||
|| (!children.children && isOutdated())
|
||||
)
|
||||
&& !node.objectFeatures.includes(EObjectFeature.dataSourceConnected)
|
||||
) {
|
||||
leaf = false;
|
||||
expanded = false;
|
||||
}
|
||||
|
||||
const handleExpand = async () => {
|
||||
if (!expanded) {
|
||||
const timeout = setTimeout(() => setProcessing(true), 1);
|
||||
const state = await navigationTreeService.loadNestedNodes(node.id);
|
||||
clearTimeout(timeout);
|
||||
setProcessing(false);
|
||||
if (!state) {
|
||||
navigationTreeService.expandNode(node.id, false);
|
||||
return;
|
||||
const handleOpen = async () => contextRef.context?.onOpen?.(node);
|
||||
const handleExpand = async () => contextRef.context?.tree.expand(node, !expanded);
|
||||
const handleSelect = async (
|
||||
multiple = false,
|
||||
nested = false
|
||||
) => contextRef.context?.tree.select(node, multiple, nested);
|
||||
const handleFilter = async (value: string) => contextRef.context?.tree.filter(node, value);
|
||||
|
||||
useEffect(() => () => {
|
||||
if (!contextRef.context?.selectionTree) {
|
||||
const state = contextRef.context?.tree.getNodeState(node.id);
|
||||
|
||||
if (state?.selected) {
|
||||
contextRef.context?.tree.select(node, true, false);
|
||||
}
|
||||
}
|
||||
navigationTreeService.expandNode(node.id, !expanded);
|
||||
};
|
||||
|
||||
const handleOpen = async () => {
|
||||
setProcessing(true);
|
||||
try {
|
||||
await context?.onOpen?.(node);
|
||||
} finally {
|
||||
setProcessing(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleSelect = (multiple = false) => {
|
||||
context?.onSelect?.(node, multiple);
|
||||
};
|
||||
|
||||
const handleFilter = (value: string) => {
|
||||
context?.onFilter?.(node, value);
|
||||
};
|
||||
|
||||
// TODO: probably should be refactored
|
||||
useEffect(() => {
|
||||
if (expanded && children.isOutdated() && !children.isLoading() && children.isLoaded() && !isOutdated()) {
|
||||
setProcessing(true);
|
||||
navigationTreeService
|
||||
.loadNestedNodes(node.id)
|
||||
.then(state => {
|
||||
setProcessing(false);
|
||||
if (!state) {
|
||||
navigationTreeService.expandNode(node.id, false);
|
||||
}
|
||||
});
|
||||
}
|
||||
}, [expanded, children.isOutdated(), children.isLoading(), children.isLoaded(), isOutdated(), node]);
|
||||
|
||||
useEffect(() => () => {
|
||||
if (!context?.selectionTree && node && context?.isSelected?.(node)) {
|
||||
context.onSelect?.(node, true);
|
||||
}
|
||||
}, [context, node.id]);
|
||||
|
||||
useEffect(() => () => {
|
||||
// TODO: seems like selection & expand should be specific for separate tree definitions
|
||||
navigationTreeService.expandNode(node.id, false);
|
||||
|
||||
context?.treeNodesState?.delete(node.id);
|
||||
}, [node.id]);
|
||||
|
||||
return {
|
||||
control: context?.control,
|
||||
selected: context?.isSelected?.(node) || false,
|
||||
control: contextRef.context?.control,
|
||||
selected: state?.selected || false,
|
||||
loading,
|
||||
expanded,
|
||||
leaf,
|
||||
@@ -124,7 +87,7 @@ export function useNavigationNode(node: NavNode): INavigationNode {
|
||||
handleOpen,
|
||||
handleSelect,
|
||||
handleFilter,
|
||||
filterValue: context?.treeNodesState?.get(node.id).filter || '',
|
||||
filterValue: state?.filter || '',
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -12,16 +12,16 @@ import { ConnectionAuthService } from '@cloudbeaver/core-connections';
|
||||
import { injectable } from '@cloudbeaver/core-di';
|
||||
import { NotificationService } from '@cloudbeaver/core-events';
|
||||
import { Executor, IExecutor } from '@cloudbeaver/core-executor';
|
||||
import { ResourceKeyUtils } from '@cloudbeaver/core-sdk';
|
||||
import { MetadataMap } from '@cloudbeaver/core-utils';
|
||||
import type { IActiveView } from '@cloudbeaver/core-view';
|
||||
|
||||
import { EObjectFeature } from '../shared/NodesManager/EObjectFeature';
|
||||
import { NavNodeExtensionsService } from '../shared/NodesManager/NavNodeExtensionsService';
|
||||
import { NavNodeInfoResource, ROOT_NODE_PATH } from '../shared/NodesManager/NavNodeInfoResource';
|
||||
import { ROOT_NODE_PATH } from '../shared/NodesManager/NavNodeInfoResource';
|
||||
import { NavNodeManagerService } from '../shared/NodesManager/NavNodeManagerService';
|
||||
import { NavTreeResource } from '../shared/NodesManager/NavTreeResource';
|
||||
import { NodeManagerUtils } from '../shared/NodesManager/NodeManagerUtils';
|
||||
import type { ITreeNodeState } from './useElementsTree';
|
||||
|
||||
export interface INavigationNodeMetadata {
|
||||
selected: boolean;
|
||||
@@ -35,7 +35,7 @@ export interface INavigationNodeSelectionData {
|
||||
|
||||
@injectable()
|
||||
export class NavigationTreeService {
|
||||
readonly navigationTreeMetadata: MetadataMap<string, INavigationNodeMetadata>;
|
||||
readonly treeState: MetadataMap<string, ITreeNodeState>;
|
||||
readonly nodeSelectionTask: IExecutor<INavigationNodeSelectionData>;
|
||||
|
||||
constructor(
|
||||
@@ -43,30 +43,19 @@ export class NavigationTreeService {
|
||||
private notificationService: NotificationService,
|
||||
private connectionAuthService: ConnectionAuthService,
|
||||
private navNodeExtensionsService: NavNodeExtensionsService,
|
||||
private navTreeResource: NavTreeResource,
|
||||
private navNodeInfoResource: NavNodeInfoResource
|
||||
private navTreeResource: NavTreeResource
|
||||
) {
|
||||
makeObservable<NavigationTreeService, 'unselectAll'>(this, {
|
||||
unselectAll: action,
|
||||
});
|
||||
this.treeState = new MetadataMap(() => ({
|
||||
filter: '',
|
||||
expanded: false,
|
||||
selected: false,
|
||||
}));
|
||||
|
||||
this.nodeSelectionTask = new Executor();
|
||||
this.navigationTreeMetadata = new MetadataMap<string, INavigationNodeMetadata>(() => ({
|
||||
selected: false,
|
||||
expanded: false,
|
||||
}));
|
||||
this.getView = this.getView.bind(this);
|
||||
|
||||
this.navNodeInfoResource.onItemDelete.addHandler(key => {
|
||||
ResourceKeyUtils.forEach(key, key => {
|
||||
this.navigationTreeMetadata.delete(key);
|
||||
});
|
||||
});
|
||||
this.navTreeResource.onItemDelete.addHandler(key => {
|
||||
ResourceKeyUtils.forEach(key, key => {
|
||||
this.expandNode(key, false);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
getChildren(id: string): string[] | undefined {
|
||||
@@ -95,7 +84,7 @@ export class NavigationTreeService {
|
||||
await this.unselectAll();
|
||||
}
|
||||
|
||||
const metadata = this.navigationTreeMetadata.get(id);
|
||||
const metadata = this.treeState.get(id);
|
||||
metadata.selected = !metadata.selected;
|
||||
|
||||
await this.nodeSelectionTask.execute({
|
||||
@@ -105,20 +94,20 @@ export class NavigationTreeService {
|
||||
}
|
||||
|
||||
isNodeExpanded(navNodeId: string): boolean {
|
||||
return this.navigationTreeMetadata.get(navNodeId).expanded;
|
||||
return this.treeState.get(navNodeId).expanded;
|
||||
}
|
||||
|
||||
isNodeSelected(navNodeId: string): boolean {
|
||||
return this.navigationTreeMetadata.get(navNodeId).selected;
|
||||
return this.treeState.get(navNodeId).selected;
|
||||
}
|
||||
|
||||
expandNode(navNodeId: string, state: boolean): void {
|
||||
const metadata = this.navigationTreeMetadata.get(navNodeId);
|
||||
const metadata = this.treeState.get(navNodeId);
|
||||
metadata.expanded = state;
|
||||
}
|
||||
|
||||
getView(): IActiveView<string> | null {
|
||||
const element = Array.from(this.navigationTreeMetadata).find(([key, metadata]) => metadata.selected);
|
||||
const element = Array.from(this.treeState).find(([key, metadata]) => metadata.selected);
|
||||
|
||||
if (!element) {
|
||||
return null;
|
||||
@@ -131,7 +120,7 @@ export class NavigationTreeService {
|
||||
}
|
||||
|
||||
private async unselectAll() {
|
||||
for (const [id, metadata] of this.navigationTreeMetadata) {
|
||||
for (const [id, metadata] of this.treeState) {
|
||||
metadata.selected = false;
|
||||
await this.nodeSelectionTask.execute({
|
||||
id,
|
||||
|
||||
@@ -8,22 +8,16 @@
|
||||
|
||||
import { createContext } from 'react';
|
||||
|
||||
import type { MetadataMap } from '@cloudbeaver/core-utils';
|
||||
|
||||
import type { NavNode } from '../shared/NodesManager/EntityTypes';
|
||||
import type { ITreeNodeState } from './ElementsTree';
|
||||
import type { IElementsTree } from './useElementsTree';
|
||||
|
||||
export interface ITreeContext {
|
||||
treeNodesState?: MetadataMap<string, ITreeNodeState>;
|
||||
getTreeNodeState?: (node: NavNode) => ITreeNodeState;
|
||||
tree: IElementsTree;
|
||||
selectionTree: boolean;
|
||||
control?: React.FC<{
|
||||
node: NavNode;
|
||||
}>;
|
||||
onOpen?: (node: NavNode) => Promise<void> | void;
|
||||
onSelect?: (node: NavNode, multiple: boolean) => void;
|
||||
isSelected?: (node: NavNode) => boolean;
|
||||
onFilter?: (node: NavNode, value: string) => void;
|
||||
}
|
||||
|
||||
export const TreeContext = createContext<ITreeContext | null>(null);
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
/*
|
||||
* CloudBeaver - Cloud Database Manager
|
||||
* Copyright (C) 2020-2021 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 { resourceKeyList } from '@cloudbeaver/core-sdk';
|
||||
import type { MetadataMap } from '@cloudbeaver/core-utils';
|
||||
|
||||
import type { NavNode } from '../shared/NodesManager/EntityTypes';
|
||||
import type { NavNodeInfoResource } from '../shared/NodesManager/NavNodeInfoResource';
|
||||
import type { IElementsTreeFilter, ITreeNodeState } from './useElementsTree';
|
||||
|
||||
function isDefined<T>(val: T | undefined | null): val is T {
|
||||
return val !== undefined && val !== null;
|
||||
}
|
||||
|
||||
export function elementsTreeNameFilter(resource: NavNodeInfoResource): IElementsTreeFilter {
|
||||
return (node: NavNode, children: string[], state: MetadataMap<string, ITreeNodeState>) => {
|
||||
const nodeState = state.get(node.id);
|
||||
|
||||
if (nodeState.filter === '') {
|
||||
return children;
|
||||
}
|
||||
|
||||
const nodes = resource
|
||||
.get(resourceKeyList(children))
|
||||
.filter(isDefined)
|
||||
.filter(child => child.name?.toLowerCase().includes(nodeState.filter.toLowerCase()));
|
||||
|
||||
return nodes.map(node => node.id);
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,107 @@
|
||||
/*
|
||||
* CloudBeaver - Cloud Database Manager
|
||||
* Copyright (C) 2020-2021 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 { ConnectionInfoResource, EConnectionFeature } from '@cloudbeaver/core-connections';
|
||||
import { resourceKeyList } from '@cloudbeaver/core-sdk';
|
||||
import type { MetadataMap } from '@cloudbeaver/core-utils';
|
||||
|
||||
import type { NavNode } from '../shared/NodesManager/EntityTypes';
|
||||
import { EObjectFeature } from '../shared/NodesManager/EObjectFeature';
|
||||
import { NavNodeInfoResource, ROOT_NODE_PATH } from '../shared/NodesManager/NavNodeInfoResource';
|
||||
import { NodeManagerUtils } from '../shared/NodesManager/NodeManagerUtils';
|
||||
import type { IElementsTreeFilter, ITreeNodeState } from './useElementsTree';
|
||||
|
||||
export const NAVIGATION_TREE_CONNECTION_GROUPS = {
|
||||
unsorted: 'nav-tree://connection-group/unsorted',
|
||||
manageable: 'nav-tree://connection-group/manageable',
|
||||
unmanageable: 'nav-tree://connection-group/unmanageable',
|
||||
};
|
||||
|
||||
function isDefined<T>(val: T | undefined | null): val is T {
|
||||
return val !== undefined && val !== null;
|
||||
}
|
||||
|
||||
function sortManageable(connectionInfoResource: ConnectionInfoResource): (nodeA: NavNode, nodeB: NavNode) => number {
|
||||
return (nodeA: NavNode, nodeB: NavNode): number => {
|
||||
const nodeAConnection = nodeA.objectFeatures.includes(EObjectFeature.dataSource);
|
||||
const nodeBConnection = nodeB.objectFeatures.includes(EObjectFeature.dataSource);
|
||||
|
||||
if (!nodeAConnection || !nodeBConnection) {
|
||||
if (nodeAConnection === nodeBConnection) {
|
||||
return 0;
|
||||
}
|
||||
return nodeAConnection ? 1 : -1;
|
||||
}
|
||||
|
||||
const connectionA = connectionInfoResource.get(NodeManagerUtils.connectionNodeIdToConnectionId(nodeA.id));
|
||||
const connectionB = connectionInfoResource.get(NodeManagerUtils.connectionNodeIdToConnectionId(nodeB.id));
|
||||
|
||||
const nodeAManageable = connectionA?.features.includes(EConnectionFeature.manageable);
|
||||
const nodeBManageable = connectionB?.features.includes(EConnectionFeature.manageable);
|
||||
|
||||
if (nodeAManageable === nodeBManageable) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
return nodeBManageable ? 1 : -1;
|
||||
};
|
||||
}
|
||||
|
||||
export function navigationTreeConnectionGroupFilter(
|
||||
connectionInfoResource: ConnectionInfoResource,
|
||||
resource: NavNodeInfoResource
|
||||
): IElementsTreeFilter {
|
||||
return (node: NavNode, children: string[]) => {
|
||||
if (node.id !== ROOT_NODE_PATH) {
|
||||
return children;
|
||||
}
|
||||
|
||||
const nodes = resource
|
||||
.get(resourceKeyList(children))
|
||||
.filter(isDefined)
|
||||
.sort(sortManageable(connectionInfoResource));
|
||||
|
||||
let groupedChildren: string[] = [];
|
||||
let lastGroup = NAVIGATION_TREE_CONNECTION_GROUPS.unsorted;
|
||||
let groups = 0;
|
||||
|
||||
for (const node of nodes) {
|
||||
const connection = connectionInfoResource.get(NodeManagerUtils.connectionNodeIdToConnectionId(node.id));
|
||||
const manageable = connection?.features.includes(EConnectionFeature.manageable);
|
||||
|
||||
let nextGroup = NAVIGATION_TREE_CONNECTION_GROUPS.unsorted;
|
||||
|
||||
if (manageable) {
|
||||
nextGroup = NAVIGATION_TREE_CONNECTION_GROUPS.manageable;
|
||||
} else if (connection) {
|
||||
nextGroup = NAVIGATION_TREE_CONNECTION_GROUPS.unmanageable;
|
||||
}
|
||||
|
||||
if (nextGroup !== lastGroup) {
|
||||
if (nextGroup === NAVIGATION_TREE_CONNECTION_GROUPS.manageable) {
|
||||
groupedChildren.push(NAVIGATION_TREE_CONNECTION_GROUPS.manageable);
|
||||
} else {
|
||||
groupedChildren.push(NAVIGATION_TREE_CONNECTION_GROUPS.unmanageable);
|
||||
}
|
||||
groups++;
|
||||
lastGroup = nextGroup;
|
||||
}
|
||||
|
||||
groupedChildren.push(node.id);
|
||||
}
|
||||
|
||||
if (groups === 1) {
|
||||
groupedChildren = groupedChildren.filter(id =>
|
||||
id !== NAVIGATION_TREE_CONNECTION_GROUPS.manageable
|
||||
&& id !== NAVIGATION_TREE_CONNECTION_GROUPS.unmanageable
|
||||
);
|
||||
}
|
||||
|
||||
return groupedChildren;
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
/*
|
||||
* CloudBeaver - Cloud Database Manager
|
||||
* Copyright (C) 2020-2021 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 styled, { css } from 'reshadow';
|
||||
|
||||
import { Translate } from '@cloudbeaver/core-localization';
|
||||
import { composes, useStyles } from '@cloudbeaver/core-theming';
|
||||
|
||||
import { NAVIGATION_TREE_CONNECTION_GROUPS } from './navigationTreeConnectionGroupFilter';
|
||||
import type { ElementsTreeCustomRendererComponent } from './useElementsTree';
|
||||
|
||||
export function navigationTreeConnectionGroupRenderer(nodeId: string): ElementsTreeCustomRendererComponent | undefined {
|
||||
if (nodeId !== NAVIGATION_TREE_CONNECTION_GROUPS.manageable
|
||||
&& nodeId !== NAVIGATION_TREE_CONNECTION_GROUPS.unmanageable) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (nodeId === NAVIGATION_TREE_CONNECTION_GROUPS.manageable) {
|
||||
return ManageableGroup;
|
||||
}
|
||||
|
||||
return UnManageableGroup;
|
||||
}
|
||||
|
||||
const styles = composes(
|
||||
css`
|
||||
connection-group {
|
||||
composes: theme-text-text-hint-on-light from global;
|
||||
}
|
||||
`,
|
||||
css`
|
||||
connection-group {
|
||||
composes: theme-typography--caption from global;
|
||||
padding: 4px 12px;
|
||||
|
||||
&:not(:first-child) {
|
||||
margin-top: 8px;
|
||||
}
|
||||
}
|
||||
`
|
||||
);
|
||||
|
||||
const ManageableGroup: ElementsTreeCustomRendererComponent = function ManageableGroup() {
|
||||
return styled(useStyles(styles))(<connection-group><Translate token='app_navigationTree_connection_group_user' /></connection-group>);
|
||||
};
|
||||
const UnManageableGroup: ElementsTreeCustomRendererComponent = function UnManageableGroup() {
|
||||
return styled(useStyles(styles))(<connection-group><Translate token='app_navigationTree_connection_group_shared' /></connection-group>);
|
||||
};
|
||||
@@ -0,0 +1,261 @@
|
||||
/*
|
||||
* CloudBeaver - Cloud Database Manager
|
||||
* Copyright (C) 2020-2021 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 { observable } from 'mobx';
|
||||
import { useState } from 'react';
|
||||
|
||||
import { useExecutor, useObjectRef } from '@cloudbeaver/core-blocks';
|
||||
import { useService } from '@cloudbeaver/core-di';
|
||||
import { ResourceKeyUtils } from '@cloudbeaver/core-sdk';
|
||||
import { MetadataMap } from '@cloudbeaver/core-utils';
|
||||
|
||||
import type { NavNode } from '../shared/NodesManager/EntityTypes';
|
||||
import { NavNodeInfoResource } from '../shared/NodesManager/NavNodeInfoResource';
|
||||
import { NavTreeResource } from '../shared/NodesManager/NavTreeResource';
|
||||
import { NavigationTreeService } from './NavigationTreeService';
|
||||
|
||||
export type ElementsTreeCustomRendererComponent = React.FC<{
|
||||
nodeId: string;
|
||||
component: React.FC<{
|
||||
nodeId: string;
|
||||
}>;
|
||||
}>;
|
||||
|
||||
export type IElementsTreeCustomRenderer = (nodeId: string) => ElementsTreeCustomRendererComponent | undefined;
|
||||
|
||||
export type IElementsTreeFilter = (
|
||||
node: NavNode,
|
||||
children: string[],
|
||||
state: MetadataMap<string, ITreeNodeState>
|
||||
) => string[];
|
||||
|
||||
export interface ITreeNodeState {
|
||||
filter: string;
|
||||
selected: boolean;
|
||||
expanded: boolean;
|
||||
}
|
||||
|
||||
interface IOptions {
|
||||
root: string;
|
||||
localState?: MetadataMap<string, ITreeNodeState>;
|
||||
filters?: IElementsTreeFilter[];
|
||||
renderers?: IElementsTreeCustomRenderer[];
|
||||
customSelect?: (node: NavNode, multiple: boolean, nested: boolean) => Promise<void> | void;
|
||||
isGroup?: (node: NavNode) => boolean;
|
||||
onExpand?: (node: NavNode, state: boolean) => Promise<void> | void;
|
||||
onSelect?: (node: NavNode, state: boolean) => Promise<void> | void;
|
||||
onFilter?: (node: NavNode, value: string) => Promise<void> | void;
|
||||
}
|
||||
|
||||
export interface IElementsTree {
|
||||
root: string;
|
||||
renderers: IElementsTreeCustomRenderer[];
|
||||
state: MetadataMap<string, ITreeNodeState>;
|
||||
getNodeState: (nodeId: string) => ITreeNodeState;
|
||||
getNodeChildren: (nodeId: string) => string[];
|
||||
filter: (node: NavNode, value: string) => Promise<void>;
|
||||
select: (node: NavNode, multiple: boolean, nested: boolean) => Promise<void>;
|
||||
expand: (node: NavNode, state: boolean) => Promise<void>;
|
||||
}
|
||||
|
||||
export function useElementsTree(options: IOptions): IElementsTree {
|
||||
const navNodeInfoResource = useService(NavNodeInfoResource);
|
||||
const navTreeService = useService(NavigationTreeService);
|
||||
const navTreeResource = useService(NavTreeResource);
|
||||
|
||||
const [localTreeNodesState] = useState(() => new MetadataMap<string, ITreeNodeState>(() => ({
|
||||
filter: '',
|
||||
selected: false,
|
||||
expanded: false,
|
||||
})));
|
||||
|
||||
const state = options.localState || localTreeNodesState;
|
||||
|
||||
async function loadTree(nodeId: string) {
|
||||
let children = [nodeId];
|
||||
|
||||
while (children.length > 0) {
|
||||
const nextChildren: string[] = [];
|
||||
|
||||
for (const child of children) {
|
||||
const nodeState = state.get(child);
|
||||
if (!nodeState.expanded && child !== options.root) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const loaded = await navTreeService.loadNestedNodes(child);
|
||||
|
||||
if (!loaded) {
|
||||
const node = navNodeInfoResource.get(child);
|
||||
|
||||
if (node) {
|
||||
await elementsTree.expand(node, false);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
nextChildren.push(...(navTreeResource.get(child) || []));
|
||||
}
|
||||
|
||||
children = nextChildren;
|
||||
}
|
||||
}
|
||||
|
||||
function getNodeChildren(nodeId: string): string[] {
|
||||
const node = navNodeInfoResource.get(nodeId);
|
||||
|
||||
if (!node) {
|
||||
return []; // Maybe filter should accept nodeId, so we be able to apply filters to empty node
|
||||
}
|
||||
|
||||
return (options.filters || [])
|
||||
.reduce(
|
||||
(children, filter) => filter(node, children, state),
|
||||
navTreeService.getChildren(node.id) || []
|
||||
);
|
||||
}
|
||||
|
||||
function getNestedChildren(nodeId: string): string [] {
|
||||
const nestedChildren: string[] = [];
|
||||
const prevChildren = getNodeChildren(nodeId);
|
||||
nestedChildren.push(...prevChildren);
|
||||
|
||||
while (prevChildren.length) {
|
||||
const nodeKey = prevChildren.shift()!;
|
||||
const children = getNodeChildren(nodeKey);
|
||||
prevChildren.push(...children);
|
||||
nestedChildren.push(...children);
|
||||
}
|
||||
|
||||
return nestedChildren;
|
||||
}
|
||||
|
||||
async function clearSelection(nodeId: string) {
|
||||
const node = navNodeInfoResource.get(nodeId);
|
||||
|
||||
const ignore = node && options.isGroup?.(node)
|
||||
? getNestedChildren(nodeId)
|
||||
: [];
|
||||
|
||||
for (const [id, nodeState] of state) {
|
||||
if (nodeState.selected && id !== nodeId && !ignore.includes(id)) {
|
||||
nodeState.selected = false;
|
||||
|
||||
if (options.onSelect) {
|
||||
const node = navNodeInfoResource.get(id);
|
||||
|
||||
if (node) {
|
||||
await options.onSelect(node, false);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function setSelection(nodeId: string, selected: boolean): Promise<void> {
|
||||
const node = navNodeInfoResource.get(nodeId);
|
||||
|
||||
if (!node) {
|
||||
return;
|
||||
}
|
||||
|
||||
const treeNodeState = state.get(nodeId);
|
||||
|
||||
if (treeNodeState.selected === selected) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (options.isGroup?.(node)) {
|
||||
const children = getNodeChildren(nodeId);
|
||||
|
||||
for (const child of children) {
|
||||
await setSelection(child, selected);
|
||||
}
|
||||
|
||||
if (children.length === 0) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
treeNodeState.selected = selected;
|
||||
await options.onSelect?.(node, selected);
|
||||
}
|
||||
|
||||
const elementsTree = useObjectRef<IElementsTree>({
|
||||
root: options.root,
|
||||
renderers: options.renderers || [],
|
||||
state,
|
||||
getNodeState(nodeId: string) {
|
||||
return this.state.get(nodeId);
|
||||
},
|
||||
getNodeChildren,
|
||||
async filter(node: NavNode, value: string) {
|
||||
const treeNodeState = this.state.get(node.id);
|
||||
treeNodeState.filter = value;
|
||||
|
||||
await options.onFilter?.(node, value);
|
||||
},
|
||||
async expand(node: NavNode, state: boolean) {
|
||||
const treeNodeState = this.state.get(node.id);
|
||||
treeNodeState.expanded = state;
|
||||
|
||||
if (state) {
|
||||
await loadTree(node.id);
|
||||
}
|
||||
|
||||
await options.onExpand?.(node, state);
|
||||
},
|
||||
async select(node: NavNode, multiple: boolean, nested: boolean) {
|
||||
if (options.customSelect) {
|
||||
await options.customSelect(node, multiple, nested);
|
||||
return;
|
||||
}
|
||||
|
||||
const treeNodeState = this.state.get(node.id);
|
||||
|
||||
if (!multiple) {
|
||||
await clearSelection(node.id);
|
||||
|
||||
if (treeNodeState.selected) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
await setSelection(node.id, !treeNodeState.selected);
|
||||
},
|
||||
}, undefined, { root: observable.ref, renderers: observable.ref });
|
||||
|
||||
useExecutor({
|
||||
executor: navTreeResource.onNodeRefresh,
|
||||
handlers: [loadTree],
|
||||
});
|
||||
|
||||
useExecutor({
|
||||
executor: navNodeInfoResource.onItemDelete,
|
||||
handlers: [function deleteNodeState(key) {
|
||||
ResourceKeyUtils.forEach(key, key => {
|
||||
state.delete(key);
|
||||
});
|
||||
}],
|
||||
});
|
||||
|
||||
useExecutor({
|
||||
executor: navTreeResource.onItemDelete,
|
||||
handlers: [async function collapseDeletedTree(key) {
|
||||
await ResourceKeyUtils.forEachAsync(key, async key => {
|
||||
const node = navNodeInfoResource.get(key);
|
||||
|
||||
if (node) {
|
||||
await elementsTree.expand(node, false);
|
||||
}
|
||||
});
|
||||
}],
|
||||
});
|
||||
|
||||
return elementsTree;
|
||||
}
|
||||
@@ -6,39 +6,30 @@
|
||||
* you may not use this file except in compliance with the License.
|
||||
*/
|
||||
|
||||
import { useCallback } from 'react';
|
||||
|
||||
import { useObjectRef } from '@cloudbeaver/core-blocks';
|
||||
import { useService } from '@cloudbeaver/core-di';
|
||||
|
||||
import type { NavNode } from '../shared/NodesManager/EntityTypes';
|
||||
import { NavigationTreeService } from './NavigationTreeService';
|
||||
|
||||
interface INavigationTree {
|
||||
isSelected: (node: NavNode) => boolean;
|
||||
navigationTreeService: NavigationTreeService;
|
||||
handleOpen: (node: NavNode) => Promise<void>;
|
||||
handleSelect: (node: NavNode, isMultiple: boolean) => void;
|
||||
handleSelect: (node: NavNode, state: boolean) => void;
|
||||
}
|
||||
|
||||
const bindActions: Array<keyof INavigationTree> = ['handleOpen', 'handleSelect'];
|
||||
|
||||
export function useNavigationTree(): INavigationTree {
|
||||
const navigationTreeService = useService(NavigationTreeService);
|
||||
|
||||
const handleOpen = useCallback(
|
||||
(node: NavNode) => navigationTreeService.navToNode(node.id, node.parentId),
|
||||
[navigationTreeService]
|
||||
);
|
||||
|
||||
const handleSelect = useCallback(
|
||||
(node: NavNode, multiple: boolean) => navigationTreeService.selectNode(node.id, multiple),
|
||||
[navigationTreeService]);
|
||||
|
||||
const isSelected = useCallback(
|
||||
(node: NavNode) => navigationTreeService.isNodeSelected(node.id),
|
||||
[navigationTreeService]
|
||||
);
|
||||
|
||||
return {
|
||||
isSelected,
|
||||
handleOpen,
|
||||
handleSelect,
|
||||
};
|
||||
return useObjectRef<INavigationTree>({
|
||||
navigationTreeService,
|
||||
handleOpen(node: NavNode) {
|
||||
return this.navigationTreeService.navToNode(node.id, node.parentId);
|
||||
},
|
||||
handleSelect(node: NavNode, state: boolean) {
|
||||
return this.navigationTreeService.selectNode(node.id, state);
|
||||
},
|
||||
}, { navigationTreeService }, undefined, bindActions);
|
||||
}
|
||||
|
||||
@@ -27,6 +27,7 @@ export * from './NavigationTree/ElementsTree';
|
||||
export * from './NavigationTree/TreeContext';
|
||||
export * from './NavigationTree/NavigationTreeService';
|
||||
export * from './NavigationTree/NavigationTreeContextMenuService';
|
||||
export * from './NavigationTree/useElementsTree';
|
||||
export * from './AppLocaleService';
|
||||
|
||||
// Models
|
||||
|
||||
@@ -18,6 +18,8 @@ export default [
|
||||
['app_navigationTree_connection_view_option_showSystemObjects', 'Show system objects'],
|
||||
['app_navigationTree_refreshNode', 'Refresh'],
|
||||
['app_navigationTree_context_disconnect', 'Disconnect'],
|
||||
['app_navigationTree_connection_group_user', 'User Connections'],
|
||||
['app_navigationTree_connection_group_shared', 'Shared Connections'],
|
||||
['app_shared_inlineEditor_dialog_title', 'Edit mode'],
|
||||
['app_shared_inlineEditor_dialog_apply', 'Apply'],
|
||||
['app_shared_inlineEditor_dialog_cancel', 'Cancel'],
|
||||
|
||||
@@ -21,6 +21,8 @@ export default [
|
||||
['app_navigationTree_connection_view_option_showSystemObjects', 'Показывать системные объекты'],
|
||||
['app_navigationTree_refreshNode', 'Обновить'],
|
||||
['app_navigationTree_context_disconnect', 'Отключить'],
|
||||
['app_navigationTree_connection_group_user', 'Пользовательские подключения'],
|
||||
['app_navigationTree_connection_group_shared', 'Общие подключения'],
|
||||
['app_shared_inlineEditor_dialog_title', 'Редактирование'],
|
||||
['app_shared_inlineEditor_dialog_apply', 'Применить'],
|
||||
['app_shared_inlineEditor_dialog_cancel', 'Отменить'],
|
||||
|
||||
@@ -60,7 +60,7 @@ export class NavNodeInfoResource extends CachedMapResource<string, NavNode> {
|
||||
});
|
||||
}
|
||||
|
||||
protected async loader(key: ResourceKey<string>) {
|
||||
protected async loader(key: ResourceKey<string>): Promise<Map<string, NavNode>> {
|
||||
if (isResourceKeyList(key)) {
|
||||
const values: NavNode[] = [];
|
||||
for (const nodePath of key.list) {
|
||||
|
||||
@@ -6,6 +6,8 @@
|
||||
* you may not use this file except in compliance with the License.
|
||||
*/
|
||||
|
||||
import { runInAction } from 'mobx';
|
||||
|
||||
import {
|
||||
ConnectionAuthService, Connection, ConnectionInfoResource
|
||||
} from '@cloudbeaver/core-connections';
|
||||
@@ -16,7 +18,7 @@ import {
|
||||
PermissionsService, EPermission, ServerService
|
||||
} from '@cloudbeaver/core-root';
|
||||
import {
|
||||
GraphQLService, resourceKeyList, ResourceKey, ResourceKeyUtils
|
||||
resourceKeyList, ResourceKey, ResourceKeyUtils
|
||||
} from '@cloudbeaver/core-sdk';
|
||||
import { NavigationService } from '@cloudbeaver/core-ui';
|
||||
|
||||
@@ -92,7 +94,6 @@ export class NavNodeManagerService extends Bootstrap {
|
||||
readonly navigator: IExecutor<INodeNavigationData>;
|
||||
|
||||
constructor(
|
||||
private graphQLService: GraphQLService,
|
||||
private permissionsService: PermissionsService,
|
||||
readonly connectionInfo: ConnectionInfoResource,
|
||||
readonly navTree: NavTreeResource,
|
||||
@@ -116,8 +117,6 @@ export class NavNodeManagerService extends Bootstrap {
|
||||
}
|
||||
|
||||
register(): void {
|
||||
// this.sessionDataResource.onDataUpdate.addHandler(this.refreshRoot.bind(this));
|
||||
// this.connectionInfo.onSessionUpdate.addHandler(this.connectionRefreshHandler.bind(this));
|
||||
this.connectionInfo.onItemAdd.addHandler(this.connectionUpdateHandler.bind(this));
|
||||
this.connectionInfo.onItemDelete.addHandler(this.connectionRemoveHandler.bind(this));
|
||||
this.connectionInfo.onConnectionCreate.addHandler(this.connectionCreateHandler.bind(this));
|
||||
@@ -135,15 +134,7 @@ export class NavNodeManagerService extends Bootstrap {
|
||||
}
|
||||
|
||||
async refreshTree(navNodeId: string): Promise<void> {
|
||||
await this.graphQLService.sdk.navRefreshNode({
|
||||
nodePath: navNodeId,
|
||||
});
|
||||
this.markTreeOutdated(navNodeId);
|
||||
await this.navTree.refresh(navNodeId);
|
||||
}
|
||||
|
||||
markTreeOutdated(navNodeId: ResourceKey<string>): void {
|
||||
this.navTree.markOutdated(resourceKeyList(this.navTree.getNestedChildren(navNodeId)));
|
||||
await this.navTree.refreshTree(navNodeId);
|
||||
}
|
||||
|
||||
getTree(navNodeId: string): string[] | undefined
|
||||
@@ -151,14 +142,14 @@ export class NavNodeManagerService extends Bootstrap {
|
||||
getTree(navNodeKey: NavNodeKey[]): Array<string[] | undefined>
|
||||
getTree(navNodeId: string | NavNodeKey | NavNodeKey[]): string[] | undefined | Array<string[] | undefined> {
|
||||
if (typeof navNodeId === 'string') {
|
||||
return this.navTree.data.get(navNodeId);
|
||||
return this.navTree.get(navNodeId);
|
||||
}
|
||||
|
||||
if (Array.isArray(navNodeId)) {
|
||||
return navNodeId.map(node => this.navTree.data.get(node.nodeId));
|
||||
}
|
||||
|
||||
return this.navTree.data.get(navNodeId.nodeId);
|
||||
return this.navTree.get(navNodeId.nodeId);
|
||||
}
|
||||
|
||||
loadTree(navNodeId: string): Promise<string[]> {
|
||||
@@ -169,10 +160,6 @@ export class NavNodeManagerService extends Bootstrap {
|
||||
this.navTree.delete(path);
|
||||
}
|
||||
|
||||
async refreshNode(navNodeId: string): Promise<void> {
|
||||
await this.navNodeInfoResource.refresh(navNodeId);
|
||||
}
|
||||
|
||||
getNode(navNodeId: string): NavNode | undefined
|
||||
getNode(navNodeKey: NavNodeKey): NavNode | undefined
|
||||
getNode(navNodeKey: NavNodeKey[]): Array<NavNode | undefined>
|
||||
@@ -329,22 +316,6 @@ export class NavNodeManagerService extends Bootstrap {
|
||||
};
|
||||
};
|
||||
|
||||
async updateRoot(): Promise<void> {
|
||||
const enabled = await this.isNavTreeEnabled();
|
||||
if (enabled) {
|
||||
await this.navTree.refresh(ROOT_NODE_PATH);
|
||||
}
|
||||
}
|
||||
|
||||
async refreshRoot(): Promise<void> {
|
||||
this.navTree.delete(ROOT_NODE_PATH);
|
||||
const enabled = await this.isNavTreeEnabled();
|
||||
if (enabled) {
|
||||
// this.navTree.markOutdated(ROOT_NODE_PATH);
|
||||
await this.navTree.refresh(ROOT_NODE_PATH);
|
||||
}
|
||||
}
|
||||
|
||||
private async connectionCreateHandler(connection: Connection) {
|
||||
const enabled = await this.isNavTreeEnabled();
|
||||
if (!enabled) {
|
||||
@@ -352,7 +323,7 @@ export class NavNodeManagerService extends Bootstrap {
|
||||
}
|
||||
|
||||
const nodeId = NodeManagerUtils.connectionIdToConnectionNodeId(connection.id);
|
||||
this.markTreeOutdated(nodeId);
|
||||
this.navTree.markTreeOutdated(nodeId);
|
||||
|
||||
const tree = await this.navTree.load(ROOT_NODE_PATH);
|
||||
|
||||
@@ -361,16 +332,6 @@ export class NavNodeManagerService extends Bootstrap {
|
||||
}
|
||||
}
|
||||
|
||||
private async connectionRefreshHandler(connections: Connection[]) {
|
||||
const enabled = await this.isNavTreeEnabled();
|
||||
if (!enabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
// this.navTree.markOutdated(ROOT_NODE_PATH);
|
||||
await this.navTree.refresh(ROOT_NODE_PATH);
|
||||
}
|
||||
|
||||
private async connectionUpdateHandler(key: ResourceKey<string>) {
|
||||
const enabled = await this.isNavTreeEnabled();
|
||||
if (!enabled) {
|
||||
@@ -380,35 +341,40 @@ export class NavNodeManagerService extends Bootstrap {
|
||||
// const count = ResourceKeyUtils.count(key);
|
||||
|
||||
await this.navTree.load(ROOT_NODE_PATH);
|
||||
await ResourceKeyUtils.forEachAsync(key, async key => {
|
||||
const nodeId = NodeManagerUtils.connectionIdToConnectionNodeId(key);
|
||||
this.markTreeOutdated(nodeId);
|
||||
|
||||
// addOpenedConnection
|
||||
const connectionInfo = this.connectionInfo.get(key);
|
||||
runInAction(() => {
|
||||
ResourceKeyUtils.forEach(key, async key => {
|
||||
const nodeId = NodeManagerUtils.connectionIdToConnectionNodeId(key);
|
||||
this.navTree.markTreeOutdated(nodeId);
|
||||
|
||||
if (!connectionInfo?.connected) {
|
||||
this.removeTree(nodeId);
|
||||
}
|
||||
if (this.navTree.has(nodeId)) {
|
||||
const connectionInfo = this.connectionInfo.get(key);
|
||||
|
||||
// if (count > 1) {
|
||||
this.navNodeInfoResource.markOutdated(nodeId);
|
||||
if (!connectionInfo?.connected) {
|
||||
this.removeTree(nodeId);
|
||||
}
|
||||
}
|
||||
|
||||
// if (count > 1) {
|
||||
this.navNodeInfoResource.markOutdated(nodeId);
|
||||
// } else {
|
||||
// await this.navNodeInfoResource.refresh(nodeId);
|
||||
// }
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
private async connectionRemoveHandler(key: ResourceKey<string>) {
|
||||
ResourceKeyUtils.forEach(key, key => {
|
||||
// deleteConnection
|
||||
const navNodeId = NodeManagerUtils.connectionIdToConnectionNodeId(key);
|
||||
private connectionRemoveHandler(key: ResourceKey<string>) {
|
||||
runInAction(() => {
|
||||
ResourceKeyUtils.forEach(key, key => {
|
||||
const navNodeId = NodeManagerUtils.connectionIdToConnectionNodeId(key);
|
||||
|
||||
const node = this.getNode(navNodeId);
|
||||
if (!node) {
|
||||
return;
|
||||
}
|
||||
this.navTree.deleteInNode(node.parentId, [navNodeId]);
|
||||
const node = this.getNode(navNodeId);
|
||||
if (!node) {
|
||||
return;
|
||||
}
|
||||
this.navTree.deleteInNode(node.parentId, [navNodeId]);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
import { action, computed, makeObservable } from 'mobx';
|
||||
|
||||
import { injectable } from '@cloudbeaver/core-di';
|
||||
import { Executor, IExecutor } from '@cloudbeaver/core-executor';
|
||||
import { SessionDataResource } from '@cloudbeaver/core-root';
|
||||
import {
|
||||
GraphQLService,
|
||||
@@ -39,6 +40,7 @@ interface INodeMetadata extends ICachedMapResourceMetadata {
|
||||
|
||||
@injectable()
|
||||
export class NavTreeResource extends CachedMapResource<string, string[]> {
|
||||
readonly onNodeRefresh: IExecutor<string>;
|
||||
protected metadata: MetadataMap<string, INodeMetadata>;
|
||||
|
||||
get childrenLimit(): number {
|
||||
@@ -66,10 +68,25 @@ export class NavTreeResource extends CachedMapResource<string, string[]> {
|
||||
exception: null,
|
||||
includes: [],
|
||||
}));
|
||||
|
||||
this.onNodeRefresh = new Executor<string>(null, (a, b) => a === b);
|
||||
this.onDataOutdated.addHandler(navNodeInfoResource.markOutdated.bind(navNodeInfoResource));
|
||||
this.sessionDataResource.onDataUpdate.addPostHandler(() => this.markOutdated());
|
||||
}
|
||||
|
||||
async refreshTree(navNodeId: string): Promise<void> {
|
||||
await this.graphQLService.sdk.navRefreshNode({
|
||||
nodePath: navNodeId,
|
||||
});
|
||||
this.markTreeOutdated(navNodeId);
|
||||
await this.refresh(navNodeId);
|
||||
await this.onNodeRefresh.execute(navNodeId);
|
||||
}
|
||||
|
||||
markTreeOutdated(navNodeId: ResourceKey<string>): void {
|
||||
this.markOutdated(resourceKeyList(this.getNestedChildren(navNodeId)));
|
||||
}
|
||||
|
||||
setDetails(keyObject: ResourceKey<string>, state: boolean): void {
|
||||
ResourceKeyUtils.forEach(keyObject, key => {
|
||||
const children = resourceKeyList(this.getNestedChildren(key));
|
||||
@@ -253,6 +270,8 @@ export class NavTreeResource extends CachedMapResource<string, string[]> {
|
||||
withDetails: metadata.withDetails,
|
||||
});
|
||||
|
||||
navNodeInfo.hasChildren = navNodeInfo.hasChildren && navNodeChildren.length > 0;
|
||||
|
||||
return { navNodeChildren: navNodeChildren.slice(0, this.childrenLimit), navNodeInfo, parentPath };
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,23 +6,40 @@
|
||||
* you may not use this file except in compliance with the License.
|
||||
*/
|
||||
|
||||
import { useCallback } from 'react';
|
||||
|
||||
import { useObjectRef } from '@cloudbeaver/core-blocks';
|
||||
import { useService } from '@cloudbeaver/core-di';
|
||||
|
||||
import type { NavNode } from './EntityTypes';
|
||||
import { NavNodeInfoResource } from './NavNodeInfoResource';
|
||||
|
||||
export function useNode(navNodeId: string) {
|
||||
interface IUseNodeHook {
|
||||
navNodeId: string;
|
||||
node: NavNode | undefined;
|
||||
isLoading: () => boolean;
|
||||
isLoaded: () => boolean;
|
||||
isOutdated: () => boolean;
|
||||
}
|
||||
|
||||
const bindActions: Array<keyof IUseNodeHook> = ['isLoading', 'isLoaded', 'isOutdated'];
|
||||
|
||||
export function useNode(navNodeId: string): IUseNodeHook {
|
||||
const navNodeInfoResource = useService(NavNodeInfoResource);
|
||||
const node = navNodeInfoResource.get(navNodeId);
|
||||
|
||||
const deps = [navNodeId];
|
||||
|
||||
const isLoading = useCallback(() => navNodeInfoResource.isDataLoading(navNodeId), deps);
|
||||
const isLoaded = useCallback(() => navNodeInfoResource.isLoaded(navNodeId), deps);
|
||||
const isOutdated = useCallback(() => navNodeInfoResource.isOutdated(navNodeId), deps);
|
||||
|
||||
return {
|
||||
node, isLoading, isLoaded, isOutdated,
|
||||
};
|
||||
return useObjectRef<IUseNodeHook>({
|
||||
navNodeId,
|
||||
node,
|
||||
isLoading() {
|
||||
return navNodeInfoResource.isDataLoading(this.navNodeId);
|
||||
},
|
||||
isLoaded() {
|
||||
return navNodeInfoResource.isLoaded(this.navNodeId);
|
||||
},
|
||||
isOutdated() {
|
||||
return navNodeInfoResource.isOutdated(this.navNodeId);
|
||||
},
|
||||
}, {
|
||||
navNodeId,
|
||||
node,
|
||||
}, undefined, bindActions);
|
||||
}
|
||||
|
||||
@@ -8,12 +8,11 @@
|
||||
|
||||
import { forwardRef } from 'react';
|
||||
|
||||
import { filterContainerFakeProps } from './filterContainerFakeProps';
|
||||
import type { IContainerProps } from './IContainerProps';
|
||||
|
||||
export const ColoredContainer = forwardRef<HTMLDivElement, IContainerProps>(function ColoredContainer({ children, className }, ref) {
|
||||
return (
|
||||
<div ref={ref} className={className}>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
export const ColoredContainer = forwardRef<HTMLDivElement, IContainerProps & React.HTMLAttributes<HTMLDivElement>>(function ColoredContainer(props, ref) {
|
||||
const divProps = filterContainerFakeProps(props);
|
||||
|
||||
return <div ref={ref} {...divProps} />;
|
||||
});
|
||||
|
||||
@@ -8,12 +8,11 @@
|
||||
|
||||
import { forwardRef } from 'react';
|
||||
|
||||
import { filterContainerFakeProps } from './filterContainerFakeProps';
|
||||
import type { IContainerProps } from './IContainerProps';
|
||||
|
||||
export const Container = forwardRef<HTMLDivElement, IContainerProps>(function Container({ children, className }, ref) {
|
||||
return (
|
||||
<div ref={ref} className={className}>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
export const Container = forwardRef<HTMLDivElement, IContainerProps & React.HTMLAttributes<HTMLDivElement>>(function Container(props, ref) {
|
||||
const divProps = filterContainerFakeProps(props);
|
||||
|
||||
return <div ref={ref} {...divProps} />;
|
||||
});
|
||||
|
||||
@@ -8,19 +8,22 @@
|
||||
|
||||
import { forwardRef } from 'react';
|
||||
|
||||
import { filterContainerFakeProps } from './filterContainerFakeProps';
|
||||
import type { IContainerProps } from './IContainerProps';
|
||||
|
||||
interface Props extends IContainerProps {
|
||||
className?: string;
|
||||
form?: boolean;
|
||||
center?: boolean;
|
||||
box?: boolean;
|
||||
}
|
||||
|
||||
export const Group = forwardRef<HTMLDivElement, Props>(function Group({ children, className }, ref) {
|
||||
return (
|
||||
<div ref={ref} className={className}>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
export const Group = forwardRef<HTMLDivElement, Props & React.HTMLAttributes<HTMLDivElement>>(function Group({
|
||||
form,
|
||||
center,
|
||||
box,
|
||||
...rest
|
||||
}, ref) {
|
||||
const divProps = filterContainerFakeProps(rest);
|
||||
|
||||
return <div ref={ref} {...divProps} />;
|
||||
});
|
||||
|
||||
@@ -6,16 +6,10 @@
|
||||
* you may not use this file except in compliance with the License.
|
||||
*/
|
||||
|
||||
import { filterLayoutFakeProps } from './filterLayoutFakeProps';
|
||||
import type { ILayoutSizeProps } from './ILayoutSizeProps';
|
||||
|
||||
interface Props extends ILayoutSizeProps {
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export const GroupItem: React.FC<Props> = function GroupItem({ children, className }) {
|
||||
return (
|
||||
<div className={className}>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
export const GroupItem: React.FC<ILayoutSizeProps & React.HTMLAttributes<HTMLDivElement>> = function GroupItem(props) {
|
||||
const divProps = filterLayoutFakeProps(props);
|
||||
return <div {...divProps} />;
|
||||
};
|
||||
|
||||
@@ -6,16 +6,10 @@
|
||||
* you may not use this file except in compliance with the License.
|
||||
*/
|
||||
|
||||
import { filterLayoutFakeProps } from './filterLayoutFakeProps';
|
||||
import type { ILayoutSizeProps } from './ILayoutSizeProps';
|
||||
|
||||
interface Props extends ILayoutSizeProps {
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export const GroupTitle: React.FC<Props> = function GroupTitle({ children, className }) {
|
||||
return (
|
||||
<h2 className={className}>
|
||||
{children}
|
||||
</h2>
|
||||
);
|
||||
export const GroupTitle: React.FC<ILayoutSizeProps & React.HTMLAttributes<HTMLHeadingElement>> = function GroupTitle(props) {
|
||||
const divProps = filterLayoutFakeProps(props);
|
||||
return <h2 {...divProps} />;
|
||||
};
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
|
||||
import type { ILayoutSizeProps } from './ILayoutSizeProps';
|
||||
|
||||
export interface IContainerProps extends ILayoutSizeProps, React.HTMLAttributes<HTMLDivElement> {
|
||||
export interface IContainerProps extends ILayoutSizeProps {
|
||||
baseline?: boolean;
|
||||
center?: boolean;
|
||||
vertical?: boolean;
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
/*
|
||||
* CloudBeaver - Cloud Database Manager
|
||||
* Copyright (C) 2020-2021 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 { filterLayoutFakeProps } from './filterLayoutFakeProps';
|
||||
import type { IContainerProps } from './IContainerProps';
|
||||
|
||||
export function filterContainerFakeProps<T extends IContainerProps>(props: T): Omit<T, keyof IContainerProps> {
|
||||
const {
|
||||
baseline,
|
||||
center,
|
||||
vertical,
|
||||
wrap,
|
||||
overflow,
|
||||
parent,
|
||||
gap,
|
||||
...rest
|
||||
} = filterLayoutFakeProps(props);
|
||||
|
||||
return rest as Omit<T, keyof IContainerProps>;
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
/*
|
||||
* CloudBeaver - Cloud Database Manager
|
||||
* Copyright (C) 2020-2021 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 { ILayoutSizeProps } from './ILayoutSizeProps';
|
||||
|
||||
export function filterLayoutFakeProps<T extends ILayoutSizeProps>(props: T): Omit<T, keyof ILayoutSizeProps> {
|
||||
const {
|
||||
keepSize,
|
||||
tiny,
|
||||
small,
|
||||
medium,
|
||||
large,
|
||||
fill,
|
||||
...rest
|
||||
} = props;
|
||||
|
||||
return rest;
|
||||
}
|
||||
@@ -48,7 +48,7 @@ type ControlledProps = BaseProps & {
|
||||
type ObjectProps<TKey extends keyof TState, TState> = BaseProps & {
|
||||
name: TKey;
|
||||
state: TState;
|
||||
mapState?: (value: TState[TKey]) => TState[TKey];
|
||||
mapState?: (value: TState[TKey]) => TState[TKey] | string | number;
|
||||
mapValue?: (value: TState[TKey]) => TState[TKey];
|
||||
onChange?: (value: TState[TKey], name: TKey) => any;
|
||||
autoHide?: boolean;
|
||||
|
||||
@@ -6,11 +6,13 @@
|
||||
* you may not use this file except in compliance with the License.
|
||||
*/
|
||||
|
||||
import { useCallback, useMemo, useRef, memo } from 'react';
|
||||
import { observable } from 'mobx';
|
||||
import { memo } from 'react';
|
||||
import styled, { use } from 'reshadow';
|
||||
|
||||
import { useStyles } from '@cloudbeaver/core-theming';
|
||||
|
||||
import { useObjectRef } from '../../useObjectRef';
|
||||
import { ITreeNodeContext, TreeNodeContext } from './TreeNodeContext';
|
||||
import { TREE_NODE_STYLES } from './TreeNodeStyles';
|
||||
|
||||
@@ -20,11 +22,11 @@ interface Props {
|
||||
expanded?: boolean;
|
||||
leaf?: boolean;
|
||||
className?: string;
|
||||
onExpand?: () => void;
|
||||
onSelect?: (multiple?: boolean) => void;
|
||||
onFilter?: (value: string) => void;
|
||||
onExpand?: () => Promise<void> | void;
|
||||
onSelect?: (multiple?: boolean, nested?: boolean) => Promise<void> | void;
|
||||
onFilter?: (value: string) => Promise<void> | void;
|
||||
filterValue?: string;
|
||||
onOpen?: () => void;
|
||||
onOpen?: () => Promise<void> | void;
|
||||
}
|
||||
|
||||
export const TreeNode: React.FC<Props> = memo(function TreeNode({
|
||||
@@ -37,41 +39,65 @@ export const TreeNode: React.FC<Props> = memo(function TreeNode({
|
||||
children,
|
||||
...handlers
|
||||
}) {
|
||||
const handlersRef = useRef(handlers);
|
||||
handlersRef.current = handlers;
|
||||
const handlersRef = useObjectRef(handlers);
|
||||
|
||||
const handleExpand = useCallback(() => {
|
||||
handlersRef.current.onExpand?.();
|
||||
}, []);
|
||||
async function processAction(action: () => Promise<void>) {
|
||||
const timeout = setTimeout(() => {
|
||||
nodeContext.processing = true;
|
||||
}, 250);
|
||||
|
||||
const handleSelect = useCallback(
|
||||
(multiple?: boolean): void => handlersRef.current.onSelect?.(multiple),
|
||||
[]
|
||||
);
|
||||
try {
|
||||
await action();
|
||||
} finally {
|
||||
clearTimeout(timeout);
|
||||
nodeContext.processing = false;
|
||||
}
|
||||
}
|
||||
|
||||
const handleFilter = useCallback(
|
||||
(value: string): void => handlersRef.current.onFilter?.(value),
|
||||
[]
|
||||
);
|
||||
|
||||
const handleOpen = useCallback(() => {
|
||||
handlersRef.current.onOpen?.();
|
||||
}, []);
|
||||
|
||||
const nodeContext = useMemo<ITreeNodeContext>(() => ({
|
||||
const nodeContext = useObjectRef<ITreeNodeContext>({
|
||||
processing: false,
|
||||
loading,
|
||||
selected,
|
||||
expanded,
|
||||
leaf,
|
||||
filterValue,
|
||||
expand: handleExpand,
|
||||
select: handleSelect,
|
||||
filter: handleFilter,
|
||||
open: handleOpen,
|
||||
}), [loading, selected, expanded, leaf, filterValue, handleExpand, handleSelect, handleOpen, handleFilter]);
|
||||
async expand() {
|
||||
await processAction(async () => {
|
||||
await handlersRef.onExpand?.();
|
||||
});
|
||||
},
|
||||
async select(multiple?: boolean, nested?: boolean) {
|
||||
await processAction(async () => {
|
||||
await handlersRef.onSelect?.(multiple, nested);
|
||||
});
|
||||
},
|
||||
async filter(value: string) {
|
||||
await processAction(async () => {
|
||||
await handlersRef.onFilter?.(value);
|
||||
});
|
||||
},
|
||||
async open() {
|
||||
await processAction(async () => {
|
||||
await handlersRef.onOpen?.();
|
||||
});
|
||||
},
|
||||
}, {
|
||||
loading,
|
||||
selected,
|
||||
expanded,
|
||||
leaf,
|
||||
filterValue,
|
||||
}, {
|
||||
processing: observable.ref,
|
||||
loading: observable.ref,
|
||||
selected: observable.ref,
|
||||
expanded: observable.ref,
|
||||
leaf: observable.ref,
|
||||
filterValue: observable.ref,
|
||||
});
|
||||
|
||||
return styled(useStyles(TREE_NODE_STYLES))(
|
||||
<node as="div" {...use({ expanded })} className={className}>
|
||||
<node {...use({ expanded })} className={className}>
|
||||
<TreeNodeContext.Provider value={nodeContext}>
|
||||
{children}
|
||||
</TreeNodeContext.Provider>
|
||||
|
||||
@@ -9,15 +9,16 @@
|
||||
import { createContext } from 'react';
|
||||
|
||||
export interface ITreeNodeContext {
|
||||
processing: boolean;
|
||||
expanded: boolean;
|
||||
loading: boolean;
|
||||
selected: boolean;
|
||||
leaf: boolean;
|
||||
select: (multiple?: boolean) => void;
|
||||
filter: (value: string) => void;
|
||||
select: (multiple?: boolean, nested?: boolean) => Promise<void>;
|
||||
filter: (value: string) => Promise<void>;
|
||||
filterValue: string;
|
||||
expand: () => void;
|
||||
open: () => void;
|
||||
expand: () => Promise<void>;
|
||||
open: () => Promise<void>;
|
||||
}
|
||||
|
||||
export const TreeNodeContext = createContext<ITreeNodeContext | null>(null);
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
* you may not use this file except in compliance with the License.
|
||||
*/
|
||||
|
||||
import { observer } from 'mobx-react-lite';
|
||||
import { useContext } from 'react';
|
||||
|
||||
import { TreeNodeContext } from './TreeNodeContext';
|
||||
@@ -20,7 +21,7 @@ interface Props {
|
||||
big?: boolean;
|
||||
}
|
||||
|
||||
export const TreeNodeControl: React.FC<Props> = function TreeNodeControl({
|
||||
export const TreeNodeControl: React.FC<Props> = observer(function TreeNodeControl({
|
||||
onClick,
|
||||
className,
|
||||
children,
|
||||
@@ -61,4 +62,4 @@ export const TreeNodeControl: React.FC<Props> = function TreeNodeControl({
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
});
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
* you may not use this file except in compliance with the License.
|
||||
*/
|
||||
|
||||
import { observer } from 'mobx-react-lite';
|
||||
import { useContext } from 'react';
|
||||
import styled from 'reshadow';
|
||||
import { css } from 'reshadow';
|
||||
@@ -28,7 +29,7 @@ interface Props {
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export const TreeNodeExpand: React.FC<Props> = function TreeNodeExpand({
|
||||
export const TreeNodeExpand: React.FC<Props> = observer(function TreeNodeExpand({
|
||||
leaf,
|
||||
big,
|
||||
className,
|
||||
@@ -51,11 +52,13 @@ export const TreeNodeExpand: React.FC<Props> = function TreeNodeExpand({
|
||||
event.preventDefault();
|
||||
};
|
||||
|
||||
const loading = context.loading || context.processing;
|
||||
|
||||
return styled(styles)(
|
||||
<arrow as="div" className={className} onClick={handleExpand} onDoubleClick={preventDoubleClick}>
|
||||
{context.loading && <Loader small fullSize />}
|
||||
{!context.loading && !context.leaf && !leaf && big && <Icon name="angle" viewBox="0 0 15 8" />}
|
||||
{!context.loading && !context.leaf && !leaf && !big && <Icon name="arrow" viewBox="0 0 16 16" />}
|
||||
<arrow className={className} onClick={handleExpand} onDoubleClick={preventDoubleClick}>
|
||||
{loading && <Loader small fullSize />}
|
||||
{!loading && !context.leaf && !leaf && big && <Icon name="angle" viewBox="0 0 15 8" />}
|
||||
{!loading && !context.leaf && !leaf && !big && <Icon name="arrow" viewBox="0 0 16 16" />}
|
||||
</arrow>
|
||||
);
|
||||
};
|
||||
});
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
* you may not use this file except in compliance with the License.
|
||||
*/
|
||||
|
||||
import { observer } from 'mobx-react-lite';
|
||||
import { useCallback, useContext, useEffect, useState } from 'react';
|
||||
import styled, { css, use } from 'reshadow';
|
||||
|
||||
@@ -56,7 +57,7 @@ interface Props {
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export const TreeNodeFilter: React.FC<Props> = function TreeNodeFilter({
|
||||
export const TreeNodeFilter: React.FC<Props> = observer(function TreeNodeFilter({
|
||||
disabled,
|
||||
className,
|
||||
}) {
|
||||
@@ -99,10 +100,10 @@ export const TreeNodeFilter: React.FC<Props> = function TreeNodeFilter({
|
||||
}
|
||||
}, [filterEnabled]);
|
||||
|
||||
useEffect(() => () => context.filter(''), []);
|
||||
useEffect(() => () => { context.filter(''); }, []);
|
||||
|
||||
return styled(useStyles(styles))(
|
||||
<filter-container as='div' className={className} onClick={handleClick} onDoubleClick={preventPropagation}>
|
||||
<filter-container className={className} onClick={handleClick} onDoubleClick={preventPropagation}>
|
||||
<InputFieldNew
|
||||
ref={inputRef}
|
||||
style={innerInputStyle}
|
||||
@@ -115,4 +116,4 @@ export const TreeNodeFilter: React.FC<Props> = function TreeNodeFilter({
|
||||
<IconButton name='search' disabled={disabled} onClick={onFilterEnabledChange} {...use({ filterEnabled })} />
|
||||
</filter-container>
|
||||
);
|
||||
};
|
||||
});
|
||||
|
||||
@@ -27,7 +27,7 @@ export const TreeNodeIcon: React.FC<Props> = function TreeNodeIcon({
|
||||
children,
|
||||
}) {
|
||||
return styled(styles)(
|
||||
<icon as="div" className={className}>
|
||||
<icon className={className}>
|
||||
<StaticImage icon={icon} />
|
||||
{children}
|
||||
</icon>
|
||||
|
||||
@@ -6,27 +6,24 @@
|
||||
* you may not use this file except in compliance with the License.
|
||||
*/
|
||||
|
||||
import { observer } from 'mobx-react-lite';
|
||||
import { useContext } from 'react';
|
||||
import styled from 'reshadow';
|
||||
import { css } from 'reshadow';
|
||||
|
||||
import { Checkbox } from '../../FormControls/Checkboxes/Checkbox';
|
||||
import { TreeNodeContext } from './TreeNodeContext';
|
||||
|
||||
const styles = css`
|
||||
Icon {
|
||||
cursor: pointer;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
}
|
||||
`;
|
||||
|
||||
interface Props {
|
||||
group?: boolean;
|
||||
onSelect?: () => void;
|
||||
selected?: boolean;
|
||||
disabled?: boolean;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export const TreeNodeSelect: React.FC<Props> = function TreeNodeSelect({
|
||||
export const TreeNodeSelect: React.FC<Props> = observer(function TreeNodeSelect({
|
||||
onSelect,
|
||||
group,
|
||||
selected,
|
||||
disabled,
|
||||
className,
|
||||
}) {
|
||||
@@ -45,9 +42,13 @@ export const TreeNodeSelect: React.FC<Props> = function TreeNodeSelect({
|
||||
event.preventDefault();
|
||||
};
|
||||
|
||||
return styled(styles)(
|
||||
return (
|
||||
<div className={className} onClick={handleClick} onDoubleClick={preventPropagation}>
|
||||
<Checkbox checked={context.selected} disabled={disabled} onChange={() => context.select(true)} />
|
||||
<Checkbox
|
||||
checked={selected ?? context.selected}
|
||||
disabled={disabled}
|
||||
onChange={onSelect ?? (() => context.select(true, group))}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
});
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
* you may not use this file except in compliance with the License.
|
||||
*/
|
||||
|
||||
import { action, makeObservable } from 'mobx';
|
||||
import { action, makeObservable, runInAction } from 'mobx';
|
||||
|
||||
import { injectable } from '@cloudbeaver/core-di';
|
||||
import { Executor, ExecutorInterrupter, IExecutor } from '@cloudbeaver/core-executor';
|
||||
@@ -102,14 +102,16 @@ export class ConnectionInfoResource extends CachedMapResource<string, Connection
|
||||
...this.getIncludesMap(),
|
||||
});
|
||||
|
||||
runInAction(() => {
|
||||
const unrestoredConnectionIdList = Array.from(this.data.values())
|
||||
.map(connection => connection.id)
|
||||
.filter(connectionId => !connections.some(connection => connection.id === connectionId));
|
||||
|
||||
this.delete(resourceKeyList(unrestoredConnectionIdList));
|
||||
});
|
||||
|
||||
await this.addList(connections);
|
||||
|
||||
const unrestoredConnectionIdList = Array.from(this.data.values())
|
||||
.map(connection => connection.id)
|
||||
.filter(connectionId => !connections.some(connection => connection.id === connectionId));
|
||||
|
||||
this.delete(resourceKeyList(unrestoredConnectionIdList));
|
||||
|
||||
await this.onSessionUpdate.execute(connections);
|
||||
});
|
||||
} finally {
|
||||
|
||||
@@ -15,7 +15,7 @@ import { useTranslate } from '@cloudbeaver/core-localization';
|
||||
const styles = css`
|
||||
footer-container {
|
||||
display: flex;
|
||||
height: 100%;
|
||||
width: min-content;
|
||||
flex: 1;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
|
||||
@@ -44,7 +44,7 @@ const styles = composes(
|
||||
css`
|
||||
CommonDialogWrapper {
|
||||
min-height: 400px;
|
||||
min-width: 600px;
|
||||
min-width: 500px;
|
||||
}
|
||||
SubmittingForm {
|
||||
overflow: auto;
|
||||
|
||||
@@ -52,22 +52,24 @@ export const ConfirmationDialog: DialogComponent<ConfirmationDialogPayload> = fu
|
||||
return styled(useStyles(commonDialogThemeStyle, commonDialogBaseStyle, style))(
|
||||
<dialog className={className}>
|
||||
<header>
|
||||
<header-title as="div">
|
||||
<icon-container>
|
||||
{icon && <IconOrImage {...use({ bigIcon })} icon={icon} viewBox={viewBox} />}
|
||||
<header-title-text as='div'>
|
||||
<h3><Translate token={title} /></h3>
|
||||
<sub-title as='div'>{subTitle}</sub-title>
|
||||
</header-title-text>
|
||||
<reject as="div">
|
||||
<Icon name="cross" viewBox="0 0 16 16" onClick={rejectDialog} />
|
||||
</reject>
|
||||
</icon-container>
|
||||
<header-title>
|
||||
<h3><Translate token={title} /></h3>
|
||||
{rejectDialog && (
|
||||
<reject>
|
||||
<Icon name="cross" viewBox="0 0 16 16" onClick={rejectDialog} />
|
||||
</reject>
|
||||
)}
|
||||
</header-title>
|
||||
{subTitle && <sub-title>{subTitle}</sub-title>}
|
||||
</header>
|
||||
<dialog-body as="div">
|
||||
<dialog-body-content as='div'>
|
||||
<dialog-body>
|
||||
<dialog-body-content>
|
||||
<Translate token={message} />
|
||||
</dialog-body-content>
|
||||
<dialog-body-overflow as='div' />
|
||||
<dialog-body-overflow />
|
||||
</dialog-body>
|
||||
<footer>
|
||||
<Button
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
* you may not use this file except in compliance with the License.
|
||||
*/
|
||||
|
||||
import { computed, makeObservable } from 'mobx';
|
||||
import { action, computed, makeObservable } from 'mobx';
|
||||
|
||||
import { Executor, IExecutor } from '@cloudbeaver/core-executor';
|
||||
import { MetadataMap } from '@cloudbeaver/core-utils';
|
||||
@@ -77,6 +77,8 @@ export abstract class CachedMapResource<
|
||||
}));
|
||||
|
||||
makeObservable(this, {
|
||||
set: action,
|
||||
delete: action,
|
||||
values: computed,
|
||||
keys: computed,
|
||||
});
|
||||
|
||||
@@ -71,7 +71,7 @@ export class AuthenticationService extends Bootstrap {
|
||||
return;
|
||||
}
|
||||
|
||||
await this.auth(true);
|
||||
await this.auth(true, null, true);
|
||||
}
|
||||
|
||||
register(): void {
|
||||
@@ -87,7 +87,7 @@ export class AuthenticationService extends Bootstrap {
|
||||
return;
|
||||
}
|
||||
|
||||
await this.auth(false);
|
||||
await this.auth(false, null, true);
|
||||
});
|
||||
this.authProviderService.requestAuthProvider.addHandler(this.requestAuthProviderHandler);
|
||||
}
|
||||
|
||||
@@ -32,7 +32,7 @@ const styles = composes(
|
||||
css`
|
||||
CommonDialogWrapper {
|
||||
min-height: 400px;
|
||||
min-width: 600px;
|
||||
min-width: 500px;
|
||||
}
|
||||
SubmittingForm {
|
||||
overflow: auto;
|
||||
|
||||
@@ -15,7 +15,7 @@ import { useTranslate } from '@cloudbeaver/core-localization';
|
||||
const styles = css`
|
||||
footer-container {
|
||||
display: flex;
|
||||
height: 100%;
|
||||
width: min-content;
|
||||
flex: 1;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
|
||||
@@ -41,7 +41,7 @@ export class PluginBootstrap extends Bootstrap {
|
||||
order: 0,
|
||||
isHidden: () => this.serverConfigResource.enabledAuthProviders.length === 0 || !!this.authInfoService.userInfo,
|
||||
title: 'authentication_login',
|
||||
onClick: () => this.authDialogService.showLoginForm(),
|
||||
onClick: () => this.authDialogService.showLoginForm(false, null, true),
|
||||
}
|
||||
);
|
||||
|
||||
|
||||
+1
-2
@@ -29,7 +29,6 @@ function getClasses(rawValue: any) {
|
||||
|
||||
export const CellFormatter: React.FC<FormatterProps> = observer(function CellFormatter({ rowIdx, row, column, isCellSelected }) {
|
||||
const editorRef = useRef<IEditorRef>(null);
|
||||
const cellRef = useRef<HTMLDivElement>(null);
|
||||
const cellContext = useContext(CellContext);
|
||||
const context = useContext(DataGridContext);
|
||||
const editingContext = useContext(EditingContext);
|
||||
@@ -66,7 +65,7 @@ export const CellFormatter: React.FC<FormatterProps> = observer(function CellFor
|
||||
|
||||
return styled()(
|
||||
<>
|
||||
<cell-formatter ref={cellRef} title={value} className={`cell-formatter ${classes}`}>
|
||||
<cell-formatter title={value} className={`cell-formatter ${classes}`}>
|
||||
{value}
|
||||
</cell-formatter>
|
||||
{(isCellSelected || cellContext?.mouse.state.mouseEnter) && context && (
|
||||
|
||||
@@ -50,11 +50,17 @@ implements IDatabaseDataActions<TOptions, TResult> {
|
||||
}
|
||||
|
||||
updateResults(results: TResult[]): void {
|
||||
const keys = Array.from(this.actions.keys());
|
||||
const actionsMap = Array.from(this.actions.entries());
|
||||
|
||||
for (const key of keys) {
|
||||
if (!results.some(result => result.id === key)) {
|
||||
for (const [key, actions] of actionsMap) {
|
||||
const result = results.find(result => result.id === key);
|
||||
|
||||
if (!result) {
|
||||
this.actions.delete(key);
|
||||
} else {
|
||||
for (const action of actions.values()) {
|
||||
action.updateResult(result);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -118,6 +118,7 @@ implements IDatabaseDataSource<TOptions, TResult> {
|
||||
|
||||
setResults(results: TResult[]): this {
|
||||
this.editor?.cancelChanges();
|
||||
this.actions.updateResults(results);
|
||||
this.results = results;
|
||||
return this;
|
||||
}
|
||||
@@ -194,8 +195,7 @@ implements IDatabaseDataSource<TOptions, TResult> {
|
||||
if (promise instanceof Promise) {
|
||||
this.activeRequest = promise;
|
||||
}
|
||||
this.editor?.cancelChanges();
|
||||
this.results = await promise;
|
||||
this.setResults(await promise);
|
||||
} finally {
|
||||
this.activeRequest = null;
|
||||
}
|
||||
@@ -220,7 +220,7 @@ implements IDatabaseDataSource<TOptions, TResult> {
|
||||
if (promise instanceof Promise) {
|
||||
this.activeSave = promise;
|
||||
}
|
||||
this.results = await promise;
|
||||
this.setResults(await promise);
|
||||
} finally {
|
||||
this.activeSave = null;
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@cloudbeaver/product-default",
|
||||
"sideEffects": false,
|
||||
"version": "21.0.1",
|
||||
"version": "21.0.2",
|
||||
"description": "",
|
||||
"license": "Apache-2.0",
|
||||
"main": "lib/index.js",
|
||||
|
||||
Reference in New Issue
Block a user