refactor: CB-874 navigation tree

This commit is contained in:
Wroud
2021-05-13 19:53:26 +03:00
parent 489178158f
commit 8aed3130a5
23 changed files with 503 additions and 281 deletions
@@ -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 { 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,63 @@ interface Props {
}>;
emptyPlaceholder: React.FC;
className?: string;
filters?: IElementsTreeFilter[];
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,
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 || [])],
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 +114,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>
@@ -58,7 +58,7 @@ export const NavigationTree = observer(function NavigationTree() {
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();
if (!isEnabled) {
return null;
@@ -68,6 +68,7 @@ export const NavigationTree = observer(function NavigationTree() {
<inside-box ref={ref} as='div' tabIndex={0}>
<ElementsTree
root={ROOT_NODE_PATH}
localState={navTreeService.treeState}
emptyPlaceholder={() => styled(navigationTreeStyles)(
<center as="div">
<message as="div">
@@ -76,9 +77,8 @@ export const NavigationTree = observer(function NavigationTree() {
</message>
</center>
)}
isSelected={isSelected}
customSelect={handleSelect}
onOpen={handleOpen}
onSelect={handleSelect}
/>
</inside-box>
);
@@ -7,9 +7,8 @@
*/
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 { NavigationNodeControl } from './NavigationNode/NavigationNodeControl';
@@ -27,8 +26,6 @@ export const NavigationNode: React.FC<Props> = observer(function NavigationNode(
node,
component,
}) {
const context = useContext(TreeNodeContext);
const {
control,
selected,
@@ -57,7 +54,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>
);
});
@@ -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>
@@ -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>
);
});
@@ -12,7 +12,7 @@ export const treeNodeMenuStyles = css`
MenuTrigger {
margin-left: 16px;
padding: 0;
height: 20px;
height: 16px;
&:before {
display: none;
@@ -8,6 +8,7 @@
import { useContext, useEffect, useState } from 'react';
import { useObjectRef } from '@cloudbeaver/core-blocks';
import { useService } from '@cloudbeaver/core-di';
import type { NavNode } from '../../shared/NodesManager/EntityTypes';
@@ -27,29 +28,29 @@ interface INavigationNode {
leaf: boolean;
handleExpand: () => void;
handleOpen: () => void;
handleSelect: (isMultiple?: boolean) => void;
handleSelect: (isMultiple?: boolean, nested?: boolean) => void;
handleFilter: (value: string) => void;
filterValue: string;
}
export function useNavigationNode(node: NavNode): INavigationNode {
const context = useContext(TreeContext);
const contextRef = useObjectRef({
context: useContext(TreeContext),
});
const navigationTreeService = useService(NavigationTreeService);
const [processing, setProcessing] = useState(false);
const { isLoading, isOutdated } = useNode(node.id);
const { isLoading } = useNode(node.id);
const children = useChildren(node.id);
const loading = isLoading() || children.isLoading() || processing;
const isExpanded = navigationTreeService.isNodeExpanded(node.id);
let leaf = isLeaf(node) || (children.children?.length === 0 && !children.isOutdated());
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;
@@ -62,61 +63,43 @@ export function useNavigationNode(node: NavNode): INavigationNode {
clearTimeout(timeout);
setProcessing(false);
if (!state) {
navigationTreeService.expandNode(node.id, false);
contextRef.context?.tree.expand(node, false);
return;
}
}
navigationTreeService.expandNode(node.id, !expanded);
contextRef.context?.tree.expand(node, !expanded);
};
const handleOpen = async () => {
setProcessing(true);
try {
await context?.onOpen?.(node);
await contextRef.context?.onOpen?.(node);
} finally {
setProcessing(false);
}
};
const handleSelect = (multiple = false) => {
context?.onSelect?.(node, multiple);
const handleSelect = (multiple = false, nested = false) => {
contextRef.context?.tree.select(node, multiple, nested);
};
const handleFilter = (value: string) => {
context?.onFilter?.(node, value);
contextRef.context?.tree.filter(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);
if (!contextRef.context?.selectionTree) {
const state = contextRef.context?.tree.getNodeState(node.id);
if (state?.selected) {
contextRef.context?.tree.select(node, true, false);
}
}
}, [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 +107,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,238 @@
/*
* 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 { 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 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[];
customSelect?: (node: NavNode, multiple: boolean, nested: boolean) => void;
isGroup?: (node: NavNode) => boolean;
onExpand?: (node: NavNode, state: boolean) => void;
onSelect?: (node: NavNode, state: boolean) => void;
onFilter?: (node: NavNode, value: string) => void;
}
export interface IElementsTree {
state: MetadataMap<string, ITreeNodeState>;
getNodeState: (nodeId: string) => ITreeNodeState;
getNodeChildren: (nodeId: string) => string[];
filter: (node: NavNode, value: string) => void;
select: (node: NavNode, multiple: boolean, nested: boolean) => void;
expand: (node: NavNode, state: boolean) => 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;
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;
}
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) {
options.onSelect(node, false);
}
}
}
}
}
function setSelection(nodeId: string, selected: boolean): 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) {
setSelection(child, selected);
}
}
treeNodeState.selected = selected;
options.onSelect?.(node, selected);
}
const elementsTree = useObjectRef<IElementsTree>({
state,
getNodeState(nodeId: string) {
return this.state.get(nodeId);
},
getNodeChildren,
filter(node: NavNode, value: string) {
const treeNodeState = this.state.get(node.id);
treeNodeState.filter = value;
options.onFilter?.(node, value);
},
expand(node: NavNode, state: boolean) {
const treeNodeState = this.state.get(node.id);
treeNodeState.expanded = state;
options.onExpand?.(node, state);
},
select(node: NavNode, multiple: boolean, nested: boolean) {
if (options.customSelect) {
options.customSelect(node, multiple, nested);
return;
}
const treeNodeState = this.state.get(node.id);
if (!multiple) {
clearSelection(node.id);
if (treeNodeState.selected) {
return;
}
}
setSelection(node.id, !treeNodeState.selected);
},
});
async function refreshNode(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) {
elementsTree.expand(node, false);
}
continue;
}
nextChildren.push(...(navTreeResource.get(child) || []));
}
children = nextChildren;
}
}
useExecutor({
executor: navTreeResource.onNodeRefresh,
handlers: [refreshNode],
});
useExecutor({
executor: navNodeInfoResource.onItemDelete,
handlers: [function deleteNodeState(key) {
ResourceKeyUtils.forEach(key, key => {
state.delete(key);
});
}],
});
useExecutor({
executor: navTreeResource.onItemDelete,
handlers: [function collapseDeletedTree(key) {
ResourceKeyUtils.forEach(key, key => {
const node = navNodeInfoResource.get(key);
if (node) {
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);
}
+1
View File
@@ -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
@@ -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) {
@@ -16,7 +16,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 +92,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 +115,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 +132,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
@@ -169,10 +158,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 +314,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 +321,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 +330,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) {
@@ -382,7 +341,7 @@ export class NavNodeManagerService extends Bootstrap {
await this.navTree.load(ROOT_NODE_PATH);
await ResourceKeyUtils.forEachAsync(key, async key => {
const nodeId = NodeManagerUtils.connectionIdToConnectionNodeId(key);
this.markTreeOutdated(nodeId);
this.navTree.markTreeOutdated(nodeId);
// addOpenedConnection
const connectionInfo = this.connectionInfo.get(key);
@@ -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 = 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);
}
@@ -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';
@@ -21,7 +23,7 @@ interface Props {
leaf?: boolean;
className?: string;
onExpand?: () => void;
onSelect?: (multiple?: boolean) => void;
onSelect?: (multiple?: boolean, nested?: boolean) => void;
onFilter?: (value: string) => void;
filterValue?: string;
onOpen?: () => void;
@@ -37,41 +39,42 @@ 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?.();
}, []);
const handleSelect = useCallback(
(multiple?: boolean): void => handlersRef.current.onSelect?.(multiple),
[]
);
const handleFilter = useCallback(
(value: string): void => handlersRef.current.onFilter?.(value),
[]
);
const handleOpen = useCallback(() => {
handlersRef.current.onOpen?.();
}, []);
const nodeContext = useMemo<ITreeNodeContext>(() => ({
const nodeContext = useObjectRef<ITreeNodeContext>({
loading,
selected,
expanded,
leaf,
filterValue,
expand: handleExpand,
select: handleSelect,
filter: handleFilter,
open: handleOpen,
}), [loading, selected, expanded, leaf, filterValue, handleExpand, handleSelect, handleOpen, handleFilter]);
expand() {
handlersRef.onExpand?.();
},
select(multiple?: boolean, nested?: boolean): void {
handlersRef.onSelect?.(multiple, nested);
},
filter(value: string): void {
handlersRef.onFilter?.(value);
},
open() {
handlersRef.onOpen?.();
},
}, {
loading,
selected,
expanded,
leaf,
filterValue,
}, {
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>
@@ -13,7 +13,7 @@ export interface ITreeNodeContext {
loading: boolean;
selected: boolean;
leaf: boolean;
select: (multiple?: boolean) => void;
select: (multiple?: boolean, nested?: boolean) => void;
filter: (value: string) => void;
filterValue: string;
expand: () => void;
@@ -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,
@@ -58,4 +59,4 @@ export const TreeNodeExpand: React.FC<Props> = function TreeNodeExpand({
{!context.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,
}) {
@@ -102,7 +103,7 @@ export const TreeNodeFilter: React.FC<Props> = function TreeNodeFilter({
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>
);
};
});
@@ -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>
);
};
});