mirror of
https://github.com/dbeaver/cloudbeaver.git
synced 2026-09-24 16:04:36 +08:00
feat: CB-668 folder children selection for navigation tree
This commit is contained in:
@@ -6,15 +6,12 @@
|
||||
* you may not use this file except in compliance with the License.
|
||||
*/
|
||||
|
||||
import { computed } from 'mobx';
|
||||
import { observer } from 'mobx-react-lite';
|
||||
import { useMemo } from 'react';
|
||||
import styled, { css } from 'reshadow';
|
||||
|
||||
import { Loader } from '@cloudbeaver/core-blocks';
|
||||
import { useService } from '@cloudbeaver/core-di';
|
||||
|
||||
import { CoreSettingsService } from '../CoreSettingsService';
|
||||
import type { NavNode } from '../shared/NodesManager/EntityTypes';
|
||||
import { useChildren } from '../shared/useChildren';
|
||||
import { NavigationNodeElement } from './NavigationTreeNode/NavigationNodeElement';
|
||||
@@ -42,7 +39,7 @@ interface Props {
|
||||
emptyPlaceholder: React.FC;
|
||||
className?: string;
|
||||
onOpen?: (node: NavNode) => Promise<void> | void;
|
||||
onSelect?: (node: NavNode, multiple: boolean) => boolean;
|
||||
onSelect?: (node: NavNode, multiple: boolean) => void;
|
||||
isSelected?: (node: NavNode) => boolean;
|
||||
}
|
||||
|
||||
@@ -55,10 +52,8 @@ export const ElementsTree: React.FC<Props> = observer(function ElementsTree({
|
||||
onSelect,
|
||||
isSelected,
|
||||
}) {
|
||||
const config = useService(CoreSettingsService);
|
||||
const nodeChildren = useChildren(root);
|
||||
const Placeholder = emptyPlaceholder;
|
||||
const limit = useMemo(() => computed(() => config.settings.getValue('app.navigationTree.childrenLimit')), [config]);
|
||||
|
||||
const context = useMemo<ITreeContext>(
|
||||
() => ({ control, onOpen, onSelect, isSelected }),
|
||||
@@ -80,7 +75,7 @@ export const ElementsTree: React.FC<Props> = observer(function ElementsTree({
|
||||
return styled(styles)(
|
||||
<TreeContext.Provider value={context}>
|
||||
<tree as="div" className={className}>
|
||||
{nodeChildren.children.slice(0, limit.get()).map(id => (
|
||||
{nodeChildren.children.map(id => (
|
||||
<NavigationNodeElement key={id} nodeId={id} />
|
||||
))}
|
||||
<Loader loading={nodeChildren.isLoading()} overlay />
|
||||
|
||||
@@ -29,11 +29,11 @@ export class NavigationTreeContextMenuService {
|
||||
private connectionInfoResource: ConnectionInfoResource
|
||||
) { }
|
||||
|
||||
getMenuToken() {
|
||||
getMenuToken(): string {
|
||||
return NavigationTreeContextMenuService.menuToken;
|
||||
}
|
||||
|
||||
getNodeViewMenuItemToken() {
|
||||
getNodeViewMenuItemToken(): string {
|
||||
return NavigationTreeContextMenuService.nodeViewMenuItemToken;
|
||||
}
|
||||
|
||||
|
||||
+2
-7
@@ -6,16 +6,13 @@
|
||||
* you may not use this file except in compliance with the License.
|
||||
*/
|
||||
|
||||
import { computed } from 'mobx';
|
||||
import { observer } from 'mobx-react-lite';
|
||||
import { useContext, useMemo } from 'react';
|
||||
import { useContext } from 'react';
|
||||
import styled from 'reshadow';
|
||||
|
||||
import { TreeNodeContext, TreeNodeNested, TREE_NODE_STYLES } from '@cloudbeaver/core-blocks';
|
||||
import { useService } from '@cloudbeaver/core-di';
|
||||
import { useStyles } from '@cloudbeaver/core-theming';
|
||||
|
||||
import { CoreSettingsService } from '../../../CoreSettingsService';
|
||||
import { useChildren } from '../../../shared/useChildren';
|
||||
|
||||
interface Props {
|
||||
@@ -29,11 +26,9 @@ export const NavigationNodeNested: React.FC<Props> = observer(function Navigatio
|
||||
nodeId,
|
||||
component,
|
||||
}) {
|
||||
const config = useService(CoreSettingsService);
|
||||
const styles = useStyles(TREE_NODE_STYLES);
|
||||
const context = useContext(TreeNodeContext);
|
||||
const children = useChildren(nodeId);
|
||||
const limit = useMemo(() => computed(() => config.settings.getValue('app.navigationTree.childrenLimit')), [config]);
|
||||
|
||||
if (!children.children || !context?.expanded) {
|
||||
return null;
|
||||
@@ -43,7 +38,7 @@ export const NavigationNodeNested: React.FC<Props> = observer(function Navigatio
|
||||
|
||||
return styled(styles)(
|
||||
<TreeNodeNested>
|
||||
{children.children.slice(0, limit.get()).map(child => <NavigationNode key={child} nodeId={child} />)}
|
||||
{children.children.map(child => <NavigationNode key={child} nodeId={child} />)}
|
||||
</TreeNodeNested>
|
||||
);
|
||||
});
|
||||
|
||||
@@ -27,18 +27,18 @@ interface INavigationNode {
|
||||
leaf: boolean;
|
||||
handleExpand: () => void;
|
||||
handleOpen: () => void;
|
||||
handleSelect: (isMultiple?: boolean) => boolean;
|
||||
handleSelect: (isMultiple?: boolean) => void;
|
||||
}
|
||||
|
||||
export function useNavigationNode(node: NavNode): INavigationNode {
|
||||
const context = useContext(TreeContext);
|
||||
const navigationTreeService = useService(NavigationTreeService);
|
||||
const [processing, setProcessing] = useState(false);
|
||||
const [isExpanded, switchExpand] = useState(false);
|
||||
const { isLoading, isOutdated } = 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());
|
||||
let expanded = isExpanded && !leaf;
|
||||
|
||||
@@ -60,11 +60,11 @@ export function useNavigationNode(node: NavNode): INavigationNode {
|
||||
clearTimeout(timeout);
|
||||
setProcessing(false);
|
||||
if (!state) {
|
||||
switchExpand(false);
|
||||
navigationTreeService.expandNode(node.id, false);
|
||||
return;
|
||||
}
|
||||
}
|
||||
switchExpand(!expanded);
|
||||
navigationTreeService.expandNode(node.id, !expanded);
|
||||
};
|
||||
|
||||
const handleOpen = async () => {
|
||||
@@ -76,7 +76,9 @@ export function useNavigationNode(node: NavNode): INavigationNode {
|
||||
}
|
||||
};
|
||||
|
||||
const handleSelect = (multiple = false) => context?.onSelect?.(node, multiple) || false;
|
||||
const handleSelect = (multiple = false) => {
|
||||
context?.onSelect?.(node, multiple);
|
||||
};
|
||||
|
||||
// TODO: probably should be refactored
|
||||
useEffect(() => {
|
||||
@@ -87,7 +89,7 @@ export function useNavigationNode(node: NavNode): INavigationNode {
|
||||
.then(state => {
|
||||
setProcessing(false);
|
||||
if (!state) {
|
||||
switchExpand(false);
|
||||
navigationTreeService.expandNode(node.id, false);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -6,43 +6,86 @@
|
||||
* you may not use this file except in compliance with the License.
|
||||
*/
|
||||
|
||||
import { observable } from 'mobx';
|
||||
import { Subject, Observable } from 'rxjs';
|
||||
import { action, computed, makeObservable } from 'mobx';
|
||||
|
||||
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 { CoreSettingsService } from '../CoreSettingsService';
|
||||
import { EObjectFeature } from '../shared/NodesManager/EObjectFeature';
|
||||
import { NavNodeExtensionsService } from '../shared/NodesManager/NavNodeExtensionsService';
|
||||
import { ROOT_NODE_PATH } from '../shared/NodesManager/NavNodeInfoResource';
|
||||
import { NavNodeInfoResource, 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';
|
||||
|
||||
export interface INavigationNodeMetadata {
|
||||
selected: boolean;
|
||||
expanded: boolean;
|
||||
}
|
||||
|
||||
export interface INavigationNodeSelectionData {
|
||||
id: string;
|
||||
selected: boolean;
|
||||
}
|
||||
|
||||
@injectable()
|
||||
export class NavigationTreeService {
|
||||
readonly selectedNodes = observable.array<string>([]);
|
||||
readonly onNodeSelect: Observable<[string, boolean]>;
|
||||
readonly navigationTreeMetadata: MetadataMap<string, INavigationNodeMetadata>;
|
||||
readonly nodeSelectionTask: IExecutor<INavigationNodeSelectionData>;
|
||||
|
||||
private nodeSelectSubject: Subject<[string, boolean]>;
|
||||
get childrenLimit(): number {
|
||||
return this.coreSettingsService.settings.getValue('app.navigationTree.childrenLimit');
|
||||
}
|
||||
|
||||
constructor(
|
||||
private navNodeManagerService: NavNodeManagerService,
|
||||
private notificationService: NotificationService,
|
||||
private connectionAuthService: ConnectionAuthService,
|
||||
private navNodeExtensionsService: NavNodeExtensionsService
|
||||
private navNodeExtensionsService: NavNodeExtensionsService,
|
||||
private navTreeResource: NavTreeResource,
|
||||
private coreSettingsService: CoreSettingsService,
|
||||
private navNodeInfoResource: NavNodeInfoResource
|
||||
) {
|
||||
this.nodeSelectSubject = new Subject();
|
||||
this.onNodeSelect = this.nodeSelectSubject.asObservable();
|
||||
makeObservable<NavigationTreeService, 'unselectAll'>(this, {
|
||||
childrenLimit: computed,
|
||||
unselectAll: action,
|
||||
});
|
||||
|
||||
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);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async navToNode(id: string, parentId: string) {
|
||||
getChildren(id: string): string[] | undefined {
|
||||
const children = this.navTreeResource.get(id);
|
||||
|
||||
if (children) {
|
||||
return children.slice(0, this.childrenLimit);
|
||||
}
|
||||
|
||||
return children;
|
||||
}
|
||||
|
||||
async navToNode(id: string, parentId: string): Promise<void> {
|
||||
await this.navNodeManagerService.navToNode(id, parentId);
|
||||
}
|
||||
|
||||
async loadNestedNodes(id = ROOT_NODE_PATH) {
|
||||
async loadNestedNodes(id = ROOT_NODE_PATH): Promise<boolean> {
|
||||
try {
|
||||
if (this.isConnectionNode(id) && !await this.tryInitConnection(id)) {
|
||||
return false;
|
||||
@@ -55,39 +98,56 @@ export class NavigationTreeService {
|
||||
return false;
|
||||
}
|
||||
|
||||
selectNode(id: string, multiple?: boolean): boolean {
|
||||
async selectNode(id: string, multiple?: boolean): Promise<void> {
|
||||
if (!multiple) {
|
||||
for (const id of this.selectedNodes) {
|
||||
this.nodeSelectSubject.next([id, false]);
|
||||
}
|
||||
this.selectedNodes.clear();
|
||||
await this.unselectAll();
|
||||
}
|
||||
if (!this.isNodeSelected(id)) {
|
||||
this.selectedNodes.push(id);
|
||||
this.nodeSelectSubject.next([id, true]);
|
||||
return true;
|
||||
}
|
||||
this.selectedNodes.remove(id);
|
||||
this.nodeSelectSubject.next([id, false]);
|
||||
return false;
|
||||
|
||||
const metadata = this.navigationTreeMetadata.get(id);
|
||||
metadata.selected = !metadata.selected;
|
||||
|
||||
await this.nodeSelectionTask.execute({
|
||||
id,
|
||||
selected: metadata.selected,
|
||||
});
|
||||
}
|
||||
|
||||
isNodeSelected(navNodeId: string) {
|
||||
return this.selectedNodes.includes(navNodeId);
|
||||
isNodeExpanded(navNodeId: string): boolean {
|
||||
return this.navigationTreeMetadata.get(navNodeId).expanded;
|
||||
}
|
||||
|
||||
isNodeSelected(navNodeId: string): boolean {
|
||||
return this.navigationTreeMetadata.get(navNodeId).selected;
|
||||
}
|
||||
|
||||
expandNode(navNodeId: string, state: boolean): void {
|
||||
const metadata = this.navigationTreeMetadata.get(navNodeId);
|
||||
metadata.expanded = state;
|
||||
}
|
||||
|
||||
getView(): IActiveView<string> | null {
|
||||
if (!this.selectedNodes.length) {
|
||||
const element = Array.from(this.navigationTreeMetadata).find(([key, metadata]) => metadata.selected);
|
||||
|
||||
if (!element) {
|
||||
return null;
|
||||
}
|
||||
const context = this.selectedNodes[0];
|
||||
|
||||
return {
|
||||
context,
|
||||
context: element[0],
|
||||
extensions: this.navNodeExtensionsService.extensions,
|
||||
};
|
||||
}
|
||||
|
||||
private async unselectAll() {
|
||||
for (const [id, metadata] of this.navigationTreeMetadata) {
|
||||
metadata.selected = false;
|
||||
await this.nodeSelectionTask.execute({
|
||||
id,
|
||||
selected: false,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private isConnectionNode(navNodeId: string) {
|
||||
const node = this.navNodeManagerService.getNode(navNodeId);
|
||||
return node?.objectFeatures.includes(EObjectFeature.dataSource);
|
||||
|
||||
@@ -15,7 +15,7 @@ export interface ITreeContext {
|
||||
node: NavNode;
|
||||
}>;
|
||||
onOpen?: (node: NavNode) => Promise<void> | void;
|
||||
onSelect?: (node: NavNode, multiple: boolean) => boolean;
|
||||
onSelect?: (node: NavNode, multiple: boolean) => void;
|
||||
isSelected?: (node: NavNode) => boolean;
|
||||
}
|
||||
|
||||
|
||||
@@ -16,7 +16,7 @@ import { NavigationTreeService } from './NavigationTreeService';
|
||||
interface INavigationTree {
|
||||
isSelected: (node: NavNode) => boolean;
|
||||
handleOpen: (node: NavNode) => Promise<void>;
|
||||
handleSelect: (node: NavNode, isMultiple: boolean) => boolean;
|
||||
handleSelect: (node: NavNode, isMultiple: boolean) => void;
|
||||
}
|
||||
|
||||
export function useNavigationTree(): INavigationTree {
|
||||
|
||||
@@ -332,20 +332,23 @@ export class NavNodeManagerService extends Bootstrap {
|
||||
};
|
||||
|
||||
async updateRoot(): Promise<void> {
|
||||
if (await this.isNavTreeEnabled()) {
|
||||
const enabled = await this.isNavTreeEnabled();
|
||||
if (enabled) {
|
||||
await this.navTree.refresh(ROOT_NODE_PATH);
|
||||
}
|
||||
}
|
||||
|
||||
async refreshRoot(): Promise<void> {
|
||||
this.navTree.delete(ROOT_NODE_PATH);
|
||||
if (await this.isNavTreeEnabled()) {
|
||||
const enabled = await this.isNavTreeEnabled();
|
||||
if (enabled) {
|
||||
await this.navTree.refresh(ROOT_NODE_PATH);
|
||||
}
|
||||
}
|
||||
|
||||
private async connectionCreateHandler(connection: Connection) {
|
||||
if (!await this.isNavTreeEnabled()) {
|
||||
const enabled = await this.isNavTreeEnabled();
|
||||
if (!enabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -360,7 +363,8 @@ export class NavNodeManagerService extends Bootstrap {
|
||||
}
|
||||
|
||||
private async connectionRefreshHandler(connections: Connection[]) {
|
||||
if (!await this.isNavTreeEnabled()) {
|
||||
const enabled = await this.isNavTreeEnabled();
|
||||
if (!enabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -368,7 +372,8 @@ export class NavNodeManagerService extends Bootstrap {
|
||||
}
|
||||
|
||||
private async connectionUpdateHandler(key: ResourceKey<string>) {
|
||||
if (!await this.isNavTreeEnabled()) {
|
||||
const enabled = await this.isNavTreeEnabled();
|
||||
if (!enabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -430,7 +435,8 @@ export class NavNodeManagerService extends Bootstrap {
|
||||
}
|
||||
|
||||
private async isNavTreeEnabled() {
|
||||
if (!await this.permissionsService.hasAsync(EPermission.public)) {
|
||||
const active = await this.permissionsService.hasAsync(EPermission.public);
|
||||
if (!active) {
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
@@ -10,6 +10,7 @@ import { useCallback } from 'react';
|
||||
|
||||
import { useService } from '@cloudbeaver/core-di';
|
||||
|
||||
import { NavigationTreeService } from '../NavigationTree/NavigationTreeService';
|
||||
import { ROOT_NODE_PATH } from './NodesManager/NavNodeInfoResource';
|
||||
import { NavTreeResource } from './NodesManager/NavTreeResource';
|
||||
|
||||
@@ -21,8 +22,9 @@ interface Hook {
|
||||
}
|
||||
|
||||
export function useChildren(navNodeId = ROOT_NODE_PATH): Hook {
|
||||
const navTreeService = useService(NavigationTreeService);
|
||||
const navTreeResource = useService(NavTreeResource);
|
||||
const children = navTreeResource.get(navNodeId);
|
||||
const children = navTreeService.getChildren(navNodeId);
|
||||
|
||||
const deps = [navNodeId];
|
||||
|
||||
|
||||
@@ -7,8 +7,8 @@
|
||||
*/
|
||||
|
||||
import { observer } from 'mobx-react-lite';
|
||||
import { useCallback, useContext } from 'react';
|
||||
import styled, { use } from 'reshadow';
|
||||
import { useCallback, useContext, useState } from 'react';
|
||||
import { use } from 'reshadow';
|
||||
|
||||
import { FormContext } from '../FormContext';
|
||||
import { isControlPresented } from '../isControlPresented';
|
||||
@@ -61,6 +61,7 @@ export const Checkbox: CheckboxType = observer(function Checkbox({
|
||||
onChange,
|
||||
...rest
|
||||
}: CheckboxControlledProps | CheckboxObjectProps<any, any>) {
|
||||
const [count, refresh] = useState(0);
|
||||
const context = useContext(FormContext);
|
||||
const handleChange = useCallback((event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
if (state) {
|
||||
@@ -83,7 +84,8 @@ export const Checkbox: CheckboxType = observer(function Checkbox({
|
||||
if (context) {
|
||||
context.onChange(event.target.checked, name);
|
||||
}
|
||||
}, [state, name, value, onChange, context]);
|
||||
refresh(count + 1);
|
||||
}, [state, name, value, onChange, context, count]);
|
||||
|
||||
if (autoHide && !isControlPresented(name, state)) {
|
||||
return null;
|
||||
@@ -103,7 +105,7 @@ export const Checkbox: CheckboxType = observer(function Checkbox({
|
||||
checked = checked.includes(value);
|
||||
}
|
||||
|
||||
return styled()(
|
||||
return (
|
||||
<CheckboxMarkup
|
||||
{...rest}
|
||||
name={name}
|
||||
|
||||
@@ -21,7 +21,7 @@ interface Props {
|
||||
leaf?: boolean;
|
||||
className?: string;
|
||||
onExpand?: () => void;
|
||||
onSelect?: (multiple?: boolean) => boolean;
|
||||
onSelect?: (multiple?: boolean) => void;
|
||||
onOpen?: () => void;
|
||||
}
|
||||
|
||||
@@ -42,7 +42,7 @@ export const TreeNode: React.FC<Props> = memo(function TreeNode({
|
||||
}, []);
|
||||
|
||||
const handleSelect = useCallback(
|
||||
(multiple?: boolean): boolean => handlersRef.current.onSelect?.(multiple) || false,
|
||||
(multiple?: boolean): void => handlersRef.current.onSelect?.(multiple),
|
||||
[]
|
||||
);
|
||||
|
||||
|
||||
@@ -13,7 +13,7 @@ export interface ITreeNodeContext {
|
||||
loading: boolean;
|
||||
selected: boolean;
|
||||
leaf: boolean;
|
||||
select: (multiple?: boolean) => boolean;
|
||||
select: (multiple?: boolean) => void;
|
||||
expand: () => void;
|
||||
open: () => void;
|
||||
}
|
||||
|
||||
@@ -43,7 +43,8 @@ export const TreeNodeExpand: React.FC<Props> = function TreeNodeExpand({
|
||||
throw new Error('Context not provided');
|
||||
}
|
||||
|
||||
const handleExpand = () => {
|
||||
const handleExpand = (event: React.MouseEvent<HTMLDivElement>) => {
|
||||
event.stopPropagation();
|
||||
if (!context.leaf && !leaf) {
|
||||
context.expand();
|
||||
}
|
||||
|
||||
@@ -22,10 +22,12 @@ const styles = css`
|
||||
`;
|
||||
|
||||
interface Props {
|
||||
disabled?: boolean;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export const TreeNodeSelect: React.FC<Props> = function TreeNodeSelect({
|
||||
disabled,
|
||||
className,
|
||||
}) {
|
||||
const context = useContext(TreeNodeContext);
|
||||
@@ -45,7 +47,7 @@ export const TreeNodeSelect: React.FC<Props> = function TreeNodeSelect({
|
||||
|
||||
return styled(styles)(
|
||||
<div className={className} onClick={handleClick} onDoubleClick={preventPropagation}>
|
||||
<Checkbox checked={context.selected} onChange={() => context.select(true)} />
|
||||
<Checkbox checked={context.selected} disabled={disabled} onChange={() => context.select(true)} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -60,7 +60,8 @@ export class AuthenticationService extends Bootstrap {
|
||||
}
|
||||
|
||||
private async requireAuthentication() {
|
||||
if (!await this.appAuthService.isAuthNeeded()) {
|
||||
const authNeeded = await this.appAuthService.isAuthNeeded();
|
||||
if (!authNeeded) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user