mirror of
https://github.com/invoke-ai/InvokeAI
synced 2024-08-30 20:32:17 +00:00
fix(ui): revise image selection after deletion
- For single image deletion, select the image in the same slot as the deleted image - For multiple image deletion, empty selection - On list images, if no images are currently selected, select the first image
This commit is contained in:
parent
5578660ccb
commit
7c01b69c12
@ -1,7 +1,6 @@
|
|||||||
import type { TypedStartListening } from '@reduxjs/toolkit';
|
import type { TypedStartListening } from '@reduxjs/toolkit';
|
||||||
import { createListenerMiddleware } from '@reduxjs/toolkit';
|
import { createListenerMiddleware } from '@reduxjs/toolkit';
|
||||||
import { addCommitStagingAreaImageListener } from 'app/store/middleware/listenerMiddleware/listeners/addCommitStagingAreaImageListener';
|
import { addCommitStagingAreaImageListener } from 'app/store/middleware/listenerMiddleware/listeners/addCommitStagingAreaImageListener';
|
||||||
import { addFirstListImagesListener } from 'app/store/middleware/listenerMiddleware/listeners/addFirstListImagesListener.ts';
|
|
||||||
import { addAnyEnqueuedListener } from 'app/store/middleware/listenerMiddleware/listeners/anyEnqueued';
|
import { addAnyEnqueuedListener } from 'app/store/middleware/listenerMiddleware/listeners/anyEnqueued';
|
||||||
import { addAppConfigReceivedListener } from 'app/store/middleware/listenerMiddleware/listeners/appConfigReceived';
|
import { addAppConfigReceivedListener } from 'app/store/middleware/listenerMiddleware/listeners/appConfigReceived';
|
||||||
import { addAppStartedListener } from 'app/store/middleware/listenerMiddleware/listeners/appStarted';
|
import { addAppStartedListener } from 'app/store/middleware/listenerMiddleware/listeners/appStarted';
|
||||||
@ -26,7 +25,7 @@ import { addGalleryImageClickedListener } from 'app/store/middleware/listenerMid
|
|||||||
import { addGalleryOffsetChangedListener } from 'app/store/middleware/listenerMiddleware/listeners/galleryOffsetChanged';
|
import { addGalleryOffsetChangedListener } from 'app/store/middleware/listenerMiddleware/listeners/galleryOffsetChanged';
|
||||||
import { addGetOpenAPISchemaListener } from 'app/store/middleware/listenerMiddleware/listeners/getOpenAPISchema';
|
import { addGetOpenAPISchemaListener } from 'app/store/middleware/listenerMiddleware/listeners/getOpenAPISchema';
|
||||||
import { addImageAddedToBoardFulfilledListener } from 'app/store/middleware/listenerMiddleware/listeners/imageAddedToBoard';
|
import { addImageAddedToBoardFulfilledListener } from 'app/store/middleware/listenerMiddleware/listeners/imageAddedToBoard';
|
||||||
import { addRequestedSingleImageDeletionListener } from 'app/store/middleware/listenerMiddleware/listeners/imageDeleted';
|
import { addImageDeletionListeners } from 'app/store/middleware/listenerMiddleware/listeners/imageDeletionListeners';
|
||||||
import { addImageDroppedListener } from 'app/store/middleware/listenerMiddleware/listeners/imageDropped';
|
import { addImageDroppedListener } from 'app/store/middleware/listenerMiddleware/listeners/imageDropped';
|
||||||
import { addImageRemovedFromBoardFulfilledListener } from 'app/store/middleware/listenerMiddleware/listeners/imageRemovedFromBoard';
|
import { addImageRemovedFromBoardFulfilledListener } from 'app/store/middleware/listenerMiddleware/listeners/imageRemovedFromBoard';
|
||||||
import { addImagesStarredListener } from 'app/store/middleware/listenerMiddleware/listeners/imagesStarred';
|
import { addImagesStarredListener } from 'app/store/middleware/listenerMiddleware/listeners/imagesStarred';
|
||||||
@ -70,7 +69,7 @@ const startAppListening = listenerMiddleware.startListening as AppStartListening
|
|||||||
addImageUploadedFulfilledListener(startAppListening);
|
addImageUploadedFulfilledListener(startAppListening);
|
||||||
|
|
||||||
// Image deleted
|
// Image deleted
|
||||||
addRequestedSingleImageDeletionListener(startAppListening);
|
addImageDeletionListeners(startAppListening);
|
||||||
addDeleteBoardAndImagesFulfilledListener(startAppListening);
|
addDeleteBoardAndImagesFulfilledListener(startAppListening);
|
||||||
addImageToDeleteSelectedListener(startAppListening);
|
addImageToDeleteSelectedListener(startAppListening);
|
||||||
|
|
||||||
@ -139,7 +138,6 @@ addModelSelectedListener(startAppListening);
|
|||||||
addAppStartedListener(startAppListening);
|
addAppStartedListener(startAppListening);
|
||||||
addModelsLoadedListener(startAppListening);
|
addModelsLoadedListener(startAppListening);
|
||||||
addAppConfigReceivedListener(startAppListening);
|
addAppConfigReceivedListener(startAppListening);
|
||||||
addFirstListImagesListener(startAppListening);
|
|
||||||
|
|
||||||
// Ad-hoc upscale workflwo
|
// Ad-hoc upscale workflwo
|
||||||
addUpscaleRequestedListener(startAppListening);
|
addUpscaleRequestedListener(startAppListening);
|
||||||
|
@ -1,27 +0,0 @@
|
|||||||
import type { AppStartListening } from 'app/store/middleware/listenerMiddleware';
|
|
||||||
import { imageSelected } from 'features/gallery/store/gallerySlice';
|
|
||||||
import { IMAGE_CATEGORIES } from 'features/gallery/store/types';
|
|
||||||
import { imagesApi } from 'services/api/endpoints/images';
|
|
||||||
import { getListImagesUrl } from 'services/api/util';
|
|
||||||
|
|
||||||
export const addFirstListImagesListener = (startAppListening: AppStartListening) => {
|
|
||||||
startAppListening({
|
|
||||||
matcher: imagesApi.endpoints.listImages.matchFulfilled,
|
|
||||||
effect: async (action, { dispatch, unsubscribe, cancelActiveListeners }) => {
|
|
||||||
// Only run this listener on the first listImages request for no-board images
|
|
||||||
if (action.meta.arg.queryCacheKey !== getListImagesUrl({ board_id: 'none', categories: IMAGE_CATEGORIES })) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// this should only run once
|
|
||||||
cancelActiveListeners();
|
|
||||||
unsubscribe();
|
|
||||||
|
|
||||||
const data = action.payload;
|
|
||||||
|
|
||||||
if (data.items.length > 0) {
|
|
||||||
dispatch(imageSelected(data.items[0] ?? null));
|
|
||||||
}
|
|
||||||
},
|
|
||||||
});
|
|
||||||
};
|
|
@ -22,11 +22,11 @@ import { imageSelected } from 'features/gallery/store/gallerySlice';
|
|||||||
import { fieldImageValueChanged } from 'features/nodes/store/nodesSlice';
|
import { fieldImageValueChanged } from 'features/nodes/store/nodesSlice';
|
||||||
import { isImageFieldInputInstance } from 'features/nodes/types/field';
|
import { isImageFieldInputInstance } from 'features/nodes/types/field';
|
||||||
import { isInvocationNode } from 'features/nodes/types/invocation';
|
import { isInvocationNode } from 'features/nodes/types/invocation';
|
||||||
import { forEach } from 'lodash-es';
|
import { forEach, intersectionBy } from 'lodash-es';
|
||||||
import { api } from 'services/api';
|
|
||||||
import { imagesApi } from 'services/api/endpoints/images';
|
import { imagesApi } from 'services/api/endpoints/images';
|
||||||
import type { ImageDTO } from 'services/api/types';
|
import type { ImageDTO } from 'services/api/types';
|
||||||
|
|
||||||
|
// Some utils to delete images from different parts of the app
|
||||||
const deleteNodesImages = (state: RootState, dispatch: AppDispatch, imageDTO: ImageDTO) => {
|
const deleteNodesImages = (state: RootState, dispatch: AppDispatch, imageDTO: ImageDTO) => {
|
||||||
state.nodes.present.nodes.forEach((node) => {
|
state.nodes.present.nodes.forEach((node) => {
|
||||||
if (!isInvocationNode(node)) {
|
if (!isInvocationNode(node)) {
|
||||||
@ -97,10 +97,11 @@ const deleteControlLayerImages = (state: RootState, dispatch: AppDispatch, image
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
export const addRequestedSingleImageDeletionListener = (startAppListening: AppStartListening) => {
|
export const addImageDeletionListeners = (startAppListening: AppStartListening) => {
|
||||||
|
// Handle single image deletion
|
||||||
startAppListening({
|
startAppListening({
|
||||||
actionCreator: imageDeletionConfirmed,
|
actionCreator: imageDeletionConfirmed,
|
||||||
effect: async (action, { dispatch, getState, condition }) => {
|
effect: async (action, { dispatch, getState }) => {
|
||||||
const { imageDTOs, imagesUsage } = action.payload;
|
const { imageDTOs, imagesUsage } = action.payload;
|
||||||
|
|
||||||
if (imageDTOs.length !== 1 || imagesUsage.length !== 1) {
|
if (imageDTOs.length !== 1 || imagesUsage.length !== 1) {
|
||||||
@ -116,49 +117,46 @@ export const addRequestedSingleImageDeletionListener = (startAppListening: AppSt
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
dispatch(isModalOpenChanged(false));
|
try {
|
||||||
const state = getState();
|
const state = getState();
|
||||||
|
await dispatch(imagesApi.endpoints.deleteImage.initiate(imageDTO)).unwrap();
|
||||||
|
|
||||||
|
if (state.gallery.selection.some((i) => i.image_name === imageDTO.image_name)) {
|
||||||
|
// The deleted image was a selected image, we need to select the next image
|
||||||
|
const newSelection = state.gallery.selection.filter((i) => i.image_name !== imageDTO.image_name);
|
||||||
|
|
||||||
|
if (newSelection.length > 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the current list of images and select the same index
|
||||||
|
const baseQueryArgs = selectListImagesQueryArgs(state);
|
||||||
|
const data = imagesApi.endpoints.listImages.select(baseQueryArgs)(state).data;
|
||||||
|
|
||||||
|
if (data) {
|
||||||
|
const deletedImageIndex = data.items.findIndex((i) => i.image_name === imageDTO.image_name);
|
||||||
|
const nextImage = data.items[deletedImageIndex + 1] ?? data.items[0] ?? null;
|
||||||
|
dispatch(imageSelected(nextImage));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// We need to reset the features where the image is in use - none of these work if their image(s) don't exist
|
// We need to reset the features where the image is in use - none of these work if their image(s) don't exist
|
||||||
if (imageUsage.isCanvasImage) {
|
if (imageUsage.isCanvasImage) {
|
||||||
dispatch(resetCanvas());
|
dispatch(resetCanvas());
|
||||||
}
|
}
|
||||||
|
|
||||||
imageDTOs.forEach((imageDTO) => {
|
|
||||||
deleteControlAdapterImages(state, dispatch, imageDTO);
|
deleteControlAdapterImages(state, dispatch, imageDTO);
|
||||||
deleteNodesImages(state, dispatch, imageDTO);
|
deleteNodesImages(state, dispatch, imageDTO);
|
||||||
deleteControlLayerImages(state, dispatch, imageDTO);
|
deleteControlLayerImages(state, dispatch, imageDTO);
|
||||||
});
|
} catch {
|
||||||
|
// no-op
|
||||||
// Delete from server
|
} finally {
|
||||||
const { requestId } = dispatch(imagesApi.endpoints.deleteImage.initiate(imageDTO));
|
dispatch(isModalOpenChanged(false));
|
||||||
|
|
||||||
// Wait for successful deletion, then trigger boards to re-fetch
|
|
||||||
const wasImageDeleted = await condition(
|
|
||||||
(action) => imagesApi.endpoints.deleteImage.matchFulfilled(action) && action.meta.requestId === requestId,
|
|
||||||
30000
|
|
||||||
);
|
|
||||||
|
|
||||||
if (wasImageDeleted) {
|
|
||||||
dispatch(api.util.invalidateTags([{ type: 'Board', id: imageDTO.board_id ?? 'none' }]));
|
|
||||||
}
|
|
||||||
|
|
||||||
const lastSelectedImage = state.gallery.selection[state.gallery.selection.length - 1]?.image_name;
|
|
||||||
|
|
||||||
if (imageDTO && imageDTO?.image_name === lastSelectedImage) {
|
|
||||||
const baseQueryArgs = selectListImagesQueryArgs(state);
|
|
||||||
const { data } = imagesApi.endpoints.listImages.select(baseQueryArgs)(state);
|
|
||||||
|
|
||||||
if (data && data.items) {
|
|
||||||
const newlySelectedImage = data?.items.find((img) => img.image_name !== imageDTO?.image_name);
|
|
||||||
dispatch(imageSelected(newlySelectedImage || null));
|
|
||||||
} else {
|
|
||||||
dispatch(imageSelected(null));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Handle multiple image deletion
|
||||||
startAppListening({
|
startAppListening({
|
||||||
actionCreator: imageDeletionConfirmed,
|
actionCreator: imageDeletionConfirmed,
|
||||||
effect: async (action, { dispatch, getState }) => {
|
effect: async (action, { dispatch, getState }) => {
|
||||||
@ -170,19 +168,17 @@ export const addRequestedSingleImageDeletionListener = (startAppListening: AppSt
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Delete from server
|
|
||||||
await dispatch(imagesApi.endpoints.deleteImages.initiate({ imageDTOs })).unwrap();
|
|
||||||
const state = getState();
|
const state = getState();
|
||||||
|
await dispatch(imagesApi.endpoints.deleteImages.initiate({ imageDTOs })).unwrap();
|
||||||
|
|
||||||
|
if (intersectionBy(state.gallery.selection, imageDTOs, 'image_name').length > 0) {
|
||||||
|
// Some selected images were deleted, need to select the next image
|
||||||
const queryArgs = selectListImagesQueryArgs(state);
|
const queryArgs = selectListImagesQueryArgs(state);
|
||||||
const { data } = imagesApi.endpoints.listImages.select(queryArgs)(state);
|
const { data } = imagesApi.endpoints.listImages.select(queryArgs)(state);
|
||||||
|
if (data) {
|
||||||
if (data && data.items[0]) {
|
|
||||||
dispatch(imageSelected(data.items[0]));
|
|
||||||
} else {
|
|
||||||
dispatch(imageSelected(null));
|
dispatch(imageSelected(null));
|
||||||
}
|
}
|
||||||
|
}
|
||||||
dispatch(isModalOpenChanged(false));
|
|
||||||
|
|
||||||
// We need to reset the features where the image is in use - none of these work if their image(s) don't exist
|
// We need to reset the features where the image is in use - none of these work if their image(s) don't exist
|
||||||
|
|
||||||
@ -197,14 +193,20 @@ export const addRequestedSingleImageDeletionListener = (startAppListening: AppSt
|
|||||||
});
|
});
|
||||||
} catch {
|
} catch {
|
||||||
// no-op
|
// no-op
|
||||||
|
} finally {
|
||||||
|
dispatch(isModalOpenChanged(false));
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// When we list images, if no images is selected, select the first one.
|
||||||
startAppListening({
|
startAppListening({
|
||||||
matcher: imagesApi.endpoints.deleteImage.matchPending,
|
matcher: imagesApi.endpoints.listImages.matchFulfilled,
|
||||||
effect: () => {
|
effect: (action, { dispatch, getState }) => {
|
||||||
//
|
const selection = getState().gallery.selection;
|
||||||
|
if (selection.length === 0) {
|
||||||
|
dispatch(imageSelected(action.payload.items[0] ?? null));
|
||||||
|
}
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
Loading…
Reference in New Issue
Block a user