mirror of
https://github.com/dbeaver/cloudbeaver.git
synced 2026-09-24 16:04:36 +08:00
refactor: CB-874 navigation tree
This commit is contained in:
@@ -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>
|
||||
);
|
||||
});
|
||||
|
||||
+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>
|
||||
);
|
||||
});
|
||||
|
||||
+1
-1
@@ -12,7 +12,7 @@ export const treeNodeMenuStyles = css`
|
||||
MenuTrigger {
|
||||
margin-left: 16px;
|
||||
padding: 0;
|
||||
height: 20px;
|
||||
height: 16px;
|
||||
|
||||
&:before {
|
||||
display: none;
|
||||
|
||||
+25
-42
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user