chore: big cleanup, typing, adding comments, better structure

This commit is contained in:
Kevin
2025-06-13 10:05:02 -05:00
parent fcf584e1f9
commit 320a7e1333
4 changed files with 249 additions and 148 deletions

View File

@ -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>
)
}

View File

@ -11,7 +11,7 @@ export const FileExplorerHeaderActions = ({
onCreateFile,
onCreateFolder,
onRefreshExplorer,
onCollapseExplorer
onCollapseExplorer,
}: {
onCreateFile: () => void
onCreateFolder: () => void

View 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>
)
}

View File

@ -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}