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