Skip to content

feat(ui): drag-and-drop reordering for reference images#9081

Open
lstein wants to merge 8 commits intomainfrom
feat/ref-image-reorder-dnd
Open

feat(ui): drag-and-drop reordering for reference images#9081
lstein wants to merge 8 commits intomainfrom
feat/ref-image-reorder-dnd

Conversation

@lstein
Copy link
Copy Markdown
Collaborator

@lstein lstein commented Apr 21, 2026

Summary

The image edit models are sensitive to the order of the reference images: I have found that the first image tends to have more weight than the second and subsequent ones. In addition, when the prompt refers to images as "image 1, image 2", etc and you want to swap out one ref image for another, the current UI only allows you to add images to the end of the list, setting up cases in which the prompt has to be edited to accommodate.

This PR enables the order of reference images to be interactively changed by dragging and dropping them:

  • Users can now drag reference image thumbnails left/right to reorder them in the panel. Order is preserved through generation and recall automatically since ref images are already stored as an ordered array that is serialized to graph metadata in order.
  • Adds a refImagesReordered reducer with validation (length match, known ids, no duplicates) and unit tests.
  • UI piece uses the existing pragmatic-drag-and-drop setup, matching the pattern in CanvasEntityGroupList.

Test plan

  • Add 2+ reference images via upload or drag from gallery.
  • Drag one thumbnail left or right of another; confirm the drop indicator appears on the correct edge and the order updates on release.
  • Generate an image and confirm the ref images are applied in the new visual order (e.g., with an IP adapter set, changing the order should change the effect).
  • Recall metadata from a prior generation and confirm the order is preserved.
  • Verify clicking a thumbnail (vs. dragging) still opens/closes the settings panel.
  • pnpm lint and pnpm test:no-watch pass.

🤖 Generated with Claude Code

Reference images are already stored as an ordered array and serialized
to metadata in order, so graph building and recall automatically respect
the new order. This change adds the UI affordance: users can drag
reference image thumbnails left/right to reorder them.

- Adds `refImagesReordered` reducer with validation against length
  mismatch, unknown ids, and duplicates.
- Adds `singleRefImageDndSource` and `useRefImageDnd` hook using
  pragmatic-drag-and-drop with horizontal edges.
- Wraps `RefImagePreview` in a draggable container with drop indicator.
- Disables native `<img>` drag so pragmatic-dnd receives the gesture.
- Adds unit tests for the new reducer.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@lstein lstein added the v6.13.x label Apr 21, 2026
@lstein lstein moved this to 6.13.x Theme: MODELS in Invoke - Community Roadmap Apr 21, 2026
@github-actions github-actions Bot added the frontend PRs that change frontend files label Apr 21, 2026
lstein and others added 5 commits April 20, 2026 21:05
iOS WebKit collapses a flex item to zero width when the width is only
implied by a child's aspect ratio. Set aspectRatio on the wrapper Box
directly so the thumbnail tile sizes correctly on iPad Chrome/Safari.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The default iOS "Save Image" / "Copy" callout fires on long-press over
the thumbnail, which interferes with drag attempts on iPad. Scope the
suppression (WebkitTouchCallout + userSelect) to the ref image wrapper
only, leaving gallery and other image views unaffected.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@Pfannkuchensack
Copy link
Copy Markdown
Collaborator

Findings

Low: Reordering decision logic in RefImageList is untested

Path: invokeai/frontend/web/src/features/controlLayers/components/RefImage/RefImageList.tsx:46-100

The entire decision logic for the new feature lives in the onDrop handler — edge extraction, the indexOfSource === indexOfTarget + edgeIndexDelta no-op short-circuit, the reorderWithEdge call, the flushSync dispatch, and the post-move flash. None of it is covered by tests.

The reducer tests in invokeai/frontend/web/src/features/controlLayers/store/refImagesSlice.test.ts only validate the reducer's defensive guards (length mismatch, unknown id, duplicate id, empty list). They never exercise how nextIds is actually computed from a (sourceId, targetId, edge) triple. The reducer is essentially a permutation applier — the easy half. A regression in the edge handling (swapping 'left'/'right', dropping the short-circuit, or feeding the wrong axis) would not be caught.

To expose this issue, add a test that takes a fixed initial id list and, for each (sourceIndex, targetIndex, edge) combination, asserts the ids array passed into refImagesReordered matches the expected permutation — asserted at the level of a small helper extracted from the onDrop handler so it can be unit-tested without DOM.


Low: closestEdgeOfTarget: null is passed through to reorderWithEdge unguarded

Path: invokeai/frontend/web/src/features/controlLayers/components/RefImage/RefImageList.tsx:70-89

When extractClosestEdge(targetData) returns null (no edge detected by the hitbox util), edgeIndexDelta stays 0, so the indexOfSource === indexOfTarget + edgeIndexDelta check collapses to indexOfSource === indexOfTarget — already rejected at line 66. The function then calls:

reorderWithEdge({ ..., closestEdgeOfTarget: null, axis: 'horizontal' })

The same pattern exists in invokeai/frontend/web/src/features/controlLayers/components/CanvasEntityList/CanvasEntityGroupList.tsx:77-106, so this is a copy of an established repo style rather than a regression. But neither call site documents what the third-party util does with a null edge. Worth a comment or an explicit guard.

To expose this issue, add a test (against the helper described above) for closestEdgeOfTarget = null to pin the intended behavior.


Low: Monitor re-registers on every ids change

Path: invokeai/frontend/web/src/features/controlLayers/components/RefImage/RefImageList.tsx:101

The effect declares [dispatch, ids] as its dependency, so the monitorForElements subscription is torn down and re-registered on every add, delete, recall, and reorder.

Functionally correct: an in-flight drag whose drop happens just after ids changes runs against a freshly registered monitor that captures the new array — which is what we want. Calling out as a deliberate design choice rather than a bug, in case the monitor is later refactored to read ids from a ref instead.


Open Questions

  • Drag cancel path in useRefImageDndinvokeai/frontend/web/src/features/controlLayers/components/RefImage/useRefImageDnd.ts:25-32 sets isDragging(true) in onDragStart and isDragging(false) in onDrop. If the drag is cancelled (Esc, mouse leaves window, touch cancel), pragmatic-dnd typically still fires onDrop with no targets — but is that guaranteed for all browsers (especially iOS)? If onDrop is skipped on cancel, isDragging stays true and the thumbnail is stuck at opacity={0.3} until a new drag completes. Worth verifying against the pragmatic-dnd contract, or adding a defensive onDropTargetChange/timeout reset.

@Pfannkuchensack
Copy link
Copy Markdown
Collaborator

The UI works fine.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

frontend PRs that change frontend files v6.13.x

Projects

Status: 6.13.x Theme: MODELS

Development

Successfully merging this pull request may close these issues.

2 participants