Skip to content

Commit

Permalink
Switch to antd tree instead of react-table
Browse files Browse the repository at this point in the history
  • Loading branch information
nathanfranklin committed Oct 31, 2024
1 parent fba6261 commit c3724f0
Show file tree
Hide file tree
Showing 4 changed files with 162 additions and 132 deletions.
55 changes: 42 additions & 13 deletions react/src/components/FeatureFileTree/FeatureFileTree.module.css
Original file line number Diff line number Diff line change
@@ -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);
}
229 changes: 114 additions & 115 deletions react/src/components/FeatureFileTree/FeatureFileTree.tsx
Original file line number Diff line number Diff line change
@@ -1,19 +1,27 @@
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';
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
Expand Down Expand Up @@ -43,81 +51,48 @@ const FeatureFileTree: React.FC<FeatureFileTreeProps> = ({
const location = useLocation();
const navigate = useNavigate();

const { height, ref } = useResizeDetector();

const [expanded, setExpanded] = useState<string[]>([]);

const searchParams = new URLSearchParams(location.search);
const selectedFeature = searchParams.get('selectedFeature');

const memoizedData = useMemo(
() => featureCollectionToFileNodeArray(featureCollection),
[featureCollection]
);

const columns = useMemo<Column<FeatureFileNode>[]>(
() => [
{
accessor: 'name',
Cell: ({ row }: CellProps<FeatureFileNode>) => (
<div
{...row.getToggleRowExpandedProps()}
className={styles.treeNode}
style={{
/* Create indentation based on the row's depth in the tree.*/
paddingLeft: `${row.depth * 1}rem`,
}}
>
{row.original.isDirectory ? (
<FontAwesomeIcon
icon={row.isExpanded ? faFolderOpen : faFolderClosed}
size="sm"
/>
) : (
<FeatureIcon featureType={row.original.featureType} />
)}
<span className={styles.fileName}>{row.original.name}</span>
{!isPublic && row.id === selectedFeature && (
<Button
size="small"
type="primary"
iconNameBefore="trash"
isLoading={isLoading}
className={styles.deleteButton}
onClick={handleDelete(row.original.nodeId)}
/>
)}
</div>
),
},
],
[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<FeatureFileNode>(
{
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) => {
Expand All @@ -140,50 +115,74 @@ const FeatureFileTree: React.FC<FeatureFileTreeProps> = ({
[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 (
<div className={styles.treeNode} onClick={handleClick}>

Check failure on line 146 in react/src/components/FeatureFileTree/FeatureFileTree.tsx

View workflow job for this annotation

GitHub Actions / React-Linting

Visible, non-interactive elements with click handlers must have at least one keyboard listener

Check failure on line 146 in react/src/components/FeatureFileTree/FeatureFileTree.tsx

View workflow job for this annotation

GitHub Actions / React-Linting

Avoid non-native interactive elements. If using native HTML is not possible, add an appropriate role and support for tabbing, mouse, keyboard, and touch inputs to an interactive content element
{featureNode.isDirectory ? (
<FontAwesomeIcon
icon={isExpanded ? faFolderOpen : faFolderClosed}
size="sm"
/>
) : (
<FeatureIcon featureType={featureNode.featureType} />
)}
<span className={styles.fileName}>{featureNode.name}</span>
{!isPublic && isSelected && (
<Button
size="small"
type="primary"
iconNameBefore="trash"
isLoading={isLoading}
className={styles.deleteButton}
onClick={(e) => handleDelete(featureNode.nodeId)(e)}
/>
)}
</div>
);
},
[expanded, setExpanded, selectedFeature, isPublic, isLoading, handleDelete]
);

return (
<div className={styles.tableContainer}>
<table {...getTableProps()} className={styles.featureFileTree}>
<tbody {...getTableBodyProps()}>
{rows.map((row) => {
prepareRow(row);
/* eslint-disable react/prop-types */
const isSelected = row.original.isDirectory
? false
: selectedFeature === row.id;
return (
<tr
{...row.getRowProps()}
key={row.id}
onClick={() => handleRowClick(row.id)}
tabIndex={0}
>
{row.cells.map((cell) => (
<td
{...cell.getCellProps()}
className={`${styles.hoverable} ${
isSelected ? styles.selected : ''
} `}
key={cell.column.id}
>
{cell.render('Cell')}
</td>
))}
</tr>
);
/* eslint-enable react/prop-types */
})}
</tbody>
</table>
<div ref={ref} className={styles.root}>
<Tree
className={styles.featureFileTree}
treeData={treeData}
expandedKeys={expanded}
selectedKeys={selectedFeature ? [selectedFeature] : []}
height={height}
titleRender={titleRender}
showIcon={false}
switcherIcon={null}
virtual
blockNode /* make whole row clickable */
/>
</div>
);
};
Expand Down
7 changes: 4 additions & 3 deletions react/src/components/FeatureIcon/FeatureIcon.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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<FeatureType, IconDefinition> = {
// 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
Expand All @@ -44,5 +45,5 @@ interface Props {
export const FeatureIcon: React.FC<Props> = ({ featureType }) => {
const icon = featureType ? featureTypeToIcon[featureType] : faQuestionCircle;

return <FontAwesomeIcon icon={icon} size="sm" />;
return <FontAwesomeIcon className={styles.icon} icon={icon} size="sm" />;
};
3 changes: 2 additions & 1 deletion react/src/components/MapProjectNavBar/MapProjectNavBar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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,
},
Expand Down

0 comments on commit c3724f0

Please sign in to comment.