diff --git a/react/src/components/FeatureFileTree/FeatureFileTree.module.css b/react/src/components/FeatureFileTree/FeatureFileTree.module.css index e6277882..4aa27db2 100644 --- a/react/src/components/FeatureFileTree/FeatureFileTree.module.css +++ b/react/src/components/FeatureFileTree/FeatureFileTree.module.css @@ -1,36 +1,65 @@ -.tableContainer { +.root { width: 100%; height: 100%; overflow: auto; } .featureFileTree { - width: 100%; - table-layout: fixed; + /* Remove switcher space */ + :global(.ant-tree-switcher) { + display: none; + } + + :global(.ant-tree-node-content-wrapper) { + padding-left: 0; + /*https://tacc-main.atlassian.net/browse/WG-390*/ + font-size: var(--global-font-family--small); + font-family: var(--global-font-family--sans); + max-width: 200px; /* fixed width */ + } + + :global(.ant-tree-node-content-wrapper .ant-tree-title) { + display: flex !important; + flex: 1; + justify-content: flex-start !important; + text-align: left !important; + min-width: 0; + max-width: none; + } + + /* Adjust indent size */ + :global(.ant-tree-indent-unit) { + width: 6px; + } + + /* Hovering over non-selected items */ + :global(.ant-tree-node-content-wrapper:hover:not(.ant-tree-node-selected)) { + background-color: var(--global-color-accent--weak) !important; + } + + /* Selected items (both normal and hover state) */ + :global(.ant-tree-node-content-wrapper.ant-tree-node-selected) { + background-color: var(--global-color-accent--weak) !important; + } } .treeNode { display: flex; align-items: center; - height: 2.25em; + justify-content: space-between; + height: 1.5em; + width: 100%; } .fileName { flex: 1; + min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; - margin-left: 0.5em; + margin-left: 0.25em; } .deleteButton { margin-left: auto; } - -.selected { - background: var(--global-color-accent--weak) !important; -} - -.hoverable:hover { - background: var(--global-color-accent--weak); -} diff --git a/react/src/components/FeatureFileTree/FeatureFileTree.tsx b/react/src/components/FeatureFileTree/FeatureFileTree.tsx index 6fdd89df..4c084617 100644 --- a/react/src/components/FeatureFileTree/FeatureFileTree.tsx +++ b/react/src/components/FeatureFileTree/FeatureFileTree.tsx @@ -1,12 +1,13 @@ -import React, { useMemo, useCallback } from 'react'; -import { useTable, useExpanded, Column, CellProps } from 'react-table'; +import React, { useMemo, useCallback, useState } from 'react'; import { useLocation, useNavigate } from 'react-router-dom'; +import { Tree } from 'antd'; +import type { DataNode } from 'antd/es/tree'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { faFolderClosed, faFolderOpen, } from '@fortawesome/free-solid-svg-icons'; - +import { useResizeDetector } from 'react-resize-detector'; import { Button } from '@tacc/core-components'; import { featureCollectionToFileNodeArray } from '@hazmapper/utils/featureTreeUtils'; import { FeatureCollection, FeatureFileNode } from '@hazmapper/types'; @@ -14,6 +15,13 @@ import { useDeleteFeature } from '@hazmapper/hooks'; import { FeatureIcon } from '@hazmapper/components/FeatureIcon'; import styles from './FeatureFileTree.module.css'; +/* interface for combining antd tree node and our tree data */ +interface TreeDataNode extends DataNode { + title?: string; + key: string; + children?: TreeDataNode[]; + featureNode: FeatureFileNode; +} interface FeatureFileTreeProps { /** * Features of map @@ -43,81 +51,48 @@ const FeatureFileTree: React.FC = ({ const location = useLocation(); const navigate = useNavigate(); + const { height, ref } = useResizeDetector(); + + const [expanded, setExpanded] = useState([]); + const searchParams = new URLSearchParams(location.search); const selectedFeature = searchParams.get('selectedFeature'); - const memoizedData = useMemo( - () => featureCollectionToFileNodeArray(featureCollection), - [featureCollection] - ); - - const columns = useMemo[]>( - () => [ - { - accessor: 'name', - Cell: ({ row }: CellProps) => ( -
- {row.original.isDirectory ? ( - - ) : ( - - )} - {row.original.name} - {!isPublic && row.id === selectedFeature && ( -
- ), - }, - ], - [isPublic, selectedFeature] - ); + const treeData = useMemo(() => { + const fileNodeArray = featureCollectionToFileNodeArray(featureCollection); + + const getDirectoryNodeIds = (nodes: FeatureFileNode[]): string[] => { + const directoryIds: string[] = []; + const stack = [...nodes]; + + while (stack.length > 0) { + const node = stack.pop(); + if (node && node.isDirectory) { + directoryIds.push(node.nodeId); + if (node.children) { + stack.push(...node.children); + } + } + } - const expandedState = useMemo(() => { - const expanded: { [key: string]: boolean } = {}; - const expandRow = (row: FeatureFileNode) => { - /* eslint-disable react/prop-types */ - expanded[row.nodeId] = true; - row.children?.forEach(expandRow); - /* eslint-enable react/prop-types */ + return directoryIds; }; - memoizedData.forEach(expandRow); - return expanded; - }, [memoizedData]); - - const { getTableProps, getTableBodyProps, rows, prepareRow } = - useTable( - { - columns, - data: memoizedData, - /* eslint-disable react/prop-types */ - getSubRows: (row: FeatureFileNode) => row.children, - getRowId: (row: FeatureFileNode) => row.nodeId, - /* eslint-enable react/prop-types */ - initialState: { - expanded: expandedState, - }, - autoResetExpanded: true, - }, - useExpanded - ); + + const expandedDirectories = getDirectoryNodeIds(fileNodeArray); + // Have all direcotories be in 'expanded' (i.e. everything is expanded) + setExpanded(expandedDirectories); + + const convertToTreeNode = (node: FeatureFileNode) => ({ + title: node.name, + key: node.nodeId, + isLeaf: !node.isDirectory, + children: node.children?.map(convertToTreeNode), + featureNode: node, + }); + + // Convert features into Antd's DataNode (i.e. TreeDataNode) and our internal class FeatureFileNode + return fileNodeArray.map(convertToTreeNode); + }, [featureCollection]); const handleDelete = useCallback( (nodeId: string) => (e: React.MouseEvent) => { @@ -140,50 +115,74 @@ const FeatureFileTree: React.FC = ({ [projectId, deleteFeature] ); - const handleRowClick = (rowId: string) => { - const newSearchParams = new URLSearchParams(searchParams); - if (selectedFeature === rowId || rowId.startsWith('DIR_')) { - newSearchParams.delete('selectedFeature'); - } else { - newSearchParams.set('selectedFeature', rowId); - } - navigate({ search: newSearchParams.toString() }, { replace: true }); - }; + const titleRender = useCallback( + (node: TreeDataNode) => { + const featureNode = node.featureNode as FeatureFileNode; + const isSelected = + selectedFeature === node.key && !featureNode.isDirectory; + const isExpanded = expanded.includes(node.key); + + // Add click handler for directory nodes + const handleClick = (e: React.MouseEvent) => { + if (featureNode.isDirectory) { + e.stopPropagation(); // Prevent default Tree selection + // Toggle expanded state + const newExpanded = expanded.includes(node.key) + ? expanded.filter((k) => k !== node.key) + : [...expanded, node.key]; + setExpanded(newExpanded); + } else { + const newSearchParams = new URLSearchParams(searchParams); + if (selectedFeature === node.key || node.key.startsWith('DIR_')) { + newSearchParams.delete('selectedFeature'); + } else { + newSearchParams.set('selectedFeature', node.key); + } + navigate({ search: newSearchParams.toString() }, { replace: true }); + } + }; + + return ( +
+ {featureNode.isDirectory ? ( + + ) : ( + + )} + {featureNode.name} + {!isPublic && isSelected && ( +
+ ); + }, + [expanded, setExpanded, selectedFeature, isPublic, isLoading, handleDelete] + ); return ( -
- - - {rows.map((row) => { - prepareRow(row); - /* eslint-disable react/prop-types */ - const isSelected = row.original.isDirectory - ? false - : selectedFeature === row.id; - return ( - handleRowClick(row.id)} - tabIndex={0} - > - {row.cells.map((cell) => ( - - ))} - - ); - /* eslint-enable react/prop-types */ - })} - -
- {cell.render('Cell')} -
+
+
); }; diff --git a/react/src/components/FeatureIcon/FeatureIcon.tsx b/react/src/components/FeatureIcon/FeatureIcon.tsx index ff46a527..e4bcaf66 100644 --- a/react/src/components/FeatureIcon/FeatureIcon.tsx +++ b/react/src/components/FeatureIcon/FeatureIcon.tsx @@ -7,21 +7,22 @@ import { faClipboardList, faMapMarkerAlt, faDrawPolygon, - faBezierCurve, faCloud, + faBezierCurve, faRoad, faLayerGroup, faQuestionCircle, } from '@fortawesome/free-solid-svg-icons'; import { FeatureType, FeatureTypeNullable } from '@hazmapper/types'; +import styles from './FeatureIcon.module.css'; const featureTypeToIcon: Record = { // Asset types image: faCameraRetro, video: faVideo, questionnaire: faClipboardList, - point_cloud: faCloud, + point_cloud: faCloud /* https://tacc-main.atlassian.net/browse/WG-391 */, streetview: faRoad, // Geometry types @@ -44,5 +45,5 @@ interface Props { export const FeatureIcon: React.FC = ({ featureType }) => { const icon = featureType ? featureTypeToIcon[featureType] : faQuestionCircle; - return ; + return ; }; diff --git a/react/src/components/MapProjectNavBar/MapProjectNavBar.tsx b/react/src/components/MapProjectNavBar/MapProjectNavBar.tsx index 74729f36..04fcb065 100644 --- a/react/src/components/MapProjectNavBar/MapProjectNavBar.tsx +++ b/react/src/components/MapProjectNavBar/MapProjectNavBar.tsx @@ -27,7 +27,8 @@ const navItems: NavItem[] = [ }, { label: 'Point Clouds', - imagePath: pointCloudImage, + imagePath: + pointCloudImage /* https://tacc-main.atlassian.net/browse/WG-391 */, panel: Panel.PointClouds, showWhenPublic: false, },