feat: CB-1405 filtering objects in nav tree

This commit is contained in:
Wroud
2022-02-02 16:47:22 +03:00
parent 3c3b0f8bcb
commit d62aeb0ba4
19 changed files with 217 additions and 121 deletions
@@ -10,8 +10,10 @@ import { observer } from 'mobx-react-lite';
import { useMemo, useCallback } from 'react';
import styled, { css } from 'reshadow';
import { FolderExplorer, FolderExplorerPath, Loader, useFolderExplorer, useMapResource } from '@cloudbeaver/core-blocks';
import { Filter, FolderExplorer, FolderExplorerPath, Loader, useFolderExplorer, useMapResource } from '@cloudbeaver/core-blocks';
import { useService } from '@cloudbeaver/core-di';
import { useTranslate } from '@cloudbeaver/core-localization';
import { ComponentStyle, composes, useStyles } from '@cloudbeaver/core-theming';
import type { MetadataMap } from '@cloudbeaver/core-utils';
import type { NavNode } from '../shared/NodesManager/EntityTypes';
@@ -19,7 +21,7 @@ import { EObjectFeature } from '../shared/NodesManager/EObjectFeature';
import { NavNodeInfoResource, ROOT_NODE_PATH } from '../shared/NodesManager/NavNodeInfoResource';
import { NavTreeResource } from '../shared/NodesManager/NavTreeResource';
import { ElementsTreeLoader } from './ElementsTreeLoader';
import { elementsTreeNameFilter } from './elementsTreeNameFilter';
import { elementsTreeNameFilter, NavNodeFilterCompareFn } from './elementsTreeNameFilter';
import type { NavTreeControlComponent } from './NavigationNodeComponent';
import { NavigationNodeNested } from './NavigationTreeNode/NavigationNode/NavigationNodeNested';
import { NavigationNodeElement } from './NavigationTreeNode/NavigationNodeElement';
@@ -27,16 +29,39 @@ import { NavigationTreeService } from './NavigationTreeService';
import { ITreeContext, TreeContext } from './TreeContext';
import { IElementsTreeCustomRenderer, IElementsTreeFilter, ITreeNodeState, useElementsTree } from './useElementsTree';
const styles = css`
tree {
position: relative;
box-sizing: border-box;
}
const styles = composes(
css`
filter {
composes: theme-background-surface from global;
}
`,
css`
box {
display: flex;
flex-direction: column;
box-sizing: border-box;
}
FolderExplorerPath {
padding: 0 12px 8px 12px;
}
`;
tree {
position: relative;
box-sizing: border-box;
flex: 1;
}
FolderExplorerPath {
padding: 0 12px 8px 12px;
}
filter {
padding: 8px 24px;
flex: 0 0 auto;
display: block;
position: sticky;
top: 0;
z-index: 1;
}
`
);
interface Props {
root?: string;
@@ -44,11 +69,14 @@ interface Props {
disabled?: boolean;
selectionTree?: boolean;
foldersTree?: boolean;
filter?: boolean;
showFolderExplorerPath?: boolean;
localState?: MetadataMap<string, ITreeNodeState>;
control?: NavTreeControlComponent;
emptyPlaceholder?: React.FC;
style?: ComponentStyle;
className?: string;
navNodeFilterCompare?: NavNodeFilterCompareFn;
filters?: IElementsTreeFilter[];
renderers?: IElementsTreeCustomRenderer[];
customSelect?: (node: NavNode, multiple: boolean, nested: boolean) => void;
@@ -58,7 +86,7 @@ interface Props {
onClick?: (node: NavNode) => Promise<void> | void;
onOpen?: (node: NavNode) => Promise<void> | void;
onSelect?: (node: NavNode, state: boolean) => void;
onFilter?: (node: NavNode, value: string) => void;
onFilter?: (value: string) => void;
}
export const ElementsTree = observer<Props>(function ElementsTree({
@@ -70,9 +98,12 @@ export const ElementsTree = observer<Props>(function ElementsTree({
selectionTree = false,
showFolderExplorerPath = false,
foldersTree = false,
filter = false,
emptyPlaceholder,
navNodeFilterCompare,
filters,
renderers,
style,
className,
isGroup,
beforeSelect,
@@ -84,10 +115,13 @@ export const ElementsTree = observer<Props>(function ElementsTree({
onFilter,
}) {
const folderExplorer = useFolderExplorer(baseRoot);
const root = folderExplorer.folder;
const fullPath = folderExplorer.fullPath;
const navTreeResource = useService(NavTreeResource);
const navNodeInfoResource = useService(NavNodeInfoResource);
const navigationTreeService = useService(NavigationTreeService);
const translate = useTranslate();
const root = folderExplorer.folder;
const fullPath = folderExplorer.fullPath;
const autoOpenFolders = useCallback(async function autoOpenFolders(nodeId: string, path: string[]) {
path = [...path];
@@ -118,7 +152,7 @@ export const ElementsTree = observer<Props>(function ElementsTree({
}, []);
const children = useMapResource(ElementsTree, NavTreeResource, root, {
const children = useMapResource(ElementsTree, navTreeResource, root, {
onLoad: async resource => {
const preload = await resource.preloadNodeParents(fullPath);
@@ -130,7 +164,11 @@ export const ElementsTree = observer<Props>(function ElementsTree({
},
});
const nameFilter = useMemo(() => elementsTreeNameFilter(navNodeInfoResource), [navNodeInfoResource]);
const nameFilter = useMemo(() => elementsTreeNameFilter(
navTreeResource,
navNodeInfoResource,
navNodeFilterCompare
), [navTreeResource, navNodeInfoResource, navNodeFilterCompare]);
const tree = useElementsTree({
baseRoot,
@@ -195,7 +233,7 @@ export const ElementsTree = observer<Props>(function ElementsTree({
const loaderAvailable = !context.tree.foldersTree || context.folderExplorer.root === root;
return styled(styles)(
return styled(useStyles(styles, style))(
<ElementsTreeLoader
root={root}
context={context}
@@ -205,13 +243,25 @@ export const ElementsTree = observer<Props>(function ElementsTree({
keepData={keepData}
>
<TreeContext.Provider value={context}>
<FolderExplorer state={folderExplorer}>
<tree className={className}>
{tree.showFolderExplorerPath && <FolderExplorerPath getName={getName} canSkip={canSkip} />}
<NavigationNodeNested nodeId={root} component={NavigationNodeElement} path={folderExplorer.path} root />
{loaderAvailable && <Loader state={[children, tree]} overlay={hasChildren} />}
</tree>
</FolderExplorer>
<box className={className}>
{filter && (
<filter>
<Filter
placeholder={translate('app_navigationTree_search')}
value={context.tree.filter}
max
onFilter={value => context.tree.setFilter(value as string)}
/>
</filter>
)}
<FolderExplorer state={folderExplorer}>
<tree>
{tree.showFolderExplorerPath && <FolderExplorerPath getName={getName} canSkip={canSkip} />}
<NavigationNodeNested nodeId={root} component={NavigationNodeElement} path={folderExplorer.path} root />
{loaderAvailable && <Loader state={[children, tree]} overlay={hasChildren} />}
</tree>
</FolderExplorer>
</box>
</TreeContext.Provider>
</ElementsTreeLoader>
);
@@ -30,12 +30,12 @@ const navigationTreeStyles = css`
display: flex;
flex: 1;
flex-direction: column;
overflow: auto;
}
ElementsTree {
padding-top: 16px;
min-width: 100%;
min-height: 100%;
width: max-content;
}
@@ -34,8 +34,6 @@ export const NavigationNode: NavigationNodeComponent = observer(function Navigat
handleExpand,
handleOpen,
handleSelect,
handleFilter,
filterValue,
} = useNavigationNode(node, path);
const Control = control || NavigationNodeControl;
@@ -53,12 +51,10 @@ export const NavigationNode: NavigationNodeComponent = observer(function Navigat
expanded={expanded}
externalExpanded={expandedExternal}
leaf={leaf}
filterValue={filterValue}
onExpand={handleExpand}
onClick={handleClick}
onOpen={handleOpen}
onSelect={handleSelect}
onFilter={handleFilter}
>
<Control node={node} />
{(expanded || expandedExternal) && <NavigationNodeNested nodeId={node.id} path={path} component={component} />}
@@ -10,7 +10,8 @@ import { observer } from 'mobx-react-lite';
import { useContext, useMemo } from 'react';
import styled from 'reshadow';
import { getComputed, TreeNodeNested, TREE_NODE_STYLES } from '@cloudbeaver/core-blocks';
import { getComputed, TreeNodeNested, TreeNodeNestedMessage, TREE_NODE_STYLES } from '@cloudbeaver/core-blocks';
import { useTranslate } from '@cloudbeaver/core-localization';
import { useStyles } from '@cloudbeaver/core-theming';
import type { NavTreeNodeComponent } from '../../NavigationNodeComponent';
@@ -31,6 +32,7 @@ export const NavigationNodeNested = observer<Props>(function NavigationNodeNeste
}) {
const styles = useStyles(TREE_NODE_STYLES);
const treeContext = useContext(TreeContext);
const translate = useTranslate();
const nextPath = useMemo(() => [...path, nodeId], [path, nodeId]);
const children = getComputed(
@@ -50,6 +52,7 @@ export const NavigationNodeNested = observer<Props>(function NavigationNodeNeste
return styled(styles)(
<TreeNodeNested root={root}>
{children.map(child => <NavigationNode key={child} nodeId={child} path={nextPath} />)}
{children.length === 0 && <TreeNodeNestedMessage>{translate('app_navigationTree_node_empty')}</TreeNodeNestedMessage>}
</TreeNodeNested>
);
});
@@ -30,8 +30,6 @@ interface INavigationNode {
handleOpen: () => Promise<void>;
handleClick: (leaf: boolean) => Promise<void>;
handleSelect: (isMultiple?: boolean, nested?: boolean) => Promise<void>;
handleFilter: (value: string) => Promise<void>;
filterValue: string;
}
export function useNavigationNode(node: NavNode, path: string[]): INavigationNode {
@@ -51,7 +49,6 @@ export function useNavigationNode(node: NavNode, path: string[]): INavigationNod
const control = getComputed(() => contextRef.context?.control);
const disabled = getComputed(() => contextRef.context?.tree.disabled || false);
const selected = getComputed(() => contextRef.context?.tree.isNodeSelected(node.id) || false);
const filterValue = getComputed(() => state?.filter || '');
const handleClick = async (leaf: boolean) => await contextRef.context?.onClick?.(node, path, leaf);
const handleOpen = async () => await contextRef.context?.onOpen?.(node, path);
@@ -60,12 +57,11 @@ export function useNavigationNode(node: NavNode, path: string[]): INavigationNod
multiple = false,
nested = false
) => await contextRef.context?.tree.select(node, multiple, nested);
const handleFilter = async (value: string) => await contextRef.context?.tree.filter(node, value);
useEffect(() => () => {
if (!contextRef.context?.selectionTree) {
if (contextRef.context?.tree.isNodeSelected(node.id)) {
contextRef.context?.tree.select(node, true, false);
contextRef.context.tree.select(node, true, false);
}
}
}, [node]);
@@ -83,8 +79,6 @@ export function useNavigationNode(node: NavNode, path: string[]): INavigationNod
handleClick,
handleOpen,
handleSelect,
handleFilter,
filterValue,
};
}
@@ -11,25 +11,75 @@ import type { MetadataMap } from '@cloudbeaver/core-utils';
import type { NavNode } from '../shared/NodesManager/EntityTypes';
import type { NavNodeInfoResource } from '../shared/NodesManager/NavNodeInfoResource';
import type { NavTreeResource } from '../shared/NodesManager/NavTreeResource';
import type { IElementsTreeFilter, ITreeNodeState } from './useElementsTree';
export type NavNodeFilterCompareFn = (node: NavNode, filter: string)=> boolean;
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 === '') {
export function elementsTreeNameFilter(
navTreeResource: NavTreeResource,
navNodeInfoResource: NavNodeInfoResource,
compare: NavNodeFilterCompareFn = compareNodes
): IElementsTreeFilter {
return (filter: string, node: NavNode, children: string[], state: MetadataMap<string, ITreeNodeState>) => {
if (filter === '' || compare(node, filter)) {
return children;
}
const nodes = resource
const nodes = navNodeInfoResource
.get(resourceKeyList(children))
.filter(isDefined)
.filter(child => child.name?.toLowerCase().includes(nodeState.filter.toLowerCase()));
.filter(child => filterNode(
navTreeResource,
navNodeInfoResource,
compare,
filter,
child,
state
));
return nodes.map(node => node.id);
};
}
function filterNode(
navTreeResource: NavTreeResource,
navNodeInfoResource: NavNodeInfoResource,
compare: NavNodeFilterCompareFn,
filter: string,
node: NavNode,
state: MetadataMap<string, ITreeNodeState>
): boolean {
const nodeState = state.get(node.id);
if (compare(node, filter)) {
return true;
}
if (nodeState.expanded) {
const children = navTreeResource.get(node.id) || [];
return navNodeInfoResource
.get(resourceKeyList(children))
.filter(isDefined)
.some(child => filterNode(
navTreeResource,
navNodeInfoResource,
compare,
filter,
child,
state
));
}
return false;
}
function compareNodes(node: NavNode, filter: string): boolean {
return node.name?.toLowerCase().includes(filter.toLowerCase()) ?? false;
}
@@ -12,7 +12,6 @@ import { resourceKeyList } from '@cloudbeaver/core-sdk';
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 } from './useElementsTree';
export const NAVIGATION_TREE_CONNECTION_GROUPS = {
@@ -62,7 +61,7 @@ export function navigationTreeConnectionGroupFilter(
connectionInfoResource: ConnectionInfoResource,
resource: NavNodeInfoResource
): IElementsTreeFilter {
return (node: NavNode, children: string[]) => {
return (filter: string, node: NavNode, children: string[]) => {
if (node.id !== ROOT_NODE_PATH) {
return children;
}
@@ -15,7 +15,7 @@ import type { IElementsTreeFilter } from './useElementsTree';
export function navigationTreeDuplicateFilter(
navNodeViewService: NavNodeViewService,
): IElementsTreeFilter {
return (node: NavNode, children: string[]) => {
return (filter: string, node: NavNode, children: string[]) => {
const { nodes, duplicates } = navNodeViewService.filterDuplicates(children);
untracked(() => {
@@ -24,13 +24,13 @@ import { NavigationTreeService } from './NavigationTreeService';
export type IElementsTreeCustomRenderer = (nodeId: string) => NavigationNodeRendererComponent | undefined;
export type IElementsTreeFilter = (
filter: string,
node: NavNode,
children: string[],
state: MetadataMap<string, ITreeNodeState>
) => string[];
export interface ITreeNodeState {
filter: string;
selected: boolean;
expanded: boolean;
}
@@ -55,12 +55,13 @@ interface IOptions {
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;
onFilter?: (value: string) => Promise<void> | void;
}
export interface IElementsTree {
baseRoot: string;
root: string;
filter: string;
loading: boolean;
disabled: boolean;
foldersTree: boolean;
@@ -71,7 +72,7 @@ export interface IElementsTree {
isNodeSelected: (nodeId: string) => boolean;
getNodeChildren: (nodeId: string) => string[];
isGroup?: (node: NavNode) => boolean;
filter: (node: NavNode, value: string) => Promise<void>;
setFilter: (value: string) => Promise<void>;
select: (node: NavNode, multiple: boolean, nested: boolean) => Promise<void>;
expand: (node: NavNode, state: boolean) => Promise<void>;
}
@@ -83,7 +84,6 @@ export function useElementsTree(options: IOptions): IElementsTree {
const connectionInfoResource = useService(ConnectionInfoResource);
const [localTreeNodesState] = useState(() => new MetadataMap<string, ITreeNodeState>(() => ({
filter: '',
selected: false,
expanded: false,
})));
@@ -170,7 +170,7 @@ export function useElementsTree(options: IOptions): IElementsTree {
return (options.filters || [])
.reduce(
(children, filter) => filter(node, children, state),
(children, filter) => filter(elementsTree.filter, node, children, state),
navTreeService.getChildren(node.id) || []
);
}
@@ -247,6 +247,7 @@ export function useElementsTree(options: IOptions): IElementsTree {
const elementsTree = useObservableRef<IElementsTree>(() => ({
state,
filter: '',
loading: options.keepData || false,
getNodeState(nodeId: string) {
return this.state.get(nodeId);
@@ -258,19 +259,19 @@ export function useElementsTree(options: IOptions): IElementsTree {
const children = getNodeChildren(nodeId);
if (children.length > 0) {
return children.every(child => elementsTree.getNodeState(child).selected);
} else {
return false;
return children.every(child => this.isNodeSelected(child));
}
return false;
}
return this.getNodeState(nodeId).selected;
},
getNodeChildren,
async filter(node: NavNode, value: string) {
const treeNodeState = this.state.get(node.id);
treeNodeState.filter = value;
async setFilter(value: string) {
this.filter = value;
await options.onFilter?.(node, value);
await options.onFilter?.(value);
},
async expand(node: NavNode, state: boolean) {
const treeNodeState = this.state.get(node.id);
@@ -318,6 +319,7 @@ export function useElementsTree(options: IOptions): IElementsTree {
isGroup: observable.ref,
disabled: observable.ref,
root: observable.ref,
filter: observable.ref,
loading: observable.ref,
renderers: observable.ref,
baseRoot: observable.ref,
@@ -11,6 +11,8 @@ export default [
['app_shared_settingsMenu_lang', 'Language'],
['app_topnavbar_connection_schema_manager_not_selected', '-- not selected --'],
['app_navigationTree_openNodeTab', 'Open'],
['app_navigationTree_search', 'Search elements...'],
['app_navigationTree_node_empty', 'Empty'],
['app_navigationTree_connection_view', 'Connection view'],
['app_navigationTree_connection_view_option_simple', 'Simple'],
['app_navigationTree_connection_view_option_advanced', 'Advanced'],
@@ -11,6 +11,8 @@ export default [
['app_shared_settingsMenu_lang', 'Lingua'],
['app_topnavbar_connection_schema_manager_not_selected', '-- non selezionato --'],
['app_navigationTree_openNodeTab', 'Apri'],
['app_navigationTree_search', 'Search elements...'],
['app_navigationTree_node_empty', 'Empty'],
['app_navigationTree_connection_view', 'Vista di connessione'],
['app_navigationTree_connection_view_option_simple', 'Semplice'],
['app_navigationTree_connection_view_option_advanced', 'Avanzato'],
@@ -14,6 +14,8 @@ export default [
'Нет открытых объектов для отображения. Дважды нажмите на элемент в дереве навигации, чтобы открыть его.',
],
['app_navigationTree_openNodeTab', 'Открыть'],
['app_navigationTree_search', 'Искать элементы...'],
['app_navigationTree_node_empty', 'Пусто'],
['app_navigationTree_connection_view', 'Отображение'],
['app_navigationTree_connection_view_option_simple', 'Упрощенное'],
['app_navigationTree_connection_view_option_advanced', 'Продвинутое'],
+39 -37
View File
@@ -1,38 +1,40 @@
export default [
['app_shared_connectionMenu_newConnection', '新连接'],
['app_shared_connectionMenu_connection', '连接'],
['app_shared_connectionMenu_custom', '手动'],
['app_shared_connectionMenu_disconnect', '断开所有连接'],
['app_shared_toolsMenu_tools', '工具'],
['app_shared_toolsMenu_logViewer', '日志查看器'],
['app_shared_toolsMenu_logViewer_placeholder', '日志查看器未激活'],
['app_shared_settingsMenu_config', '配置'],
['app_shared_settingsMenu_theme', '主题'],
['app_shared_settingsMenu_lang', '语言'],
['app_topnavbar_connection_schema_manager_not_selected', '-- 为选择 --'],
['app_navigationTree_openNodeTab', '打开'],
['app_navigationTree_connection_view', '连接视图'],
['app_navigationTree_connection_view_option_simple', '简单'],
['app_navigationTree_connection_view_option_advanced', '完善'],
['app_navigationTree_connection_view_option_custom', '自定义'],
['app_navigationTree_connection_view_option_showSystemObjects', '展示系统对象'],
['app_navigationTree_refreshNode', '刷新'],
['app_navigationTree_node_change_name_error', '更改名称时发生错误'],
['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', '取消'],
[
'app_shared_navigationTabsBar_placeholder',
'没有要显示的对象。双击导航树中的对象将其打开。',
],
['app_shared_sql_generators_panel_title', '生成SQL'],
['app_shared_sql_generators_dialog_title', '生成的SQL'],
['app_log_view_clear_log', '清除日志'],
['app_log_view_entry_type', '类型'],
['app_log_view_entry_timestamp', '时间戳'],
['app_log_view_entry_message', '消息'],
['app_log_view_entry_details', '详情'],
];
['app_shared_connectionMenu_newConnection', '新连接'],
['app_shared_connectionMenu_connection', '连接'],
['app_shared_connectionMenu_custom', '手动'],
['app_shared_connectionMenu_disconnect', '断开所有连接'],
['app_shared_toolsMenu_tools', '工具'],
['app_shared_toolsMenu_logViewer', '日志查看器'],
['app_shared_toolsMenu_logViewer_placeholder', '日志查看器未激活'],
['app_shared_settingsMenu_config', '配置'],
['app_shared_settingsMenu_theme', '主题'],
['app_shared_settingsMenu_lang', '语言'],
['app_topnavbar_connection_schema_manager_not_selected', '-- 为选择 --'],
['app_navigationTree_search', 'Search elements...'],
['app_navigationTree_node_empty', 'Empty'],
['app_navigationTree_openNodeTab', '打开'],
['app_navigationTree_connection_view', '连接视图'],
['app_navigationTree_connection_view_option_simple', '简单'],
['app_navigationTree_connection_view_option_advanced', '完善'],
['app_navigationTree_connection_view_option_custom', '自定义'],
['app_navigationTree_connection_view_option_showSystemObjects', '展示系统对象'],
['app_navigationTree_refreshNode', '刷新'],
['app_navigationTree_node_change_name_error', '更改名称时发生错误'],
['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', '取消'],
[
'app_shared_navigationTabsBar_placeholder',
'没有要显示的对象。双击导航树中的对象将其打开。',
],
['app_shared_sql_generators_panel_title', '生成SQL'],
['app_shared_sql_generators_dialog_title', '生成的SQL'],
['app_log_view_clear_log', '清除日志'],
['app_log_view_entry_type', '类型'],
['app_log_view_entry_timestamp', '时间戳'],
['app_log_view_entry_message', '消息'],
['app_log_view_entry_details', '详情'],
];
@@ -14,5 +14,4 @@ export interface ITreeNodeState {
externalExpanded?: boolean;
expanded?: boolean;
leaf?: boolean;
filterValue?: string;
}
@@ -23,7 +23,6 @@ interface Props extends ITreeNodeState {
onClick?: (leaf: boolean) => Promise<void> | void;
onExpand?: () => Promise<void> | void;
onSelect?: (multiple?: boolean, nested?: boolean) => Promise<void> | void;
onFilter?: (value: string) => Promise<void> | void;
onOpen?: () => Promise<void> | void;
}
@@ -36,7 +35,6 @@ export const TreeNode: React.FC<Props> = observer(function TreeNode({
loading = false,
selected = false,
disabled = false,
filterValue = '',
expanded = false,
externalExpanded,
leaf = false,
@@ -76,11 +74,6 @@ export const TreeNode: React.FC<Props> = observer(function TreeNode({
await handlersRef.onSelect?.(multiple, nested);
});
},
async filter(value: string) {
await processAction(async () => {
await handlersRef.onFilter?.(value);
});
},
async open() {
await processAction(async () => {
await handlersRef.onOpen?.();
@@ -96,7 +89,6 @@ export const TreeNode: React.FC<Props> = observer(function TreeNode({
expanded: observable.ref,
externalExpanded: observable.ref,
leaf: observable.ref,
filterValue: observable.ref,
}, {
group,
disabled,
@@ -105,7 +97,6 @@ export const TreeNode: React.FC<Props> = observer(function TreeNode({
expanded,
externalExpanded: externalExpanded || false,
leaf,
filterValue,
});
return styled(useStyles(TREE_NODE_STYLES))(
@@ -18,11 +18,9 @@ export interface ITreeNodeContext {
leaf: boolean;
group: boolean;
readonly select: (multiple?: boolean, nested?: boolean) => Promise<void>;
readonly filter: (value: string) => Promise<void>;
filterValue: string;
readonly click?: () => Promise<void>;
readonly expand: () => Promise<void>;
readonly open: () => Promise<void>;
}
export const TreeNodeContext = createContext<ITreeNodeContext>(undefined as any);
export const TreeNodeContext = createContext<ITreeNodeContext>(undefined as any); // TODO: remove cast to any
@@ -21,17 +21,18 @@ const KEY = {
};
interface Props extends ITreeNodeState {
title?: string;
onClick?: (event: React.MouseEvent<HTMLDivElement>) => void;
className?: string;
big?: boolean;
}
export const TreeNodeControl = observer<Props>(function TreeNodeControl({
title,
group,
disabled,
loading,
selected,
filterValue,
expanded,
externalExpanded,
leaf,
@@ -41,6 +42,10 @@ export const TreeNodeControl = observer<Props>(function TreeNodeControl({
}) {
const context = useContext(TreeNodeContext);
if (!context) {
throw new Error('Context not provided');
}
if (group !== undefined) {
context.group = group;
}
@@ -57,10 +62,6 @@ export const TreeNodeControl = observer<Props>(function TreeNodeControl({
context.selected = selected;
}
if (filterValue !== undefined) {
context.filterValue = filterValue;
}
if (expanded !== undefined) {
context.expanded = expanded;
}
@@ -109,6 +110,7 @@ export const TreeNodeControl = observer<Props>(function TreeNodeControl({
return (
<div
tabIndex={0}
title={title}
aria-selected={context.selected}
className={className}
onClick={handleClick}
@@ -40,6 +40,10 @@ export const TreeNodeExpand = observer<Props>(function TreeNodeExpand({
}) {
const context = useContext(TreeNodeContext);
if (!context) {
throw new Error('Context not provided');
}
if (context.externalExpanded || context.disabled) {
disabled = true;
}
@@ -58,7 +58,7 @@ export const TreeNodeSelect = observer<Props>(function TreeNodeSelect({
async function handleSelect() {
await onSelect?.();
await context.select(true, group);
await context!.select(true, group);
}
const handleClick = (event: React.MouseEvent<HTMLInputElement>) => {
@@ -74,12 +74,12 @@ export const TreeNodeSelect = observer<Props>(function TreeNodeSelect({
{loadIndicator && context.loading
? <Loader small />
: (
<Checkbox
checked={selected ?? context.selected}
disabled={disabled}
onChange={handleSelect}
/>
)}
<Checkbox
checked={selected ?? context.selected}
disabled={disabled}
onChange={handleSelect}
/>
)}
</div>
);
});