Editor has no path-duplicate or 'Save as New' / 'Save Copy' — saving an in-progress edit clobbers the original design, and the user has no way to fork a design #187

Closed
opened 2026-10-03 23:46:39 +00:00 by Mark · 1 comment
Owner

Finding

src/components/editor/Editor.tsx's handleSave (line 810) calls saveDesignToGallery with loadedDesignIdRef.current. The result has updated: true when an existing design was updated, updated: false when a new one was created (per saveDesign.ts:138-197).

The Editor UI offers only one Save button. There is no "Save Copy" / "Save As" / "Save as New" option. Once a user loads a design from the gallery, edits it, and clicks Save, the gallery row is updated in place. The original is gone unless they explicitly duplicated it before opening the Editor.

The Gallery has a per-card Duplicate action (per RightClickMenu), but a user who:

  • Loads a design
  • Wants to experiment ("let me try a different colorway")
  • Hits Save

...now has a single design with the experiment, not the original + the experiment.

Path-level duplicate is also missing: the Editor has no "duplicate this path" operation. A user with a 30-node path they want to mirror or offset has to redraw it. grep -rn 'duplicate.*path\|clonePath' src/ returns 0 hits.

Where

  • Editor save: src/pages/Editor.tsx:810-840
  • No Save Copy UI: src/components/editor/EditorHeader.tsx (single Save button at line 220)
  • No path duplicate: src/components/editor/EditorRightPanel.tsx (no Duplicate button next to Add Node / Delete)
  • No path duplicate reducer: grep -n 'DUPLICATE_PATH\|CLONE_PATH' src/components/editor/ returns 0 hits

Suggested fix shape

Save Copy in the Editor's Save button (turn it into a dropdown):

<div className="relative">
  <button onClick={onSave} data-testid="editor-save">Save</button>
  <button onClick={() => setShowSaveMenu(true)} data-testid="editor-save-menu">▾</button>
  {showSaveMenu && (
    <DropdownMenu>
      <MenuItem onClick={handleSave}>Save (update existing)</MenuItem>
      <MenuItem onClick={handleSaveAsNew}>Save as New...</MenuItem>
    </DropdownMenu>
  )}
</div>

handleSaveAsNew clears loadedDesignIdRef.current (or sets it to null) before calling saveDesignToGallery, forcing a new row.

Path duplicate in the Editor's right panel — add a "Duplicate Path" button next to "Add Node" / "Delete" that:

<button onClick={onDuplicateSelectedPath}>
  Duplicate Path
</button>

onDuplicateSelectedPath dispatches a DUPLICATE_PATH action that clones the path (deep copy nodes, new id, offset by +10px), inserts it into the paths array, and pushes history so undo works.

Severity

Medium. The "Save Copy" is a common pattern in any editor; users expect it. Path duplicate is lower priority.

Discovered by

Gap analysis (2026-10-02) of editor save semantics.

  • #150 (closed) — Editor Save button. Lands the basic Save; Save Copy is the natural follow-up.
  • #152 (closed) — Gallery → Editor bridge carries loadedDesignId. Same bridge; same place to add a "save as new" handler.
  • #179 (closed) — Gallery multi-select. Bulk duplicate via the Gallery is a separate but related story.
