From ec92a1ba4667bd060cb8afc84168a76a3ed87e5c Mon Sep 17 00:00:00 2001 From: Nils Petter Fremming <35219649+nilscognite@users.noreply.github.com> Date: Tue, 5 Nov 2024 15:29:54 +0100 Subject: [PATCH] fix(react-components): TreeView: Add info icon for clicking for further info. Also add support for long names. (#4847) * Add info * XFix treenode * Update TreeView.stories.tsx * Add Generics * Update parent --- react-components/package.json | 2 +- .../architecture/base/treeView/TreeNode.ts | 52 ++++++++++-------- .../Architecture/DomainObjectPanel.tsx | 4 +- .../Architecture/IconComponentMapper.tsx | 2 + .../Architecture/TreeView/TreeViewNode.tsx | 3 + .../Architecture/TreeView/TreeViewProps.ts | 5 ++ .../TreeView/components/TreeNodeCaret.tsx | 11 +--- .../TreeView/components/TreeViewInfo.tsx | 55 +++++++++++++++++++ .../TreeView/components/TreeViewLabel.tsx | 13 ++++- .../TreeView/utilities/constants.ts | 3 + react-components/stories/TreeView.stories.tsx | 19 +++++-- 11 files changed, 126 insertions(+), 43 deletions(-) create mode 100644 react-components/src/components/Architecture/TreeView/components/TreeViewInfo.tsx diff --git a/react-components/package.json b/react-components/package.json index c9f98e50777..1ab55177ec7 100644 --- a/react-components/package.json +++ b/react-components/package.json @@ -1,6 +1,6 @@ { "name": "@cognite/reveal-react-components", - "version": "0.65.1", + "version": "0.65.2", "exports": { ".": { "import": "./dist/index.js", diff --git a/react-components/src/architecture/base/treeView/TreeNode.ts b/react-components/src/architecture/base/treeView/TreeNode.ts index be99c7a7e5c..b6504dc6695 100644 --- a/react-components/src/architecture/base/treeView/TreeNode.ts +++ b/react-components/src/architecture/base/treeView/TreeNode.ts @@ -7,7 +7,7 @@ import { type IconName } from '../utilities/IconName'; import { type ITreeNode } from './ITreeNode'; import { CheckBoxState, type TreeNodeAction, type IconColor, type LoadNodesAction } from './types'; -export class TreeNode implements ITreeNode { +export class TreeNode implements ITreeNode { // ================================================== // INSTANCE FIELDS // ================================================== @@ -24,12 +24,13 @@ export class TreeNode implements ITreeNode { private _isLoadingSiblings: boolean = false; private _needLoadChildren = false; private _needLoadSiblings = false; + public userData: T | undefined = undefined; - protected _children: TreeNode[] | undefined = undefined; - protected _parent: TreeNode | undefined = undefined; + protected _children: Array> | undefined = undefined; + protected _parent: TreeNode | undefined = undefined; // ================================================== - // INSTANCE PROPERTIES (Some are implementation of ITreeNode) + // INSTANCE PROPERTIES (Some are implementation of ITreeNode) // ================================================== public get label(): string { @@ -171,18 +172,23 @@ export class TreeNode implements ITreeNode { return this._children !== undefined && this._children.length > 0; } + public get parent(): TreeNode | undefined { + return this._parent; + } + // ================================================== // INSTANCE METHODS: Parent children methods // ================================================== - public getRoot(): TreeNode { - if (this._parent !== undefined) { - return this._parent.getRoot(); + // eslint-disable-next-line @typescript-eslint/prefer-return-this-type + public getRoot(): TreeNode { + if (this.parent !== undefined) { + return this.parent.getRoot(); } return this; } - public addChild(child: TreeNode): void { + public addChild(child: TreeNode): void { if (this._children === undefined) { this._children = []; } @@ -190,7 +196,7 @@ export class TreeNode implements ITreeNode { child._parent = this; } - public insertChild(index: number, child: TreeNode): void { + public insertChild(index: number, child: TreeNode): void { if (this._children === undefined) { this._children = []; } @@ -213,7 +219,7 @@ export class TreeNode implements ITreeNode { if (!(child instanceof TreeNode)) { continue; } - this.addChild(child); + this.addChild(child as TreeNode); } } this.needLoadChildren = false; @@ -226,7 +232,7 @@ export class TreeNode implements ITreeNode { if (siblings === undefined || siblings.length === 0) { return; } - const parent = this._parent; + const parent = this.parent; if (parent === undefined || parent._children === undefined) { return; } @@ -240,7 +246,7 @@ export class TreeNode implements ITreeNode { continue; } index++; - parent.insertChild(index, child); + parent.insertChild(index, child as TreeNode); } this.needLoadSiblings = false; parent.update(); @@ -250,8 +256,8 @@ export class TreeNode implements ITreeNode { // INSTANCE METHODS: Get selection and checked nodes // ================================================== - public getSelectedNodes(): TreeNode[] { - const nodes: TreeNode[] = []; + public getSelectedNodes(): Array> { + const nodes: Array> = []; for (const child of this.getThisAndDescendants()) { if (child.isSelected) { nodes.push(child); @@ -260,8 +266,8 @@ export class TreeNode implements ITreeNode { return nodes; } - public getCheckedNodes(): TreeNode[] { - const nodes: TreeNode[] = []; + public getCheckedNodes(): Array> { + const nodes: Array> = []; for (const child of this.getThisAndDescendants()) { if (child.checkBoxState === CheckBoxState.All) { nodes.push(child); @@ -274,11 +280,11 @@ export class TreeNode implements ITreeNode { // INSTANCE METHODS: Iterators // ================================================== - public *getChildren(loadNodes?: LoadNodesAction): Generator { + public *getChildren(loadNodes?: LoadNodesAction): Generator> { if (this.isLoadingChildren) { loadNodes = undefined; } - const canLoad = this.isParent && this._parent !== undefined; + const canLoad = this.isParent && this.parent !== undefined; if (canLoad && loadNodes !== undefined && this.needLoadChildren) { void this.loadChildren(loadNodes); } @@ -290,7 +296,7 @@ export class TreeNode implements ITreeNode { } } - public *getDescendants(): Generator { + public *getDescendants(): Generator> { for (const child of this.getChildren()) { yield child; for (const descendant of child.getDescendants()) { @@ -299,18 +305,18 @@ export class TreeNode implements ITreeNode { } } - public *getThisAndDescendants(): Generator { + public *getThisAndDescendants(): Generator> { yield this; for (const descendant of this.getDescendants()) { yield descendant; } } - public *getAncestors(): Generator { - let ancestor = this._parent; + public *getAncestors(): Generator> { + let ancestor = this.parent; while (ancestor !== undefined) { yield ancestor; - ancestor = ancestor._parent; + ancestor = ancestor.parent; } } diff --git a/react-components/src/components/Architecture/DomainObjectPanel.tsx b/react-components/src/components/Architecture/DomainObjectPanel.tsx index 4d7c4e2a9a0..d12c6d707ac 100644 --- a/react-components/src/components/Architecture/DomainObjectPanel.tsx +++ b/react-components/src/components/Architecture/DomainObjectPanel.tsx @@ -36,11 +36,9 @@ export const DomainObjectPanel = (): ReactElement => { useEffect(() => { DomainObjectPanelUpdater.setDomainObjectDelegate(setCurrentDomainObjectInfo); - - // Set in the get string on the copy command if any }, [setCurrentDomainObjectInfo, commands]); - // Fore the getString to be updated + // Force the getString to be updated if (commands !== undefined && info !== undefined) { for (const command of commands) { if (command instanceof CopyToClipboardCommand) diff --git a/react-components/src/components/Architecture/IconComponentMapper.tsx b/react-components/src/components/Architecture/IconComponentMapper.tsx index 09c117cd08e..80a49416493 100644 --- a/react-components/src/components/Architecture/IconComponentMapper.tsx +++ b/react-components/src/components/Architecture/IconComponentMapper.tsx @@ -33,6 +33,7 @@ import { FlipVerticalIcon, GrabIcon, type IconProps, + InfoIcon, LocationIcon, PerspectiveAltIcon, PerspectiveIcon, @@ -89,6 +90,7 @@ const defaultMappings: Array<[IconName, IconType]> = [ ['FlipHorizontal', FlipHorizontalIcon], ['FlipVertical', FlipVerticalIcon], ['Grab', GrabIcon], + ['Info', InfoIcon], ['Location', LocationIcon], ['Perspective', PerspectiveIcon], ['PerspectiveAlt', PerspectiveAltIcon], diff --git a/react-components/src/components/Architecture/TreeView/TreeViewNode.tsx b/react-components/src/components/Architecture/TreeView/TreeViewNode.tsx index ade3007ca90..01e6c165364 100644 --- a/react-components/src/components/Architecture/TreeView/TreeViewNode.tsx +++ b/react-components/src/components/Architecture/TreeView/TreeViewNode.tsx @@ -26,6 +26,7 @@ import { SELECTED_TEXT_COLOR, TEXT_COLOR } from './utilities/constants'; +import { TreeViewInfo } from './components/TreeViewInfo'; // ================================================== // MAIN COMPONENT @@ -51,6 +52,7 @@ export const TreeViewNode = ({ const hasHover = props.hasHover ?? true; const hasCheckBoxes = props.hasCheckboxes ?? false; const hasIcons = props.hasIcons ?? false; + const hasInfo = props.hasInfo ?? false; const marginLeft = level * gapToChildren + 'px'; // This force to update the component when the node changes @@ -92,6 +94,7 @@ export const TreeViewNode = ({ {hasIcons && } + {hasInfo && } {children !== undefined && children.map((node, index) => ( diff --git a/react-components/src/components/Architecture/TreeView/TreeViewProps.ts b/react-components/src/components/Architecture/TreeView/TreeViewProps.ts index f1e91fa7a99..12c5ef2e807 100644 --- a/react-components/src/components/Architecture/TreeView/TreeViewProps.ts +++ b/react-components/src/components/Architecture/TreeView/TreeViewProps.ts @@ -19,6 +19,8 @@ export type TreeViewProps = { hoverBackgroundColor?: string; caretColor?: string; hoverCaretColor?: string; + infoColor?: string; + hoverInfoColor?: string; // Sizes gapBetweenItems?: number; @@ -29,12 +31,15 @@ export type TreeViewProps = { hasHover?: boolean; // Default true, If this is set, it uses the hover color for the mouse over effect hasCheckboxes?: boolean; // Default is false hasIcons?: boolean; // Default is false + hasInfo?: boolean; // Default is false loadingLabel?: string; // Default is 'Loading...' loadMoreLabel?: string; // Default is 'Load more...' + maxLabelLength?: number; // Event handlers onSelectNode?: TreeNodeAction; onCheckNode?: TreeNodeAction; + onClickInfo?: TreeNodeAction; loadNodes?: LoadNodesAction; // The root node of the tree, the root is not rendered. diff --git a/react-components/src/components/Architecture/TreeView/components/TreeNodeCaret.tsx b/react-components/src/components/Architecture/TreeView/components/TreeNodeCaret.tsx index 450495b9d60..abe9341102d 100644 --- a/react-components/src/components/Architecture/TreeView/components/TreeNodeCaret.tsx +++ b/react-components/src/components/Architecture/TreeView/components/TreeNodeCaret.tsx @@ -25,7 +25,7 @@ export const TreeNodeCaret = ({ props: TreeViewProps; }): ReactElement => { const [isHoverOver, setHoverOver] = useState(false); - const color = getCaretColor(node, props, isHoverOver); + const color = getColor(node, props, isHoverOver); const size = props.caretSize ?? CARET_SIZE; const sizePx = size + 'px'; const style = { color, marginTop: '0px', width: sizePx, height: sizePx }; @@ -50,14 +50,7 @@ export const TreeNodeCaret = ({ return ; }; -function getCaretColor( - node: ITreeNode, - props: TreeViewProps, - isHoverOver: boolean -): string | undefined { - if (!node.isParent) { - return 'transparent'; - } +function getColor(node: ITreeNode, props: TreeViewProps, isHoverOver: boolean): string | undefined { if (isHoverOver) { return props.hoverCaretColor ?? HOVER_CARET_COLOR; } diff --git a/react-components/src/components/Architecture/TreeView/components/TreeViewInfo.tsx b/react-components/src/components/Architecture/TreeView/components/TreeViewInfo.tsx new file mode 100644 index 00000000000..fd60a4c7d59 --- /dev/null +++ b/react-components/src/components/Architecture/TreeView/components/TreeViewInfo.tsx @@ -0,0 +1,55 @@ +/*! + * Copyright 2024 Cognite AS + */ + +/* eslint-disable react/prop-types */ + +import { useState, type ReactElement } from 'react'; +import { type TreeViewProps } from '../TreeViewProps'; +import { type ITreeNode } from '../../../../architecture/base/treeView/ITreeNode'; +import { InfoIcon } from '@cognite/cogs.js'; +import { HOVER_INFO_COLOR, INFO_COLOR } from '../utilities/constants'; + +// ================================================== +// MAIN COMPONENT +// ================================================== + +export const TreeViewInfo = ({ + node, + props +}: { + node: ITreeNode; + props: TreeViewProps; +}): ReactElement => { + const Icon = InfoIcon; + const [isHoverOver, setHoverOver] = useState(false); + const color = getColor(props, isHoverOver); + return ( + { + onClickInfo(node); + }} + onMouseEnter={() => { + setHoverOver(true); + }} + onMouseLeave={() => { + setHoverOver(false); + }} + /> + ); + + function onClickInfo(node: ITreeNode): void { + if (props.onClickInfo === undefined) { + return; + } + props.onClickInfo(node); + } +}; + +function getColor(props: TreeViewProps, isHoverOver: boolean): string | undefined { + if (isHoverOver) { + return props.hoverInfoColor ?? HOVER_INFO_COLOR; + } + return props.infoColor ?? INFO_COLOR; +} diff --git a/react-components/src/components/Architecture/TreeView/components/TreeViewLabel.tsx b/react-components/src/components/Architecture/TreeView/components/TreeViewLabel.tsx index d28600a877b..28edadcc365 100644 --- a/react-components/src/components/Architecture/TreeView/components/TreeViewLabel.tsx +++ b/react-components/src/components/Architecture/TreeView/components/TreeViewLabel.tsx @@ -7,7 +7,7 @@ import { type ReactElement } from 'react'; import { type TreeViewProps } from '../TreeViewProps'; import { type ITreeNode } from '../../../../architecture/base/treeView/ITreeNode'; -import { LOADING_LABEL } from '../utilities/constants'; +import { LOADING_LABEL, MAX_LABEL_LENGTH } from '../utilities/constants'; // ================================================== // MAIN COMPONENT @@ -20,7 +20,16 @@ export const TreeViewLabel = ({ node: ITreeNode; props: TreeViewProps; }): ReactElement => { - const label = node.isLoadingChildren ? (props.loadingLabel ?? LOADING_LABEL) : node.label; + let label: string; + if (node.isLoadingChildren) { + label = props.loadingLabel ?? LOADING_LABEL; + } else { + label = node.label; + const maxLabelLength = props.maxLabelLength ?? MAX_LABEL_LENGTH; + if (label.length > maxLabelLength) { + label = label.substring(0, maxLabelLength) + '...'; + } + } if (node.hasBoldLabel) { return {label}; } diff --git a/react-components/src/components/Architecture/TreeView/utilities/constants.ts b/react-components/src/components/Architecture/TreeView/utilities/constants.ts index c517d9c85aa..782545c1b1b 100644 --- a/react-components/src/components/Architecture/TreeView/utilities/constants.ts +++ b/react-components/src/components/Architecture/TreeView/utilities/constants.ts @@ -12,9 +12,12 @@ export const HOVER_TEXT_COLOR = 'black'; export const HOVER_BACKGROUND_COLOR = 'lightgray'; export const CARET_COLOR = 'gray'; export const HOVER_CARET_COLOR = 'highlight'; +export const INFO_COLOR = 'black'; +export const HOVER_INFO_COLOR = 'highlight'; export const CARET_SIZE = 20; export const GAP_TO_CHILDREN = 16; export const GAP_BETWEEN_ITEMS = 4; export const LOADING_LABEL = 'Loading ...'; export const LOAD_MORE_LABEL = 'Load more ...'; export const BACKGROUND_COLOR = 'white'; +export const MAX_LABEL_LENGTH = 25; diff --git a/react-components/stories/TreeView.stories.tsx b/react-components/stories/TreeView.stories.tsx index 9646cfd37fe..ecabc6707b5 100644 --- a/react-components/stories/TreeView.stories.tsx +++ b/react-components/stories/TreeView.stories.tsx @@ -49,8 +49,11 @@ export const Main: Story = { onSelectNode={onSingleSelectNode} onCheckNode={onDependentCheckNode} loadNodes={loadNodes} + onClickInfo={onClickInfo} hasCheckboxes hasIcons + maxLabelLength={4} + hasInfo /> (); child.label = 'Leaf ' + getRandomIntByMax(1000); child.icon = i % 3 === 0 ? 'Cube' : 'CylinderHorizontal'; child.isExpanded = false; @@ -127,13 +130,14 @@ async function loadNodes( return await promise; } -function createTreeMock(lazyLoading: boolean): TreeNode { - const root = new TreeNode(); +function createTreeMock(lazyLoading: boolean): TreeNode { + const root = new TreeNode(); root.label = 'Root'; root.isExpanded = true; for (let i = 1; i <= 100; i++) { - const parent = new TreeNode(); + const parent = new TreeNode(); + parent.userData = 'Index ' + i; parent.label = 'Folder ' + i; parent.isExpanded = true; parent.icon = 'Snow'; @@ -144,7 +148,7 @@ function createTreeMock(lazyLoading: boolean): TreeNode { parent.icon = i % 2 === 0 ? 'CubeFrontRight' : 'CubeFrontLeft'; for (let j = 1; j <= 10; j++) { - const child = new TreeNode(); + const child = new TreeNode(); child.label = 'Child ' + i + '.' + j; switch (j % 3) { case 0: @@ -172,3 +176,8 @@ function createTreeMock(lazyLoading: boolean): TreeNode { } return root; } + +function onClickInfo(_node: ITreeNode): void { + // const n = _node as TreeNode; + // console.log('Info clicked: ' + n.label + 'UserData: ' + n.userData); +}