mirror of
https://github.com/dbeaver/cloudbeaver.git
synced 2026-09-24 16:04:36 +08:00
feat: CB-1405 filtering objects in nav tree
This commit is contained in:
@@ -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} />}
|
||||
|
||||
+4
-1
@@ -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', 'Продвинутое'],
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user