From 31773ee73bb7271b76ba923f6edebf69df22ad82 Mon Sep 17 00:00:00 2001 From: Ashley Stanton-Nurse Date: Thu, 1 Aug 2024 10:02:36 -0700 Subject: [PATCH] Redesign resource tree (#1865) * start redesign work * add left padding to all tree nodes * fiddling with padding * align tab bar line with first item in resource tree * final touch ups * fix a strange password manager autofill prompt * add keyboard shortcuts * revert testing change * nudge messagebar to layout row height * tidy up * switch to Allotment to stop ResizeObserver issues with monaco * refmt and fix lints * fabric touch-ups * update snapshots * remove explicit react-icons dependency * reinstall packages * remove background from FluentProvider * fix alignment of message bar * undo temporary workaround * restore refresh button * fix e2e tests and reformat * fix compiler error * remove uiw/react-split * uncomment selection change on expand --- .npmrc | 5 +- less/Common/Constants.less | 2 +- less/documentDB.less | 34 +- less/documentDBFabric.less | 2 +- less/resourceTree.less | 13 - package-lock.json | 55 +- package.json | 4 +- patches/@uiw+react-split+5.9.3.patch | 11 - src/Common/CollapsedResourceTree.tsx | 55 - src/Common/ResourceTreeContainer.tsx | 82 -- src/Explorer/Controls/TreeComponent/Styles.ts | 65 ++ .../TreeComponent/TreeNodeComponent.test.tsx | 30 +- .../TreeComponent/TreeNodeComponent.tsx | 91 +- .../TreeNodeComponent.test.tsx.snap | 1016 ++++++----------- src/Explorer/Explorer.tsx | 8 +- .../CommandBarComponentButtonFactory.tsx | 103 +- .../AddDatabasePanel/AddDatabasePanel.tsx | 3 + .../AddDatabasePanel.test.tsx.snap | 2 + src/Explorer/Sidebar.tsx | 312 +++++ .../Tabs/DocumentsTabV2/DocumentsTabV2.tsx | 335 +++--- .../DocumentsTableComponent.tsx | 28 +- .../DocumentsTabV2.test.tsx.snap | 606 ++-------- .../DocumentsTableComponent.test.tsx.snap | 194 ++-- src/Explorer/Tabs/Tabs.tsx | 43 +- src/Explorer/Theme/ThemeUtil.ts | 31 - src/Explorer/Theme/ThemeUtil.tsx | 95 ++ src/Explorer/Tree/Database.tsx | 9 +- src/Explorer/Tree/ResourceTree.tsx | 122 +- .../__snapshots__/treeNodeUtil.test.ts.snap | 124 +- src/Explorer/Tree/treeNodeUtil.ts | 19 +- src/KeyboardShortcuts.tsx | 5 +- src/Main.tsx | 42 +- src/hooks/useTabs.ts | 7 +- test/cassandra/container.spec.ts | 6 +- test/fx.ts | 30 +- test/gremlin/container.spec.ts | 6 +- test/mongo/container.spec.ts | 6 +- test/sql/container.spec.ts | 6 +- test/sql/resourceToken.spec.ts | 2 +- test/tables/container.spec.ts | 6 +- tsconfig.strict.json | 3 +- 41 files changed, 1551 insertions(+), 2067 deletions(-) delete mode 100644 patches/@uiw+react-split+5.9.3.patch delete mode 100644 src/Common/CollapsedResourceTree.tsx delete mode 100644 src/Common/ResourceTreeContainer.tsx create mode 100644 src/Explorer/Controls/TreeComponent/Styles.ts create mode 100644 src/Explorer/Sidebar.tsx delete mode 100644 src/Explorer/Theme/ThemeUtil.ts create mode 100644 src/Explorer/Theme/ThemeUtil.tsx diff --git a/.npmrc b/.npmrc index 449691b70..bcfa17a91 100644 --- a/.npmrc +++ b/.npmrc @@ -1 +1,4 @@ -save-exact=true \ No newline at end of file +save-exact=true + +# Ignore peer dependency conflicts +force=true # TODO: Remove this when we update to React 17 or higher! \ No newline at end of file diff --git a/less/Common/Constants.less b/less/Common/Constants.less index d79c3e4e0..8c3a66b99 100644 --- a/less/Common/Constants.less +++ b/less/Common/Constants.less @@ -168,7 +168,7 @@ @FabricBoxBorderRadius: 8px; @FabricBoxBorderShadow: rgba(0, 0, 0, 0.133) 0px 1.6px 3.6px 0px, rgba(0, 0, 0, 0.11) 0px 0.3px 0.9px 0px; -@FabricBoxMargin: 4px 3px 4px 3px; +@FabricBoxMargin: 4px 8px 4px 8px; @FabricAccentMediumHigh: #0c695a; @FabricAccentMedium: #117865; diff --git a/less/documentDB.less b/less/documentDB.less index d9c0a61cb..125bb1143 100644 --- a/less/documentDB.less +++ b/less/documentDB.less @@ -1906,7 +1906,7 @@ input::-webkit-calendar-picker-indicator::after { } .nav-tabs-margin { - padding-top: 8px; + padding-top: 5px; background-color: #f2f2f2; } @@ -2074,14 +2074,6 @@ a:link { display: inline; } -.resourceTreeAndTabs { - display: flex; - flex: 1 1 auto; - overflow-x: clip; - overflow-y: auto; - height: 100%; -} - .collectiontitle { font-size: 14px; text-transform: uppercase; @@ -2325,11 +2317,6 @@ td a:hover { outline: 1px dotted; } -#content.active .tabdocuments .scrollable { - height: 100%; - overflow-y: auto; -} - .table-fixed thead { width: 97%; padding-left: 18px; @@ -2365,10 +2352,8 @@ a:link { .tabsManagerContainer { height: 100%; - flex-grow: 1; - display: flex; - flex-direction: column; - min-height: 300px; + display: grid; + grid-template-rows: 36px 36px 1fr; min-width: 0; // This prevents it to grow past the parent's width if its content is too wide } @@ -2580,18 +2565,6 @@ a:link { cursor: pointer; } -.documentsTab { - .documentsTable { - .documentsTableCell { - border-left: 1px solid @BaseMedium; - height: 100%; - } - .documentsTableHeader { - border-bottom: 1px solid @BaseMedium; - } - } -} - .querydropdown { border: 1px solid @BaseMedium; font-style: normal; @@ -2637,6 +2610,7 @@ a:link { } .tabPanesContainer { + grid-row: span 2; // Fill the remaining space display: flex; height: 100%; overflow: hidden; diff --git a/less/documentDBFabric.less b/less/documentDBFabric.less index ea4001780..5f89f72ea 100644 --- a/less/documentDBFabric.less +++ b/less/documentDBFabric.less @@ -38,7 +38,7 @@ a:focus { } .nav-tabs-margin { - padding-top: 8px; + padding-top: 5px; background-color: #ffffff } diff --git a/less/resourceTree.less b/less/resourceTree.less index 6144a9b3f..3469e8594 100644 --- a/less/resourceTree.less +++ b/less/resourceTree.less @@ -3,19 +3,6 @@ .dataResourceTree { margin-left: @MediumSpace; overflow: auto; - - .databaseHeader { - padding: 1px; - font-size: 14px; - } - - .collectionHeader { - font-size: 12px; - } - - .loadMoreHeader { - color: RGB(5, 99, 193); - } } .notebookResourceTree { diff --git a/package-lock.json b/package-lock.json index 09f09716b..af43decbf 100644 --- a/package-lock.json +++ b/package-lock.json @@ -51,8 +51,8 @@ "@types/lodash": "4.14.171", "@types/mkdirp": "1.0.1", "@types/node-fetch": "2.5.7", - "@uiw/react-split": "5.9.3", "@xmldom/xmldom": "0.7.13", + "allotment": "1.20.2", "applicationinsights": "1.8.0", "bootstrap": "3.4.1", "canvas": "2.11.2", @@ -13063,17 +13063,6 @@ "url": "https://opencollective.com/typescript-eslint" } }, - "node_modules/@uiw/react-split": { - "version": "5.9.3", - "license": "MIT", - "funding": { - "url": "https://jaywcjlove.github.io/#/sponsor" - }, - "peerDependencies": { - "react": ">=16.8.0", - "react-dom": ">=16.8.0" - } - }, "node_modules/@ungap/url-search-params": { "version": "0.2.2", "license": "ISC" @@ -13496,6 +13485,28 @@ "ajv": "^6.9.1" } }, + "node_modules/allotment": { + "version": "1.20.2", + "resolved": "https://registry.npmjs.org/allotment/-/allotment-1.20.2.tgz", + "integrity": "sha512-TaCuHfYNcsJS9EPk04M7TlG5Rl3vbAdHeAyrTE9D5vbpzV+wxnRoUrulDbfnzaQcPIZKpHJNixDOoZNuzliKEA==", + "dependencies": { + "classnames": "^2.3.0", + "eventemitter3": "^5.0.0", + "lodash.clamp": "^4.0.0", + "lodash.debounce": "^4.0.0", + "lodash.isequal": "^4.5.0", + "use-resize-observer": "^9.0.0" + }, + "peerDependencies": { + "react": "^17.0.0 || ^18.0.0", + "react-dom": "^17.0.0 || ^18.0.0" + } + }, + "node_modules/allotment/node_modules/eventemitter3": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.1.tgz", + "integrity": "sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==" + }, "node_modules/anser": { "version": "1.4.10", "license": "MIT" @@ -27962,6 +27973,11 @@ "version": "4.3.0", "license": "MIT" }, + "node_modules/lodash.clamp": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/lodash.clamp/-/lodash.clamp-4.0.3.tgz", + "integrity": "sha512-HvzRFWjtcguTW7yd8NJBshuNaCa8aqNFtnswdT7f/cMd/1YKy5Zzoq4W/Oxvnx9l7aeY258uSdDfM793+eLsVg==" + }, "node_modules/lodash.clonedeep": { "version": "4.5.0", "license": "MIT" @@ -28012,7 +28028,8 @@ }, "node_modules/lodash.isequal": { "version": "4.5.0", - "dev": true, + "resolved": "https://registry.npmjs.org/lodash.isequal/-/lodash.isequal-4.5.0.tgz", + "integrity": "sha512-pDo3lu8Jhfjqls6GkMgpahsF9kCyayhgykjyLMNFTKWrpVdAQtYyB4muAMWozBB4ig/dtWAmsMxLEI8wuz+DYQ==", "license": "MIT" }, "node_modules/lodash.isinteger": { @@ -35658,6 +35675,18 @@ "react-dom": ">=16.8.0 <19.0.0" } }, + "node_modules/use-resize-observer": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/use-resize-observer/-/use-resize-observer-9.1.0.tgz", + "integrity": "sha512-R25VqO9Wb3asSD4eqtcxk8sJalvIOYBqS8MNZlpDSQ4l4xMQxC/J7Id9HoTqPq8FwULIn0PVW+OAqF2dyYbjow==", + "dependencies": { + "@juggle/resize-observer": "^3.3.1" + }, + "peerDependencies": { + "react": "16.8.0 - 18", + "react-dom": "16.8.0 - 18" + } + }, "node_modules/use-sync-external-store": { "version": "1.2.0", "license": "MIT", diff --git a/package.json b/package.json index c6e0f963d..d174a7674 100644 --- a/package.json +++ b/package.json @@ -46,8 +46,8 @@ "@types/lodash": "4.14.171", "@types/mkdirp": "1.0.1", "@types/node-fetch": "2.5.7", - "@uiw/react-split": "5.9.3", "@xmldom/xmldom": "0.7.13", + "allotment": "1.20.2", "applicationinsights": "1.8.0", "bootstrap": "3.4.1", "canvas": "2.11.2", @@ -98,8 +98,8 @@ "react-redux": "7.1.3", "react-splitter-layout": "4.0.0", "react-string-format": "1.0.1", - "react-youtube": "9.0.1", "react-window": "1.8.10", + "react-youtube": "9.0.1", "reflect-metadata": "0.1.13", "rx-jupyter": "5.5.12", "sanitize-html": "2.3.3", diff --git a/patches/@uiw+react-split+5.9.3.patch b/patches/@uiw+react-split+5.9.3.patch deleted file mode 100644 index 3e5307463..000000000 --- a/patches/@uiw+react-split+5.9.3.patch +++ /dev/null @@ -1,11 +0,0 @@ -diff --git a/node_modules/@uiw/react-split/cjs/index.d.ts b/node_modules/@uiw/react-split/cjs/index.d.ts -index 644bcc3..f794760 100644 ---- a/node_modules/@uiw/react-split/cjs/index.d.ts -+++ b/node_modules/@uiw/react-split/cjs/index.d.ts -@@ -56,5 +56,5 @@ export default class Split extends React.Component { - onMouseDown(paneNumber: number, env: React.MouseEvent): void; - onDragging(env: Event): void; - onDragEnd(): void; -- render(): import("react/jsx-runtime").JSX.Element; -+ render(): JSX.Element; - } diff --git a/src/Common/CollapsedResourceTree.tsx b/src/Common/CollapsedResourceTree.tsx deleted file mode 100644 index 0247c7012..000000000 --- a/src/Common/CollapsedResourceTree.tsx +++ /dev/null @@ -1,55 +0,0 @@ -import React, { FunctionComponent, MutableRefObject, useEffect, useRef } from "react"; -import arrowLeftImg from "../../images/imgarrowlefticon.svg"; -import { getApiShortDisplayName } from "../Utils/APITypeUtils"; -import { NormalizedEventKey } from "./Constants"; - -export interface CollapsedResourceTreeProps { - toggleLeftPaneExpanded: () => void; - isLeftPaneExpanded: boolean; -} - -export const CollapsedResourceTree: FunctionComponent = ({ - toggleLeftPaneExpanded, - isLeftPaneExpanded, -}: CollapsedResourceTreeProps): JSX.Element => { - const focusButton = useRef() as MutableRefObject; - - useEffect(() => { - if (focusButton.current) { - focusButton.current.focus(); - } - }); - - const onKeyPressToggleLeftPaneExpanded = (event: React.KeyboardEvent) => { - if (event.key === NormalizedEventKey.Space || event.key === NormalizedEventKey.Enter) { - toggleLeftPaneExpanded(); - event.stopPropagation(); - } - }; - - return ( -
-
-
    -
  • - - Expand - - - {getApiShortDisplayName()} - -
  • -
-
-
- ); -}; diff --git a/src/Common/ResourceTreeContainer.tsx b/src/Common/ResourceTreeContainer.tsx deleted file mode 100644 index 0259c42e2..000000000 --- a/src/Common/ResourceTreeContainer.tsx +++ /dev/null @@ -1,82 +0,0 @@ -import React, { FunctionComponent, MutableRefObject, useEffect, useRef } from "react"; -import arrowLeftImg from "../../images/imgarrowlefticon.svg"; -import refreshImg from "../../images/refresh-cosmos.svg"; -import Explorer from "../Explorer/Explorer"; -import { ResourceTree } from "../Explorer/Tree/ResourceTree"; -import { userContext } from "../UserContext"; -import { getApiShortDisplayName } from "../Utils/APITypeUtils"; -import { NormalizedEventKey } from "./Constants"; - -export interface ResourceTreeContainerProps { - toggleLeftPaneExpanded: () => void; - isLeftPaneExpanded: boolean; - container: Explorer; -} - -export const ResourceTreeContainer: FunctionComponent = ({ - toggleLeftPaneExpanded, - isLeftPaneExpanded, - container, -}: ResourceTreeContainerProps): JSX.Element => { - const focusButton = useRef() as MutableRefObject; - - useEffect(() => { - if (isLeftPaneExpanded) { - if (focusButton.current) { - focusButton.current.focus(); - } - } - }); - - const onKeyPressToggleLeftPaneExpanded = (event: React.KeyboardEvent) => { - if (event.key === NormalizedEventKey.Space || event.key === NormalizedEventKey.Enter) { - toggleLeftPaneExpanded(); - event.stopPropagation(); - } - }; - return ( -
- {/* Collections Window - - Start */} -
- {/* Collections Window Title/Command Bar - Start */} -
-
- {getApiShortDisplayName()} -
- - Refresh Tree - - - Hide - -
-
-
- {userContext.features.enableKoResourceTree ? ( -
- ) : ( - - )} -
- {/* Collections Window - End */} -
- ); -}; diff --git a/src/Explorer/Controls/TreeComponent/Styles.ts b/src/Explorer/Controls/TreeComponent/Styles.ts new file mode 100644 index 000000000..7fcdb7ef7 --- /dev/null +++ b/src/Explorer/Controls/TreeComponent/Styles.ts @@ -0,0 +1,65 @@ +import { makeStyles, shorthands, treeItemLevelToken } from "@fluentui/react-components"; +import { cosmosShorthands, tokens } from "Explorer/Theme/ThemeUtil"; + +export type TreeStyleName = keyof ReturnType; + +const treeIconWidth = "--cosmos-Tree--iconWidth" as const; +const leafNodeSpacing = "--cosmos-Tree--leafNodeSpacing" as const; +const actionButtonBackground = "--cosmos-Tree--actionButtonBackground" as const; + +export const useTreeStyles = makeStyles({ + treeContainer: { + height: "100%", + ...shorthands.overflow("auto"), + }, + tree: { + width: "fit-content", + minWidth: "100%", + rowGap: "0px", + paddingTop: "0px", + [treeIconWidth]: "20px", + [leafNodeSpacing]: "24px", + }, + nodeIcon: { + width: `var(${treeIconWidth})`, + height: `var(${treeIconWidth})`, + }, + treeItem: {}, + nodeLabel: {}, + treeItemLayout: { + fontSize: tokens.fontSizeBase300, + height: tokens.layoutRowHeight, + ...cosmosShorthands.borderBottom(), + paddingLeft: `calc(var(${treeItemLevelToken}, 1) * ${tokens.spacingHorizontalXXL})`, + + // Some sneaky CSS variables stuff to change the background color of the action button on hover. + [actionButtonBackground]: tokens.colorNeutralBackground1, + "&:hover": { + [actionButtonBackground]: tokens.colorNeutralBackground1Hover, + }, + }, + actionsButtonContainer: { + position: "sticky", + right: 0, + }, + actionsButton: { + backgroundColor: `var(${actionButtonBackground})`, + }, + treeItemLayoutNoIcon: { + // Pad the text out by the level, the width of the icon, AND the usual spacing between the icon and the level. + // It would be nice to see if we can use Grid layout or something here, but that would require overriding a lot of the existing Tree component behavior. + paddingLeft: `calc((var(${treeItemLevelToken}, 1) * ${tokens.spacingHorizontalXXL}) + var(${leafNodeSpacing}))`, + }, + selectedItem: { + backgroundColor: tokens.colorNeutralBackground1Selected, + }, + databaseNode: { + fontWeight: tokens.fontWeightSemibold, + }, + collectionNode: { + fontWeight: tokens.fontWeightSemibold, + }, + loadMoreNode: { + color: tokens.colorBrandForegroundLink, + }, +}); diff --git a/src/Explorer/Controls/TreeComponent/TreeNodeComponent.test.tsx b/src/Explorer/Controls/TreeComponent/TreeNodeComponent.test.tsx index e0cdf5700..2b12de615 100644 --- a/src/Explorer/Controls/TreeComponent/TreeNodeComponent.test.tsx +++ b/src/Explorer/Controls/TreeComponent/TreeNodeComponent.test.tsx @@ -1,4 +1,4 @@ -import { TreeItem, TreeItemLayout, tokens } from "@fluentui/react-components"; +import { TreeItem, TreeItemLayout } from "@fluentui/react-components"; import PromiseSource from "Utils/PromiseSource"; import { mount, shallow } from "enzyme"; import React from "react"; @@ -9,7 +9,7 @@ function generateTestNode(id: string, additionalProps?: Partial): Tree const node: TreeNode = { id, label: `${id}Label`, - className: `${id}Class`, + className: "nodeIcon", iconSrc: `${id}Icon`, onClick: jest.fn().mockName(`${id}Click`), ...additionalProps, @@ -20,7 +20,7 @@ function generateTestNode(id: string, additionalProps?: Partial): Tree describe("TreeNodeComponent", () => { it("renders a single node", () => { const node = generateTestNode("root"); - const component = shallow(); + const component = shallow(); expect(component).toMatchSnapshot(); // The "click" handler is actually attached to onOpenChange, with a type of "Click". @@ -45,7 +45,7 @@ describe("TreeNodeComponent", () => { }, ], }); - const component = shallow(); + const component = shallow(); expect(component).toMatchSnapshot(); }); @@ -54,7 +54,7 @@ describe("TreeNodeComponent", () => { const node = generateTestNode("root", { onExpanded: () => loading.promise, }); - const component = shallow(); + const component = shallow(); act(() => { component @@ -72,10 +72,7 @@ describe("TreeNodeComponent", () => { const node = generateTestNode("root", { isSelected: () => true, }); - const component = shallow(); - expect(component.find(TreeItemLayout).props().style?.backgroundColor).toStrictEqual( - tokens.colorNeutralBackground1Selected, - ); + const component = shallow(); expect(component).toMatchSnapshot(); }); @@ -89,10 +86,7 @@ describe("TreeNodeComponent", () => { generateTestNode("child2"), ], }); - const component = shallow(); - expect(component.find(TreeItemLayout).props().style?.backgroundColor).toStrictEqual( - tokens.colorNeutralBackground1Selected, - ); + const component = shallow(); expect(component).toMatchSnapshot(); }); @@ -111,7 +105,7 @@ describe("TreeNodeComponent", () => { generateTestNode("child2"), ], }); - const component = shallow(); + const component = shallow(); expect(component.find(TreeItemLayout).props().style?.backgroundColor).toBeUndefined(); expect(component).toMatchSnapshot(); }); @@ -120,7 +114,7 @@ describe("TreeNodeComponent", () => { const node = generateTestNode("root", { iconSrc: "the-icon.svg", }); - const component = shallow(); + const component = shallow(); expect(component).toMatchSnapshot(); }); @@ -134,7 +128,7 @@ describe("TreeNodeComponent", () => { generateTestNode("child2"), ], }); - const component = shallow(); + const component = shallow(); expect(component).toMatchSnapshot(); }); @@ -148,7 +142,7 @@ describe("TreeNodeComponent", () => { generateTestNode("child2"), ], }); - const component = shallow(); + const component = shallow(); expect(component).toMatchSnapshot(); }); @@ -175,7 +169,7 @@ describe("TreeNodeComponent", () => { }), ], }); - const component = mount(); + const component = mount(); // Find and expand the child3Expanding node const expandingChild = component.find(TreeItem).filterWhere((n) => n.props().value === "root/child3ExpandingLabel"); diff --git a/src/Explorer/Controls/TreeComponent/TreeNodeComponent.tsx b/src/Explorer/Controls/TreeComponent/TreeNodeComponent.tsx index 9ec2c904a..bebf23012 100644 --- a/src/Explorer/Controls/TreeComponent/TreeNodeComponent.tsx +++ b/src/Explorer/Controls/TreeComponent/TreeNodeComponent.tsx @@ -11,11 +11,13 @@ import { Tree, TreeItem, TreeItemLayout, + TreeItemValue, TreeOpenChangeData, TreeOpenChangeEvent, + mergeClasses, } from "@fluentui/react-components"; -import { MoreHorizontal20Regular } from "@fluentui/react-icons"; -import { tokens } from "@fluentui/react-theme"; +import { ChevronDown20Regular, ChevronRight20Regular, MoreHorizontal20Regular } from "@fluentui/react-icons"; +import { TreeStyleName, useTreeStyles } from "Explorer/Controls/TreeComponent/Styles"; import * as React from "react"; import { useCallback } from "react"; @@ -32,15 +34,14 @@ export interface TreeNode { id?: string; children?: TreeNode[]; contextMenu?: TreeNodeMenuItem[]; - iconSrc?: string; + iconSrc?: string | JSX.Element; isExpanded?: boolean; - className?: string; + className?: TreeStyleName; isAlphaSorted?: boolean; // data?: any; // Piece of data corresponding to this node timestamp?: number; isLeavesParentsSeparate?: boolean; // Display parents together first, then leaves isLoading?: boolean; - isScrollable?: boolean; isSelected?: () => boolean; onClick?: () => void; // Only if a leaf, other click will expand/collapse onExpanded?: () => Promise; @@ -52,6 +53,7 @@ export interface TreeNodeComponentProps { node: TreeNode; className?: string; treeNodeId: string; + openItems: TreeItemValue[]; } /** Function that returns true if any descendant (at any depth) of this node is selected. */ @@ -66,13 +68,13 @@ function isAnyDescendantSelected(node: TreeNode): boolean { ); } -const getTreeIcon = (iconSrc: string): JSX.Element => ; - export const TreeNodeComponent: React.FC = ({ node, treeNodeId, + openItems, }: TreeNodeComponentProps): JSX.Element => { const [isLoading, setIsLoading] = React.useState(false); + const treeStyles = useTreeStyles(); const getSortedChildren = (treeNode: TreeNode): TreeNode[] => { if (!treeNode || !treeNode.children) { @@ -145,45 +147,72 @@ export const TreeNodeComponent: React.FC = ({ )); + // We use the expandIcon slot to hold the node icon too. + // We only show a node icon for leaf nodes, even if a branch node has an iconSrc. + const expandIcon = isLoading ? ( + + ) : !isBranch ? ( + typeof node.iconSrc === "string" ? ( + + ) : ( + node.iconSrc + ) + ) : openItems.includes(treeNodeId) ? ( + + ) : ( + + ); + const treeItem = ( 0 && ( - - - - ) + contextMenuItems.length > 0 && { + className: treeStyles.actionsButtonContainer, + children: ( + + + + ), + } } - expandIcon={isLoading ? : undefined} - iconBefore={node.iconSrc && getTreeIcon(node.iconSrc)} - style={{ - backgroundColor: shouldShowAsSelected ? tokens.colorNeutralBackground1Selected : undefined, - }} + expandIcon={expandIcon} > - {node.label} + {node.label} {!node.isLoading && node.children?.length > 0 && ( - + {getSortedChildren(node).map((childNode: TreeNode) => ( - + ))} )} diff --git a/src/Explorer/Controls/TreeComponent/__snapshots__/TreeNodeComponent.test.tsx.snap b/src/Explorer/Controls/TreeComponent/__snapshots__/TreeNodeComponent.test.tsx.snap index ed547eb1f..bbd8c93c4 100644 --- a/src/Explorer/Controls/TreeComponent/__snapshots__/TreeNodeComponent.test.tsx.snap +++ b/src/Explorer/Controls/TreeComponent/__snapshots__/TreeNodeComponent.test.tsx.snap @@ -2,6 +2,7 @@ exports[`TreeNodeComponent does not render children if the node is loading 1`] = ` - } - style={ - { - "backgroundColor": undefined, - } - } + expandIcon={} > - rootLabel + + rootLabel + `; @@ -42,7 +31,7 @@ exports[`TreeNodeComponent fully renders a tree 1`] = ` { "children": [ { - "className": "grandchild1Class", + "className": "nodeIcon", "iconSrc": "grandchild1Icon.svg", "id": "grandchild1", "isSelected": [Function], @@ -50,14 +39,14 @@ exports[`TreeNodeComponent fully renders a tree 1`] = ` "onClick": [MockFunction grandchild1Click], }, { - "className": "grandchild2Class", + "className": "nodeIcon", "iconSrc": "grandchild2Icon", "id": "grandchild2", "label": "grandchild2Label", "onClick": [MockFunction grandchild2Click], }, ], - "className": "child1Class", + "className": "nodeIcon", "iconSrc": "child1Icon", "id": "child1", "label": "child1Label", @@ -66,14 +55,14 @@ exports[`TreeNodeComponent fully renders a tree 1`] = ` { "children": [ { - "className": "grandchild3NotRenderedClass", + "className": "nodeIcon", "iconSrc": "grandchild3NotRenderedIcon", "id": "grandchild3NotRendered", "label": "grandchild3NotRenderedLabel", "onClick": [MockFunction grandchild3NotRenderedClick], }, ], - "className": "child2LoadingClass", + "className": "nodeIcon", "iconSrc": "child2LoadingIcon", "id": "child2Loading", "isLoading": true, @@ -81,7 +70,7 @@ exports[`TreeNodeComponent fully renders a tree 1`] = ` "onClick": [MockFunction child2LoadingClick], }, { - "className": "child3ExpandingClass", + "className": "nodeIcon", "iconSrc": "child3ExpandingIcon", "id": "child3Expanding", "label": "child3ExpandingLabel", @@ -99,7 +88,7 @@ exports[`TreeNodeComponent fully renders a tree 1`] = ` "onExpanded": [Function], }, ], - "className": "rootClass", + "className": "nodeIcon", "iconSrc": "rootIcon", "id": "root", "isSelected": [Function], @@ -107,9 +96,11 @@ exports[`TreeNodeComponent fully renders a tree 1`] = ` "onClick": [MockFunction rootClick], } } + openItems={[]} treeNodeId="root" >
@@ -417,97 +373,48 @@ exports[`TreeNodeComponent fully renders a tree 1`] = ` > - } - style={ - { - "backgroundColor": undefined, - } - } + expandIcon={} >
- - + - - - - - -
-
- + /> + +
- rootLabel + + rootLabel +
, @@ -769,97 +658,48 @@ exports[`TreeNodeComponent fully renders a tree 1`] = ` > - } - style={ - { - "backgroundColor": undefined, - } - } + expandIcon={} >
- - + - - - - - -
-
- + /> + +
- child1Label + + child1Label +