Skip to content

Commit ac067af

Browse files
Add folder creation functionality to directory picker (#496)
1 parent 521f1e0 commit ac067af

8 files changed

Lines changed: 75 additions & 15 deletions

File tree

src/main/frontend/app/components/directory-picker/directory-picker.tsx

Lines changed: 45 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { useCallback, useEffect, useState } from 'react'
22
import FolderIcon from '/icons/solar/Folder.svg?react'
3+
import NameInputDialog from '~/components/file-structure/name-input-dialog'
34
import { filesystemService } from '~/services/filesystem-service'
45
import type { FilesystemEntry } from '~/types/filesystem.types'
56
import { ApiError } from '~/utils/api'
@@ -26,11 +27,13 @@ export default function DirectoryPicker({
2627
const [selectedEntry, setSelectedEntry] = useState<string | null>(null)
2728
const [loading, setLoading] = useState(false)
2829
const [error, setError] = useState<string | null>(null)
30+
const [isCreatingFolder, setIsCreatingFolder] = useState(false)
2931

3032
const loadEntries = useCallback(async (path: string) => {
3133
setLoading(true)
3234
setError(null)
3335
setSelectedEntry(null)
36+
setIsCreatingFolder(false)
3437
try {
3538
const result = await filesystemService.browse(path)
3639
setEntries(result.entries)
@@ -47,10 +50,6 @@ export default function DirectoryPicker({
4750
}
4851
}, [])
4952

50-
const handleNavigateUp = () => {
51-
loadEntries(parentPath)
52-
}
53-
5453
useEffect(() => {
5554
if (isOpen) {
5655
setSelectedEntry(null)
@@ -60,8 +59,6 @@ export default function DirectoryPicker({
6059

6160
if (!isOpen) return null
6261

63-
const canGoUp = parentPath !== ''
64-
6562
const handleClick = (entry: FilesystemEntry) => {
6663
setSelectedEntry(entry.path)
6764
}
@@ -70,6 +67,21 @@ export default function DirectoryPicker({
7067
loadEntries(entry.path)
7168
}
7269

70+
const handleCreateFolder = async (folderName: string) => {
71+
const basePath = selectedEntry ?? currentPath
72+
const separator = basePath.includes('\\') ? '\\' : '/'
73+
const newPath = `${basePath}${separator}${folderName}`
74+
setIsCreatingFolder(false)
75+
try {
76+
await filesystemService.createDirectory(newPath)
77+
await loadEntries(basePath)
78+
setSelectedEntry(newPath)
79+
} catch {
80+
setError('Failed to create folder')
81+
}
82+
}
83+
84+
const canGoUp = parentPath !== ''
7385
const activePath = selectedEntry ?? currentPath
7486

7587
return (
@@ -87,21 +99,32 @@ export default function DirectoryPicker({
8799

88100
<div className="border-border flex items-center gap-2 border-b px-4 py-2">
89101
<Button
90-
onClick={handleNavigateUp}
102+
onClick={() => loadEntries(parentPath)}
91103
disabled={!canGoUp}
92104
className="disabled:text-foreground-muted text-xs disabled:opacity-30"
93105
>
94106
..
95107
</Button>
96-
<span className="text-foreground-muted truncate text-xs">{currentPath || rootLabel}</span>
108+
<span className="text-foreground-muted flex-1 truncate text-xs">{currentPath || rootLabel}</span>
109+
{currentPath && !isCreatingFolder && (
110+
<Button
111+
onClick={() => setIsCreatingFolder(true)}
112+
className="text-foreground-muted hover:text-foreground shrink-0 text-xs"
113+
title="Create a new folder here"
114+
>
115+
New Folder
116+
</Button>
117+
)}
97118
</div>
98119

99-
<div className="flex-1 overflow-y-auto p-2">
100-
{loading && <p className="text-foreground-muted p-4 text-center text-xs">Loading...</p>}
120+
<div className="flex-1 overflow-y-auto py-1">
121+
{loading && <p className="text-foreground-muted p-4 text-center text-xs">Loading</p>}
101122
{error && <p className="p-4 text-center text-xs text-red-500">{error}</p>}
102-
{!loading && !error && entries.length === 0 && (
123+
124+
{!loading && !error && entries.length === 0 && !isCreatingFolder && (
103125
<p className="text-foreground-muted p-4 text-center text-xs italic">No subdirectories</p>
104126
)}
127+
105128
{!loading &&
106129
!error &&
107130
entries.map((entry) => (
@@ -113,15 +136,24 @@ export default function DirectoryPicker({
113136
selectedEntry === entry.path ? 'bg-backdrop font-medium' : 'hover:bg-backdrop/50'
114137
}`}
115138
>
116-
<span className="relative text-xs">
117-
<FolderIcon className="fill-foreground w-4 flex-shrink-0" />
139+
<span className="relative flex-shrink-0 text-xs">
140+
<FolderIcon className="fill-foreground w-4" />
118141
{entry.projectRoot && (
119142
<span className="absolute bottom-0.5 h-1.5 w-1.5 rounded-full bg-black" style={{ left: '65%' }} />
120143
)}
121144
</span>
122145
<span className="truncate">{entry.name}</span>
123146
</button>
124147
))}
148+
149+
{isCreatingFolder && (
150+
<NameInputDialog
151+
title="New Folder"
152+
submitLabel="Create"
153+
onSubmit={(name) => void handleCreateFolder(name)}
154+
onCancel={() => setIsCreatingFolder(false)}
155+
/>
156+
)}
125157
</div>
126158

127159
<div className="border-border flex items-center justify-between border-t px-4 py-3">

src/main/frontend/app/components/file-structure/file-tree-dialogs.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ export default function FileTreeDialogs({
4949
<NameInputDialog
5050
title={nameDialog.title}
5151
initialValue={nameDialog.initialValue}
52+
submitLabel={nameDialog.submitLabel}
5253
onSubmit={nameDialog.onSubmit}
5354
onCancel={onCloseNameDialog}
5455
patterns={nameDialog.patterns}

src/main/frontend/app/components/file-structure/name-input-dialog.tsx

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ export const FOLDER_OR_ADAPTER_NAME_PATTERNS: Record<string, RegExp> = BASE_NAME
2525
interface NameInputDialogProps {
2626
title: string
2727
initialValue?: string
28+
submitLabel?: string
2829
onSubmit: (name: string) => void
2930
onCancel: () => void
3031
patterns?: Record<string, RegExp>
@@ -33,6 +34,7 @@ interface NameInputDialogProps {
3334
export default function NameInputDialog({
3435
title,
3536
initialValue = '',
37+
submitLabel = 'OK',
3638
onSubmit,
3739
onCancel,
3840
patterns = FOLDER_OR_ADAPTER_NAME_PATTERNS,
@@ -63,7 +65,7 @@ export default function NameInputDialog({
6365
return createPortal(
6466
<div
6567
ref={overlayRef}
66-
className="fixed inset-0 z-50 flex items-center justify-center bg-black/30"
68+
className="fixed inset-0 z-[70] flex items-center justify-center bg-black/30"
6769
onClick={handleOverlayClick}
6870
>
6971
<div className="bg-background border-border w-80 rounded-md border p-4 shadow-lg">
@@ -82,7 +84,7 @@ export default function NameInputDialog({
8284
Cancel
8385
</Button>
8486
<Button onClick={handleSubmit} disabled={!isValid} className="px-3 py-1 text-sm">
85-
OK
87+
{submitLabel}
8688
</Button>
8789
</div>
8890
</div>

src/main/frontend/app/components/file-structure/studio-file-tree-dialogs.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@ export default function StudioFileTreeDialogs({
5151
<NameInputDialog
5252
title={nameDialog.title}
5353
initialValue={nameDialog.initialValue}
54+
submitLabel={nameDialog.submitLabel}
5455
onSubmit={nameDialog.onSubmit}
5556
onCancel={onCloseNameDialog}
5657
patterns={nameDialog.patterns}

src/main/frontend/app/components/file-structure/use-file-tree-context-menu.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ export interface ContextMenuState {
2020
export interface NameDialogState {
2121
title: string
2222
initialValue?: string
23+
submitLabel?: string
2324
onSubmit: (name: string) => void
2425
patterns?: Record<string, RegExp>
2526
}
@@ -118,6 +119,7 @@ export function useFileTreeContextMenu({
118119

119120
setNameDialog({
120121
title: 'New File',
122+
submitLabel: 'Create',
121123
onSubmit: async (name: string) => {
122124
if (!ensureHasCorrectExtension(name)) {
123125
showErrorToast(`Filename must have one of the following extensions: ${ALLOWED_EXTENSIONS.join(', ')}`)
@@ -148,6 +150,7 @@ export function useFileTreeContextMenu({
148150

149151
setNameDialog({
150152
title: 'New Folder',
153+
submitLabel: 'Create',
151154
onSubmit: async (name: string) => {
152155
try {
153156
await createFolderInProject(projectName, `${parentPath}/${name}`)

src/main/frontend/app/components/file-structure/use-studio-context-menu.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ export interface StudioContextMenuState {
2727
export interface NameDialogState {
2828
title: string
2929
initialValue?: string
30+
submitLabel?: string
3031
onSubmit: (name: string) => void
3132
patterns?: Record<string, RegExp>
3233
}
@@ -171,6 +172,7 @@ export function useStudioContextMenu({ projectName, dataProvider }: UseStudioCon
171172

172173
setNameDialog({
173174
title: 'New Configuration File',
175+
submitLabel: 'Create',
174176
onSubmit: async (name: string) => {
175177
const fileName = ensureXmlExtension(name)
176178
try {
@@ -201,6 +203,7 @@ export function useStudioContextMenu({ projectName, dataProvider }: UseStudioCon
201203

202204
setNameDialog({
203205
title: 'New Adapter',
206+
submitLabel: 'Create',
204207
onSubmit: async (name: string) => {
205208
try {
206209
await createAdapter(projectName, name, menu.path)
@@ -224,6 +227,7 @@ export function useStudioContextMenu({ projectName, dataProvider }: UseStudioCon
224227

225228
setNameDialog({
226229
title: 'New Folder',
230+
submitLabel: 'Create',
227231
onSubmit: async (name: string) => {
228232
try {
229233
await createFolderInProject(projectName, `${menu.folderPath}/${name}`)

src/main/frontend/app/services/filesystem-service.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,4 +10,8 @@ export const filesystemService = {
1010
const result = await this.browse(path)
1111
return result.resolvedPath
1212
},
13+
14+
async createDirectory(path: string): Promise<void> {
15+
return apiFetch(`/filesystem/mkdir?path=${encodeURIComponent(path)}`, { method: 'POST' })
16+
},
1317
}

src/main/java/org/frankframework/flow/filesystem/FilesystemController.java

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
import org.springframework.http.HttpStatus;
66
import org.springframework.http.ResponseEntity;
77
import org.springframework.web.bind.annotation.GetMapping;
8+
import org.springframework.web.bind.annotation.PostMapping;
89
import org.springframework.web.bind.annotation.RequestMapping;
910
import org.springframework.web.bind.annotation.RequestParam;
1011
import org.springframework.web.bind.annotation.RestController;
@@ -28,4 +29,16 @@ public ResponseEntity<BrowseResult> browse(@RequestParam(required = false, defau
2829
return ResponseEntity.status(HttpStatus.FORBIDDEN).build();
2930
}
3031
}
32+
33+
@PostMapping("/mkdir")
34+
public ResponseEntity<Void> mkdir(@RequestParam String path) throws IOException {
35+
try {
36+
fileSystemStorage.createProjectDirectory(path);
37+
return ResponseEntity.ok().build();
38+
} catch (AccessDeniedException _) {
39+
return ResponseEntity.status(HttpStatus.FORBIDDEN).build();
40+
} catch (SecurityException _) {
41+
return ResponseEntity.status(HttpStatus.BAD_REQUEST).build();
42+
}
43+
}
3144
}

0 commit comments

Comments
 (0)