Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
3e5b946
improved by putting api calls in services and use hooks to fetch the …
stijnpotters1 Jan 28, 2026
73ad5e1
Remove unnecessary comments in navigation-store and project-landing f…
stijnpotters1 Jan 29, 2026
ca7bb61
Implement filesystem browser and project management features
stijnpotters1 Feb 4, 2026
4271b77
Refactor project management to use recent projects and improve filesy…
stijnpotters1 Feb 4, 2026
d0b234d
Add project cloning functionality and enhance project management UI
stijnpotters1 Feb 4, 2026
e36e9bb
Add ProjectCloneDTO for handling project cloning data
stijnpotters1 Feb 4, 2026
a20063d
Refactor project modals to use DirectoryPicker for folder selection
stijnpotters1 Feb 4, 2026
dca5d86
Add project removal functionality to recent projects list
stijnpotters1 Feb 4, 2026
57ed3a4
Enhance project fetching and configuration loading in services
stijnpotters1 Feb 4, 2026
144ecf1
Refactor FileTreeServiceTest to improve readability and add new test …
stijnpotters1 Feb 4, 2026
6f9f0bc
Add ToastContainer to AppLayout and update error handling in project …
stijnpotters1 Feb 5, 2026
e50a34f
feat: made it filesystem cloud and local proof
stijnpotters1 Feb 12, 2026
afb686c
fix: introduced JGIT and solved sonar hotspot
stijnpotters1 Feb 17, 2026
ebe5bfe
feat: improved code and tests
stijnpotters1 Feb 17, 2026
3a43435
chore: applied spotless
stijnpotters1 Feb 17, 2026
9c59d52
fix: improved failing FileTreeService test
stijnpotters1 Feb 17, 2026
bc35a38
fix: solved sonar security issues and added tests to cover new implem…
stijnpotters1 Feb 17, 2026
cf86905
fix: improved failing tests
stijnpotters1 Feb 17, 2026
28dc258
fix: improved failing test because of IOException
stijnpotters1 Feb 17, 2026
7998518
fix: solved stubbing issues in projectservicetest
stijnpotters1 Feb 17, 2026
7b11eaf
fix: solved stubbing issues in FileTreeServiceTest
stijnpotters1 Feb 17, 2026
0cdfbb4
fix: solved sonar hotspots
stijnpotters1 Feb 17, 2026
5579a12
fix: improved code according to feedback
stijnpotters1 Feb 17, 2026
37a5b4c
fix: improved code according to feedback
stijnpotters1 Feb 17, 2026
2e6b099
fix: made spring profile default cloud so its compatible for kubernet…
stijnpotters1 Feb 17, 2026
2c76a7a
Update src/main/java/org/frankframework/flow/filesystem/FileSystemSto…
stijnpotters1 Feb 17, 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
5 changes: 5 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,11 @@

Easily create configurations for the Frank!Framework with the Flow Studio and Editor!

Please use profile on locally:
-Dspring.profiles.active=local
or with docker
-Dspring.profiles.active=cloud

## Build from source

Building the project requires Java, Maven, NodeJS, PNPM and Docker installed on your system.
Expand Down
2 changes: 2 additions & 0 deletions docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,5 +4,7 @@ services:
build:
dockerfile: ./docker/Dockerfile
context: .
environment:
SPRING_PROFILES_ACTIVE: cloud
ports:
- "8080:8080"
2 changes: 2 additions & 0 deletions docker/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -13,4 +13,6 @@ COPY --chown=spring ./target/flow*.jar /app/flow.jar

EXPOSE 8080

ENV SPRING_PROFILES_ACTIVE=cloud

CMD ["java","-jar","/app/flow.jar"]
11 changes: 11 additions & 0 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@
<properties>
<revision>3.0.0-SNAPSHOT</revision>
<java.version>21</java.version>
<jackson.version>2.20.1</jackson.version>
<lombok.version>1.18.42</lombok.version>
<spotless.version>3.0.0</spotless.version>
<checkstyle.version>3.6.0</checkstyle.version>
Expand Down Expand Up @@ -111,11 +112,21 @@
<artifactId>spring-boot-testcontainers</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.datatype</groupId>
<artifactId>jackson-datatype-hibernate6</artifactId>
<version>${jackson.version}</version>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-compress</artifactId>
<version>${apache.commons.version}</version>
</dependency>
<dependency>
<groupId>org.eclipse.jgit</groupId>
<artifactId>org.eclipse.jgit</artifactId>
<version>7.1.0.202411261347-r</version>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ function AddFieldForm({ fieldType, onSave, parents, formatDefinition, initialDat
const propertyRules = format?.properties.find((a) => a.name == variableType)
setDefaultValueRules(propertyRules?.rules)
setDefaultValueInputType(propertyRules?.type)
}, [variableType])
}, [fieldType, formatDefinition, variableType])

