diff --git a/src/main/frontend/app/components/directory-picker/directory-picker.tsx b/src/main/frontend/app/components/directory-picker/directory-picker.tsx index 0a7f93f3..32b8c75f 100644 --- a/src/main/frontend/app/components/directory-picker/directory-picker.tsx +++ b/src/main/frontend/app/components/directory-picker/directory-picker.tsx @@ -1,5 +1,6 @@ import { useCallback, useEffect, useState } from 'react' import FolderIcon from '/icons/solar/Folder.svg?react' +import NameInputDialog from '~/components/file-structure/name-input-dialog' import { filesystemService } from '~/services/filesystem-service' import type { FilesystemEntry } from '~/types/filesystem.types' import { ApiError } from '~/utils/api' @@ -26,11 +27,13 @@ export default function DirectoryPicker({ const [selectedEntry, setSelectedEntry] = useState(null) const [loading, setLoading] = useState(false) const [error, setError] = useState(null) + const [isCreatingFolder, setIsCreatingFolder] = useState(false) const loadEntries = useCallback(async (path: string) => { setLoading(true) setError(null) setSelectedEntry(null) + setIsCreatingFolder(false) try { const result = await filesystemService.browse(path) setEntries(result.entries) @@ -47,10 +50,6 @@ export default function DirectoryPicker({ } }, []) - const handleNavigateUp = () => { - loadEntries(parentPath) - } - useEffect(() => { if (isOpen) { setSelectedEntry(null) @@ -60,8 +59,6 @@ export default function DirectoryPicker({ if (!isOpen) return null - const canGoUp = parentPath !== '' - const handleClick = (entry: FilesystemEntry) => { setSelectedEntry(entry.path) } @@ -70,6 +67,21 @@ export default function DirectoryPicker({ loadEntries(entry.path) } + const handleCreateFolder = async (folderName: string) => { + const basePath = selectedEntry ?? currentPath + const separator = basePath.includes('\\') ? '\\' : '/' + const newPath = `${basePath}${separator}${folderName}` + setIsCreatingFolder(false) + try { + await filesystemService.createDirectory(newPath) + await loadEntries(basePath) + setSelectedEntry(newPath) + } catch { + setError('Failed to create folder') + } + } + + const canGoUp = parentPath !== '' const activePath = selectedEntry ?? currentPath return ( @@ -87,21 +99,32 @@ export default function DirectoryPicker({
- {currentPath || rootLabel} + {currentPath || rootLabel} + {currentPath && !isCreatingFolder && ( + + )}
-
- {loading &&

Loading...

} +
+ {loading &&

Loading…

} {error &&

{error}

} - {!loading && !error && entries.length === 0 && ( + + {!loading && !error && entries.length === 0 && !isCreatingFolder && (

No subdirectories

)} + {!loading && !error && entries.map((entry) => ( @@ -113,8 +136,8 @@ export default function DirectoryPicker({ selectedEntry === entry.path ? 'bg-backdrop font-medium' : 'hover:bg-backdrop/50' }`} > - - + + {entry.projectRoot && ( )} @@ -122,6 +145,15 @@ export default function DirectoryPicker({ {entry.name} ))} + + {isCreatingFolder && ( + void handleCreateFolder(name)} + onCancel={() => setIsCreatingFolder(false)} + /> + )}
diff --git a/src/main/frontend/app/components/file-structure/file-tree-dialogs.tsx b/src/main/frontend/app/components/file-structure/file-tree-dialogs.tsx index 5de34c46..9d92d427 100644 --- a/src/main/frontend/app/components/file-structure/file-tree-dialogs.tsx +++ b/src/main/frontend/app/components/file-structure/file-tree-dialogs.tsx @@ -49,6 +49,7 @@ export default function FileTreeDialogs({ = BASE_NAME interface NameInputDialogProps { title: string initialValue?: string + submitLabel?: string onSubmit: (name: string) => void onCancel: () => void patterns?: Record @@ -33,6 +34,7 @@ interface NameInputDialogProps { export default function NameInputDialog({ title, initialValue = '', + submitLabel = 'OK', onSubmit, onCancel, patterns = FOLDER_OR_ADAPTER_NAME_PATTERNS, @@ -63,7 +65,7 @@ export default function NameInputDialog({ return createPortal(
@@ -82,7 +84,7 @@ export default function NameInputDialog({ Cancel
diff --git a/src/main/frontend/app/components/file-structure/studio-file-tree-dialogs.tsx b/src/main/frontend/app/components/file-structure/studio-file-tree-dialogs.tsx index 684c0e39..6ed4d8c5 100644 --- a/src/main/frontend/app/components/file-structure/studio-file-tree-dialogs.tsx +++ b/src/main/frontend/app/components/file-structure/studio-file-tree-dialogs.tsx @@ -51,6 +51,7 @@ export default function StudioFileTreeDialogs({ void patterns?: Record } @@ -118,6 +119,7 @@ export function useFileTreeContextMenu({ setNameDialog({ title: 'New File', + submitLabel: 'Create', onSubmit: async (name: string) => { if (!ensureHasCorrectExtension(name)) { showErrorToast(`Filename must have one of the following extensions: ${ALLOWED_EXTENSIONS.join(', ')}`) @@ -148,6 +150,7 @@ export function useFileTreeContextMenu({ setNameDialog({ title: 'New Folder', + submitLabel: 'Create', onSubmit: async (name: string) => { try { await createFolderInProject(projectName, `${parentPath}/${name}`) diff --git a/src/main/frontend/app/components/file-structure/use-studio-context-menu.ts b/src/main/frontend/app/components/file-structure/use-studio-context-menu.ts index 82aedfe0..b3fb0aac 100644 --- a/src/main/frontend/app/components/file-structure/use-studio-context-menu.ts +++ b/src/main/frontend/app/components/file-structure/use-studio-context-menu.ts @@ -27,6 +27,7 @@ export interface StudioContextMenuState { export interface NameDialogState { title: string initialValue?: string + submitLabel?: string onSubmit: (name: string) => void patterns?: Record } @@ -171,6 +172,7 @@ export function useStudioContextMenu({ projectName, dataProvider }: UseStudioCon setNameDialog({ title: 'New Configuration File', + submitLabel: 'Create', onSubmit: async (name: string) => { const fileName = ensureXmlExtension(name) try { @@ -201,6 +203,7 @@ export function useStudioContextMenu({ projectName, dataProvider }: UseStudioCon setNameDialog({ title: 'New Adapter', + submitLabel: 'Create', onSubmit: async (name: string) => { try { await createAdapter(projectName, name, menu.path) @@ -224,6 +227,7 @@ export function useStudioContextMenu({ projectName, dataProvider }: UseStudioCon setNameDialog({ title: 'New Folder', + submitLabel: 'Create', onSubmit: async (name: string) => { try { await createFolderInProject(projectName, `${menu.folderPath}/${name}`) diff --git a/src/main/frontend/app/services/filesystem-service.ts b/src/main/frontend/app/services/filesystem-service.ts index e59cb5f0..6a20654f 100644 --- a/src/main/frontend/app/services/filesystem-service.ts +++ b/src/main/frontend/app/services/filesystem-service.ts @@ -10,4 +10,8 @@ export const filesystemService = { const result = await this.browse(path) return result.resolvedPath }, + + async createDirectory(path: string): Promise { + return apiFetch(`/filesystem/mkdir?path=${encodeURIComponent(path)}`, { method: 'POST' }) + }, } diff --git a/src/main/java/org/frankframework/flow/filesystem/FilesystemController.java b/src/main/java/org/frankframework/flow/filesystem/FilesystemController.java index 5ba77fc7..2802881f 100644 --- a/src/main/java/org/frankframework/flow/filesystem/FilesystemController.java +++ b/src/main/java/org/frankframework/flow/filesystem/FilesystemController.java @@ -5,6 +5,7 @@ import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; @@ -28,4 +29,16 @@ public ResponseEntity browse(@RequestParam(required = false, defau return ResponseEntity.status(HttpStatus.FORBIDDEN).build(); } } + + @PostMapping("/mkdir") + public ResponseEntity mkdir(@RequestParam String path) throws IOException { + try { + fileSystemStorage.createProjectDirectory(path); + return ResponseEntity.ok().build(); + } catch (AccessDeniedException _) { + return ResponseEntity.status(HttpStatus.FORBIDDEN).build(); + } catch (SecurityException _) { + return ResponseEntity.status(HttpStatus.BAD_REQUEST).build(); + } + } }