frontend/app/browse/NewTreeView.tsx (132 lines of code) (raw):

import React, {useState, useEffect} from "react"; import {FormLabel, Grid, makeStyles, MenuItem, Select, Typography} from "@material-ui/core"; import {TreeItem, TreeItemClassKey, TreeView} from "@material-ui/lab"; import {ChevronRight, ExpandMore} from "@material-ui/icons"; import {BrowseDirectoryResponse, PathEntry} from "../types"; import axios from "axios"; import {formatError} from "../common/ErrorViewComponent"; interface NewTreeViewProps { currentCollection: string; collectionList: string[]; collectionDidChange: (newCollection:string)=>void; pathSelectionChanged: (newPath:string)=>void; onError: (errorDesc:string)=>void; } const useStyles = makeStyles({ root: { width: "100%" }, scrollable: { overflowY: "auto" } }); interface TreeLeafProps { path:PathEntry; leafWasSelected:(entry:PathEntry)=>void; collectionName: string; parentKey:string; onError?: (errorDesc:string)=>void; } /** * loads in another level of paths below the path given * @param collectionName bucket to load paths from * @param from directory to start at. Use an empty string to mean "root". */ const loadInPaths = async (collectionName:string, from:string)=> { const nameExtractor = /\/([^\/]+)\/$/; const result = await axios.get<BrowseDirectoryResponse>(`/api/browse/${collectionName}?prefix=${from}`); const pathsToAdd: PathEntry[] = result.data.entries.sort(function (a, b) { return a.toLowerCase().localeCompare(b.toLowerCase()); }).map((fullpath, idx) => { const extracted = nameExtractor.exec("/"+fullpath); const name = extracted ? extracted[1] : ""; return { name: name, fullpath: fullpath, idx: idx } }); return pathsToAdd } /** * TreeLeaf is a recursive component that handles a single level of directories. */ const TreeLeaf:React.FC<TreeLeafProps> = (props) => { const [childNodes, setChildNodes] = useState<PathEntry[]>([]); const [isLoaded, setIsLoaded] = useState(false); const handleToggle = (evt:React.MouseEvent<Element, MouseEvent>) => { if(!isLoaded) { //save the event so we can re-play it once the async call is done if(evt) evt.persist(); loadInPaths(props.collectionName, props.path.fullpath) .then(morePaths => { setChildNodes(morePaths); setIsLoaded(true); /* the component view has already refreshed by the time our load has finished. * so, we must re-trigger the event here to get it to actually display the new data otherwise * it won't get displayed until the user clicks to expand for a second time */ if(evt) evt.target.dispatchEvent(evt.nativeEvent); }) .catch(err => { console.error(`Could not load more paths for ${props.path.fullpath} on ${props.collectionName}`, err); if (props.onError) props.onError(formatError(err, false)) }) } } const handleSelect = (evt:React.MouseEvent<Element, MouseEvent>)=> { evt.preventDefault(); props.leafWasSelected(props.path); } return <TreeItem nodeId={props.path.fullpath} label={<Typography>{props.path.name}</Typography>} collapseIcon={<ExpandMore/>} key={props.parentKey} //icon={isOpen ? <ExpandMore/> : <ChevronRight/>} expandIcon={<ChevronRight/>} endIcon={<ChevronRight/>} onIconClick={handleToggle} onLabelClick={handleSelect} >{ childNodes.map((childPath, idx)=> { return <TreeLeaf path={childPath} key={`${props.path.idx}${idx}`} parentKey={`${props.path.idx}-${idx}`} leafWasSelected={props.leafWasSelected} onError={props.onError} collectionName={props.collectionName} /> }) }</TreeItem> } const NewTreeView:React.FC<NewTreeViewProps> = (props) => { const classes = useStyles(); const [loadedPaths, setLoadedPaths] = useState<PathEntry[]>([]); const treeItemSelected = (path:PathEntry) => { const newPath = path.fullpath.endsWith("/") ? path.fullpath.slice(0, path.fullpath.length-1) : path.fullpath console.log("You selected ", newPath); props.pathSelectionChanged(newPath); } useEffect(()=>{ if(props.currentCollection=="") { setLoadedPaths([]); } else { loadInPaths(props.currentCollection, "") .then(paths=>setLoadedPaths(paths)) .catch(err=>{ props.onError(formatError(err, false)) }) } }, [props.currentCollection]); return <Grid container direction="column" spacing={3}> <Grid item> <FormLabel htmlFor="collection-selector">Collection</FormLabel> <Select id="collection-selector" onChange={(evt)=>props.collectionDidChange(evt.target.value as string)} className={classes.root} value={props.currentCollection}> { props.collectionList.map((entry, idx)=><MenuItem key={idx} value={entry}>{entry}</MenuItem>) } </Select> </Grid> <Grid item> <TreeView className={classes.root} defaultCollapseIcon={<ChevronRight/>} defaultExpandIcon={<ExpandMore/>} defaultExpanded={[]}> { loadedPaths.length==0 ? <Typography variant="caption">No subfolders present</Typography> : loadedPaths.map((path, idx)=><TreeLeaf path={path} key={idx.toString()} parentKey={idx.toString()} leafWasSelected={treeItemSelected} collectionName={props.currentCollection}/>) } </TreeView> </Grid> </Grid> } export {loadInPaths, TreeLeaf}; export default NewTreeView;