dbeaver/pro#4742 add tree context menu provider (#3745)

* dbeaver/pro#4742 add tree context menu provider

* dbeaver/pro#4742 add useTreeMenu hook

* dbeaver/pro#4742 revert hook renaming

---------

Co-authored-by: Alexey Potsetsuev <wrouds@gmail.com>
Co-authored-by: Evgenia <139753579+EvgeniaBzzz@users.noreply.github.com>
This commit is contained in:
alex
2025-09-22 19:43:27 +08:00
committed by GitHub
co-authored by Alexey Potsetsuev Evgenia
parent da7d4eef19
commit 39ea1e2b7f
7 changed files with 134 additions and 6 deletions
@@ -1,6 +1,6 @@
/*
* CloudBeaver - Cloud Database Manager
* Copyright (C) 2020-2024 DBeaver Corp and others
* Copyright (C) 2020-2025 DBeaver Corp and others
*
* Licensed under the Apache License, Version 2.0.
* you may not use this file except in compliance with the License.
@@ -16,23 +16,34 @@ export interface IContextMenuPositionCoords {
export interface IContextMenuPosition {
position: IContextMenuPositionCoords | null;
handleContextMenuOpen: (event: React.MouseEvent<HTMLDivElement>) => void;
handleContextMenuOpen: (event: React.MouseEvent) => void;
}
export function useContextMenuPosition(): IContextMenuPosition {
return useObservableRef<IContextMenuPosition>(
() => ({
position: null,
handleContextMenuOpen(event: React.MouseEvent<HTMLDivElement>) {
handleContextMenuOpen(event: React.MouseEvent) {
if (!event.currentTarget.contains(event.target as Node)) {
return;
}
event.preventDefault();
event.stopPropagation();
let x = event.clientX;
let y = event.clientY;
if (x === 0 && y === 0) {
const rect = event.currentTarget.getBoundingClientRect();
x = rect.left + rect.width / 2;
y = rect.top + rect.height / 2;
}
this.position = {
x: event.clientX,
y: event.clientY,
x,
y,
};
},
}),
@@ -13,3 +13,4 @@ export { devToolsPlugin };
export default devToolsPlugin;
export * from './DevToolsService.js';
export * from './menu/MENU_DEVTOOLS.js';
@@ -24,10 +24,13 @@ import { useTree } from './useTree.js';
import { useTreeDnD } from './useTreeDnD.js';
import type { ITreeSelection } from './useTreeSelection.js';
import { useTreeVirtualization } from './useTreeVirtualization.js';
import { TreeMenuContextProvider } from './contexts/TreeMenuContext/TreeMenuContextProvider.js';
import type { ITreeMenu } from './useTreeMenu.js';
export interface NavigationTreeNewProps {
data: ITreeData;
selection?: ITreeSelection;
menu?: ITreeMenu;
nodeRenderers?: INodeRenderer[];
emptyPlaceholder?: NodeEmptyPlaceholderComponent;
className?: string;
@@ -40,6 +43,7 @@ export interface NavigationTreeNewProps {
export const Tree = observer<NavigationTreeNewProps>(function Tree({
data,
selection,
menu,
nodeRenderers,
emptyPlaceholder,
className,
@@ -69,7 +73,9 @@ export const Tree = observer<NavigationTreeNewProps>(function Tree({
<TreeVirtualizationContext.Provider value={mountOptimization}>
<TreeContext.Provider value={tree}>
<TreeDnDContext.Provider value={treeDnD}>
<NodeChildren nodeId={data.rootId} offsetHeight={0} emptyPlaceholder={emptyPlaceholder} root />
<TreeMenuContextProvider menu={menu ?? null}>
<NodeChildren nodeId={data.rootId} offsetHeight={0} emptyPlaceholder={emptyPlaceholder} root />
</TreeMenuContextProvider>
</TreeDnDContext.Provider>
</TreeContext.Provider>
</TreeVirtualizationContext.Provider>
@@ -0,0 +1,23 @@
/*
* CloudBeaver - Cloud Database Manager
* Copyright (C) 2020-2025 DBeaver Corp and others
*
* Licensed under the Apache License, Version 2.0.
* you may not use this file except in compliance with the License.
*/
import { createContext, use } from 'react';
import type { ITreeMenu } from '../../useTreeMenu.js';
export const TreeMenuContext = createContext<ITreeMenu | null>(null);
export function useTreeMenuContext() {
const context = use(TreeMenuContext);
if (!context) {
throw new Error('useTreeMenuContext must be used within a TreeMenuContextProvider.');
}
return context;
}
@@ -0,0 +1,33 @@
/*
* CloudBeaver - Cloud Database Manager
* Copyright (C) 2020-2025 DBeaver Corp and others
*
* Licensed under the Apache License, Version 2.0.
* you may not use this file except in compliance with the License.
*/
import { observer } from 'mobx-react-lite';
import type { PropsWithChildren } from 'react';
import { ContextMenu } from '@cloudbeaver/core-ui';
import type { ITreeMenu } from '../../useTreeMenu.js';
import { TreeMenuContext } from './TreeMenuContext.js';
interface Props {
menu: ITreeMenu | null;
}
export const TreeMenuContextProvider = observer<PropsWithChildren<Props>>(function TreeMenuContextProvider({ menu, children }) {
return (
<TreeMenuContext value={menu}>
{children}
{menu && (
<div className="tw:absolute tw:invisible">
<ContextMenu menu={menu.menu} contextMenuPosition={menu.position} modal />
</div>
)}
</TreeMenuContext>
);
});
@@ -0,0 +1,52 @@
/*
* CloudBeaver - Cloud Database Manager
* Copyright (C) 2020-2025 DBeaver Corp and others
*
* Licensed under the Apache License, Version 2.0.
* you may not use this file except in compliance with the License.
*/
import { action } from 'mobx';
import { useId } from 'react';
import { useContextMenuPosition, useObjectRef, useObservableRef, type IContextMenuPosition } from '@cloudbeaver/core-blocks';
import type { IDataContext } from '@cloudbeaver/core-data-context';
import { DATA_CONTEXT_NAV_NODE_ID } from '@cloudbeaver/core-navigation-tree';
import type { IMenuData } from '@cloudbeaver/core-view';
export interface ITreeMenu {
menu: IMenuData;
position: IContextMenuPosition;
openMenu(event: React.MouseEvent, nodeId: string): void;
}
interface ITreeMenuOptions {
menu: IMenuData;
setContext?: (context: IDataContext, id: string) => void;
}
export function useTreeMenu(options: ITreeMenuOptions): Readonly<ITreeMenu> {
options = useObjectRef(options);
const id = useId();
const position = useContextMenuPosition();
const state = useObservableRef(
() => ({
openMenu(event: React.MouseEvent, nodeId: string) {
this.menu.context.deleteForId(this.id);
this.menu.context.set(DATA_CONTEXT_NAV_NODE_ID, nodeId, this.id);
if (options.setContext) {
options.setContext(this.menu.context, this.id);
}
this.position.handleContextMenuOpen(event);
},
}),
{ openMenu: action.bound },
{ id, menu: options.menu, position },
);
return state;
}
@@ -66,6 +66,8 @@ export * from './NodesManager/NavNodeContextMenuService.js';
export * from './NodesManager/useDatabaseObjectInfo.js';
export * from './NodesManager/useNode.js';
export * from './NodesManager/useChildren.js';
export * from './TreeNew/contexts/TreeMenuContext/TreeMenuContext.js';
export * from './TreeNew/useTreeMenu.js';
export * from './NodesManager/NodeLinkLoader.js';