const isFormIncomplete = !variableType || !label

Expand Down
158 changes: 158 additions & 0 deletions src/main/frontend/app/components/directory-picker/directory-picker.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
import { useCallback, useEffect, useState } from 'react'
import FolderIcon from '/icons/solar/Folder.svg?react'
import { filesystemService } from '~/services/filesystem-service'
import type { FilesystemEntry } from '~/types/filesystem.types'
import { ApiError } from '~/utils/api'

interface DirectoryPickerProperties {
isOpen: boolean
onSelect: (absolutePath: string) => void
onCancel: () => void
rootLabel?: string
}

export default function DirectoryPicker({
isOpen,
onSelect,
onCancel,
rootLabel = 'Computer',
}: Readonly<DirectoryPickerProperties>) {
const [currentPath, setCurrentPath] = useState('')
const [entries, setEntries] = useState<FilesystemEntry[]>([])
const [selectedEntry, setSelectedEntry] = useState<string | null>(null)
const [loading, setLoading] = useState(false)
const [error, setError] = useState<string | null>(null)

const loadEntries = useCallback(async (path: string) => {
setLoading(true)
setError(null)
setSelectedEntry(null)
try {
const result = await filesystemService.browse(path)
setEntries(result)
setCurrentPath(path)
} catch (error_) {
const status = error_ instanceof ApiError ? error_.status : 0
if (status === 403) {
setError('Access denied')
} else {
setError(error_ instanceof Error ? error_.message : 'Failed to load directories')
}
} finally {
setLoading(false)
}
}, [])

useEffect(() => {
if (isOpen) {
setSelectedEntry(null)
loadEntries('')
}
}, [isOpen, loadEntries])

if (!isOpen) return null

const isRoot = !currentPath
const canGoUp = !isRoot

const handleNavigateUp = () => {
if (/^[a-zA-Z]:[/\\]?$/.test(currentPath) || currentPath === '/') {
loadEntries('')
return
}
const parentPath = currentPath.replace(/[\\/][^\\/]*$/, '')
if (!parentPath || parentPath === currentPath) {
loadEntries('')
} else if (/^[a-zA-Z]:$/.test(parentPath)) {
loadEntries(`${parentPath}\\`)
} else {
loadEntries(parentPath)
}
}

const handleClick = (entry: FilesystemEntry) => {
setSelectedEntry(entry.path)
}

const handleDoubleClick = (entry: FilesystemEntry) => {
loadEntries(entry.path)
}

const activePath = selectedEntry ?? currentPath

return (
<div className="bg-background/50 absolute inset-0 z-[60] flex items-center justify-center">
<div className="bg-background border-border flex h-[450px] w-[500px] flex-col rounded-lg border shadow-lg">
<div className="border-border flex items-center justify-between border-b px-4 py-3">
<h3 className="text-sm font-semibold">Select Directory</h3>
<button
onClick={onCancel}
className="text-foreground-muted hover:text-foreground cursor-pointer text-lg leading-none"
>
&times;
</button>
</div>

<div className="border-border flex items-center gap-2 border-b px-4 py-2">
<button
onClick={handleNavigateUp}
disabled={!canGoUp}
className="bg-backdrop border-border cursor-pointer rounded border px-2 py-0.5 text-xs disabled:opacity-30"
>
..
</button>
<span className="text-foreground-muted truncate text-xs">{currentPath || rootLabel}</span>
</div>

<div className="flex-1 overflow-y-auto p-2">
{loading && <p className="text-foreground-muted p-4 text-center text-xs">Loading...</p>}
{error && <p className="p-4 text-center text-xs text-red-500">{error}</p>}
{!loading && !error && entries.length === 0 && (
<p className="text-foreground-muted p-4 text-center text-xs italic">No subdirectories</p>
)}
{!loading &&
!error &&
entries.map((entry) => (
<button
key={entry.path}
onClick={() => handleClick(entry)}
onDoubleClick={() => handleDoubleClick(entry)}
className={`flex w-full cursor-pointer items-center gap-2 rounded px-3 py-1.5 text-left text-sm ${
selectedEntry === entry.path ? 'bg-backdrop font-medium' : 'hover:bg-backdrop/50'
}`}
>
<span className="relative text-xs">
<FolderIcon className="fill-foreground w-4 flex-shrink-0" />
{entry.projectRoot && (
<span className="absolute bottom-0.5 h-1.5 w-1.5 rounded-full bg-black" style={{ left: '65%' }} />
)}
</span>
<span className="truncate">{entry.name}</span>
</button>
))}
</div>

<div className="border-border flex items-center justify-between border-t px-4 py-3">
<span className="text-foreground-muted max-w-[280px] truncate text-xs">
{activePath || 'Select a directory'}
</span>
<div className="flex gap-2">
<button
onClick={onCancel}
className="border-border hover:bg-backdrop cursor-pointer rounded border px-3 py-1 text-sm"
>
Cancel
</button>
<button
onClick={() => onSelect(activePath)}
disabled={!activePath}
className="bg-backdrop hover:bg-background border-border cursor-pointer rounded border px-3 py-1 text-sm disabled:cursor-not-allowed disabled:opacity-50"
>
Select
</button>
</div>
</div>
</div>
</div>
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,14 @@ import { sortChildren } from './tree-utilities'
export interface FileNode {
name: string
path: string
projectRoot?: boolean
}

export interface FileTreeNode {
name: string
path: string
type: 'FILE' | 'DIRECTORY'
projectRoot?: boolean
children?: FileTreeNode[]
}

Expand All @@ -22,11 +24,11 @@ export default class EditorFilesDataProvider implements TreeDataProvider {

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

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

Expand All @@ -40,27 +42,12 @@ export default class EditorFilesDataProvider implements TreeDataProvider {

this.data['root'] = {
index: 'root',
data: { name: tree.name, path: tree.path },
data: { name: tree.name, path: tree.path, projectRoot: true },
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.data['root'].children = this.buildChildren('root', tree.children)
this.loadedDirectories.add(tree.path)
this.notifyListeners(['root'])
} catch (error) {
Expand All @@ -79,34 +66,35 @@ export default class EditorFilesDataProvider implements TreeDataProvider {
const directory = await fetchDirectoryByPath(this.projectName, item.data.path)
if (!directory) {
console.warn('[EditorFilesDataProvider] Received empty directory from API')
this.data = {}
return
}

const sortedChildren = sortChildren(directory.children)

const children: TreeItemIndex[] = []
item.children = this.buildChildren(itemId, directory.children)
this.loadedDirectories.add(item.data.path)
this.notifyListeners([itemId])
} catch (error) {
console.error('Failed to load directory', error)
}
}

for (const child of sortedChildren) {
const childIndex = `${itemId}/${child.name}`
private buildChildren(parentIndex: TreeItemIndex, children?: FileTreeNode[]): TreeItemIndex[] {
const sorted = sortChildren(children)
const childIds: TreeItemIndex[] = []

this.data[childIndex] = {
index: childIndex,
data: { name: child.name, path: child.path },
isFolder: child.type === 'DIRECTORY',
children: child.type === 'DIRECTORY' ? [] : undefined,
}
for (const child of sorted) {
const childIndex = `${parentIndex}/${child.name}`

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

item.children = children

this.loadedDirectories.add(item.data.path)
this.notifyListeners([itemId])
} catch (error) {
console.error('Failed to load directory', error)
childIds.push(childIndex)
}

return childIds
}

public async getAllItems(): Promise<TreeItem<FileNode>[]> {
Expand Down
17 changes: 0 additions & 17 deletions src/main/frontend/app/hooks/use-backend-folders.ts

This file was deleted.

8 changes: 4 additions & 4 deletions src/main/frontend/app/hooks/use-projects.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { useAsync } from './use-async'
import { fetchProjects } from '~/services/project-service'
import type { Project } from '~/routes/projectlanding/project-landing'
import type { RecentProject } from '~/types/project.types'
import { fetchRecentProjects } from '~/services/recent-project-service'

export function useProjects() {
return useAsync<Project[]>((signal) => fetchProjects(signal))
export function useRecentProjects() {
return useAsync<RecentProject[]>((signal) => fetchRecentProjects(signal))
}
10 changes: 8 additions & 2 deletions src/main/frontend/app/root.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { useEffect } from 'react'
import { isRouteErrorResponse, Links, Meta, Outlet, Scripts, ScrollRestoration } from 'react-router'

import { ToastContainer } from 'react-toastify'
import 'react-toastify/dist/ReactToastify.css'
import type { Route } from './+types/root'
import 'allotment/dist/style.css'
import './app.css'
Expand Down Expand Up @@ -40,7 +41,12 @@ function ThemeProvider({ children }: { children: React.ReactNode }) {
document.documentElement.dataset.theme = theme
}, [theme])

return <>{children}</>
return (
<>
{children}
<ToastContainer theme={theme} position="bottom-right" />
</>
)
}

export function Layout({ children }: Readonly<{ children: React.ReactNode }>) {
Expand Down
Loading
Loading