CB-3864 migrate tree to css modules (#2003)

* CB-3864 migrate tree to css modules

* CB-3864 migrate extra components

* CB-3864 review fixes

* CB-3864 move editing and dragging props from base nav components

* CB-3864 add styles to renderer

* CB-3864 fix aws styles

* CB-3864 remove padding in projects renderer

* CB-3864 fix node hover effect

* CB-3864 remove custom tags

* CB-3864 remove config changes

* CB-3864 review fixes

* CB-3864 move NavigationNodeControl styles closer to the component

* CB-3864 chore: code cleanup

---------

Co-authored-by: Alexey <wrouds@gmail.com>
Co-authored-by: EvgeniaBzzz <139753579+EvgeniaBzzz@users.noreply.github.com>
Co-authored-by: mr-anton-t <42037741+mr-anton-t@users.noreply.github.com>
This commit is contained in:
alex
2023-09-28 02:02:49 +03:00
committed by GitHub
co-authored by Alexey EvgeniaBzzz mr-anton-t
parent a2d9fddc5e
commit 571d6b7c0a
68 changed files with 839 additions and 859 deletions
@@ -0,0 +1,10 @@
.actionIconButton {
composes: theme-form-element-radius theme-ripple from global;
padding: 4px !important;
margin: 2px !important;
width: 24px !important;
height: 24px !important;
overflow: hidden;
flex-shrink: 0;
}
@@ -0,0 +1,16 @@
.status {
composes: theme-background-positive from global;
position: absolute;
opacity: 0;
transition: opacity 0.3s ease;
bottom: 0;
right: 0;
box-sizing: border-box;
width: 8px;
height: 8px;
border-radius: 50%;
&.connected {
opacity: 1;
}
}
@@ -5,32 +5,18 @@
* Licensed under the Apache License, Version 2.0.
* you may not use this file except in compliance with the License.
*/
import styled, { css, use } from 'reshadow';
import { observer } from 'mobx-react-lite';
const styles = css`
status {
composes: theme-background-positive from global;
position: absolute;
opacity: 0;
transition: opacity 0.3s ease;
bottom: 0;
right: 0;
box-sizing: border-box;
width: 8px;
height: 8px;
border-radius: 50%;
&[|connected] {
opacity: 1;
}
}
`;
import { s } from '../s';
import { useS } from '../useS';
import style from './ConnectionMark.m.css';
interface Props {
connected: boolean;
className?: string;
}
export const ConnectionMark: React.FC<Props> = function ConnectionMark({ connected, className }) {
return styled(styles)(<status {...use({ connected })} className={className} />);
};
export const ConnectionMark: React.FC<Props> = observer(function ConnectionMark({ connected, className }) {
const styles = useS(style);
return <div className={s(styles, { status: true, connected }, className)} />;
});
@@ -14,4 +14,4 @@
width: 100%;
height: 100%;
}
}
}
@@ -0,0 +1,5 @@
.node {
box-sizing: border-box;
width: inherit;
min-width: 100%;
}
@@ -8,14 +8,14 @@
import { computed, observable } from 'mobx';
import { observer } from 'mobx-react-lite';
import { forwardRef } from 'react';
import styled, { use } from 'reshadow';
import { getComputed } from '../../getComputed';
import { s } from '../../s';
import { useObjectRef } from '../../useObjectRef';
import { useObservableRef } from '../../useObservableRef';
import { useS } from '../../useS';
import type { ITreeNodeState } from './ITreeNodeState';
import style from './TreeNode.m.css';
import { ITreeNodeContext, TreeNodeContext } from './TreeNodeContext';
import { TREE_NODE_STYLES } from './TreeNodeStyles';
interface Props extends ITreeNodeState {
className?: string;
@@ -48,6 +48,7 @@ export const TreeNode = observer<Props, HTMLDivElement | null>(
},
ref,
) {
const styles = useS(style);
const handlersRef = useObjectRef(handlers);
async function processAction(action: () => Promise<void>) {
@@ -113,12 +114,10 @@ export const TreeNode = observer<Props, HTMLDivElement | null>(
},
);
const elementExpanded = getComputed(() => nodeContext.externalExpanded ?? nodeContext.expanded);
return styled(TREE_NODE_STYLES)(
<node {...use({ expanded: elementExpanded })} ref={ref} className={className}>
return (
<div ref={ref} className={s(styles, { node: true }, className)}>
<TreeNodeContext.Provider value={nodeContext}>{children}</TreeNodeContext.Provider>
</node>,
</div>
);
}),
);
@@ -0,0 +1,24 @@
.treeNodeControl {
composes: theme-ripple theme-ripple-selectable from global;
box-sizing: border-box;
height: 22px;
display: flex;
align-items: center;
padding: 2px 0 2px 0;
user-select: none;
white-space: nowrap;
position: initial !important;
outline: none;
&::before {
left: 0 !important;
top: auto !important;
height: inherit !important;
width: 100% !important;
}
& > * {
margin-right: 0;
margin-left: 4px;
}
}
@@ -10,11 +10,14 @@ import React, { forwardRef, useContext } from 'react';
import { EventContext, EventStopPropagationFlag } from '@cloudbeaver/core-events';
import { s } from '../../s';
import { useS } from '../../useS';
import { EventTreeNodeClickFlag } from './EventTreeNodeClickFlag';
import { EventTreeNodeExpandFlag } from './EventTreeNodeExpandFlag';
import { EventTreeNodeSelectFlag } from './EventTreeNodeSelectFlag';
import type { ITreeNodeState } from './ITreeNodeState';
import { TreeNodeContext } from './TreeNodeContext';
import style from './TreeNodeControl.m.css';
const KEY = {
ENTER: 'Enter',
@@ -25,15 +28,15 @@ interface Props extends ITreeNodeState {
onClick?: (event: React.MouseEvent<HTMLDivElement>) => void;
onMouseDown?: (event: React.MouseEvent<HTMLDivElement>) => void;
className?: string;
big?: boolean;
children?: React.ReactNode;
}
export const TreeNodeControl = observer<Props & React.HTMLAttributes<HTMLDivElement>, HTMLDivElement>(
forwardRef(function TreeNodeControl(
{ title, group, disabled, loading, selected, expanded, externalExpanded, leaf, onClick, onMouseDown, className, children, big, ...rest },
{ title, group, disabled, loading, selected, expanded, externalExpanded, leaf, onClick, onMouseDown, className, children, ...rest },
ref,
) {
const styles = useS(style);
const context = useContext(TreeNodeContext);
if (!context) {
@@ -112,7 +115,7 @@ export const TreeNodeControl = observer<Props & React.HTMLAttributes<HTMLDivElem
tabIndex={0}
title={title}
aria-selected={context.selected}
className={className}
className={s(styles, { treeNodeControl: true }, className)}
onClick={handleClick}
onMouseDown={handleMouseDown}
onKeyDown={handleEnter}
@@ -0,0 +1,23 @@
.treeNodeExpand {
position: relative;
display: flex;
box-sizing: border-box;
flex-shrink: 0;
opacity: 0.5;
width: 16px;
height: 16px;
transform: rotate(-90deg);
&.small {
display: block;
}
&.expanded {
transform: rotate(0deg);
}
}
.icon {
cursor: pointer;
height: 100%;
width: 100%;
}
@@ -7,24 +7,18 @@
*/
import { observer } from 'mobx-react-lite';
import { useContext } from 'react';
import styled, { css } from 'reshadow';
import { EventContext } from '@cloudbeaver/core-events';
import { getComputed } from '../../getComputed';
import { Icon } from '../../Icon';
import { Loader } from '../../Loader/Loader';
import { s } from '../../s';
import { useS } from '../../useS';
import { useStateDelay } from '../../useStateDelay';
import { EventTreeNodeExpandFlag } from './EventTreeNodeExpandFlag';
import { TreeNodeContext } from './TreeNodeContext';
const styles = css`
Icon {
cursor: pointer;
height: 100%;
width: 100%;
}
`;
import style from './TreeNodeExpand.m.css';
interface Props {
leaf?: boolean;
@@ -35,6 +29,7 @@ interface Props {
}
export const TreeNodeExpand = observer<Props>(function TreeNodeExpand({ leaf, big, filterActive, disabled, className }) {
const styles = useS(style);
const context = useContext(TreeNodeContext);
if (!context) {
@@ -90,10 +85,14 @@ export const TreeNodeExpand = observer<Props>(function TreeNodeExpand({ leaf, bi
}
}
return styled(styles)(
<arrow className={className} onClick={handleExpand} onDoubleClick={handleDbClick}>
return (
<div
className={s(styles, { treeNodeExpand: true, expanded: context.expanded, big }, className)}
onClick={handleExpand}
onDoubleClick={handleDbClick}
>
{loading && <Loader small fullSize />}
{expandable && <Icon name={iconName} viewBox={viewBox} />}
</arrow>,
{expandable && <Icon name={iconName} className={s(styles, { icon: true })} viewBox={viewBox} />}
</div>
);
});
@@ -0,0 +1,12 @@
.treeNodeIcon {
position: relative;
box-sizing: border-box;
flex-shrink: 0;
width: 16px;
height: 16px;
}
.staticImage {
position: relative;
height: 100%;
}
@@ -5,30 +5,25 @@
* Licensed under the Apache License, Version 2.0.
* you may not use this file except in compliance with the License.
*/
import styled, { css } from 'reshadow';
import type { ComponentStyle } from '@cloudbeaver/core-theming';
import { observer } from 'mobx-react-lite';
import { s } from '../../s';
import { StaticImage } from '../../StaticImage';
import { useStyles } from '../../useStyles';
const styles = css`
StaticImage {
height: 100%;
}
`;
import { useS } from '../../useS';
import style from './TreeNodeIcon.m.css';
interface Props {
icon?: string;
style?: ComponentStyle;
className?: string;
}
export const TreeNodeIcon: React.FC<React.PropsWithChildren<Props>> = function TreeNodeIcon({ icon, style, className, children }) {
return styled(useStyles(styles, style))(
<icon className={className}>
{icon && <StaticImage icon={icon} />}
export const TreeNodeIcon: React.FC<React.PropsWithChildren<Props>> = observer(function TreeNodeIcon({ icon, className, children }) {
const styles = useS(style);
return (
<div className={s(styles, { treeNodeIcon: true }, className)}>
{icon && <StaticImage className={s(styles, { staticImage: true })} icon={icon} />}
{children}
</icon>,
</div>
);
};
});
@@ -0,0 +1,8 @@
.treeNodeName {
position: relative;
box-sizing: border-box;
padding-right: 0;
display: flex;
align-items: center;
margin-right: 4px;
}
@@ -5,15 +5,22 @@
* 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 { s } from '../../s';
import { useS } from '../../useS';
import style from './TreeNodeName.m.css';
interface Props extends React.HTMLAttributes<HTMLDivElement> {
className?: string;
}
export const TreeNodeName: React.FC<Props> = function TreeNodeName({ className, children, ...rest }) {
export const TreeNodeName: React.FC<Props> = observer(function TreeNodeName({ className, children, ...rest }) {
const styles = useS(style);
return (
<div className={className} {...rest}>
<div className={s(styles, { treeNodeName: true }, className)} {...rest}>
{children}
</div>
);
};
});
@@ -0,0 +1,10 @@
.treeNodeNested {
box-sizing: border-box;
padding-left: 8px;
display: none;
&.root {
padding: 0;
display: block;
}
}
@@ -5,18 +5,27 @@
* 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 { forwardRef } from 'react';
import { s } from '../../s';
import { useS } from '../../useS';
import style from './TreeNodeNested.m.css';
interface Props extends React.PropsWithChildren {
expanded?: boolean;
root?: boolean;
className?: string;
}
export const TreeNodeNested = forwardRef<HTMLDivElement, Props>(function TreeNodeNested({ className, children }, ref) {
return (
<div ref={ref} className={className}>
{children}
</div>
);
});
export const TreeNodeNested = observer(
forwardRef<HTMLDivElement, Props>(function TreeNodeNested({ root, className, children }, ref) {
const styles = useS(style);
return (
<div ref={ref} className={s(styles, { treeNodeNested: true, root }, className)}>
{children}
</div>
);
}),
);
@@ -0,0 +1,4 @@
.treeNodeNestedMessage {
composes: theme-typography--caption from global;
padding: 4px 12px;
}
@@ -5,12 +5,17 @@
* 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 { s } from '../../s';
import { useS } from '../../useS';
import style from './TreeNodeNestedMessage.m.css';
interface Props {
big?: boolean;
className?: string;
}
export const TreeNodeNestedMessage: React.FC<React.PropsWithChildren<Props>> = function TreeNodeNestedMessage({ className, children }) {
return <div className={className}>{children}</div>;
};
export const TreeNodeNestedMessage: React.FC<React.PropsWithChildren<Props>> = observer(function TreeNodeNestedMessage({ className, children }) {
const styles = useS(style);
return <div className={s(styles, { treeNodeNestedMessage: true }, className)}>{children}</div>;
});
@@ -0,0 +1,4 @@
.loader {
width: 40px;
height: 40px;
}
@@ -7,21 +7,16 @@
*/
import { observer } from 'mobx-react-lite';
import { useContext } from 'react';
import styled, { css } from 'reshadow';
import { EventContext } from '@cloudbeaver/core-events';
import { Checkbox } from '../../FormControls/Checkboxes/Checkbox';
import { Loader } from '../../Loader/Loader';
import { s } from '../../s';
import { useS } from '../../useS';
import { EventTreeNodeSelectFlag } from './EventTreeNodeSelectFlag';
import { TreeNodeContext } from './TreeNodeContext';
const styles = css`
Loader {
width: 40px;
height: 40px;
}
`;
import style from './TreeNodeSelect.m.css';
interface Props {
group?: boolean;
@@ -42,6 +37,7 @@ export const TreeNodeSelect = observer<Props>(function TreeNodeSelect({
loadIndicator,
className,
}) {
const styles = useS(style);
const context = useContext(TreeNodeContext);
if (!context) {
@@ -67,9 +63,13 @@ export const TreeNodeSelect = observer<Props>(function TreeNodeSelect({
EventContext.set(event, EventTreeNodeSelectFlag);
}
return styled(styles)(
return (
<div className={className} onClick={handleClick} onDoubleClick={handleDbClick}>
{loading ? <Loader small /> : <Checkbox checked={selected} indeterminate={indeterminate} disabled={disabled} onChange={handleSelect} />}
</div>,
{loading ? (
<Loader className={s(styles, { loader: true })} small />
) : (
<Checkbox checked={selected} indeterminate={indeterminate} disabled={disabled} onChange={handleSelect} />
)}
</div>
);
});
@@ -1,188 +0,0 @@
/*
* CloudBeaver - Cloud Database Manager
* Copyright (C) 2020-2023 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 { css } from 'reshadow';
export const TREE_NODE_STYLES = css`
TreeNodeControl {
composes: theme-ripple theme-ripple-selectable from global;
}
TreeNodeNestedMessage {
composes: theme-text-text-hint-on-light from global;
}
node {
box-sizing: border-box;
width: inherit;
min-width: 100%;
&[use|expanded] {
& > TreeNodeNested {
display: block;
}
& > TreeNodeControl > TreeNodeExpand {
transform: rotate(0deg);
}
}
& TreeNodeControl {
box-sizing: border-box;
height: 22px;
display: flex;
align-items: center;
padding: 2px 0 2px 0;
user-select: none;
white-space: nowrap;
position: initial;
outline: none;
&::before {
left: 0;
top: auto;
height: inherit;
width: 100%;
}
&[|dragging] {
opacity: 0.6;
}
&[|editing]::before {
display: none;
}
& > * {
margin-right: 0;
margin-left: 4px;
}
& TreeNodeName {
margin-right: 4px;
}
}
}
TreeNodeExpand {
display: flex;
box-sizing: border-box;
flex-shrink: 0;
opacity: 0.5;
width: 16px;
height: 16px;
transform: rotate(-90deg);
& [|size='small'] {
display: block;
}
& [|size='big'] {
display: none;
}
}
NavigationNode[dragging] {
opacity: 0.5 !important;
}
TreeNodeIcon {
box-sizing: border-box;
flex-shrink: 0;
width: 16px;
height: 16px;
}
TreeNodeName {
box-sizing: border-box;
padding-right: 0;
display: flex;
align-items: center;
margin-right: 4px;
}
TreeNodeFilter {
position: relative;
min-width: 24px;
min-height: 24px;
display: flex;
align-items: center;
}
TreeNodeIcon,
TreeNodeExpand,
TreeNodeName {
position: relative;
}
TreeNodeNested {
box-sizing: border-box;
padding-left: 8px;
display: none;
&[root] {
padding: 0;
display: block;
}
&[expanded] {
display: block;
}
}
TreeNodeNestedMessage {
composes: theme-typography--caption from global;
padding: 4px 12px;
&[big] {
padding: 4px 24px;
}
}
tree-box[|big] {
& TreeNodeNestedMessage {
padding: 4px 24px;
}
}
node TreeNodeControl[big] {
height: 46px;
padding: 0px 16px;
& TreeNodeName {
margin: 0;
padding-right: 8px;
padding-left: 8px;
}
& TreeNodeIcon {
width: 24px;
height: 24px;
}
& TreeNodeExpand {
width: 16px;
height: 16px;
}
& TreeNodeExpand [|size='small'] {
display: none;
}
& TreeNodeExpand [|size='big'] {
display: block;
}
& > * {
margin-right: 8px;
margin-left: 8px;
}
& TreeNodeSelect {
margin: 0;
}
}
node TreeNodeControl[big] + TreeNodeNested {
padding-left: 24px;
}
`;
+3 -1
View File
@@ -25,6 +25,7 @@ export * from './ConnectionImageWithMask/ConnectionImageWithMask';
export { default as ConnectionImageWithMaskSvgStyles } from './ConnectionImageWithMask/ConnectionImageWithMaskSvg.m.css';
export * from './Menu/ACTION_ICON_BUTTON_STYLES';
export { default as ActionIconButtonStyles } from './ActionIconButton.m.css';
export * from './Menu/Menu';
export { default as MenuStyles } from './Menu/Menu.m.css';
export * from './Menu/MenuBarSmallItem';
@@ -106,12 +107,13 @@ export * from './Tree/TreeNode/TreeNodeName';
export * from './Tree/TreeNode/TreeNodeNested';
export * from './Tree/TreeNode/TreeNodeNestedMessage';
export * from './Tree/TreeNode/TreeNodeSelect';
export * from './Tree/TreeNode/TreeNodeStyles';
export * from './Button';
export * from './ToolsPanel/ToolsAction';
export * from './ToolsPanel/ToolsPanel';
export { default as ToolsPanelStyles } from './ToolsPanel/ToolsPanel.m.css';
export { default as ToolsActionStyles } from './ToolsPanel/ToolsAction.m.css';
export { default as TreeNodeNestedMessageStyles } from './Tree/TreeNode/TreeNodeNestedMessage.m.css';
export { default as TreeNodeStyles } from './Tree/TreeNode/TreeNode.m.css';
export * from './FormControls/Checkboxes/Checkbox';
export * from './FormControls/Checkboxes/FieldCheckbox';
export * from './FormControls/Checkboxes/CheckboxMarkup';
@@ -242,6 +242,7 @@ module.exports = (env, argv) => {
plugins: [
new ForkTsCheckerWebpackPlugin({
typescript: {
typescriptPath: require.resolve('typescript'),
configFile: resolve('tsconfig.json'),
configOverwrite: {
include: ['**/src/**/*.ts', '**/src/**/*.tsx'],
@@ -19,13 +19,13 @@ function DisplayErrorInfo({ error }: { error: IErrorInfo }) {
return (
<>
<property className={s(styles, { property: true })}>
{error.isHtml ? <Iframe srcDoc={error.message} /> : <message className={s(styles, { message: true })}>{error.message}</message>}
</property>
<div className={s(styles, { property: true })}>
{error.isHtml ? <Iframe srcDoc={error.message} /> : <div className={s(styles, { message: true })}>{error.message}</div>}
</div>
{error.stackTrace && (
<property className={s(styles, { property: true })}>
<div className={s(styles, { property: true })}>
<Textarea className={s(styles, { textarea: true })} value={error.stackTrace} readOnly embedded />
</property>
</div>
)}
</>
);
@@ -6,9 +6,8 @@
* you may not use this file except in compliance with the License.
*/
import { observer } from 'mobx-react-lite';
import styled from 'reshadow';
import { ACTION_ICON_BUTTON_STYLES, IconButton, s, TableColumnValue, TableItem, useS } from '@cloudbeaver/core-blocks';
import { ActionIconButtonStyles, IconButton, s, TableColumnValue, TableItem, useS } from '@cloudbeaver/core-blocks';
import styles from './FiltersTableItem.m.css';
@@ -21,14 +20,14 @@ interface Props {
}
export const FiltersTableItem = observer<Props>(function FiltersTableItem({ id, name, disabled, className, onDelete }) {
const style = useS(styles);
const style = useS(ActionIconButtonStyles, styles);
return styled(ACTION_ICON_BUTTON_STYLES)(
return (
<TableItem className={s(style, { tableItem: true }, className)} item={id} title={name} disabled={disabled} selectDisabled={disabled}>
<TableColumnValue>{name}</TableColumnValue>
<TableColumnValue className={s(style, { deleteColumnCell: true })} flex>
<IconButton name="cross-bold" onClick={() => onDelete(id)} />
<IconButton className={s(ActionIconButtonStyles, { actionIconButton: true })} name="cross-bold" onClick={() => onDelete(id)} />
</TableColumnValue>
</TableItem>,
</TableItem>
);
});
@@ -0,0 +1,35 @@
.treeNodeControl {
transition: opacity 0.3s ease;
opacity: 1;
&.outdated {
opacity: 0.5;
}
}
.treeNodeControl:hover > .portal,
.treeNodeControl[aria-selected='true'] > .portal,
.portal:focus-within {
visibility: visible;
}
.treeNodeName {
composes: theme-text-text-hint-on-light theme-typography--caption from global;
height: 100%;
max-width: 250px;
overflow: hidden;
text-overflow: ellipsis;
}
.portal {
position: relative;
box-sizing: border-box;
margin-left: auto !important;
margin-right: 8px !important;
visibility: hidden;
}
.nameBox {
overflow: hidden;
text-overflow: ellipsis;
}
@@ -9,7 +9,7 @@ import { observer } from 'mobx-react-lite';
import React, { forwardRef, useContext } from 'react';
import styled, { css, use } from 'reshadow';
import { getComputed, TREE_NODE_STYLES, TreeNodeContext, TreeNodeControl, TreeNodeName, useMouseContextMenu } from '@cloudbeaver/core-blocks';
import { getComputed, s, TreeNodeContext, TreeNodeControl, TreeNodeName, useMouseContextMenu, useS } from '@cloudbeaver/core-blocks';
import { useService } from '@cloudbeaver/core-di';
import { EventContext, EventStopPropagationFlag } from '@cloudbeaver/core-events';
import { NavNodeInfoResource } from '@cloudbeaver/core-navigation-tree';
@@ -23,47 +23,15 @@ import {
NavTreeControlProps,
TreeNodeMenuLoader,
} from '@cloudbeaver/plugin-navigation-tree';
import { ResourceManagerService } from '@cloudbeaver/plugin-resource-manager';
import { getRmProjectNodeId } from '../../NavNodes/getRmProjectNodeId';
import { DATA_CONTEXT_RESOURCE_MANAGER_TREE_RESOURCE_TYPE_ID } from '../DATA_CONTEXT_RESOURCE_MANAGER_TREE_RESOURCE_TYPE_ID';
import { ResourceManagerService } from '@cloudbeaver/plugin-resource-manager';
const styles = css`
TreeNodeControl {
transition: opacity 0.3s ease;
opacity: 1;
&[|outdated] {
opacity: 0.5;
}
}
TreeNodeControl:hover > portal,
TreeNodeControl:global([aria-selected='true']) > portal,
portal:focus-within {
visibility: visible;
}
TreeNodeName {
composes: theme-text-text-hint-on-light theme-typography--caption from global;
height: 100%;
max-width: 250px;
overflow: hidden;
text-overflow: ellipsis;
}
portal {
position: relative;
box-sizing: border-box;
margin-left: auto !important;
margin-right: 8px !important;
visibility: hidden;
}
name-box {
overflow: hidden;
text-overflow: ellipsis;
}
`;
import style from './NavigationNodeProjectControl.m.css';
export const NavigationNodeProjectControl: NavTreeControlComponent = observer<NavTreeControlProps, HTMLDivElement>(
forwardRef(function NavigationNodeProjectControl({ node, dndElement, dndPlaceholder, className }, ref) {
const styles = useS(style);
const mouseContextMenu = useMouseContextMenu();
const viewContext = useContext(CaptureViewContext);
const elementsTreeContext = useContext(ElementsTreeContext);
@@ -125,26 +93,22 @@ export const NavigationNodeProjectControl: NavTreeControlComponent = observer<Na
return null;
}
return styled(
TREE_NODE_STYLES,
styles,
)(
return (
<TreeNodeControl
ref={ref}
onClick={handleClick}
onContextMenu={handleContextMenuOpen}
{...use({ outdated, dragging: dndElement })}
className={className}
className={s(styles, { treeNodeControl: true, outdated, }, className)}
>
<TreeNodeName title={name}>
<name-box>{name}</name-box>
<TreeNodeName title={name} className={s(styles, { treeNodeName: true })}>
<div className={s(styles, { nameBox: true })}>{name}</div>
</TreeNodeName>
{!dndPlaceholder && (
<portal onClick={handlePortalClick}>
<div onClick={handlePortalClick} className={s(styles, { portal: true })}>
<TreeNodeMenuLoader mouseContextMenu={mouseContextMenu} node={node} selected={selected} />
</portal>
</div>
)}
</TreeNodeControl>,
</TreeNodeControl>
);
}),
);
@@ -0,0 +1,12 @@
.projectNode {
margin-top: 8px;
&.project:only-child,
&.hideProjects {
margin-top: 0px;
}
& > .navigationNodeNested {
padding-left: 0 !important;
}
}
@@ -7,9 +7,8 @@
*/
import { observer } from 'mobx-react-lite';
import { useContext } from 'react';
import styled, { css, use } from 'reshadow';
import { getComputed, Translate, TREE_NODE_STYLES, TreeNodeNestedMessage } from '@cloudbeaver/core-blocks';
import { getComputed, s, SContext, StyleRegistry, Translate, TreeNodeNestedMessage, useS } from '@cloudbeaver/core-blocks';
import { useService } from '@cloudbeaver/core-di';
import type { NavNodeInfoResource, ProjectsNavNodeService } from '@cloudbeaver/core-navigation-tree';
import { ProjectsService } from '@cloudbeaver/core-projects';
@@ -19,32 +18,33 @@ import {
ElementsTreeContext,
type IElementsTreeCustomRenderer,
isDraggingInsideProject,
NavigationNodeControlRendererStyles,
NavigationNodeNestedStyles,
type NavigationNodeRendererComponent,
NavigationNodeRendererLoader,
useNode,
} from '@cloudbeaver/plugin-navigation-tree';
import { NavigationNodeProjectControl } from './NavigationNodeProjectControl';
import type { ResourceManagerService } from '@cloudbeaver/plugin-resource-manager';
const nestedStyles = css`
TreeNode {
margin-top: 8px;
import { NavigationNodeProjectControl } from './NavigationNodeProjectControl';
import style from './NavigationTreeProjectsRendererRenderer.m.css';
&[|project]:only-child,
&[|hideProjects] {
margin-top: 0px;
& Control {
display: none;
}
}
& NavigationNodeNested {
padding-left: 0 !important;
}
}
`;
const registry: StyleRegistry = [
[
NavigationNodeNestedStyles,
{
mode: 'append',
styles: [style],
},
],
[
NavigationNodeControlRendererStyles,
{
mode: 'append',
styles: [style],
},
],
];
export function navigationTreeProjectsRendererRenderer(
navNodeInfoResource: NavNodeInfoResource,
@@ -88,6 +88,7 @@ const ProjectRenderer: NavigationNodeRendererComponent = observer(function Manag
className,
expanded,
}) {
const styles = useS(style);
const projectsService = useService(ProjectsService);
const elementsTreeContext = useContext(ElementsTreeContext);
@@ -105,26 +106,26 @@ const ProjectRenderer: NavigationNodeRendererComponent = observer(function Manag
const singleProject = projectsService.activeProjects.length === 1;
if (!node) {
return styled(TREE_NODE_STYLES)(
return (
<TreeNodeNestedMessage>
<Translate token="app_navigationTree_node_not_found" />
</TreeNodeNestedMessage>,
</TreeNodeNestedMessage>
);
}
const project = node.nodeType === NAV_NODE_TYPE_RM_PROJECT && singleProject && !isDragging;
return styled(nestedStyles)(
<NavigationNodeRendererLoader
node={node}
path={path}
expanded={expanded}
dragging={dragging}
className={className}
control={NavigationNodeProjectControl}
style={nestedStyles}
component={component}
{...use({ hideProjects, project })}
/>,
return (
<SContext registry={registry}>
<NavigationNodeRendererLoader
node={node}
path={path}
expanded={expanded}
dragging={dragging}
className={s(styles, { projectNode: true, hideProjects, project }, className)}
control={NavigationNodeProjectControl}
component={component}
/>
</SContext>
);
});
@@ -0,0 +1,29 @@
.captureView {
outline: none;
display: flex;
flex: 1;
flex-direction: column;
overflow: auto;
}
.elementsTreeLoader {
min-width: 100%;
width: max-content;
}
.loader {
display: flex;
height: 100%;
width: 100%;
min-width: 240px;
}
.center {
height: 100%;
width: 100%;
display: flex;
}
.message {
padding: 24px;
box-sizing: border-box;
max-width: 240px;
text-align: center;
margin: auto;
}
@@ -7,9 +7,8 @@
*/
import { observer } from 'mobx-react-lite';
import { useMemo } from 'react';
import styled, { css } from 'reshadow';
import { getComputed, useResource, useUserData } from '@cloudbeaver/core-blocks';
import { getComputed, s, useResource, useS, useUserData } from '@cloudbeaver/core-blocks';
import { useService } from '@cloudbeaver/core-di';
import { NavNodeInfoResource, NavTreeResource, ProjectsNavNodeService } from '@cloudbeaver/core-navigation-tree';
import { ProjectInfoResource, ProjectsService } from '@cloudbeaver/core-projects';
@@ -25,6 +24,7 @@ import {
NavigationTreeService,
validateElementsTreeSettings,
} from '@cloudbeaver/plugin-navigation-tree';
import { ResourceManagerService } from '@cloudbeaver/plugin-resource-manager';
import { navigationTreeProjectFilter } from './ProjectsRenderer/navigationTreeProjectFilter';
import { navigationTreeProjectSearchCompare } from './ProjectsRenderer/navigationTreeProjectSearchCompare';
@@ -33,47 +33,16 @@ import { navigationTreeProjectsRendererRenderer } from './ProjectsRenderer/navig
import { navigationTreeResourceTypeFilter } from './ProjectsRenderer/navigationTreeResourceTypeFilter';
import { ProjectsSettingsPlaceholderElement } from './ProjectsRenderer/ProjectsSettingsForm';
import { navigationTreeResourceExpandStateGetter } from './ResourceFolderRenderer/navigationTreeResourceExpandStateGetter';
import style from './ResourceManagerTree.m.css';
import { ResourceManagerTreeCaptureViewContext } from './ResourceManagerTreeCaptureViewContext';
import { transformResourceNodeInfo } from './ResourceRenderer/transformResourceNodeInfo';
import { ResourceManagerService } from '@cloudbeaver/plugin-resource-manager';
const styles = css`
CaptureView {
outline: none;
display: flex;
flex: 1;
flex-direction: column;
overflow: auto;
}
ElementsTreeLoader {
min-width: 100%;
width: max-content;
}
Loader {
display: flex;
height: 100%;
width: 100%;
min-width: 240px;
}
center {
height: 100%;
width: 100%;
display: flex;
}
message {
padding: 24px;
box-sizing: border-box;
max-width: 240px;
text-align: center;
margin: auto;
}
`;
interface Props extends React.PropsWithChildren {
resourceTypeId?: string;
}
export const ResourceManagerTree: React.FC<Props> = observer(function ResourceManagerTree({ resourceTypeId, children }) {
const styles = useS(style);
const root = RESOURCES_NODE_PATH;
const projectsNavNodeService = useService(ProjectsNavNodeService);
@@ -131,8 +100,8 @@ export const ResourceManagerTree: React.FC<Props> = observer(function ResourceMa
const settingsElements = useMemo(() => [ProjectsSettingsPlaceholderElement], []);
return styled(styles)(
<CaptureView view={navTreeService}>
return (
<CaptureView view={navTreeService} className={s(styles, { captureView: true })}>
<ResourceManagerTreeCaptureViewContext resourceTypeId={resourceTypeId} />
<ElementsTreeLoader
root={root}
@@ -145,15 +114,14 @@ export const ResourceManagerTree: React.FC<Props> = observer(function ResourceMa
expandStateGetters={[projectsExpandStateGetter, resourceExpandStateGetter]}
navNodeFilterCompare={navigationTreeProjectSearchCompare}
settingsElements={settingsElements}
emptyPlaceholder={() =>
styled(styles)(
<center>
<message>{children}</message>
</center>,
)
}
className={s(styles, { elementsTreeLoader: true })}
emptyPlaceholder={() => (
<div className={s(styles, { center: true })}>
<div className={s(styles, { message: true })}>{children}</div>
</div>
)}
onOpen={node => navTreeService.navToNode(node.id, node.parentId)}
/>
</CaptureView>,
</CaptureView>
);
});
@@ -0,0 +1,8 @@
.connectionGroup {
composes: theme-text-text-hint-on-light theme-typography--caption from global;
padding: 4px 12px;
&:not(:first-child) {
margin-top: 8px;
}
}
@@ -5,12 +5,11 @@
* Licensed under the Apache License, Version 2.0.
* you may not use this file except in compliance with the License.
*/
import styled, { css } from 'reshadow';
import { Translate } from '@cloudbeaver/core-blocks';
import { s, Translate, useS } from '@cloudbeaver/core-blocks';
import type { NavigationNodeRendererComponent } from '../ElementsTree/NavigationNodeComponent';
import { NAVIGATION_TREE_CONNECTION_GROUPS } from './navigationTreeConnectionGroupFilter';
import style from './NavigationTreeConnectionGroupRenderer.m.css';
export function navigationTreeConnectionGroupRenderer(nodeId: string): NavigationNodeRendererComponent | undefined {
if (nodeId !== NAVIGATION_TREE_CONNECTION_GROUPS.manageable && nodeId !== NAVIGATION_TREE_CONNECTION_GROUPS.unmanageable) {
@@ -24,28 +23,19 @@ export function navigationTreeConnectionGroupRenderer(nodeId: string): Navigatio
return UnManageableGroup;
}
const styles = css`
connection-group {
composes: theme-text-text-hint-on-light theme-typography--caption from global;
padding: 4px 12px;
&:not(:first-child) {
margin-top: 8px;
}
}
`;
const ManageableGroup: NavigationNodeRendererComponent = function ManageableGroup() {
return styled(styles)(
<connection-group>
const styles = useS(style);
return (
<div className={s(styles, { connectionGroup: true })}>
<Translate token="app_navigationTree_connection_group_user" />
</connection-group>,
</div>
);
};
const UnManageableGroup: NavigationNodeRendererComponent = function UnManageableGroup() {
return styled(styles)(
<connection-group>
const styles = useS(style);
return (
<div className={s(styles, { connectionGroup: true })}>
<Translate token="app_navigationTree_connection_group_shared" />
</connection-group>,
</div>
);
};
@@ -7,30 +7,31 @@
*/
import { observer } from 'mobx-react-lite';
import React, { forwardRef, useContext } from 'react';
import styled, { use } from 'reshadow';
import {
ConnectionImageWithMask,
getComputed,
Loader,
TREE_NODE_STYLES,
s,
TreeNodeContext,
TreeNodeControl,
TreeNodeIcon,
TreeNodeName,
useMouseContextMenu,
useS,
} from '@cloudbeaver/core-blocks';
import { useService } from '@cloudbeaver/core-di';
import { EventContext, EventStopPropagationFlag } from '@cloudbeaver/core-events';
import { EObjectFeature, NavNodeInfoResource, NavTreeResource } from '@cloudbeaver/core-navigation-tree';
import type { NavTreeControlComponent, NavTreeControlProps } from '../ElementsTree/NavigationNodeComponent';
import { NAVIGATION_NODE_CONTROL_STYLES } from '../ElementsTree/NavigationTreeNode/NavigationNode/NAVIGATION_NODE_CONTROL_STYLES';
import style from '../ElementsTree/NavigationTreeNode/NavigationNode/NavigationNodeControl.m.css';
import { NavigationNodeExpand } from '../ElementsTree/NavigationTreeNode/NavigationNode/NavigationNodeExpand';
import { TreeNodeMenuLoader } from '../ElementsTree/NavigationTreeNode/TreeNodeMenu/TreeNodeMenuLoader';
export const ConnectionNavNodeControl: NavTreeControlComponent = observer<NavTreeControlProps, HTMLDivElement>(
forwardRef(function ConnectionNavNodeControl({ node, nodeInfo, dndElement, dndPlaceholder, className, onClick }, ref) {
const styles = useS(style);
const mouseContextMenu = useMouseContextMenu();
const treeNodeContext = useContext(TreeNodeContext);
const navNodeInfoResource = useService(NavNodeInfoResource);
@@ -58,26 +59,28 @@ export const ConnectionNavNodeControl: NavTreeControlComponent = observer<NavTre
treeNodeContext.select();
}
return styled(
TREE_NODE_STYLES,
NAVIGATION_NODE_CONTROL_STYLES,
)(
<TreeNodeControl ref={ref} className={className} onClick={onClick} onContextMenu={handleContextMenuOpen} {...use({ dragging: dndElement })}>
return (
<TreeNodeControl
ref={ref}
className={s(styles, { treeNodeControl: true, dragging: !!dndElement }, className)}
onClick={onClick}
onContextMenu={handleContextMenuOpen}
>
<NavigationNodeExpand nodeId={node.id} />
<TreeNodeIcon {...use({ connected })}>
<TreeNodeIcon>
<ConnectionImageWithMask icon={icon} connected={connected} maskId="tree-node-icon" />
</TreeNodeIcon>
<TreeNodeName title={title}>
<TreeNodeName title={title} className={s(styles, { treeNodeName: true })}>
<Loader suspense inline fullSize>
<name-box>{name}</name-box>
<div className={s(styles, { nameBox: true })}>{name}</div>
</Loader>
</TreeNodeName>
{!dndPlaceholder && (
<portal onClick={handlePortalClick}>
<div className={s(styles, { portal: true })} onClick={handlePortalClick}>
<TreeNodeMenuLoader mouseContextMenu={mouseContextMenu} node={node} selected={selected} />
</portal>
</div>
)}
</TreeNodeControl>,
</TreeNodeControl>
);
}),
);
@@ -6,9 +6,8 @@
* you may not use this file except in compliance with the License.
*/
import { observer } from 'mobx-react-lite';
import styled from 'reshadow';
import { Translate, TREE_NODE_STYLES, TreeNodeNestedMessage } from '@cloudbeaver/core-blocks';
import { Translate, TreeNodeNestedMessage } from '@cloudbeaver/core-blocks';
import { NAV_NODE_TYPE_CONNECTION } from '@cloudbeaver/core-connections';
import { useService } from '@cloudbeaver/core-di';
import { NavNodeInfoResource } from '@cloudbeaver/core-navigation-tree';
@@ -42,10 +41,10 @@ const ConnectionRenderer: NavigationNodeRendererComponent = observer(function Ma
const node = navNodeInfoResource.get(nodeId);
if (!node) {
return styled(TREE_NODE_STYLES)(
return (
<TreeNodeNestedMessage>
<Translate token="app_navigationTree_node_not_found" />
</TreeNodeNestedMessage>,
</TreeNodeNestedMessage>
);
}
@@ -1,3 +1,54 @@
.box {
display: flex;
flex-direction: column;
box-sizing: border-box;
}
.tree {
position: relative;
box-sizing: border-box;
display: flex;
flex: 1;
flex-direction: column;
& .treeElements {
position: relative;
box-sizing: border-box;
display: flex;
flex-direction: column;
flex: 1;
}
}
.treeBox {
flex: 1;
overflow: auto;
display: flex;
width: 250px;
min-width: 100%;
max-width: 100%;
}
.folderExplorerPath {
padding: 0 4px 8px 4px;
}
.dropOutside {
composes: theme-border-color-background from global;
border: dashed 2px;
border-radius: var(--theme-group-element-radius);
margin: 12px;
box-sizing: border-box;
position: relative;
&.bottom {
order: 2;
}
&:not(.showDropOutside) {
display: none;
}
&.active {
border-color: var(--theme-positive) !important;
}
}
@@ -7,8 +7,6 @@
*/
import { observer } from 'mobx-react-lite';
import { useCallback, useMemo, useState } from 'react';
import styled, { css, use } from 'reshadow';
import elementsTreeStyles from './ElementsTree.m.css';
import {
EventTreeNodeClickFlag,
@@ -19,18 +17,16 @@ import {
PlaceholderElement,
s,
Translate,
TREE_NODE_STYLES,
TreeNodeNested,
TreeNodeNestedMessage,
useS,
useStyles,
} from '@cloudbeaver/core-blocks';
import { useService } from '@cloudbeaver/core-di';
import { EventContext, EventStopPropagationFlag } from '@cloudbeaver/core-events';
import { EObjectFeature, type NavNode, NavNodeInfoResource, NavTreeResource, ROOT_NODE_PATH } from '@cloudbeaver/core-navigation-tree';
import type { ComponentStyle } from '@cloudbeaver/core-theming';
import { useNavTreeDropBox } from '../useNavTreeDropBox';
import style from './ElementsTree.m.css';
import { ElementsTreeContentLoader } from './ElementsTreeContentLoader';
import { ElementsTreeContext, IElementsTreeContext } from './ElementsTreeContext';
import { elementsTreeNameFilter } from './elementsTreeNameFilter';
@@ -46,66 +42,11 @@ import { useDropOutside } from './useDropOutside';
import { IElementsTreeOptions, useElementsTree } from './useElementsTree';
import { useElementsTreeFolderExplorer } from './useElementsTreeFolderExplorer';
const oldStyles = css`
box {
display: flex;
flex-direction: column;
box-sizing: border-box;
}
tree {
position: relative;
box-sizing: border-box;
display: flex;
flex: 1;
flex-direction: column;
& tree-elements {
position: relative;
box-sizing: border-box;
display: flex;
flex-direction: column;
flex: 1;
}
}
tree-box {
flex: 1;
overflow: auto;
display: flex;
width: 250px;
min-width: 100%;
max-width: 100%;
}
drop-outside {
composes: theme-border-color-background from global;
border: dashed 2px;
border-radius: var(--theme-group-element-radius);
margin: 12px;
box-sizing: border-box;
position: relative;
&[|bottom] {
order: 2;
}
&:not([|showDropOutside]) {
display: none;
}
&[|active] {
border-color: var(--theme-positive) !important;
}
}
`;
export interface ElementsTreeProps extends IElementsTreeOptions, React.PropsWithChildren {
root?: string;
selectionTree?: boolean;
control?: NavTreeControlComponent;
emptyPlaceholder?: React.FC;
big?: boolean;
style?: ComponentStyle;
className?: string;
settingsElements?: PlaceholderElement<IElementsTreeSettingsProps>[];
navNodeFilterCompare?: NavNodeFilterCompareFn;
@@ -127,8 +68,6 @@ export const ElementsTree = observer<ElementsTreeProps>(function ElementsTree({
renderers = [],
expandStateGetters,
settingsElements,
big,
style,
className,
getChildren,
loadChildren,
@@ -142,7 +81,7 @@ export const ElementsTree = observer<ElementsTreeProps>(function ElementsTree({
onSelect,
onFilter,
}) {
const styles = useS(elementsTreeStyles);
const styles = useS(style);
const navTreeResource = useService(NavTreeResource);
const navNodeInfoResource = useService(NavNodeInfoResource);
const [treeRootRef, setTreeRootRef] = useState<HTMLDivElement | null>(null);
@@ -217,22 +156,23 @@ export const ElementsTree = observer<ElementsTreeProps>(function ElementsTree({
tree.resetSelection();
}
return styled(useStyles(TREE_NODE_STYLES, oldStyles, style))(
return (
<>
<ElementsTreeTools tree={tree} settingsElements={settingsElements} style={style} />
<tree-box ref={setTreeRootRef} {...use({ big })}>
<ElementsTreeTools tree={tree} settingsElements={settingsElements} />
<div ref={setTreeRootRef} className={s(styles, { treeBox: true })}>
<ElementsTreeContext.Provider value={context}>
<box className={className}>
<div className={s(styles, { box: true }, className)}>
<FolderExplorer state={folderExplorer}>
<tree ref={dropOutside.mouse.reference} onClick={handleClick}>
<div ref={dropOutside.mouse.reference} className={s(styles, { tree: true })} onClick={handleClick}>
{settings?.showFolderExplorerPath && (
<FolderExplorerPath className={s(styles, { folderExplorerPath: true })} getName={getName} canSkip={canSkip} />
)}
<drop-outside
<div
ref={dndBox.setRef}
{...use({
className={s(styles, {
dropOutside: true,
showDropOutside: dropOutside.showDropOutside,
active: dropOutside.zoneActive,
active: !!dropOutside.zoneActive,
bottom: dropOutside.bottom,
})}
>
@@ -241,9 +181,9 @@ export const ElementsTree = observer<ElementsTreeProps>(function ElementsTree({
<Translate token="app_navigationTree_drop_here" />
</TreeNodeNestedMessage>
</TreeNodeNested>
</drop-outside>
</div>
<ElementsTreeContentLoader context={context} emptyPlaceholder={emptyPlaceholder} childrenState={tree}>
<tree-elements>
<div className={s(styles, { treeElements: true })}>
<NavigationNodeNested
ref={dropOutside.nestedRef}
nodeId={root}
@@ -251,13 +191,13 @@ export const ElementsTree = observer<ElementsTreeProps>(function ElementsTree({
path={folderExplorer.state.path}
root
/>
</tree-elements>
</div>
</ElementsTreeContentLoader>
</tree>
</div>
</FolderExplorer>
</box>
</div>
</ElementsTreeContext.Provider>
</tree-box>
</>,
</div>
</>
);
});
@@ -6,9 +6,8 @@
* you may not use this file except in compliance with the License.
*/
import { observer } from 'mobx-react-lite';
import styled from 'reshadow';
import { getComputed, Loader, Translate, TREE_NODE_STYLES, TreeNodeNestedMessage } from '@cloudbeaver/core-blocks';
import { getComputed, Loader, Translate, TreeNodeNestedMessage } from '@cloudbeaver/core-blocks';
import type { ILoadableState } from '@cloudbeaver/core-utils';
import type { IElementsTreeContext } from './ElementsTreeContext';
@@ -33,10 +32,10 @@ export const ElementsTreeContentLoader = observer<React.PropsWithChildren<Props>
if (loading) {
return <Loader />;
} else if (context.tree.filtering) {
return styled(TREE_NODE_STYLES)(
return (
<TreeNodeNestedMessage>
<Translate token="app_navigationTree_node_no_results" filter={context.tree.filter} />
</TreeNodeNestedMessage>,
</TreeNodeNestedMessage>
);
} else if (context.folderExplorer.root === context.folderExplorer.state.folder) {
return <>{Placeholder && <Placeholder />}</>;
@@ -0,0 +1,5 @@
.filterBox {
composes: theme-background-surface from global;
padding: 8px 12px;
flex: 0 0 auto;
}
@@ -6,20 +6,12 @@
* you may not use this file except in compliance with the License.
*/
import { observer } from 'mobx-react-lite';
import styled, { css } from 'reshadow';
import { Filter, useFocus, useStyles, useTranslate } from '@cloudbeaver/core-blocks';
import { Filter, s, useFocus, useS, useTranslate } from '@cloudbeaver/core-blocks';
import type { ComponentStyle } from '@cloudbeaver/core-theming';
import type { IElementsTree } from '../useElementsTree';
const filterStyle = css`
filter-box {
composes: theme-background-surface from global;
padding: 8px 12px;
flex: 0 0 auto;
}
`;
import styles from './ElementsTreeFilter.m.css';
interface Props {
tree: IElementsTree;
@@ -30,15 +22,15 @@ interface Props {
export const ElementsTreeFilter = observer<Props>(function ElementsTreeFilter({ tree, style, className }) {
const [focusedRef] = useFocus<HTMLDivElement>({ focusFirstChild: true });
const translate = useTranslate();
const styles = useStyles(filterStyle, style);
const computedStyles = useS(styles, style);
if (!tree.settings?.filter) {
return null;
}
return styled(styles)(
<filter-box ref={focusedRef} className={className} as="div">
return (
<div ref={focusedRef} className={s(computedStyles, { filterBox: true }, className)}>
<Filter placeholder={translate('app_navigationTree_search')} value={tree.filter} max onFilter={value => tree.setFilter(value as string)} />
</filter-box>,
</div>
);
});
@@ -10,7 +10,7 @@ import React, { useState } from 'react';
import styled from 'reshadow';
import {
ACTION_ICON_BUTTON_STYLES,
ActionIconButtonStyles,
Fill,
IconButton,
IconButtonStyles,
@@ -55,8 +55,8 @@ export const ElementsTreeTools = observer<React.PropsWithChildren<Props>>(functi
const root = tree.root;
const translate = useTranslate();
const [opened, setOpen] = useState(false);
const deprecatedStyles = useStyles(ACTION_ICON_BUTTON_STYLES, style);
const styles = useS(ElementsTreeToolsStyles, ElementsTreeToolsIconButtonStyles);
const deprecatedStyles = useStyles(style);
const styles = useS(ElementsTreeToolsStyles, ElementsTreeToolsIconButtonStyles, ActionIconButtonStyles);
useCaptureViewContext(context => {
context?.set(DATA_CONTEXT_NAV_TREE_ROOT, tree.baseRoot);
@@ -67,13 +67,13 @@ export const ElementsTreeTools = observer<React.PropsWithChildren<Props>>(functi
return styled(deprecatedStyles)(
<SContext registry={registry}>
<tools className={s(styles, { tools: true })}>
<actions className={s(styles, { actions: true })}>
<div className={s(styles, { tools: true })}>
<div className={s(styles, { actions: true })}>
{tree.settings?.configurable && (
<IconButton
name="/icons/settings_cog_sm.svg"
title={translate('ui_settings')}
className={s(styles, { primary: true, opened })}
className={s(styles, { primary: true, actionIconButton: true, opened })}
img
onClick={() => setOpen(!opened)}
/>
@@ -84,15 +84,15 @@ export const ElementsTreeTools = observer<React.PropsWithChildren<Props>>(functi
name="/icons/refresh_sm.svg#root"
title={translate('app_navigationTree_refresh')}
disabled={loading}
className={s(styles, { primary: true, loading })}
className={s(styles, { primary: true, actionIconButton: true, loading })}
img
onClick={() => tree.refresh(root)}
/>
</actions>
</div>
{tree.settings && opened && <NavigationTreeSettings tree={tree} elements={settingsElements} style={style} />}
<ElementsTreeFilter tree={tree} style={style} />
{children}
</tools>
</div>
</SContext>,
);
});
@@ -17,6 +17,7 @@ export type NavTreeNodeComponent = React.FC<{
path: string[];
dragging?: boolean;
expanded?: boolean;
big?: boolean;
className?: string;
}>;
@@ -0,0 +1,9 @@
.treeNode {
&.hovered ::before {
opacity: 0.16;
}
&.expanded > .navNodeNested {
display: block;
}
}
@@ -7,9 +7,8 @@
*/
import { observer } from 'mobx-react-lite';
import { useDeferredValue, useEffect } from 'react';
import styled, { css, use } from 'reshadow';
import { getComputed, TreeNode, useMergeRefs, useStyles } from '@cloudbeaver/core-blocks';
import { getComputed, s, TreeNode, useMergeRefs, useS } from '@cloudbeaver/core-blocks';
import { useService } from '@cloudbeaver/core-di';
import { DATA_CONTEXT_NAV_NODE, DATA_CONTEXT_NAV_NODES, NavNodeManagerService } from '@cloudbeaver/core-navigation-tree';
import { useDNDData } from '@cloudbeaver/core-ui';
@@ -17,17 +16,12 @@ import { useDataContext } from '@cloudbeaver/core-view';
import { useNavTreeDropBox } from '../../useNavTreeDropBox';
import type { NavigationNodeComponent } from '../NavigationNodeComponent';
import style from './NavigationNode.m.css';
import { DATA_ATTRIBUTE_NODE_EDITING } from './NavigationNode/DATA_ATTRIBUTE_NODE_EDITING';
import { NavigationNodeNested } from './NavigationNode/NavigationNodeNested';
import { NavigationNodeControlRenderer } from './NavigationNodeControlRenderer';
import { useNavigationNode } from './useNavigationNode';
const styles = css`
TreeNode[|hovered] ::before {
opacity: 0.16;
}
`;
export const NavigationNode: NavigationNodeComponent = observer(function NavigationNode({
node,
component,
@@ -35,8 +29,8 @@ export const NavigationNode: NavigationNodeComponent = observer(function Navigat
control: externalControl,
expanded: externalExpanded,
className,
style,
}) {
const styles = useS(style);
const navNodeManagerService = useService(NavNodeManagerService);
const navNode = useNavigationNode(node, path);
const context = useDataContext();
@@ -87,7 +81,7 @@ export const NavigationNode: NavigationNodeComponent = observer(function Navigat
[],
);
return styled(useStyles(style, styles))(
return (
<TreeNode
ref={dndBox.setRef}
group={navNode.group}
@@ -99,23 +93,15 @@ export const NavigationNode: NavigationNodeComponent = observer(function Navigat
showInFilter={navNode.showInFilter}
externalExpanded={externalExpanded}
leaf={navNode.leaf}
className={className}
className={s(styles, { treeNode: true, hovered: hasNodes, expanded: externalExpanded ?? navNode.expanded }, className)}
onExpand={navNode.expand}
onClick={navNode.click}
onOpen={navNode.open}
onSelect={navNode.select}
{...use({ hovered: hasNodes })}
>
{/* <DNDPreview data={dndData} src="/icons/empty.svg" /> */}
<NavigationNodeControlRenderer
ref={controlRef}
navNode={navNode}
dragging={dndData.state.isDragging}
control={externalControl}
style={style}
node={node}
/>
{expanded && <NavigationNodeNested nodeId={node.id} path={path} component={component} />}
</TreeNode>,
<NavigationNodeControlRenderer ref={controlRef} navNode={navNode} dragging={dndData.state.isDragging} control={externalControl} node={node} />
{expanded && <NavigationNodeNested nodeId={node.id} path={path} component={component} className={s(styles, { navNodeNested: true })} />}
</TreeNode>
);
});
@@ -1,39 +0,0 @@
/*
* CloudBeaver - Cloud Database Manager
* Copyright (C) 2020-2023 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 { css } from 'reshadow';
export const NAVIGATION_NODE_CONTROL_STYLES = css`
TreeNodeControl:hover > portal,
TreeNodeControl:global([aria-selected='true']) > portal,
portal:focus-within {
visibility: visible;
}
TreeNodeName {
height: 100%;
max-width: 320px;
overflow: hidden;
text-overflow: ellipsis;
&[|editing] {
padding: 0;
overflow: visible;
margin-left: 2px;
}
}
portal {
position: relative;
box-sizing: border-box;
margin-left: auto !important;
margin-right: 8px !important;
visibility: hidden;
}
name-box {
overflow: hidden;
text-overflow: ellipsis;
}
`;
@@ -0,0 +1,41 @@
.treeNodeControl.dragging {
opacity: 0.6;
}
.treeNodeControl.editing {
&::before {
display: none;
}
& .treeNodeName {
padding: 0;
overflow: visible;
margin-left: 2px;
}
}
.treeNodeControl:hover > .portal,
.treeNodeControl[aria-selected='true'] > .portal,
.portal:focus-within {
visibility: visible;
}
.treeNodeName {
height: 100%;
max-width: 320px;
overflow: hidden;
text-overflow: ellipsis;
}
.portal {
position: relative;
box-sizing: border-box;
margin-left: auto !important;
margin-right: 8px !important;
visibility: hidden;
}
.nameBox {
overflow: hidden;
text-overflow: ellipsis;
}
@@ -8,12 +8,11 @@
import { action, observable } from 'mobx';
import { observer } from 'mobx-react-lite';
import React, { forwardRef, useContext } from 'react';
import styled, { use } from 'reshadow';
import {
getComputed,
Loader,
TREE_NODE_STYLES,
s,
TreeNodeContext,
TreeNodeControl,
TreeNodeIcon,
@@ -21,6 +20,7 @@ import {
useMouseContextMenu,
useObjectRef,
useObservableRef,
useS,
} from '@cloudbeaver/core-blocks';
import { useService } from '@cloudbeaver/core-di';
import { EventContext, EventStopPropagationFlag } from '@cloudbeaver/core-events';
@@ -29,7 +29,7 @@ import { getNodePlainName, type INodeActions, NavNodeInfoResource, NavTreeResour
import type { NavTreeControlComponent, NavTreeControlProps } from '../../NavigationNodeComponent';
import { TreeNodeMenuLoader } from '../TreeNodeMenu/TreeNodeMenuLoader';
import { DATA_ATTRIBUTE_NODE_EDITING } from './DATA_ATTRIBUTE_NODE_EDITING';
import { NAVIGATION_NODE_CONTROL_STYLES } from './NAVIGATION_NODE_CONTROL_STYLES';
import style from './NavigationNodeControl.m.css';
import { NavigationNodeExpand } from './NavigationNodeExpand';
import { NavigationNodeEditorLoader } from './NavigationNodeLoaders';
@@ -49,6 +49,7 @@ interface IEditingState {
export const NavigationNodeControl: NavTreeControlComponent = observer<NavTreeControlProps, HTMLDivElement>(
forwardRef(function NavigationNodeControl({ node, nodeInfo, dndElement, dndPlaceholder, className, onClick }, ref) {
const styles = useS(style);
const mouseContextMenu = useMouseContextMenu();
const treeNodeContext = useContext(TreeNodeContext);
const navNodeInfoResource = useService(NavNodeInfoResource);
@@ -129,35 +130,31 @@ export const NavigationNodeControl: NavTreeControlComponent = observer<NavTreeCo
const attributes = { [DATA_ATTRIBUTE_NODE_EDITING]: editing };
return styled(
TREE_NODE_STYLES,
NAVIGATION_NODE_CONTROL_STYLES,
)(
return (
<TreeNodeControl
ref={ref}
{...attributes}
className={className}
className={s(styles, { treeNodeControl: true, dragging: !!dndElement, editing }, className)}
onClick={onClick}
onContextMenu={handleContextMenuOpen}
{...use({ editing, dragging: dndElement })}
>
<NavigationNodeExpand nodeId={node.id} />
<TreeNodeIcon icon={icon} />
<TreeNodeName title={title} {...use({ editing })}>
<TreeNodeName title={title} className={s(styles, { treeNodeName: true })}>
<Loader suspense inline fullSize>
{editing ? (
<NavigationNodeEditorLoader name={getNodePlainName(node)} disabled={saving} onSave={editingState.save} onClose={editingState.cancel} />
) : (
<name-box>{name}</name-box>
<div className={s(styles, { nameBox: true })}>{name}</div>
)}
</Loader>
</TreeNodeName>
{!editing && !dndPlaceholder && (
<portal onClick={handlePortalClick}>
<div className={s(styles, { portal: true })} onClick={handlePortalClick}>
<TreeNodeMenuLoader mouseContextMenu={mouseContextMenu} node={node} actions={nodeActions} selected={selected} />
</portal>
</div>
)}
</TreeNodeControl>,
</TreeNodeControl>
);
}),
);
@@ -0,0 +1,9 @@
.inlineEditor {
height: 22px !important;
& input,
& input[disabled],
& input[readonly] {
padding: 1px !important;
}
}
@@ -7,22 +7,12 @@
*/
import { observer } from 'mobx-react-lite';
import { useState } from 'react';
import styled, { css } from 'reshadow';
import { s, useS } from '@cloudbeaver/core-blocks';
import { EventContext, EventStopPropagationFlag } from '@cloudbeaver/core-events';
import { InlineEditor } from '@cloudbeaver/core-ui';
const styles = css`
InlineEditor {
height: 22px;
& input,
& input[disabled],
& input[readonly] {
padding: 1px;
}
}
`;
import style from './NavigationNodeEditor.m.css';
interface Props {
name: string;
@@ -32,6 +22,7 @@ interface Props {
}
export const NavigationNodeEditor = observer<Props>(function NavigationNodeEditor({ name: initialName, disabled, onSave, onClose }) {
const styles = useS(style);
const [name, setName] = useState(initialName);
const isNameChanged = initialName !== name;
const isDisabledSave = disabled || !isNameChanged;
@@ -44,13 +35,13 @@ export const NavigationNodeEditor = observer<Props>(function NavigationNodeEdito
EventContext.set(event, EventStopPropagationFlag);
}
return styled(styles)(
return (
<InlineEditor
value={name}
disabled={disabled}
disableSave={isDisabledSave}
controlsPosition="inside"
style={styles}
className={s(styles, { inlineEditor: true })}
simple
autofocus
onChange={setName}
@@ -59,6 +50,6 @@ export const NavigationNodeEditor = observer<Props>(function NavigationNodeEdito
onReject={onClose}
onBlur={onClose}
onClick={stopPropagation}
/>,
/>
);
});
@@ -7,9 +7,8 @@
*/
import { observer } from 'mobx-react-lite';
import { useContext, useDeferredValue } from 'react';
import styled from 'reshadow';
import { getComputed, TREE_NODE_STYLES, TreeNodeExpand } from '@cloudbeaver/core-blocks';
import { getComputed, TreeNodeExpand } from '@cloudbeaver/core-blocks';
import { ElementsTreeContext } from '../../ElementsTreeContext';
@@ -27,5 +26,5 @@ export const NavigationNodeExpand = observer<Props>(function NavigationNodeExpan
return null;
}
return styled(TREE_NODE_STYLES)(<TreeNodeExpand filterActive={filterActive} />);
return <TreeNodeExpand filterActive={filterActive} />;
});
@@ -0,0 +1,3 @@
.navigationNodeNested {
box-sizing: border-box;
}
@@ -7,13 +7,13 @@
*/
import { observer } from 'mobx-react-lite';
import { forwardRef, useContext, useDeferredValue, useMemo } from 'react';
import styled from 'reshadow';
import { getComputed, TREE_NODE_STYLES, TreeNodeNested, TreeNodeNestedMessage, useTranslate } from '@cloudbeaver/core-blocks';
import { getComputed, s, TreeNodeNested, TreeNodeNestedMessage, useS, useTranslate } from '@cloudbeaver/core-blocks';
import { isArraysEqual } from '@cloudbeaver/core-utils';
import { ElementsTreeContext } from '../../ElementsTreeContext';
import type { NavTreeNodeComponent } from '../../NavigationNodeComponent';
import style from './NavigationNodeNested.m.css';
interface Props {
nodeId?: string;
@@ -25,6 +25,7 @@ interface Props {
export const NavigationNodeNested = observer(
forwardRef<HTMLDivElement, Props>(function NavigationNodeNested({ nodeId, component, path, root, className }, ref) {
const styles = useS(style);
const treeContext = useContext(ElementsTreeContext);
const translate = useTranslate();
@@ -50,11 +51,11 @@ export const NavigationNodeNested = observer(
empty = useDeferredValue(empty);
if (nodeId !== undefined && rootFolder) {
return styled(TREE_NODE_STYLES)(<NavigationNode nodeId={nodeId} path={path} expanded />);
return <NavigationNode nodeId={nodeId} path={path} expanded />;
}
return styled(TREE_NODE_STYLES)(
<TreeNodeNested ref={ref} root={root} className={className}>
return (
<TreeNodeNested ref={ref} root={root} className={s(styles, { navigationNodeNested: true }, className)}>
{children.map(child => (
<NavigationNode key={child} nodeId={child} path={nextPath} />
))}
@@ -63,7 +64,7 @@ export const NavigationNodeNested = observer(
{translate(nodeId === undefined ? 'app_navigationTree_connection_group_user' : 'app_navigationTree_node_empty')}
</TreeNodeNestedMessage>
)}
</TreeNodeNested>,
</TreeNodeNested>
);
}),
);
@@ -0,0 +1,8 @@
.control {
transition: opacity 0.3s ease;
opacity: 1;
&.outdated {
opacity: 0.5;
}
}
@@ -7,16 +7,15 @@
*/
import { observer } from 'mobx-react-lite';
import { forwardRef, useCallback, useContext, useDeferredValue, useEffect, useRef, useState } from 'react';
import styled, { css, use } from 'reshadow';
import { getComputed, TreeNodeContext, useMergeRefs, useObjectRef } from '@cloudbeaver/core-blocks';
import { getComputed, s, TreeNodeContext, useMergeRefs, useObjectRef, useS } from '@cloudbeaver/core-blocks';
import { useService } from '@cloudbeaver/core-di';
import { type NavNode, NavNodeInfoResource } from '@cloudbeaver/core-navigation-tree';
import type { ComponentStyle } from '@cloudbeaver/core-theming';
import { ElementsTreeContext } from '../ElementsTreeContext';
import type { NavTreeControlComponent } from '../NavigationNodeComponent';
import { NavigationNodeControlLoader } from './NavigationNode/NavigationNodeLoaders';
import style from './NavigationNodeControlRenderer.m.css';
import type { INavigationNode } from './useNavigationNode';
interface Props {
@@ -24,22 +23,11 @@ interface Props {
navNode: INavigationNode;
dragging?: boolean;
control?: NavTreeControlComponent | undefined;
style?: ComponentStyle;
}
const styles = css`
Control {
transition: opacity 0.3s ease;
opacity: 1;
&[|outdated] {
opacity: 0.5;
}
}
`;
export const NavigationNodeControlRenderer = observer<Props, HTMLDivElement>(
forwardRef(function NavigationNodeControlRenderer({ node, navNode, dragging, control: externalControl, style }, ref) {
forwardRef(function NavigationNodeControlRenderer({ node, navNode, dragging, control: externalControl }, ref) {
const styles = useS(style);
const elementRef = useRef<HTMLDivElement | null>(null);
const [size, setSize] = useState(24);
const contextRef = useObjectRef({
@@ -108,8 +96,15 @@ export const NavigationNodeControlRenderer = observer<Props, HTMLDivElement>(
treeNodeContext.select(event.ctrlKey || event.metaKey);
}
return styled(styles)(
<Control ref={mergedRef} {...use({ outdated })} nodeInfo={nodeInfo} dndElement={dragging} style={style} node={node} onClick={onClickHandler} />,
return (
<Control
ref={mergedRef}
className={s(styles, { control: true, outdated })}
nodeInfo={nodeInfo}
dndElement={dragging}
node={node}
onClick={onClickHandler}
/>
);
}),
);
@@ -7,16 +7,15 @@
*/
import { observer } from 'mobx-react-lite';
import { useContext } from 'react';
import styled from 'reshadow';
import { getComputed, TreeNode, useObjectRef, useStyles } from '@cloudbeaver/core-blocks';
import { getComputed, TreeNode, useObjectRef } from '@cloudbeaver/core-blocks';
import { ElementsTreeContext } from '../ElementsTreeContext';
import type { NavigationNodeComponent } from '../NavigationNodeComponent';
import { transformNodeInfo } from '../transformNodeInfo';
import { NavigationNodeControlLoader } from './NavigationNode/NavigationNodeLoaders';
export const NavigationNodeDragged: NavigationNodeComponent = observer(function NavigationNode({ node, className, control: externalControl, style }) {
export const NavigationNodeDragged: NavigationNodeComponent = observer(function NavigationNodeDragged({ node, className, control: externalControl }) {
const contextRef = useObjectRef({
context: useContext(ElementsTreeContext),
});
@@ -25,10 +24,10 @@ export const NavigationNodeDragged: NavigationNodeComponent = observer(function
const Control = control || externalControl || NavigationNodeControlLoader;
const nodeInfo = transformNodeInfo(node, contextRef.context?.tree.nodeInfoTransformers ?? []);
return styled(useStyles(style))(
return (
<TreeNode externalExpanded={false} className={className} leaf>
{/* <DNDPreview data={dndData} src="/icons/empty.svg" /> */}
<Control nodeInfo={nodeInfo} node={node} dndPlaceholder />
</TreeNode>,
</TreeNode>
);
});
@@ -7,9 +7,8 @@
*/
import { observer } from 'mobx-react-lite';
import { useContext } from 'react';
import styled from 'reshadow';
import { Translate, TREE_NODE_STYLES, TreeNodeNestedMessage } from '@cloudbeaver/core-blocks';
import { Translate, TreeNodeNestedMessage } from '@cloudbeaver/core-blocks';
import { useService } from '@cloudbeaver/core-di';
import { NavNodeInfoResource } from '@cloudbeaver/core-navigation-tree';
@@ -19,7 +18,6 @@ import { NavigationNodeRendererLoader } from './NavigationNodeRendererLoader';
export const NavigationNodeElement: NavTreeNodeComponent = observer(function NavigationNodeElement({ nodeId, path, expanded, dragging, className }) {
const context = useContext(ElementsTreeContext);
const navNodeInfoResource = useService(NavNodeInfoResource);
if (context?.tree.renderers) {
for (const renderer of context.tree.renderers) {
@@ -40,17 +38,21 @@ export const NavigationNodeElement: NavTreeNodeComponent = observer(function Nav
}
}
return <NavigationNodeRenderer nodeId={nodeId} path={path} expanded={expanded} dragging={dragging} className={className} />;
});
const NavigationNodeRenderer: NavTreeNodeComponent = observer(function NavigationNodeRenderer({ nodeId, path, expanded, dragging, className }) {
const navNodeInfoResource = useService(NavNodeInfoResource);
const node = navNodeInfoResource.get(nodeId);
if (!node) {
return styled(TREE_NODE_STYLES)(
return (
<TreeNodeNestedMessage>
<Translate token="app_navigationTree_node_not_found" />
</TreeNodeNestedMessage>,
</TreeNodeNestedMessage>
);
}
// TODO: after node update reference can be lost and NavigationNode skip update
return (
<NavigationNodeRendererLoader
node={node}
@@ -0,0 +1,21 @@
.contextMenu {
padding: 0;
height: 16px;
&:before {
display: none;
}
&.selected svg,
&:hover svg,
&:focus svg {
fill: #338fcc;
}
}
.icon {
cursor: pointer;
width: 16px;
height: 10px;
fill: #dedede;
}
@@ -6,9 +6,8 @@
* you may not use this file except in compliance with the License.
*/
import { observer } from 'mobx-react-lite';
import styled, { use } from 'reshadow';
import { getComputed, Icon, IMouseContextMenu } from '@cloudbeaver/core-blocks';
import { getComputed, Icon, IMouseContextMenu, s, useS } from '@cloudbeaver/core-blocks';
import { ConnectionInfoResource } from '@cloudbeaver/core-connections';
import { useService } from '@cloudbeaver/core-di';
import { DATA_CONTEXT_NAV_NODE, type INodeActions, type NavNode } from '@cloudbeaver/core-navigation-tree';
@@ -18,7 +17,7 @@ import { DATA_CONTEXT_CONNECTION } from '@cloudbeaver/plugin-connections';
import { MENU_NAV_TREE } from '../../MENU_NAV_TREE';
import { DATA_CONTEXT_NAV_NODE_ACTIONS } from './DATA_CONTEXT_NAV_NODE_ACTIONS';
import { treeNodeMenuStyles } from './treeNodeMenuStyles';
import style from './TreeNodeMenu.m.css';
interface Props {
node: NavNode;
@@ -29,6 +28,7 @@ interface Props {
}
export const TreeNodeMenu = observer<Props>(function TreeNodeMenu({ node, actions, selected, mouseContextMenu, onClose }) {
const styles = useS(style);
const connectionsInfoResource = useService(ConnectionInfoResource);
const menu = useMenu({ menu: MENU_NAV_TREE });
menu.context.set(DATA_CONTEXT_NAV_NODE, node);
@@ -46,9 +46,15 @@ export const TreeNodeMenu = observer<Props>(function TreeNodeMenu({ node, action
}
}
return styled(treeNodeMenuStyles)(
<ContextMenu menu={menu} mouseContextMenu={mouseContextMenu} onVisibleSwitch={handleVisibleSwitch} {...use({ selected })} modal>
<Icon name="snack" viewBox="0 0 16 10" />
</ContextMenu>,
return (
<ContextMenu
menu={menu}
className={s(styles, { contextMenu: true, selected })}
mouseContextMenu={mouseContextMenu}
modal
onVisibleSwitch={handleVisibleSwitch}
>
<Icon className={s(styles, { icon: true })} name="snack" viewBox="0 0 16 10" />
</ContextMenu>
);
});
@@ -0,0 +1,8 @@
.tools {
composes: theme-border-color-background from global;
}
.tools > *:last-child:not(:first-child) {
border-bottom: solid 1px;
border-color: inherit;
}
@@ -0,0 +1,29 @@
.captureView {
outline: none;
display: flex;
flex: 1;
flex-direction: column;
overflow: hidden;
}
.elementsTree {
min-width: 100%;
width: max-content;
}
.center {
display: flex;
height: 100%;
width: 100%;
& .message {
margin: auto;
text-align: center;
}
}
.message {
box-sizing: border-box;
max-width: 240px;
padding: 24px;
}
@@ -7,14 +7,14 @@
*/
import { observer } from 'mobx-react-lite';
import { useMemo } from 'react';
import styled, { css } from 'reshadow';
import { Translate, useUserData } from '@cloudbeaver/core-blocks';
import { s, SContext, StyleRegistry, Translate, useS, useUserData } from '@cloudbeaver/core-blocks';
import { useService } from '@cloudbeaver/core-di';
import { NavNodeInfoResource, NavTreeResource, ProjectsNavNodeService, ROOT_NODE_PATH } from '@cloudbeaver/core-navigation-tree';
import { ProjectsService } from '@cloudbeaver/core-projects';
import { CaptureView } from '@cloudbeaver/core-view';
import { ElementsTreeToolsStyles } from '../index';
import { NavNodeViewService } from '../NodesManager/NavNodeView/NavNodeViewService';
import { navigationTreeConnectionGroupFilter } from './ConnectionGroup/navigationTreeConnectionGroupFilter';
import { navigationTreeConnectionGroupRenderer } from './ConnectionGroup/navigationTreeConnectionGroupRenderer';
@@ -26,7 +26,9 @@ import {
} from './ElementsTree/ElementsTreeTools/NavigationTreeSettings/createElementsTreeSettings';
import { transformFilteredNodeInfo } from './ElementsTree/transformFilteredNodeInfo';
import type { IElementsTreeSettings } from './ElementsTree/useElementsTree';
import elementsTreeToolsStyles from './ElementsTreeTools.m.css';
import { getNavigationTreeUserSettingsId } from './getNavigationTreeUserSettingsId';
import style from './NavigationTree.m.css';
import { navigationTreeDuplicateFilter } from './navigationTreeDuplicateIdFilter';
import { NavigationTreeService } from './NavigationTreeService';
import { navigationTreeProjectFilter } from './ProjectsRenderer/navigationTreeProjectFilter';
@@ -36,49 +38,18 @@ import { navigationTreeProjectsRendererRenderer } from './ProjectsRenderer/navig
import { ProjectsSettingsPlaceholderElement } from './ProjectsRenderer/ProjectsSettingsForm';
import { useNavigationTree } from './useNavigationTree';
const navigationTreeStyles = css`
CaptureView {
outline: none;
display: flex;
flex: 1;
flex-direction: column;
overflow: hidden;
}
ElementsTree {
min-width: 100%;
width: max-content;
}
center {
display: flex;
height: 100%;
width: 100%;
& message {
margin: auto;
text-align: center;
}
}
message {
box-sizing: border-box;
max-width: 240px;
padding: 24px;
}
`;
const elementsTreeStyles = css`
tools {
composes: theme-border-color-background from global;
}
tools > *:last-child:not(:first-child) {
border-bottom: solid 1px;
border-color: inherit;
}
`;
const registry: StyleRegistry = [
[
ElementsTreeToolsStyles,
{
mode: 'append',
styles: [elementsTreeToolsStyles],
},
],
];
export const NavigationTree = observer(function NavigationTree() {
const styles = useS(style);
const projectsNavNodeService = useService(ProjectsNavNodeService);
const projectsService = useService(ProjectsService);
const navTreeService = useService(NavigationTreeService);
@@ -113,34 +84,34 @@ export const NavigationTree = observer(function NavigationTree() {
const settingsElements = useMemo(() => [ProjectsSettingsPlaceholderElement], []);
return styled(navigationTreeStyles)(
<CaptureView view={navTreeService}>
<ElementsTree
root={root}
localState={navTreeService.treeState}
filters={[duplicateFilter, connectionGroupFilter, projectFilter]}
renderers={[projectsRendererRenderer, navigationTreeConnectionGroupRenderer, connectionRenderer]}
navNodeFilterCompare={navigationTreeProjectSearchCompare}
nodeInfoTransformers={[transformFilteredNode]}
expandStateGetters={[projectsExpandStateGetter]}
settingsElements={settingsElements}
emptyPlaceholder={() =>
styled(navigationTreeStyles)(
<center>
<message>
return (
<SContext registry={registry}>
<CaptureView view={navTreeService} className={s(styles, { captureView: true })}>
<ElementsTree
root={root}
localState={navTreeService.treeState}
filters={[duplicateFilter, connectionGroupFilter, projectFilter]}
renderers={[projectsRendererRenderer, navigationTreeConnectionGroupRenderer, connectionRenderer]}
navNodeFilterCompare={navigationTreeProjectSearchCompare}
nodeInfoTransformers={[transformFilteredNode]}
expandStateGetters={[projectsExpandStateGetter]}
settingsElements={settingsElements}
className={s(styles, { elementsTree: true })}
emptyPlaceholder={() => (
<div className={s(styles, { center: true })}>
<div className={s(styles, { message: true })}>
<Translate token="app_navigationTree_empty_placeholder" />
</message>
</center>,
)
}
customSelect={handleSelect}
customSelectReset={handleSelectReset}
settings={settings}
style={elementsTreeStyles}
getChildren={navTreeService.getChildren}
loadChildren={navTreeService.loadNestedNodes}
onOpen={handleOpen}
/>
</CaptureView>,
</div>
</div>
)}
customSelect={handleSelect}
customSelectReset={handleSelectReset}
settings={settings}
getChildren={navTreeService.getChildren}
loadChildren={navTreeService.loadNestedNodes}
onOpen={handleOpen}
/>
</CaptureView>
</SContext>
);
});
@@ -0,0 +1,6 @@
.container {
display: flex;
flex-direction: column;
flex: 1;
overflow: auto;
}
@@ -6,25 +6,19 @@
* you may not use this file except in compliance with the License.
*/
import { observer } from 'mobx-react-lite';
import styled, { css } from 'reshadow';
import { s, useS } from '@cloudbeaver/core-blocks';
import type { TabContainerPanelComponent } from '@cloudbeaver/core-ui';
import { NavigationTreeLoader } from './NavigationTreeLoader';
const styles = css`
container {
display: flex;
flex-direction: column;
flex: 1;
overflow: auto;
}
`;
import style from './NavigationTreePanel.m.css';
export const NavigationTreePanel: TabContainerPanelComponent = observer(function NavigationTreePanel() {
return styled(styles)(
<container>
const styles = useS(style);
return (
<div className={s(styles, { container: true })}>
<NavigationTreeLoader />
</container>,
</div>
);
});
@@ -0,0 +1,35 @@
.treeNodeControl {
transition: opacity 0.3s ease;
opacity: 1;
&.outdated {
opacity: 0.5;
}
}
.treeNodeControl:hover > .portal,
.treeNodeControl[aria-selected='true'] > .portal,
.portal:focus-within {
visibility: visible;
}
.treeNodeName {
composes: theme-text-text-hint-on-light theme-typography--caption from global;
height: 100%;
max-width: 250px;
overflow: hidden;
text-overflow: ellipsis;
}
.portal {
position: relative;
box-sizing: border-box;
margin-left: auto !important;
margin-right: 8px !important;
visibility: hidden;
}
.nameBox {
overflow: hidden;
text-overflow: ellipsis;
}
@@ -7,9 +7,8 @@
*/
import { observer } from 'mobx-react-lite';
import React, { forwardRef, useContext } from 'react';
import styled, { css, use } from 'reshadow';
import { getComputed, TREE_NODE_STYLES, TreeNodeContext, TreeNodeControl, TreeNodeName, useMouseContextMenu } from '@cloudbeaver/core-blocks';
import { getComputed, s, TreeNodeContext, TreeNodeControl, TreeNodeName, useMouseContextMenu, useS } from '@cloudbeaver/core-blocks';
import { useService } from '@cloudbeaver/core-di';
import { EventContext, EventStopPropagationFlag } from '@cloudbeaver/core-events';
import { NavNodeInfoResource } from '@cloudbeaver/core-navigation-tree';
@@ -18,43 +17,11 @@ import { ElementsTreeContext } from '../ElementsTree/ElementsTreeContext';
import type { NavTreeControlComponent, NavTreeControlProps } from '../ElementsTree/NavigationNodeComponent';
import { isDraggingInsideProject } from '../ElementsTree/NavigationTreeNode/isDraggingInsideProject';
import { TreeNodeMenuLoader } from '../ElementsTree/NavigationTreeNode/TreeNodeMenu/TreeNodeMenuLoader';
const styles = css`
TreeNodeControl {
transition: opacity 0.3s ease;
opacity: 1;
&[|outdated] {
opacity: 0.5;
}
}
TreeNodeControl:hover > portal,
TreeNodeControl:global([aria-selected='true']) > portal,
portal:focus-within {
visibility: visible;
}
TreeNodeName {
composes: theme-text-text-hint-on-light theme-typography--caption from global;
height: 100%;
max-width: 250px;
overflow: hidden;
text-overflow: ellipsis;
}
portal {
position: relative;
box-sizing: border-box;
margin-left: auto !important;
margin-right: 8px !important;
visibility: hidden;
}
name-box {
overflow: hidden;
text-overflow: ellipsis;
}
`;
import style from './NavigationNodeProjectControl.m.css';
export const NavigationNodeProjectControl: NavTreeControlComponent = observer<NavTreeControlProps, HTMLDivElement>(
forwardRef(function NavigationNodeProjectControl({ node, dndElement, dndPlaceholder, className }, ref) {
const styles = useS(style);
const mouseContextMenu = useMouseContextMenu();
const treeNodeContext = useContext(TreeNodeContext);
const elementsTreeContext = useContext(ElementsTreeContext);
@@ -92,27 +59,23 @@ export const NavigationNodeProjectControl: NavTreeControlComponent = observer<Na
return null;
}
return styled(
TREE_NODE_STYLES,
styles,
)(
return (
<TreeNodeControl
ref={ref}
onClick={handleClick}
onDoubleClick={handleDbClick}
onContextMenu={handleContextMenuOpen}
{...use({ outdated, dragging: dndElement })}
className={className}
className={s(styles, { treeNodeControl: true, outdated, dragging: isDragging }, className)}
>
<TreeNodeName title={node.name}>
<name-box>{node.name}</name-box>
<TreeNodeName className={s(styles, { treeNodeName: true })} title={node.name}>
<div className={s(styles, { nameBox: true })}>{node.name}</div>
</TreeNodeName>
{!dndPlaceholder && (
<portal onClick={handlePortalClick}>
<div className={s(styles, { portal: true })} onClick={handlePortalClick}>
<TreeNodeMenuLoader mouseContextMenu={mouseContextMenu} node={node} selected={selected} />
</portal>
</div>
)}
</TreeNodeControl>,
</TreeNodeControl>
);
}),
);
@@ -0,0 +1,12 @@
.projectNode {
margin-top: 8px;
&.project:only-child,
&.hideProjects {
margin-top: 0px;
}
& > .navigationNodeNested {
padding-left: 0 !important;
}
}
@@ -7,13 +7,13 @@
*/
import { observer } from 'mobx-react-lite';
import { useContext } from 'react';
import styled, { css, use } from 'reshadow';
import { getComputed, Translate, TREE_NODE_STYLES, TreeNodeNestedMessage } from '@cloudbeaver/core-blocks';
import { getComputed, s, SContext, StyleRegistry, Translate, TreeNodeNestedMessage, useS } from '@cloudbeaver/core-blocks';
import { useService } from '@cloudbeaver/core-di';
import type { NavNodeInfoResource } from '@cloudbeaver/core-navigation-tree';
import { NAV_NODE_TYPE_PROJECT, ProjectsService } from '@cloudbeaver/core-projects';
import { NavigationNodeControlRendererStyles, NavigationNodeNestedStyles } from '../../index';
import { useNode } from '../../NodesManager/useNode';
import { ElementsTreeContext } from '../ElementsTree/ElementsTreeContext';
import type { NavigationNodeRendererComponent } from '../ElementsTree/NavigationNodeComponent';
@@ -21,25 +21,24 @@ import { isDraggingInsideProject } from '../ElementsTree/NavigationTreeNode/isDr
import { NavigationNodeRendererLoader } from '../ElementsTree/NavigationTreeNode/NavigationNodeRendererLoader';
import type { IElementsTreeCustomRenderer } from '../ElementsTree/useElementsTree';
import { NavigationNodeProjectControl } from './NavigationNodeProjectControl';
import style from './NavigationTreeProjectsRendererRenderer.m.css';
const nestedStyles = css`
TreeNode {
margin-top: 8px;
&[|project]:only-child,
&[|hideProjects] {
margin-top: 0px;
& Control {
display: none;
}
}
& NavigationNodeNested {
padding-left: 0 !important;
}
}
`;
const registry: StyleRegistry = [
[
NavigationNodeNestedStyles,
{
mode: 'append',
styles: [style],
},
],
[
NavigationNodeControlRendererStyles,
{
mode: 'append',
styles: [style],
},
],
];
export function navigationTreeProjectsRendererRenderer(navNodeInfoResource: NavNodeInfoResource): IElementsTreeCustomRenderer {
return nodeId => {
@@ -61,6 +60,7 @@ const ProjectRenderer: NavigationNodeRendererComponent = observer(function Manag
className,
expanded,
}) {
const styles = useS(style);
const projectsService = useService(ProjectsService);
const elementsTreeContext = useContext(ElementsTreeContext);
@@ -78,26 +78,26 @@ const ProjectRenderer: NavigationNodeRendererComponent = observer(function Manag
const hideProjects = elementsTreeContext?.tree.settings?.projects === false && !isDragging;
if (!node) {
return styled(TREE_NODE_STYLES)(
return (
<TreeNodeNestedMessage>
<Translate token="app_navigationTree_node_not_found" />
</TreeNodeNestedMessage>,
</TreeNodeNestedMessage>
);
}
const project = node.nodeType === NAV_NODE_TYPE_PROJECT && singleProject && !isDragging;
return styled(nestedStyles)(
<NavigationNodeRendererLoader
node={node}
path={path}
expanded={expanded}
dragging={dragging}
className={className}
control={NavigationNodeProjectControl}
style={nestedStyles}
component={component}
{...use({ hideProjects, project })}
/>,
return (
<SContext registry={registry}>
<NavigationNodeRendererLoader
node={node}
path={path}
expanded={expanded}
dragging={dragging}
className={s(styles, { projectNode: true, hideProjects, project }, className)}
control={NavigationNodeProjectControl}
component={component}
/>
</SContext>
);
});
@@ -28,6 +28,10 @@ export * from './NavigationTree/ElementsTree/ElementsTreeTools/ElementsTreeTools
export * from './NavigationTree/ElementsTree/elementsTreeNameFilter';
export * from './NavigationTree/NavigationTreeBootstrap';
export * from './NavigationTree/NavigationTreeService';
export { default as ElementsTreeToolsStyles } from './NavigationTree/ElementsTree/ElementsTreeTools/ElementsTreeTools.m.css';
export { default as ElementsTreeFilterStyles } from './NavigationTree/ElementsTree/ElementsTreeTools/ElementsTreeFilter.m.css';
export { default as NavigationNodeNestedStyles } from './NavigationTree/ElementsTree/NavigationTreeNode/NavigationNode/NavigationNodeNested.m.css';
export { default as NavigationNodeControlRendererStyles } from './NavigationTree/ElementsTree/NavigationTreeNode/NavigationNodeControlRenderer.m.css';
export * from './NavigationTree/NavigationTreeLoader';