From ee5f5e84acc1ec19873e4daa617eec1373932e92 Mon Sep 17 00:00:00 2001 From: gsxdsm Date: Sat, 6 Jun 2026 06:31:44 -0700 Subject: [PATCH] Address PR review feedback (#1466) - routes.ts: use isAbsolute() before resolve() for proper path validation - routes.ts: wrap mkdir() in try/catch to map EEXIST/ENOENT/ENOTDIR to 4xx - DirectoryPicker.tsx: use !browser.currentPath guard to prevent re-fetch loop - DirectoryPicker.tsx: disable Create button during in-flight requests - DirectoryPicker.tsx: add fetchEntries to useEffect dependency array - DirectoryPicker.tsx: client-side validation for folder names - Add changeset for the new create-folder feature --- .changeset/directory-picker-create-folder.md | 17 +++++++++++ .../app/components/DirectoryPicker.tsx | 28 +++++++++++++------ packages/dashboard/src/routes.ts | 28 ++++++++++++++----- 3 files changed, 57 insertions(+), 16 deletions(-) create mode 100644 .changeset/directory-picker-create-folder.md diff --git a/.changeset/directory-picker-create-folder.md b/.changeset/directory-picker-create-folder.md new file mode 100644 index 0000000000..ba7cf88158 --- /dev/null +++ b/.changeset/directory-picker-create-folder.md @@ -0,0 +1,17 @@ +--- +"@runfusion/fusion": minor +--- + +Add "New folder" button to DirectoryPicker for project setup + +The directory picker in the project setup flow now includes a "New folder" +button that lets users create folders directly when selecting a project path. +This includes: + +- New `POST /api/create-directory` endpoint for creating directories +- Create folder UI in DirectoryPicker with inline error handling +- Keyboard support (Enter to create, Escape to cancel) +- Client-side validation for folder names (no path separators or traversal) + +Also fixes a bug where navigating into an empty folder would revert to the +previous directory. diff --git a/packages/dashboard/app/components/DirectoryPicker.tsx b/packages/dashboard/app/components/DirectoryPicker.tsx index 3b4fe09e27..0731106cc5 100644 --- a/packages/dashboard/app/components/DirectoryPicker.tsx +++ b/packages/dashboard/app/components/DirectoryPicker.tsx @@ -74,13 +74,12 @@ export function DirectoryPicker({ value, onChange, placeholder, onInputKeyDown, }); }, []); - // Fetch when browser opens + // Fetch when browser opens (only for the initial open before any path has been fetched) useEffect(() => { - if (browser.isOpen && !browser.loading && browser.entries.length === 0 && !browser.error) { - // Use browser.currentPath if available (user has navigated), otherwise fall back to value prop - fetchEntries(browser.currentPath || value || undefined, browser.showHidden); + if (browser.isOpen && !browser.loading && !browser.currentPath && !browser.error) { + fetchEntries(value || undefined, browser.showHidden); } - }, [browser.isOpen, browser.loading, browser.entries.length, browser.error, value, browser.showHidden, fetchEntries, nodeId, localNodeId]); + }, [browser.isOpen, browser.loading, browser.currentPath, browser.error, value, browser.showHidden, fetchEntries, nodeId, localNodeId]); const handleNavigate = useCallback( (path: string) => { @@ -106,7 +105,8 @@ export function DirectoryPicker({ value, onChange, placeholder, onInputKeyDown, if (browser.isOpen && browser.currentPath) { fetchEntries(browser.currentPath, browser.showHidden); } - }, [browser.showHidden]); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [browser.showHidden, fetchEntries]); const handleToggleCreateFolder = useCallback(() => { setBrowser((prev) => ({ @@ -120,12 +120,22 @@ export function DirectoryPicker({ value, onChange, placeholder, onInputKeyDown, const handleCreateFolder = useCallback(async () => { if (!newFolderName.trim() || !browser.currentPath) return; + // Validate folder name doesn't contain path separators or traversal + const trimmedName = newFolderName.trim(); + if (trimmedName.includes("/") || trimmedName.includes("\\") || trimmedName.includes("..")) { + setBrowser((prev) => ({ + ...prev, + createFolderError: "Folder name cannot contain path separators or '..'", + })); + return; + } + // Normalize path separator for the current platform by using the same // separator already present in currentPath const sep = browser.currentPath.includes("\\") ? "\\" : "/"; const folderPath = browser.currentPath.endsWith(sep) - ? browser.currentPath + newFolderName.trim() - : browser.currentPath + sep + newFolderName.trim(); + ? browser.currentPath + trimmedName + : browser.currentPath + sep + trimmedName; setBrowser((prev) => ({ ...prev, loading: true, createFolderError: null })); try { @@ -287,7 +297,7 @@ export function DirectoryPicker({ value, onChange, placeholder, onInputKeyDown, type="button" className="btn btn-sm btn-primary" onClick={handleCreateFolder} - disabled={!newFolderName.trim()} + disabled={!newFolderName.trim() || browser.loading} > {t("dirPicker.createFolderConfirm", "Create")} diff --git a/packages/dashboard/src/routes.ts b/packages/dashboard/src/routes.ts index cfcc6c4868..a73e15dcc5 100644 --- a/packages/dashboard/src/routes.ts +++ b/packages/dashboard/src/routes.ts @@ -4463,22 +4463,22 @@ export function createApiRoutes(store: TaskStore, options?: ServerOptions): Rout */ router.post("/create-directory", async (req, res) => { try { - const { resolve } = await import("node:path"); + const { resolve, isAbsolute } = await import("node:path"); const { mkdir, stat } = await import("node:fs/promises"); - const rawPath = req.body?.path as string | undefined; + const rawPath = typeof req.body?.path === "string" ? req.body.path.trim() : ""; if (!rawPath) { throw badRequest("Path is required"); } // Validate: must be absolute, no .. traversal - const resolvedPath = resolve(rawPath); + if (!isAbsolute(rawPath)) { + throw badRequest("Path must be absolute"); + } if (rawPath.includes("..")) { throw badRequest("Path must not contain '..' traversal"); } - if (resolvedPath !== resolve(resolvedPath)) { - throw badRequest("Path must be absolute"); - } + const resolvedPath = resolve(rawPath); // Check if path already exists try { @@ -4512,7 +4512,21 @@ export function createApiRoutes(store: TaskStore, options?: ServerOptions): Rout } // Create the directory - await mkdir(resolvedPath); + try { + await mkdir(resolvedPath); + } catch (err: unknown) { + const e = err as NodeJS.ErrnoException; + if (e.code === "EEXIST") { + throw badRequest("Directory already exists"); + } + if (e.code === "ENOENT") { + throw badRequest("Parent directory does not exist"); + } + if (e.code === "ENOTDIR") { + throw badRequest("Parent path is not a directory"); + } + throw err; + } res.json({ success: true, path: resolvedPath }); } catch (err: unknown) {