Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
98ac2de
Revamp backend for file system reading and writing
Daan0709 Jan 8, 2026
dc59186
Editor now fetches files from actual filesystem
Daan0709 Jan 8, 2026
48e6b80
Saving now works again for the editor, now directly writes to filesystem
Daan0709 Jan 8, 2026
389ebd3
Can now save and read directly from local file system
Daan0709 Jan 8, 2026
539100a
Updated controllertests
Daan0709 Jan 9, 2026
9171b39
Test updates
Daan0709 Jan 9, 2026
99c171d
Pipeline fix?
Daan0709 Jan 9, 2026
f18b6c0
Added testing for filetreeservice
Daan0709 Jan 9, 2026
b2fd17c
Refactored traverse functions
Daan0709 Jan 12, 2026
2187a37
Now caches provider data on first load, and only updates the cache if…
Daan0709 Jan 12, 2026
f041e82
Filetree now requests folderdata when expanded, instead of preloading…
Daan0709 Jan 13, 2026
5ba9500
Merge branch 'master' into feat/optimize-filetree-request-path
Daan0709 Jan 13, 2026
c1bc0e1
Merge changes
Daan0709 Jan 13, 2026
1e7b306
Pipeline and security fixes
Daan0709 Jan 13, 2026
0be8c30
Added tests
Daan0709 Jan 13, 2026
1828c9c
Added test
Daan0709 Jan 14, 2026
a750f12
Removed auto load of dummy projects, added duplication protection for…
Daan0709 Jan 14, 2026
fd6fa86
Reused sorting function instead of duplicating
Daan0709 Jan 30, 2026
bd2cc4c
Merge branch 'master' into feat/optimize-filetree-request-path
Daan0709 Jan 30, 2026
9b356a2
Fixed merge issues
Daan0709 Jan 30, 2026
5fe6ce8
Removed unused function
Daan0709 Jan 30, 2026
93564b9
Removed accidental duplicated code
Daan0709 Jan 30, 2026
0dfe53f
Refactored error message handling
Daan0709 Jan 30, 2026
33b275a
Removed unnecessary stub from testing
Daan0709 Jan 30, 2026
92a2ba8
Added test for projectAlreadyExistsException
Daan0709 Jan 30, 2026
8003edd
Linter fix
Daan0709 Jan 30, 2026
486786f
Merge branch 'master' into feat/optimize-filetree-request-path
Daan0709 Feb 4, 2026
577ff98
Now loads root on construction of editorfilesdataprovider, fixed some…
Daan0709 Feb 4, 2026
847a465
Merge branch 'master' into feat/optimize-filetree-request-path
Daan0709 Feb 4, 2026
4113863
Merge branch 'feat/optimize-filetree-request-path' into remove-legacy…
Daan0709 Feb 4, 2026
4b4f450
Error when creating a new project is now caught and displayed in the …
Daan0709 Feb 4, 2026
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
@@ -1,5 +1,6 @@
import type { Disposable, TreeDataProvider, TreeItem, TreeItemIndex } from 'react-complex-tree'
import { fetchProjectTree } from '~/services/project-service'
import { fetchDirectoryByPath, fetchProjectRootTree } from '~/services/project-service'
import { sortChildren } from './tree-utilities'

export interface FileNode {
name: string
Expand All @@ -17,33 +18,50 @@ 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.loadRoot()
}

/**
* Public method to initialize data loading.
* Call this from your React component's useEffect.
*/
public async loadData(): Promise<void> {
await this.fetchAndBuildTree()
}

/** Fetch file tree from backend and build the provider's data */
private async fetchAndBuildTree() {
/** Fetch root directory from backend and build the provider's data */
private async loadRoot() {
try {
if (!this.projectName) return

const tree = await fetchProjectTree(this.projectName)
const tree = await fetchProjectRootTree(this.projectName)

if (!tree) {
console.warn('[EditorFilesDataProvider] Received empty tree from API')
this.data = {}
return
}

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

// Sort directories first, then files, both alphabetically
const sortedChildren = sortChildren(tree.children)

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(tree.path)
this.notifyListeners(['root'])
} catch (error) {
console.error('[EditorFilesDataProvider] Unexpected error loading tree:', error)
Expand All @@ -52,38 +70,43 @@ export default class EditorFilesDataProvider implements TreeDataProvider {
}
}

/** 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' : node.path || `${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

try {
const directory = await fetchDirectoryByPath(this.projectName, item.data.path)
if (!directory) {
console.warn('[EditorFilesDataProvider] Received empty directory from API')
this.data = {}
return
}

if (node.type === 'DIRECTORY' && node.children) {
for (const child of node.children) {
const childIndex = traverse(child, index)
newData[index].children!.push(childIndex)
const sortedChildren = sortChildren(directory.children)

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

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

this.data = newData
}

public async getAllItems(): Promise<TreeItem<FileNode>[]> {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,6 @@ export default function EditorFileStructure() {

const initProvider = async () => {
const provider = new EditorFilesDataProvider(project.name)
await provider.loadData()

if (isMounted) {
setDataProvider(provider)
Expand Down Expand Up @@ -114,23 +113,21 @@ export default function EditorFileStructure() {
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)
},
[dataProvider, openFileTab],
)

const handleItemClick = (items: TreeItemIndex[], _treeId: string): void => {
void handleItemClickAsync(items)
}

useEffect(() => {
const handleKeyDown = (event: KeyboardEvent) => {
if (event.key === 'Escape') {
Expand Down Expand Up @@ -184,12 +181,19 @@ export default function EditorFileStructure() {
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 Expand Up @@ -250,7 +254,7 @@ export default function EditorFileStructure() {
viewState={{}}
getItemTitle={getItemTitle}
dataProvider={dataProvider}
onSelectItems={handleItemClick}
onSelectItems={handleItemClickAsync}
canSearch={false}
renderItemArrow={renderItemArrow}
renderItemTitle={renderItemTitle}
Expand Down
145 changes: 0 additions & 145 deletions src/main/frontend/app/components/file-structure/files-data-provider.ts

This file was deleted.

Loading
Loading