Gallery folders have no rename or delete — user can only create folders via the + button, no way to rename a misnamed folder or delete a folder that's no longer needed #234

Closed
opened 2026-10-04 02:32:09 +00:00 by Mark · 1 comment
Owner

Finding

src/components/gallery/FolderNav.tsx renders a list of folders as buttons. The buttons:

  • Click → set as active folder (filter)
  • Drag-over / drop → reorder designs between folders

There's no:

  • Right-click context menu (no onContextMenu handler on the folder button)
  • Edit/pencil button on hover
  • Delete button on hover

A user who:

  • Creates "Work Projects" folder
  • Realizes it should be "Work 2024"
  • Has to manually delete all designs from the folder, delete the folder, recreate it, move the designs back

…or just live with the misnamed folder. There's no edit affordance.

grep -n 'renameFolder\|deleteFolder' src/ returns 0 hits. No reducer actions, no UI.

Where

  • FolderNav: src/components/gallery/FolderNav.tsx (read-only folder chips + create)
  • No rename: grep -n 'renameFolder' src/ returns 0
  • No delete: grep -n 'deleteFolder' src/ returns 0
  • The folder type: src/components/gallery/types.ts:23 (id, name)

Suggested fix shape

Two options:

  1. Right-click context menu on the folder button:
<ContextMenu>
  <ContextMenuTrigger asChild>
    <button>{folder.name}</button>
  </ContextMenuTrigger>
  <ContextMenuContent>
    <ContextMenuItem onClick={() => onRenameFolder(folder.id)}>
      <Pencil size={12} /> Rename
    </ContextMenuItem>
    <ContextMenuItem onClick={() => onDeleteFolder(folder.id)} disabled={folder.id === 'all'}>
      <Trash2 size={12} /> Delete
    </ContextMenuItem>
  </ContextMenuContent>
</ContextMenu>

onRenameFolder opens a small modal with the current name pre-filled; Save calls a RENAME_FOLDER reducer action.

onDeleteFolder (disabled for the 'all' folder) opens a confirmation modal: "Delete folder X? N designs will be moved to 'No folder' (or deleted, your choice)." The confirm dispatches DELETE_FOLDER which either:

  • Sets folderId: null on all designs in the folder (preserves them in 'all')
  • Or deletes them (irreversible; requires the explicit choice)
  1. Hover-revealed edit/delete icons (no context menu needed):
<button onClick={...}>
  {folder.name}
  <span onClick={(e) => { e.stopPropagation(); onRenameFolder(folder.id); }}>
    <Pencil size={10} />
  </span>
  {folder.id !== 'all' && (
    <span onClick={(e) => { e.stopPropagation(); onDeleteFolder(folder.id); }}>
      <X size={10} />
    </span>
  )}
</button>

Both are valid. The right-click menu is more standard for power users; the hover icons are more discoverable for new users.

Severity

Low. The user can work around by recreating folders. Power users with many folders notice.

Discovered by

Gap analysis (2026-10-02) of gallery folder management.

  • The folder create dialog (NewFolderDialog) is in place. Rename and delete are the missing CRUD.
  • Folder drag-and-drop reorder is in place (per onFolderDragOver); rename/delete are siblings.
  • The designs' folderId field is a string; the 'all' folder is a sentinel value (folderId === 'all'), not a real row in the folders array. The 'no folder' state is folderId === null.
## Finding `src/components/gallery/FolderNav.tsx` renders a list of folders as buttons. The buttons: - Click → set as active folder (filter) - Drag-over / drop → reorder designs between folders There's no: - Right-click context menu (no `onContextMenu` handler on the folder button) - Edit/pencil button on hover - Delete button on hover A user who: - Creates "Work Projects" folder - Realizes it should be "Work 2024" - Has to manually delete all designs from the folder, delete the folder, recreate it, move the designs back …or just live with the misnamed folder. There's no edit affordance. `grep -n 'renameFolder\|deleteFolder' src/` returns 0 hits. No reducer actions, no UI. ## Where - FolderNav: `src/components/gallery/FolderNav.tsx` (read-only folder chips + create) - No rename: `grep -n 'renameFolder' src/` returns 0 - No delete: `grep -n 'deleteFolder' src/` returns 0 - The folder type: `src/components/gallery/types.ts:23` (`id`, `name`) ## Suggested fix shape Two options: 1. **Right-click context menu** on the folder button: ```tsx <ContextMenu> <ContextMenuTrigger asChild> <button>{folder.name}</button> </ContextMenuTrigger> <ContextMenuContent> <ContextMenuItem onClick={() => onRenameFolder(folder.id)}> <Pencil size={12} /> Rename </ContextMenuItem> <ContextMenuItem onClick={() => onDeleteFolder(folder.id)} disabled={folder.id === 'all'}> <Trash2 size={12} /> Delete </ContextMenuItem> </ContextMenuContent> </ContextMenu> ``` `onRenameFolder` opens a small modal with the current name pre-filled; Save calls a `RENAME_FOLDER` reducer action. `onDeleteFolder` (disabled for the 'all' folder) opens a confirmation modal: "Delete folder X? N designs will be moved to 'No folder' (or deleted, your choice)." The confirm dispatches `DELETE_FOLDER` which either: - Sets `folderId: null` on all designs in the folder (preserves them in 'all') - Or deletes them (irreversible; requires the explicit choice) 2. **Hover-revealed edit/delete icons** (no context menu needed): ```tsx <button onClick={...}> {folder.name} <span onClick={(e) => { e.stopPropagation(); onRenameFolder(folder.id); }}> <Pencil size={10} /> </span> {folder.id !== 'all' && ( <span onClick={(e) => { e.stopPropagation(); onDeleteFolder(folder.id); }}> <X size={10} /> </span> )} </button> ``` Both are valid. The right-click menu is more standard for power users; the hover icons are more discoverable for new users. ## Severity Low. The user can work around by recreating folders. Power users with many folders notice. ## Discovered by Gap analysis (2026-10-02) of gallery folder management. ## Related - The folder create dialog (`NewFolderDialog`) is in place. Rename and delete are the missing CRUD. - Folder drag-and-drop reorder is in place (per `onFolderDragOver`); rename/delete are siblings. - The designs' `folderId` field is a string; the 'all' folder is a sentinel value (`folderId === 'all'`), not a real row in the `folders` array. The 'no folder' state is `folderId === null`.
Author
Owner

