import { useEffect , useState } from "react" ; import { useRecoilValue } from "recoil" ; import { default as styled } from "styled-components" ; import { clientState } from "../utils/atoms" ; import { FixedSizeList as List } from "react-window" ; import AutoSizer from "react-virtualized-auto-sizer" ; import { DirItemInfo } from "../utils/client" ; import { ReactSVG } from "react-svg" ; const ExplorerContainer = styled . div `
  margin: 5px;
  height: calc(100% - 10px);
` ; const ExplorerItemContainer = styled . div < { isFile : boolean ; isOpened : boolean ; } > `
  max-width: 300px;
  display: flex;
  align-items: center;
  cursor: pointer;
  outline: 0;
  white-space: nowrap;
  position: relative;
  background: ${ ( { theme } ) => theme . elements . explorer . item . background };
  color: ${ ( { theme } ) => theme . elements . explorer . item . text . color };
  font-size: 12px;
  border-radius: 5px;
  line-break: none;
  text-overflow: elliptic;
  overflow: hidden;
  border: none;
  min-width: 170px;
  max-width: 200px;
  text-align: left;
  user-select: none;
  &:hover {
    background: ${ ( { theme } ) => theme . elements . explorer . item . hover . background };
  }
  & .arrow svg {
    margin-right: 7px;
    margin-left: 15px;
    margin-top: 2px;
    width: 8px;
    transform: ${ ( { isOpened } ) => isOpened ? " rotate(0deg)" : " rotate(-90deg)" };
    & > rect {
      fill: ${ ( { theme } ) => theme . elements . explorer . item . arrow . fill };
      stroke: ${ ( { theme } ) => theme . elements . explorer . item . arrow . fill };
    }
  }
  & .file svg {
    width: 20px;
    margin-right: 4px;
    margin-top: 3px;
    ${ ( { isFile } ) => isFile && "padding-left: 29px;" }
  }
` ; interface ExplorerOptions { initialRoute : string ; filesystem_name : string ; onSelected : ( path : TreeItemInfo ) => void ; } interface TreeItems { [ key ] : TreeItem ; } export interface TreeItemInfo { name : string ; isFile : boolean ; path : string ; depth : number ; } interface TreeItem { items : TreeItems ; name : string ; isFile : boolean ; } function mapTree ( list , item , depth ) { Object . keys ( item . items ) . forEach ( ( item_path ) => { const itemInfo = item . items [ item_path ] ; list . push ( { path : item_path , name : itemInfo . name , isFile : itemInfo . isFile , depth , } ) ; mapTree ( list , itemInfo , depth + 1 ) ; } ) ; return list ; } function removeSubTreeByPath ( tree , path ) { Object . keys ( tree . items ) . forEach ( ( item_path ) => { if ( path === item_path ) { tree . items [ item_path ] . items = { } ; } if ( path . startsWith ( item_path ) ) { removeSubTreeByPath ( tree . items [ item_path ] , path ) ; } } ) ; return tree ; } function addItemsToSubTreeByPath ( tree , path , subTreeItems ) { Object . keys ( tree . items ) . forEach ( ( item_path ) => { if ( path === item_path ) { tree . items [ item_path ] . items = subTreeItems ; return ; } if ( path . startsWith ( item_path ) ) { addItemsToSubTreeByPath ( tree . items [ item_path ] , path , subTreeItems ) ; } } ) ; return tree ; } function isSubTreeByPathOpened ( tree , path ) { let res = false ; Object . keys ( tree . items ) . forEach ( ( item_path ) => { if ( path === item_path ) { if ( Object . keys ( tree . items [ item_path ] . items ) . length > 0 ) { res = true ; return ; } } if ( path . startsWith ( item_path ) ) { res = isSubTreeByPathOpened ( tree . items [ item_path ] , path ) ; return ; } } ) ; return res ; } function mapItemsListToSubTreeItem ( items ) { const subTreeItems = { } ; items . forEach ( ( item ) => ( subTreeItems [ item . path ] = { name : item . name , isFile : item . is_file , items : { } , } ) ) ; return subTreeItems ; } function FilesystemExplorer ( { initialRoute , onSelected , filesystem_name , } ) { const client = useRecoilValue ( clientState ) ; const defaultState = [ { name : initialRoute , isFile : false , items : { } , } , [ ] , ] ; const [ [ folderTree , folderItems ] , setFolderData ] = useState ( defaultState ) ; useEffect ( ( ) => { client . list_dir_by_path ( initialRoute , filesystem_name ) . then ( ( pathItems ) => { if ( pathItems . Ok ) { const subTree = { name : initialRoute , isFile : false , items : mapItemsListToSubTreeItem ( pathItems . Ok ) , } ; setFolderData ( [ subTree , mapTree ( [ ] , subTree , 0 ) ] ) ; } else { } } ) ; } , [ ] ) ; function closeFolder ( path ) { client . list_dir_by_path ( path , filesystem_name ) . then ( ( pathItems ) => { if ( pathItems . Ok ) { const subTreeItems = mapItemsListToSubTreeItem ( pathItems . Ok ) ; addItemsToSubTreeByPath ( folderTree , path , subTreeItems ) ; setFolderData ( [ { ... folderTree } , mapTree ( [ ] , folderTree , 0 ) ] ) ; } else { } } ) ; } function openFolder ( path ) { const newFolderTree = removeSubTreeByPath ( folderTree , path ) ; setFolderData ( [ { ... newFolderTree } , mapTree ( [ ] , newFolderTree , 0 ) ] ) ; } function ListItem ( { index , style } ) { const itemInfo = folderItems [ index ] ; const itemStyle = { ... style , marginLeft : itemInfo . depth * 10 , } ; const isOpened = isSubTreeByPathOpened ( folderTree , itemInfo . path ) ; function onClick ( ) { onSelected ( itemInfo ) ; if ( ! itemInfo . isFile ) { if ( isOpened ) { openFolder ( itemInfo . path ) ; } else { closeFolder ( itemInfo . path ) ; } } } return ( < ExplorerItemContainer key = { itemInfo . path } onClick = { onClick } style = { itemStyle } isFile = { itemInfo . isFile } isOpened = { isOpened } title = { itemInfo . path } > { ! itemInfo . isFile && ( < ReactSVG src = "/icons/collapse_arrow.svg" className = "arrow" / > ) } { itemInfo . isFile ? ( < ReactSVG src = "/icons/files/unknown.svg" className = "file" / > ) : isOpened ? ( < ReactSVG src = "/icons/files/folder_opened.svg" className = "file" / > ) : ( < ReactSVG src = "/icons/files/folder_closed.svg" className = "file" / > ) } < span > { itemInfo . name } < /span> < /ExplorerItemContainer>); } return ( < ExplorerContainer > < AutoSizer > { ( { height , width } ) => { return ( < List itemCount = { folderItems . length } width = { width } height = { height } itemSize = { 26 } overscanCount = { 10 } > { ListItem } < /List>); } } < /AutoSizer> < /ExplorerContainer>); } export default FilesystemExplorer ;