Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -16,55 +16,95 @@ export default class EditorFilesDataProvider implements TreeDataProvider {
private data: Record<TreeItemIndex, TreeItem<FileNode>> = {}
private readonly treeChangeListeners: ((changedItemIds: TreeItemIndex[]) => void)[] = []
private readonly projectName: string
private loadedDirectories = new Set<string>()

constructor(projectName: string) {
this.projectName = projectName
this.fetchAndBuildTree()
this.loadRoot()
}

/** Fetch file tree from backend and build the provider's data */
private async fetchAndBuildTree() {
private async loadRoot() {
try {
const response = await fetch(`/api/projects/${this.projectName}/tree`)
if (!response.ok) throw new Error(`HTTP error ${response.status}`)

const tree: FileTreeNode = await response.json()
this.buildTreeFromFileTree(tree)
const root: FileTreeNode = await response.json()

this.data['root'] = {
index: 'root',
data: { name: root.name, path: root.path },
isFolder: true,
children: [],
}

// Sort directories first, then files, both alphabetically
const sortedChildren = (root.children ?? []).toSorted((a, b) => {
if (a.type !== b.type) {
return a.type === 'DIRECTORY' ? -1 : 1
}
return a.name.localeCompare(b.name)
})

for (const child of sortedChildren) {
const childIndex = `root/${child.name}`

this.data[childIndex] = {
index: childIndex,
data: { name: child.name, path: child.path },
isFolder: child.type === 'DIRECTORY',
children: child.type === 'DIRECTORY' ? [] : undefined,
}

this.data['root'].children!.push(childIndex)
}

this.loadedDirectories.add(root.path)
this.notifyListeners(['root'])
} catch (error) {
console.error('Failed to load project tree for EditorFilesDataProvider', error)
console.error('Failed to load root directory', error)
}
}

/** Converts the backend file tree to react-complex-tree data */
private buildTreeFromFileTree(rootNode: FileTreeNode) {
const newData: Record<TreeItemIndex, TreeItem<FileNode>> = {}

const traverse = (node: FileTreeNode, parentIndex: TreeItemIndex | null): TreeItemIndex => {
const index = parentIndex === null ? 'root' : `${parentIndex}/${node.name}`

newData[index] = {
index,
data: {
name: node.name,
path: node.path,
},
children: node.type === 'DIRECTORY' ? [] : undefined,
isFolder: node.type === 'DIRECTORY',
}
public async loadDirectory(itemId: TreeItemIndex): Promise<void> {
const item = this.data[itemId]
if (!item || !item.isFolder) return
if (this.loadedDirectories.has(item.data.path)) return

if (node.type === 'DIRECTORY' && node.children) {
for (const child of node.children) {
const childIndex = traverse(child, index)
newData[index].children!.push(childIndex)
try {
const response = await fetch(`/api/projects/${this.projectName}?path=${encodeURIComponent(item.data.path)}`)
if (!response.ok) throw new Error('Failed to fetch directory')

const dir: FileTreeNode = await response.json()

const sortedChildren = (dir.children ?? []).toSorted((a, b) => {
if (a.type !== b.type) {
return a.type === 'DIRECTORY' ? -1 : 1
}
return a.name.localeCompare(b.name)
Comment on lines +80 to +83
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This sorted function is the same as the one used for the root, maybe you could reuse that instead recreating it here and possibly having to edit both?

})

const children: TreeItemIndex[] = []

for (const child of sortedChildren) {
const childIndex = `${itemId}/${child.name}`

this.data[childIndex] = {
index: childIndex,
data: { name: child.name, path: child.path },
isFolder: child.type === 'DIRECTORY',
children: child.type === 'DIRECTORY' ? [] : undefined,
}

children.push(childIndex)
}

return index
}
item.children = children

traverse(rootNode, null)
this.data = newData
this.loadedDirectories.add(item.data.path)
this.notifyListeners([itemId])
} catch (error) {
console.error('Failed to load directory', error)
}
}

public async getAllItems(): Promise<TreeItem<FileNode>[]> {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React, { type JSX, useCallback, useEffect, useRef, useState } from 'react'
import React, { type JSX, useEffect, useRef, useState } from 'react'
import Search from '~/components/search/search'
import FolderIcon from '../../../icons/solar/Folder.svg?react'
import FolderOpenIcon from '../../../icons/solar/Folder Open.svg?react'
Expand Down Expand Up @@ -28,7 +28,7 @@

export default function EditorFileStructure() {
const project = useProjectStore((state) => state.project)
const filepaths = project?.filepaths ?? []

Check warning on line 31 in src/main/frontend/app/components/file-structure/editor-file-structure.tsx

View workflow job for this annotation

GitHub Actions / Build & Run All Tests

The 'filepaths' logical expression could make the dependencies of useEffect Hook (at line 82) change on every render. Move it inside the useEffect callback. Alternatively, wrap the initialization of 'filepaths' in its own useMemo() Hook

const [searchTerm, setSearchTerm] = useState('')
const [matchingItemIds, setMatchingItemIds] = useState<string[]>([])
Expand Down Expand Up @@ -79,7 +79,7 @@
}

findMatchingItems()
}, [searchTerm, filepaths])
}, [searchTerm, filepaths, dataProvider])

const openFileTab = (filePath: string, fileName: string) => {
if (!getTab(filePath)) {
Expand All @@ -95,19 +95,21 @@
void handleItemClickAsync(items)
}

const handleItemClickAsync = async (itemIds: TreeItemIndex[]) => {

Check warning on line 98 in src/main/frontend/app/components/file-structure/editor-file-structure.tsx

View workflow job for this annotation

GitHub Actions / Build & Run All Tests

The 'handleItemClickAsync' function makes the dependencies of useEffect Hook (at line 143) change on every render. To fix this, wrap the definition of 'handleItemClickAsync' in its own useCallback() Hook
if (!dataProvider || itemIds.length === 0) return

const itemId = itemIds[0]
if (typeof itemId !== 'string') return

const item = await dataProvider.getTreeItem(itemId)
if (!item || item.isFolder) return
if (!item) return

const filePath = item.data.path
const fileName = item.data.name
// Fetch contents and expand folder if folder
if (item.isFolder) {
await dataProvider.loadDirectory(itemId)
return
}

openFileTab(filePath, fileName)
// Load file in editor tab if file
openFileTab(item.data.path, item.data.name)
}

/* Keyboard navigation */
Expand Down Expand Up @@ -164,12 +166,19 @@
const renderItemArrow = ({ item, context }: { item: TreeItem; context: TreeItemRenderContext }) => {
if (!item.isFolder) return null
const Icon = context.isExpanded ? AltArrowDownIcon : AltArrowRightIcon
return (
<Icon
onClick={context.toggleExpandedState}
className="rct-tree-item-arrow-isFolder rct-tree-item-arrow fill-foreground"
/>
)

const handleClick = async (event: React.MouseEvent) => {
event.stopPropagation()

// Only load when expanding
if (!context.isExpanded && dataProvider) {
await dataProvider.loadDirectory(item.index)
}

context.toggleExpandedState()
}

return <Icon onClick={handleClick} className="rct-tree-item-arrow-isFolder rct-tree-item-arrow fill-foreground" />
}

const renderItemTitle = ({
Expand Down
129 changes: 0 additions & 129 deletions src/main/frontend/app/components/file-structure/files-data-provider.ts

This file was deleted.

Loading
Loading