diff --git a/webapp/packages/core-app/src/NavigationTree/ElementsTree.tsx b/webapp/packages/core-app/src/NavigationTree/ElementsTree.tsx index 92d2cb6cea..8591e26aa2 100644 --- a/webapp/packages/core-app/src/NavigationTree/ElementsTree.tsx +++ b/webapp/packages/core-app/src/NavigationTree/ElementsTree.tsx @@ -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; - 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 = 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( () => ({ control, onOpen, onSelect, isSelected }), @@ -80,7 +75,7 @@ export const ElementsTree: React.FC = observer(function ElementsTree({ return styled(styles)( - {nodeChildren.children.slice(0, limit.get()).map(id => ( + {nodeChildren.children.map(id => ( ))} diff --git a/webapp/packages/core-app/src/NavigationTree/NavigationTreeContextMenuService.ts b/webapp/packages/core-app/src/NavigationTree/NavigationTreeContextMenuService.ts index 6e82e7f876..1357a6ecd3 100644 --- a/webapp/packages/core-app/src/NavigationTree/NavigationTreeContextMenuService.ts +++ b/webapp/packages/core-app/src/NavigationTree/NavigationTreeContextMenuService.ts @@ -29,11 +29,11 @@ export class NavigationTreeContextMenuService { private connectionInfoResource: ConnectionInfoResource ) { } - getMenuToken() { + getMenuToken(): string { return NavigationTreeContextMenuService.menuToken; } - getNodeViewMenuItemToken() { + getNodeViewMenuItemToken(): string { return NavigationTreeContextMenuService.nodeViewMenuItemToken; } diff --git a/webapp/packages/core-app/src/NavigationTree/NavigationTreeNode/NavigationNode/NavigationNodeNested.tsx b/webapp/packages/core-app/src/NavigationTree/NavigationTreeNode/NavigationNode/NavigationNodeNested.tsx index 3b469ca521..24ae84d5e9 100644 --- a/webapp/packages/core-app/src/NavigationTree/NavigationTreeNode/NavigationNode/NavigationNodeNested.tsx +++ b/webapp/packages/core-app/src/NavigationTree/NavigationTreeNode/NavigationNode/NavigationNodeNested.tsx @@ -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 = 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 = observer(function Navigatio return styled(styles)( - {children.children.slice(0, limit.get()).map(child => )} + {children.children.map(child => )} ); }); diff --git a/webapp/packages/core-app/src/NavigationTree/NavigationTreeNode/useNavigationNode.ts b/webapp/packages/core-app/src/NavigationTree/NavigationTreeNode/useNavigationNode.ts index 80d5e4bf48..ed0754f932 100644 --- a/webapp/packages/core-app/src/NavigationTree/NavigationTreeNode/useNavigationNode.ts +++ b/webapp/packages/core-app/src/NavigationTree/NavigationTreeNode/useNavigationNode.ts @@ -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); } }); } diff --git a/webapp/packages/core-app/src/NavigationTree/NavigationTreeService.ts b/webapp/packages/core-app/src/NavigationTree/NavigationTreeService.ts index 3eeee5654f..75211ac378 100644 --- a/webapp/packages/core-app/src/NavigationTree/NavigationTreeService.ts +++ b/webapp/packages/core-app/src/NavigationTree/NavigationTreeService.ts @@ -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([]); - readonly onNodeSelect: Observable<[string, boolean]>; + readonly navigationTreeMetadata: MetadataMap; + readonly nodeSelectionTask: IExecutor; - 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(this, { + childrenLimit: computed, + unselectAll: action, + }); + + this.nodeSelectionTask = new Executor(); + this.navigationTreeMetadata = new MetadataMap(() => ({ + 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 { await this.navNodeManagerService.navToNode(id, parentId); } - async loadNestedNodes(id = ROOT_NODE_PATH) { + async loadNestedNodes(id = ROOT_NODE_PATH): Promise { 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 { 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 | 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); diff --git a/webapp/packages/core-app/src/NavigationTree/TreeContext.ts b/webapp/packages/core-app/src/NavigationTree/TreeContext.ts index 97e7e217c1..0abbdec834 100644 --- a/webapp/packages/core-app/src/NavigationTree/TreeContext.ts +++ b/webapp/packages/core-app/src/NavigationTree/TreeContext.ts @@ -15,7 +15,7 @@ export interface ITreeContext { node: NavNode; }>; onOpen?: (node: NavNode) => Promise | void; - onSelect?: (node: NavNode, multiple: boolean) => boolean; + onSelect?: (node: NavNode, multiple: boolean) => void; isSelected?: (node: NavNode) => boolean; } diff --git a/webapp/packages/core-app/src/NavigationTree/useNavigationTree.ts b/webapp/packages/core-app/src/NavigationTree/useNavigationTree.ts index 14d08e89e7..8883ec0a03 100644 --- a/webapp/packages/core-app/src/NavigationTree/useNavigationTree.ts +++ b/webapp/packages/core-app/src/NavigationTree/useNavigationTree.ts @@ -16,7 +16,7 @@ import { NavigationTreeService } from './NavigationTreeService'; interface INavigationTree { isSelected: (node: NavNode) => boolean; handleOpen: (node: NavNode) => Promise; - handleSelect: (node: NavNode, isMultiple: boolean) => boolean; + handleSelect: (node: NavNode, isMultiple: boolean) => void; } export function useNavigationTree(): INavigationTree { diff --git a/webapp/packages/core-app/src/shared/NodesManager/NavNodeManagerService.ts b/webapp/packages/core-app/src/shared/NodesManager/NavNodeManagerService.ts index 082eb22bc3..452bd334d6 100644 --- a/webapp/packages/core-app/src/shared/NodesManager/NavNodeManagerService.ts +++ b/webapp/packages/core-app/src/shared/NodesManager/NavNodeManagerService.ts @@ -332,20 +332,23 @@ export class NavNodeManagerService extends Bootstrap { }; async updateRoot(): Promise { - if (await this.isNavTreeEnabled()) { + const enabled = await this.isNavTreeEnabled(); + if (enabled) { await this.navTree.refresh(ROOT_NODE_PATH); } } async refreshRoot(): Promise { 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) { - 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; } diff --git a/webapp/packages/core-app/src/shared/useChildren.ts b/webapp/packages/core-app/src/shared/useChildren.ts index 1fef9d8b33..0cf5d40acf 100644 --- a/webapp/packages/core-app/src/shared/useChildren.ts +++ b/webapp/packages/core-app/src/shared/useChildren.ts @@ -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]; diff --git a/webapp/packages/core-blocks/src/FormControls/Checkboxes/Checkbox.tsx b/webapp/packages/core-blocks/src/FormControls/Checkboxes/Checkbox.tsx index 10f271678c..6022a5ba50 100644 --- a/webapp/packages/core-blocks/src/FormControls/Checkboxes/Checkbox.tsx +++ b/webapp/packages/core-blocks/src/FormControls/Checkboxes/Checkbox.tsx @@ -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) { + const [count, refresh] = useState(0); const context = useContext(FormContext); const handleChange = useCallback((event: React.ChangeEvent) => { 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 ( void; - onSelect?: (multiple?: boolean) => boolean; + onSelect?: (multiple?: boolean) => void; onOpen?: () => void; } @@ -42,7 +42,7 @@ export const TreeNode: React.FC = memo(function TreeNode({ }, []); const handleSelect = useCallback( - (multiple?: boolean): boolean => handlersRef.current.onSelect?.(multiple) || false, + (multiple?: boolean): void => handlersRef.current.onSelect?.(multiple), [] ); diff --git a/webapp/packages/core-blocks/src/Tree/TreeNode/TreeNodeContext.ts b/webapp/packages/core-blocks/src/Tree/TreeNode/TreeNodeContext.ts index 6f79062579..c3ed80eb82 100644 --- a/webapp/packages/core-blocks/src/Tree/TreeNode/TreeNodeContext.ts +++ b/webapp/packages/core-blocks/src/Tree/TreeNode/TreeNodeContext.ts @@ -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; } diff --git a/webapp/packages/core-blocks/src/Tree/TreeNode/TreeNodeExpand.tsx b/webapp/packages/core-blocks/src/Tree/TreeNode/TreeNodeExpand.tsx index 7a3455f23a..2fe997e0d7 100644 --- a/webapp/packages/core-blocks/src/Tree/TreeNode/TreeNodeExpand.tsx +++ b/webapp/packages/core-blocks/src/Tree/TreeNode/TreeNodeExpand.tsx @@ -43,7 +43,8 @@ export const TreeNodeExpand: React.FC = function TreeNodeExpand({ throw new Error('Context not provided'); } - const handleExpand = () => { + const handleExpand = (event: React.MouseEvent) => { + event.stopPropagation(); if (!context.leaf && !leaf) { context.expand(); } diff --git a/webapp/packages/core-blocks/src/Tree/TreeNode/TreeNodeSelect.tsx b/webapp/packages/core-blocks/src/Tree/TreeNode/TreeNodeSelect.tsx index 06a98ceb06..74c16bbc1b 100644 --- a/webapp/packages/core-blocks/src/Tree/TreeNode/TreeNodeSelect.tsx +++ b/webapp/packages/core-blocks/src/Tree/TreeNode/TreeNodeSelect.tsx @@ -22,10 +22,12 @@ const styles = css` `; interface Props { + disabled?: boolean; className?: string; } export const TreeNodeSelect: React.FC = function TreeNodeSelect({ + disabled, className, }) { const context = useContext(TreeNodeContext); @@ -45,7 +47,7 @@ export const TreeNodeSelect: React.FC = function TreeNodeSelect({ return styled(styles)(
- context.select(true)} /> + context.select(true)} />
); }; diff --git a/webapp/packages/plugin-authentication/src/AuthenticationService.ts b/webapp/packages/plugin-authentication/src/AuthenticationService.ts index ab1f208a62..8e991dcb5e 100644 --- a/webapp/packages/plugin-authentication/src/AuthenticationService.ts +++ b/webapp/packages/plugin-authentication/src/AuthenticationService.ts @@ -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; }