Shipped in commit d8a905e on origin/main.

Implementation (chose option 1 from the issue — right-click context menu):

  • FolderNav now listens for onContextMenu on each folder chip and shows a small popup with Rename and Delete. The pop-up uses pure DOM (matches the existing RightClickMenu pattern for design cards) so the click coordinates stay local.
  • Default folders (All Designs, Unsorted, Favorites) still open the menu on right-click, but both items render disabled (aria-disabled="true", not-allowed cursor, dim color). The menu opening on every folder avoids the surprise of right-click doing nothing on sentinels; the items inside just refuse the action.
  • RenameFolderDialog (new, mirrors NewFolderDialog): pre-fills the current name, rejects empty / duplicate names against the rest of the user folders, Save is disabled while the name is unchanged.
  • DeleteFolderDialog (new): confirmation quotes the folder and shows the live design count (0 / 1 design / N designs) plus the moved-to-Unsorted promise. Delete folder button is the destructive red (#DC2626), Cancel is the outline.
  • Designs are moved to unsorted, not deleted. handleDeleteFolder reassigns folderId to unsorted for every design that was in the folder, so they remain in the All Designs view and can be re-foldered later. The active-folder filter resets to all if the deleted folder was selected.

Defense in depth: FolderNav gates the menu items by isDefault, and Gallery.tsx's handleRenameFolder / handleDeleteFolder re-check isDefault as a second layer in case the gate is bypassed.

Tests: 18 new tests in src/components/gallery/issue-234-folder-actions.test.tsx — menu wiring, both default-folder gates, dialog pre-fill / disabled-Save / duplicate-name / confirm-payload, and an end-to-end Gallery mount that verifies the delete confirms the design count and the designs survive (with folderId: unsorted). Full suite green: 1192 passing (was 1174), 96 test files.

npm run lint and npm run build both clean. Push to origin/main succeeded (36a56d4..d8a905e).

Shipped in commit d8a905e on origin/main. **Implementation (chose option 1 from the issue — right-click context menu):** - `FolderNav` now listens for `onContextMenu` on each folder chip and shows a small popup with **Rename** and **Delete**. The pop-up uses pure DOM (matches the existing `RightClickMenu` pattern for design cards) so the click coordinates stay local. - Default folders (`All Designs`, `Unsorted`, `Favorites`) still open the menu on right-click, but both items render disabled (`aria-disabled="true"`, `not-allowed` cursor, dim color). The menu opening on every folder avoids the surprise of right-click doing nothing on sentinels; the items inside just refuse the action. - `RenameFolderDialog` (new, mirrors `NewFolderDialog`): pre-fills the current name, rejects empty / duplicate names against the rest of the user folders, **Save** is disabled while the name is unchanged. - `DeleteFolderDialog` (new): confirmation quotes the folder and shows the live design count (`0 / 1 design / N designs`) plus the moved-to-Unsorted promise. **Delete folder** button is the destructive red (`#DC2626`), **Cancel** is the outline. - **Designs are moved to `unsorted`, not deleted.** `handleDeleteFolder` reassigns `folderId` to `unsorted` for every design that was in the folder, so they remain in the `All Designs` view and can be re-foldered later. The active-folder filter resets to `all` if the deleted folder was selected. **Defense in depth:** `FolderNav` gates the menu items by `isDefault`, and `Gallery.tsx`'s `handleRenameFolder` / `handleDeleteFolder` re-check `isDefault` as a second layer in case the gate is bypassed. **Tests:** 18 new tests in `src/components/gallery/issue-234-folder-actions.test.tsx` — menu wiring, both default-folder gates, dialog pre-fill / disabled-Save / duplicate-name / confirm-payload, and an end-to-end Gallery mount that verifies the delete confirms the design count and the designs survive (with `folderId: unsorted`). Full suite green: **1192 passing** (was 1174), 96 test files. `npm run lint` and `npm run build` both clean. Push to `origin/main` succeeded (`36a56d4..d8a905e`).
Mark closed this issue 2026-10-04 07:34:11 +00:00
Sign in to join this conversation.
No description provided.