chore: big cleanup, typing, adding comments, better structure
This commit is contained in:
		| @ -1,162 +1,233 @@ | ||||
| import type { Project, FileEntry } from '@src/lib/project' | ||||
| import Tooltip from '@src/components/Tooltip' | ||||
| import { FILE_EXT, INSERT_FOREIGN_TOAST_ID } from '@src/lib/constants' | ||||
| import type {ReactNode} from 'react' | ||||
| import {useState, useEffect} from 'react' | ||||
| import { ActionButton } from '@src/components/ActionButton' | ||||
| import { ActionIcon } from '@src/components/ActionIcon' | ||||
| import { FILE_EXT} from '@src/lib/constants' | ||||
| import type { ReactNode } from 'react' | ||||
| import { useState, useEffect } from 'react' | ||||
| import type { CustomIconName } from '@src/components/CustomIcon' | ||||
| import { CustomIcon } from '@src/components/CustomIcon' | ||||
| import { | ||||
|   systemIOActor, | ||||
| } from '@src/lib/singletons' | ||||
| import { sortFilesAndDirectories } from '@src/lib/desktopFS' | ||||
|  | ||||
| interface Explorer { | ||||
|  | ||||
| } | ||||
|  | ||||
| interface ExplorerState { | ||||
|  | ||||
| } | ||||
|  | ||||
| interface FileExplorerRowContents { | ||||
|   icon: CustomIconName, | ||||
|   name: string, | ||||
|   isFolder: boolean, | ||||
|   status?: ReactNode, | ||||
|   isOpen: boolean | ||||
| } | ||||
|  | ||||
| interface FileExplorerEntry extends FileEntry { | ||||
|   parentPath: string | ||||
|   level: number | ||||
| } | ||||
|  | ||||
|  | ||||
| export const StatusDot = () => { | ||||
|   return (<span>•</span>) | ||||
| interface FileExplorerRow extends FileExplorerEntry { | ||||
|   icon: CustomIconName | ||||
|   name: string | ||||
|   isFolder: boolean | ||||
|   status?: ReactNode | ||||
|   isOpen: boolean | ||||
|   rowClicked: () => void | ||||
| } | ||||
|  | ||||
| export const Spacer = (level: number) => { | ||||
|   const tailwindSpacing = `${(level)}rem` | ||||
|   return level === 0 ? (<div></div>) : (<div style={{width:tailwindSpacing}}></div>) | ||||
| const StatusDot = () => { | ||||
|   return <span>•</span> | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * Implement a dynamic spacer with rem to offset the row | ||||
|  * in the tree based on the level within the tree | ||||
|  * level 0 to level N | ||||
|  */ | ||||
| const Spacer = (level: number) => { | ||||
|   const remSpacing = `${level}rem` | ||||
|   return level === 0 ? ( | ||||
|     <div></div> | ||||
|   ) : ( | ||||
|     <div style={{ width: remSpacing }}></div> | ||||
|   ) | ||||
| } | ||||
|  | ||||
| const flattenProjectHelper = (f: FileEntry, list: FileEntry[], path: string, level: number) => { | ||||
|   f.parentPath = path | ||||
|   f.level = level | ||||
|   list.push(f) | ||||
| const constructPath = ({ | ||||
|   parentPath, | ||||
|   name | ||||
| }: { | ||||
|   parentPath: string, | ||||
|   name: string | ||||
| }) => { | ||||
|   // do not worry about the forward slash, this is not a real disk path | ||||
|   // the slash could be any delimiter this will be used as a key to parse | ||||
|   // and use in a hash table | ||||
|   return parentPath + '/' + name | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * Recursive helper function to traverse the project tree and flatten the tree structure | ||||
|  * into an array called list. | ||||
|  */ | ||||
| const flattenProjectHelper = ( | ||||
|   f: FileEntry, | ||||
|   list: FileExplorerEntry[], // accumulator list that is built up through recursion | ||||
|   parentPath: string, // the parentPath for the given f:FileEntry passed in | ||||
|   level: number, // the level within the tree for the given f:FileEntry, level starts at 0 goes to positive N | ||||
| ) => { | ||||
|   // mark the parent and level of the FileEntry | ||||
|   const content : FileExplorerEntry= { | ||||
|     ...f, | ||||
|     parentPath, | ||||
|     level | ||||
|   } | ||||
|   // keep track of the file once within the recursive list that will be built up | ||||
|   list.push(content) | ||||
|  | ||||
|   // if a FileEntry has no children stop | ||||
|   if (f.children === null) { | ||||
|     return | ||||
|   } | ||||
|   for (let i = 0; i < f.children.length; i++) { | ||||
|     flattenProjectHelper(f.children[i], list, path+'/'+f.name, level + 1) | ||||
|   } | ||||
|  | ||||
|   // keep recursing down the children | ||||
|   for (let i = 0; i < f.children.length; i++) { | ||||
|     flattenProjectHelper(f.children[i], list, constructPath({parentPath: parentPath, name: f.name}), level + 1) | ||||
|   } | ||||
| } | ||||
|  | ||||
| const flattenProject = (project: Project) : FileEntry[] => { | ||||
|   if (project.children === null) { | ||||
|     return [] | ||||
| /** | ||||
|  * A Project type will have a set of children, pass the children as fileEntries | ||||
|  * since that is level 0 of the tree, everything is under the projectName | ||||
|  * | ||||
|  * fileEntries should be sorted already with sortFilesAndDirectories | ||||
|  */ | ||||
| const flattenProject = (projectChildren: FileEntry[], projectName:string): FileExplorerEntry[] => { | ||||
|   const flattenTreeInOrder: FileExplorerEntry[] = [] | ||||
|   // For all children of the project, start the recursion to flatten the tree data structure | ||||
|   for (let index = 0; index < projectChildren.length; index++) { | ||||
|     flattenProjectHelper( | ||||
|       projectChildren[index], | ||||
|       flattenTreeInOrder, | ||||
|       projectName, // first parent | ||||
|       0 | ||||
|     ) | ||||
|   } | ||||
|  | ||||
|   const flattenTreeInOrder : FileEntry [] = [] | ||||
|   for (let i = 0; i < project.children.length; i++) { | ||||
|     flattenProjectHelper(project.children[i], flattenTreeInOrder, project.name, 0) | ||||
|   } | ||||
|  | ||||
|   return flattenTreeInOrder | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * Render all the rows of the file explorer in linear layout in the DOM. | ||||
|  * each row is rendered one after another in the same parent DOM element | ||||
|  * rows will have aria support to understand the linear div soup layout | ||||
|  */ | ||||
| export const FileExplorer = ({ | ||||
|   parentProject | ||||
| }:{ | ||||
|   parentProject, | ||||
| }: { | ||||
|   parentProject: Project | ||||
| }) => { | ||||
|   let flattenedData : FileEntry[] = [] | ||||
|   if (parentProject) { | ||||
|     flattenedData = flattenProject(parentProject) | ||||
|   // Wrap the FileEntry in a FileExplorerEntry to keep track for more metadata | ||||
|   let flattenedData: FileExplorerEntry[] = [] | ||||
|  | ||||
|   if (parentProject && parentProject.children) { | ||||
|     // moves all folders up and files down, files are sorted within folders | ||||
|     const sortedData = sortFilesAndDirectories(parentProject.children) | ||||
|     // pre order traversal of the tree | ||||
|     flattenedData = flattenProject(sortedData, parentProject.name) | ||||
|   } | ||||
|   const [openedRows, setOpenedRows] = useState<{[key:string]:boolean}>({}) | ||||
|   const [rowsToRender, setRowsToRender] = useState<FileExplorerRowContents[]>([]) | ||||
|  | ||||
|   useEffect(()=> { | ||||
|     /* const allRows = parentProject?.children?.map((child)=>{ */ | ||||
|     const allRows = flattenedData.map((child)=>{ | ||||
|       const isFile = child.children === null | ||||
|       const isKCLFile = isFile && child.name?.endsWith(FILE_EXT) | ||||
|   // cache the state of opened rows to allow nested rows to be opened if a parent one is closed | ||||
|   // when the parent opens the children will already be opened | ||||
|   const [openedRows, setOpenedRows] = useState<{ [key: string]: boolean }>({}) | ||||
|   const [rowsToRender, setRowsToRender] = useState<FileExplorerRow[]>( | ||||
|     [] | ||||
|   ) | ||||
|   const [selectedRow, setSelectedRow] = useState<FileEntry | null>(null) | ||||
|  | ||||
|       let icon : CustomIconName = 'file' | ||||
|       if (isKCLFile) { | ||||
|         icon = 'kcl' | ||||
|       } else if (!isFile) { | ||||
|         icon = 'folder' | ||||
|       } | ||||
|   useEffect(() => { | ||||
|     // TODO What to do when a different parentProject comes in? Clear old state. | ||||
|     // Clear openedRows | ||||
|     // Clear rowsToRender | ||||
|     // Clear selected information | ||||
|  | ||||
|       /** | ||||
|        * If any parent is closed, keep the history of open children | ||||
|        */ | ||||
|       let isAnyParentClosed = false | ||||
|       const pathIterator = child.parentPath.split('/') | ||||
|     const requestedRowsToRender : FileExplorerRow[] = | ||||
|       flattenedData.map((child) => { | ||||
|         const isFile = child.children === null | ||||
|         const isKCLFile = isFile && child.name?.endsWith(FILE_EXT) | ||||
|  | ||||
|       while (pathIterator.length > 0) { | ||||
|         const key = pathIterator.join('/') | ||||
|         const isOpened = openedRows[key] || parentProject.name === key | ||||
|         isAnyParentClosed = isAnyParentClosed || !isOpened | ||||
|         pathIterator.pop() | ||||
|       } | ||||
|  | ||||
|  | ||||
|       return { | ||||
|         name: child.name, | ||||
|         icon: icon, | ||||
|         isFolder: !isFile, | ||||
|         status: StatusDot(), | ||||
|         isOpen: (openedRows[child.parentPath] || parentProject.name === child.parentPath) && !isAnyParentClosed, | ||||
|         parentPath: child.parentPath, | ||||
|         level: child.level, | ||||
|         rowClicked: () => { | ||||
|           const newOpenedRows = {...openedRows} | ||||
|           const key = child.parentPath + '/' + child.name | ||||
|           const value = openedRows[key] | ||||
|           newOpenedRows[key] = !value | ||||
|           setOpenedRows(newOpenedRows) | ||||
|         let icon: CustomIconName = 'file' | ||||
|         if (isKCLFile) { | ||||
|           icon = 'kcl' | ||||
|         } else if (!isFile) { | ||||
|           icon = 'folder' | ||||
|         } | ||||
|       } | ||||
|     }) ||  [] | ||||
|  | ||||
|     setRowsToRender(allRows) | ||||
|         /** | ||||
|          * If any parent is closed, keep the history of open children | ||||
|          */ | ||||
|         let isAnyParentClosed = false | ||||
|         const pathIterator = child.parentPath.split('/') | ||||
|  | ||||
|   },[parentProject, openedRows]) | ||||
|         while (pathIterator.length > 0) { | ||||
|           const key = pathIterator.join('/') | ||||
|           const isOpened = openedRows[key] || parentProject.name === key | ||||
|           isAnyParentClosed = isAnyParentClosed || !isOpened | ||||
|           pathIterator.pop() | ||||
|         } | ||||
|  | ||||
| // Local state for selection and what is opened | ||||
| // diff this against new Project value that comes in | ||||
| return (<div> | ||||
|   {rowsToRender.map((row)=>{ | ||||
|     return (row.isOpen ? <FileExplorerRow | ||||
|               row={row} | ||||
|             ></FileExplorerRow> : null) | ||||
|   })} | ||||
| </div>) | ||||
| } | ||||
|         const row : FileExplorerRow =  { | ||||
|           // copy over all the other data that was built up to the DOM render row | ||||
|           ...child, | ||||
|           icon: icon, | ||||
|           isFolder: !isFile, | ||||
|           status: StatusDot(), | ||||
|           isOpen: | ||||
|             (openedRows[child.parentPath] || | ||||
|               parentProject.name === child.parentPath) && | ||||
|             !isAnyParentClosed, | ||||
|           rowClicked: () => { | ||||
|             const newOpenedRows = { ...openedRows } | ||||
|             const key = constructPath({ | ||||
|               parentPath: child.parentPath, | ||||
|               name: child.name | ||||
|             }) | ||||
|             const value = openedRows[key] | ||||
|             newOpenedRows[key] = !value | ||||
|             setOpenedRows(newOpenedRows) | ||||
|             setSelectedRow(child) | ||||
|           }, | ||||
|         } | ||||
|  | ||||
| export const FileExplorerRow = ({ | ||||
|   row | ||||
| }:{ | ||||
|   row: any | ||||
| }) => { | ||||
|   return (<div className="h-6 flex flex-row" | ||||
|                onClick={()=>{row.rowClicked()} } | ||||
|           > | ||||
|     {Spacer(row.level)} | ||||
|     <CustomIcon | ||||
|       name={row.icon} | ||||
|       className="inline-block w-4 text-current mr-1" | ||||
|     /> | ||||
|     <span className="overflow-hidden whitespace-nowrap text-ellipsis">{row.name}</span> | ||||
|     <div className="ml-auto"> | ||||
|     {row.status} | ||||
|         return row | ||||
|       }) || [] | ||||
|  | ||||
|     setRowsToRender(requestedRowsToRender) | ||||
|   }, [parentProject, openedRows]) | ||||
|  | ||||
|   // Local state for selection and what is opened | ||||
|   // diff this against new Project value that comes in | ||||
|   return ( | ||||
|     <div> | ||||
|       {rowsToRender.map((row) => { | ||||
|         return row.isOpen ? <FileExplorerRow row={row} selectedRow={selectedRow}></FileExplorerRow> : null | ||||
|       })} | ||||
|     </div> | ||||
|   </div>) | ||||
|   ) | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * Making div soup! | ||||
|  * A row is a folder or a file. | ||||
|  */ | ||||
| export const FileExplorerRow = ({ | ||||
|   row, | ||||
|   selectedRow | ||||
| }: { | ||||
|   row: any, | ||||
|   selectedRow : any | ||||
| }) => { | ||||
|   return ( | ||||
|     <div | ||||
|       className={`h-6 flex flex-row items-center text-xs ${row.name === selectedRow?.name ? 'bg-red-200' : ''}`} | ||||
|       onClick={() => { | ||||
|         row.rowClicked() | ||||
|       }} | ||||
|     > | ||||
|       {Spacer(row.level)} | ||||
|       <CustomIcon | ||||
|         name={row.icon} | ||||
|         className="inline-block w-4 text-current mr-1" | ||||
|       /> | ||||
|       <span className="overflow-hidden whitespace-nowrap text-ellipsis"> | ||||
|         {row.name} | ||||
|       </span> | ||||
|       <div className="ml-auto">{row.status}</div> | ||||
|     </div> | ||||
|   ) | ||||
| } | ||||
|  | ||||
| @ -11,7 +11,7 @@ export const FileExplorerHeaderActions = ({ | ||||
|   onCreateFile, | ||||
|   onCreateFolder, | ||||
|   onRefreshExplorer, | ||||
|   onCollapseExplorer | ||||
|   onCollapseExplorer, | ||||
| }: { | ||||
|   onCreateFile: () => void | ||||
|   onCreateFolder: () => void | ||||
|  | ||||
							
								
								
									
										46
									
								
								src/components/Explorer/ProjectExplorer.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										46
									
								
								src/components/Explorer/ProjectExplorer.tsx
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,46 @@ | ||||
| import type { Project, FileEntry } from '@src/lib/project' | ||||
| import { FileExplorer } from '@src/components/Explorer/FileExplorer' | ||||
| import { FileExplorerHeaderActions } from '@src/components/Explorer/FileExplorerHeaderActions' | ||||
|  | ||||
| /** | ||||
|  * Wrap the header and the tree into a single component | ||||
|  * This is important because the action header buttons need to know | ||||
|  * the selection logic of the tree since add file will be based on your | ||||
|  * selection within the tree. | ||||
|  * | ||||
|  * pass a Project type which is compatiable with the data stored in | ||||
|  * the systemIOMachine | ||||
|  * | ||||
|  */ | ||||
| export const ProjectExplorer = ({project}:{ | ||||
|   project: Project | ||||
| }) => { | ||||
|   return ( | ||||
|     <div> | ||||
|         <div className="flex flex-row justify-between"> | ||||
|           <div>{project?.name || 'No Project Selected'}</div> | ||||
|           <div className="h-6 flex flex-row gap-1"> | ||||
|             <FileExplorerHeaderActions | ||||
|               onCreateFile={() => { | ||||
|                 console.log('onCreateFile TODO') | ||||
|               }} | ||||
|               onCreateFolder={() => { | ||||
|                 console.log('onCreateFolder TODO') | ||||
|               }} | ||||
|               onRefreshExplorer={() => { | ||||
|                 console.log('onRefreshExplorer TODO') | ||||
|               }} | ||||
|               onCollapseExplorer={() => { | ||||
|                 console.log('onCollapseExplorer TODO') | ||||
|               }} | ||||
|             ></FileExplorerHeaderActions> | ||||
|           </div> | ||||
|         </div> | ||||
|         <div className="h-96 overflow-y-auto overflow-x-hidden"> | ||||
|           {project && ( | ||||
|             <FileExplorer parentProject={project}></FileExplorer> | ||||
|           )} | ||||
|         </div> | ||||
|     </div> | ||||
|   ) | ||||
| } | ||||
| @ -61,8 +61,7 @@ import { | ||||
| import { CustomIcon } from '@src/components/CustomIcon' | ||||
| import Tooltip from '@src/components/Tooltip' | ||||
| import { ML_EXPERIMENTAL_MESSAGE } from '@src/lib/constants' | ||||
| import { FileExplorer} from "@src/components/Explorer/FileExplorer" | ||||
| import { FileExplorerHeaderActions } from "@src/components/Explorer/FileExplorerHeaderActions" | ||||
| import { ProjectExplorer } from "@src/components/Explorer/ProjectExplorer" | ||||
|  | ||||
| type ReadWriteProjectState = { | ||||
|   value: boolean | ||||
| @ -212,6 +211,7 @@ const Home = () => { | ||||
|   const sidebarButtonClasses = | ||||
|     'flex items-center p-2 gap-2 leading-tight border-transparent dark:border-transparent enabled:dark:border-transparent enabled:hover:border-primary/50 enabled:dark:hover:border-inherit active:border-primary dark:bg-transparent hover:bg-transparent' | ||||
|  | ||||
|   const kclSamples = projects.find((p)=>{ return p.name === 'level1'}) | ||||
|   return ( | ||||
|     <div className="relative flex flex-col items-stretch h-screen w-screen overflow-hidden"> | ||||
|       <AppHeader | ||||
| @ -390,29 +390,13 @@ const Home = () => { | ||||
|           </ul> | ||||
|         </aside> | ||||
|  | ||||
|       <section data-testid="file-explorer-section" className="w-96"> | ||||
|       <div className="flex flex-row justify-between"> | ||||
|       <div>{projects.length > 0 ? projects[0].name : ''}</div> | ||||
|       <div className="h-6 flex flex-row gap-1"> | ||||
|             <FileExplorerHeaderActions | ||||
|               onCreateFile={()=>{console.log('onCreateFile TODO')}} | ||||
|               onCreateFolder={()=>{console.log('onCreateFolder TODO')}} | ||||
|               onRefreshExplorer={()=>{console.log('onRefreshExplorer TODO')}} | ||||
|               onCollapseExplorer={()=>{console.log('onCollapseExplorer TODO')}} | ||||
|       > | ||||
|       </FileExplorerHeaderActions> | ||||
|           </div> | ||||
|         </div> | ||||
|         <div className="h-96 overflow-y-auto overflow-x-hidden"> | ||||
|       { projects.length > 0 && | ||||
|           <FileExplorer | ||||
|             parentProject={projects[0]} | ||||
|         ></FileExplorer> | ||||
|         } | ||||
|         </div> | ||||
|       </section > | ||||
|       <ProjectGrid | ||||
|     searchResults={searchResults} | ||||
|         <section data-testid="file-explorer-section" className="w-96"> | ||||
|           <ProjectExplorer | ||||
|             project={kclSamples} | ||||
|           ></ProjectExplorer> | ||||
|         </section> | ||||
|         <ProjectGrid | ||||
|           searchResults={searchResults} | ||||
|           projects={projects} | ||||
|           query={query} | ||||
|           sort={sort} | ||||
|  | ||||
		Reference in New Issue
	
	Block a user