Merge pull request #1466 from Runfusion/gsxdsm/issue-1454-manage-projet-bug

fix: empty folder selection reverts in DirectoryPicker + add create-folder button
This commit is contained in:
gsxdsm
2026-06-06 09:24:31 -07:00
committed by GitHub
13 changed files with 523 additions and 7 deletions

View File

@@ -0,0 +1,99 @@
---
title: "DirectoryPicker empty folder reverts on navigation"
date: 2026-06-06
category: ui-bugs
module: packages/dashboard/app/components/DirectoryPicker
problem_type: ui_bug
component: frontend_stimulus
symptoms:
- "Selecting an empty folder in the directory picker automatically reverts to the previous folder"
- "Navigation into folders with subdirectories works correctly; empty folders do not"
- "The 'Select' button shows the wrong path after clicking into an empty directory"
root_cause: logic_error
resolution_type: code_fix
severity: medium
related_components:
- testing_framework
tags:
- react-useeffect
- state-management
- directory-picker
- setup-wizard
- project-management
- stale-prop
---
# DirectoryPicker empty folder reverts on navigation
## Problem
In the Fusion dashboard's project setup flow (Setup Wizard and Project Overview "Add Project"), the `DirectoryPicker` component allowed users to browse the filesystem and select a project directory. However, when a user navigated into a folder that contained **no subdirectories**, the picker would automatically jump back to the previously browsed directory. This made it impossible to select an empty folder as a project path — a common scenario when creating a new project in a freshly created directory.
## Symptoms
- Open the directory picker, navigate to a folder with no subdirectories.
- The picker briefly shows "No subdirectories" then reverts to the parent directory.
- If the target folder contains even a single empty subdirectory, navigation works correctly.
- The `currentPath` display in the action footer shows the wrong path after the revert.
## What Didn't Work
- Adding defensive checks for `entries.length === 0` in the render path — the render was correct; the problem was a re-fetch triggered by a `useEffect` that overwrote the navigated state.
- Suspecting a race condition between `onChange` and browser close — the revert happened even without clicking "Select", purely on navigation.
- Suspecting the API response for empty folders — the API correctly returned `entries: []`; the bug was in how the frontend handled that response.
## Solution
In `packages/dashboard/app/components/DirectoryPicker.tsx`, the `useEffect` that auto-fetches entries when the browser panel opens was using the stale `value` prop instead of `browser.currentPath`:
**Before:**
```tsx
useEffect(() => {
if (browser.isOpen && !browser.loading && browser.entries.length === 0 && !browser.error) {
fetchEntries(value || undefined, browser.showHidden);
}
}, [browser.isOpen, browser.loading, browser.entries.length, browser.error, value, browser.showHidden, fetchEntries, nodeId, localNodeId]);
```
**After:**
```tsx
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);
}
}, [browser.isOpen, browser.loading, browser.entries.length, browser.error, value, browser.showHidden, fetchEntries, nodeId, localNodeId]);
```
The fix prioritizes `browser.currentPath` (which reflects the user's current navigation) over the `value` prop (which only updates when the user explicitly clicks "Select").
## Why This Works
The `useEffect` fires when `browser.entries.length === 0` — which is true both on initial open (no entries loaded yet) and after navigating into an empty folder (API returned `entries: []`).
- **On initial open**: `browser.currentPath` is `""`, so `value || undefined` fetches the initial directory. Correct.
- **After navigating into an empty folder**: `browser.currentPath` is the navigated path, so `browser.currentPath || value` fetches the current directory. Correct.
- **Without the fix**: After navigating into an empty folder, the effect used `value` (still `""` or the previous path), causing a refetch of the wrong directory and overwriting `browser.currentPath`.
The bug was a **stale closure / stale prop** issue: the effect captured `value` at render time, but `value` is only updated by the parent when `handleSelect` calls `onChange(browser.currentPath)`. Until then, `value` lags behind the user's navigation.
## Prevention
- **When a `useEffect` re-fetches based on empty-state conditions, prefer derived/local state over props.** Props that update via callbacks are inherently stale until the callback fires. Local state (`browser.currentPath`) reflects the immediate user interaction.
- **Audit empty-state refetch effects** in components with browse/navigate patterns. Any effect keyed on `entries.length === 0` or `data.length === 0` that uses a prop for the fetch path is vulnerable to this pattern.
- **Regression test**: Mock `browseDirectory` to return `entries: []` for a subdirectory, navigate into it, and assert the component does not re-fetch the parent path. The test in `DirectoryPicker.test.tsx` ("does not revert to previous folder when navigating into an empty directory") captures this invariant.
- **Grep heuristic** for similar bugs:
```bash
git grep -nE "fetchEntries\(.*value.*\)" -- '*.tsx'
git grep -nE "useEffect.*entries\.length === 0" -- '*.tsx'
```
Look for effects that fetch based on empty data and use a prop as the fetch key.
## Related
- PR #1466 — fix and "New folder" button feature.
- `docs/solutions/ui-bugs/skill-autocomplete-highlight-reset-on-swr-revalidation.md` — a related class of `useEffect` bugs keyed on stale/array identity, with broader prevention guidance.
- `packages/dashboard/app/components/DirectoryPicker.tsx` — the fixed component.
- `packages/dashboard/app/components/__tests__/DirectoryPicker.test.tsx` — regression tests.