## Finding `src/components/editor/Editor.tsx`'s `handleSave` (line 810) calls `saveDesignToGallery` with `loadedDesignIdRef.current`. The result has `updated: true` when an existing design was updated, `updated: false` when a new one was created (per `saveDesign.ts:138-197`). The Editor UI offers only one Save button. There is no "Save Copy" / "Save As" / "Save as New" option. Once a user loads a design from the gallery, edits it, and clicks Save, the gallery row is updated in place. The original is gone unless they explicitly duplicated it before opening the Editor. The Gallery has a per-card Duplicate action (per `RightClickMenu`), but a user who: - Loads a design - Wants to experiment ("let me try a different colorway") - Hits Save ...now has a single design with the experiment, not the original + the experiment. Path-level duplicate is also missing: the Editor has no "duplicate this path" operation. A user with a 30-node path they want to mirror or offset has to redraw it. `grep -rn 'duplicate.*path\|clonePath' src/` returns 0 hits. ## Where - Editor save: `src/pages/Editor.tsx:810-840` - No Save Copy UI: `src/components/editor/EditorHeader.tsx` (single Save button at line 220) - No path duplicate: `src/components/editor/EditorRightPanel.tsx` (no Duplicate button next to Add Node / Delete) - No path duplicate reducer: `grep -n 'DUPLICATE_PATH\|CLONE_PATH' src/components/editor/` returns 0 hits ## Suggested fix shape **Save Copy** in the Editor's Save button (turn it into a dropdown): ```tsx <div className="relative"> <button onClick={onSave} data-testid="editor-save">Save</button> <button onClick={() => setShowSaveMenu(true)} data-testid="editor-save-menu">▾</button> {showSaveMenu && ( <DropdownMenu> <MenuItem onClick={handleSave}>Save (update existing)</MenuItem> <MenuItem onClick={handleSaveAsNew}>Save as New...</MenuItem> </DropdownMenu> )} </div> ``` `handleSaveAsNew` clears `loadedDesignIdRef.current` (or sets it to null) before calling `saveDesignToGallery`, forcing a new row. **Path duplicate** in the Editor's right panel — add a "Duplicate Path" button next to "Add Node" / "Delete" that: ```tsx <button onClick={onDuplicateSelectedPath}> Duplicate Path </button> ``` `onDuplicateSelectedPath` dispatches a `DUPLICATE_PATH` action that clones the path (deep copy nodes, new id, offset by +10px), inserts it into the paths array, and pushes history so undo works. ## Severity Medium. The "Save Copy" is a common pattern in any editor; users expect it. Path duplicate is lower priority. ## Discovered by Gap analysis (2026-10-02) of editor save semantics. ## Related - #150 (closed) — Editor Save button. Lands the basic Save; Save Copy is the natural follow-up. - #152 (closed) — Gallery → Editor bridge carries `loadedDesignId`. Same bridge; same place to add a "save as new" handler. - #179 (closed) — Gallery multi-select. Bulk duplicate via the Gallery is a separate but related story.
Author
Owner

Closed by commit 117c193 (origin/main).

fix(#187): Editor Save split-button + Duplicate Path so in-progress edits can fork without clobbering the original.

  • Save is now a split-button: left half is the existing Save (update loaded gallery row), right caret opens a Radix DropdownMenu with 'Save (update existing)' and 'Save as New...'. The new handler clears loadedDesignIdRef before calling saveDesignToGallery so it always lands on a fresh row, mirroring the Workspace's post-save behaviour. Subsequent Saves also create new rows once the binding is cleared.
  • Right panel gets a 'Duplicate Path' button between Add Node and Delete. The Editor's handler deep-clones the selected path (new path id, new node ids, +10/+10 mm offset), pushes a history snapshot for Ctrl+Z, and flips isDirty so the #161 beforeunload and #184 Link-confirm prompts both fire.
  • 11 new tests across Editor, EditorHeader, and EditorRightPanel. tsc -b clean, eslint clean. CI run queued on Forgejo (id 7459) but no runner was available during the session — same waiting pattern as #185/#186 in this push storm.

Closes: #187.

Closed by commit 117c193 (origin/main). fix(#187): Editor Save split-button + Duplicate Path so in-progress edits can fork without clobbering the original. - Save is now a split-button: left half is the existing Save (update loaded gallery row), right caret opens a Radix DropdownMenu with 'Save (update existing)' and 'Save as New...'. The new handler clears loadedDesignIdRef before calling saveDesignToGallery so it always lands on a fresh row, mirroring the Workspace's post-save behaviour. Subsequent Saves also create new rows once the binding is cleared. - Right panel gets a 'Duplicate Path' button between Add Node and Delete. The Editor's handler deep-clones the selected path (new path id, new node ids, +10/+10 mm offset), pushes a history snapshot for Ctrl+Z, and flips isDirty so the #161 beforeunload and #184 Link-confirm prompts both fire. - 11 new tests across Editor, EditorHeader, and EditorRightPanel. tsc -b clean, eslint clean. CI run queued on Forgejo (id 7459) but no runner was available during the session — same waiting pattern as #185/#186 in this push storm. Closes: #187.
Mark closed this issue 2026-10-04 00:06:26 +00:00
Sign in to join this conversation.
No description provided.