From 85dd78b8df4c6660d0b8c23cc26fff7c2f8a5b3f Mon Sep 17 00:00:00 2001
From: psychedelicious <4822129+psychedelicious@users.noreply.github.com>
Date: Fri, 3 May 2024 10:44:07 +1000
Subject: [PATCH] fix(ui): handle deleting images in use in generation tab
---
.../listeners/boardAndImagesDeleted.ts | 11 +-
.../listeners/imageDeleted.ts | 163 +++++++++---------
.../components/DeleteImageModal.tsx | 9 +-
.../components/ImageUsageMessage.tsx | 5 +-
.../deleteImageModal/store/selectors.ts | 37 +++-
.../features/deleteImageModal/store/types.ts | 1 +
.../components/Boards/DeleteBoardModal.tsx | 9 +-
7 files changed, 139 insertions(+), 96 deletions(-)
diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/boardAndImagesDeleted.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/boardAndImagesDeleted.ts
index d7ab8430ca..a0b07b9419 100644
--- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/boardAndImagesDeleted.ts
+++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/boardAndImagesDeleted.ts
@@ -1,6 +1,7 @@
import type { AppStartListening } from 'app/store/middleware/listenerMiddleware';
import { resetCanvas } from 'features/canvas/store/canvasSlice';
import { controlAdaptersReset } from 'features/controlAdapters/store/controlAdaptersSlice';
+import { allLayersDeleted } from 'features/controlLayers/store/controlLayersSlice';
import { getImageUsage } from 'features/deleteImageModal/store/selectors';
import { nodeEditorReset } from 'features/nodes/store/nodesSlice';
import { imagesApi } from 'services/api/endpoints/images';
@@ -16,10 +17,11 @@ export const addDeleteBoardAndImagesFulfilledListener = (startAppListening: AppS
let wasCanvasReset = false;
let wasNodeEditorReset = false;
let wereControlAdaptersReset = false;
+ let wereControlLayersReset = false;
- const { generation, canvas, nodes, controlAdapters } = getState();
+ const { canvas, nodes, controlAdapters, controlLayers } = getState();
deleted_images.forEach((image_name) => {
- const imageUsage = getImageUsage(generation, canvas, nodes, controlAdapters, image_name);
+ const imageUsage = getImageUsage(canvas, nodes, controlAdapters, controlLayers.present, image_name);
if (imageUsage.isCanvasImage && !wasCanvasReset) {
dispatch(resetCanvas());
@@ -35,6 +37,11 @@ export const addDeleteBoardAndImagesFulfilledListener = (startAppListening: AppS
dispatch(controlAdaptersReset());
wereControlAdaptersReset = true;
}
+
+ if (imageUsage.isControlLayerImage && !wereControlLayersReset) {
+ dispatch(allLayersDeleted());
+ wereControlLayersReset = true;
+ }
});
},
});
diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/imageDeleted.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/imageDeleted.ts
index 451c26629e..95d17da653 100644
--- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/imageDeleted.ts
+++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/imageDeleted.ts
@@ -1,5 +1,6 @@
import { logger } from 'app/logging/logger';
import type { AppStartListening } from 'app/store/middleware/listenerMiddleware';
+import type { AppDispatch, RootState } from 'app/store/store';
import { resetCanvas } from 'features/canvas/store/canvasSlice';
import {
controlAdapterImageChanged,
@@ -7,6 +8,13 @@ import {
selectControlAdapterAll,
} from 'features/controlAdapters/store/controlAdaptersSlice';
import { isControlNetOrT2IAdapter } from 'features/controlAdapters/store/types';
+import {
+ isControlAdapterLayer,
+ isInitialImageLayer,
+ isIPAdapterLayer,
+ isRegionalGuidanceLayer,
+ layerDeleted,
+} from 'features/controlLayers/store/controlLayersSlice';
import { imageDeletionConfirmed } from 'features/deleteImageModal/store/actions';
import { isModalOpenChanged } from 'features/deleteImageModal/store/slice';
import { selectListImagesQueryArgs } from 'features/gallery/store/gallerySelectors';
@@ -17,8 +25,79 @@ import { isInvocationNode } from 'features/nodes/types/invocation';
import { clamp, forEach } from 'lodash-es';
import { api } from 'services/api';
import { imagesApi } from 'services/api/endpoints/images';
+import type { ImageDTO } from 'services/api/types';
import { imagesSelectors } from 'services/api/util';
+const deleteNodesImages = (state: RootState, dispatch: AppDispatch, imageDTO: ImageDTO) => {
+ state.nodes.nodes.forEach((node) => {
+ if (!isInvocationNode(node)) {
+ return;
+ }
+
+ forEach(node.data.inputs, (input) => {
+ if (isImageFieldInputInstance(input) && input.value?.image_name === imageDTO.image_name) {
+ dispatch(
+ fieldImageValueChanged({
+ nodeId: node.data.id,
+ fieldName: input.name,
+ value: undefined,
+ })
+ );
+ }
+ });
+ });
+};
+
+const deleteControlAdapterImages = (state: RootState, dispatch: AppDispatch, imageDTO: ImageDTO) => {
+ forEach(selectControlAdapterAll(state.controlAdapters), (ca) => {
+ if (
+ ca.controlImage === imageDTO.image_name ||
+ (isControlNetOrT2IAdapter(ca) && ca.processedControlImage === imageDTO.image_name)
+ ) {
+ dispatch(
+ controlAdapterImageChanged({
+ id: ca.id,
+ controlImage: null,
+ })
+ );
+ dispatch(
+ controlAdapterProcessedImageChanged({
+ id: ca.id,
+ processedControlImage: null,
+ })
+ );
+ }
+ });
+};
+
+const deleteControlLayerImages = (state: RootState, dispatch: AppDispatch, imageDTO: ImageDTO) => {
+ state.controlLayers.present.layers.forEach((l) => {
+ if (isRegionalGuidanceLayer(l)) {
+ if (l.ipAdapters.some((ipa) => ipa.image?.imageName === imageDTO.image_name)) {
+ dispatch(layerDeleted(l.id));
+ }
+ }
+ if (isControlAdapterLayer(l)) {
+ if (
+ l.controlAdapter.image?.imageName === imageDTO.image_name ||
+ l.controlAdapter.processedImage?.imageName === imageDTO.image_name
+ ) {
+ dispatch(layerDeleted(l.id));
+ }
+ }
+ if (isIPAdapterLayer(l)) {
+ if (l.ipAdapter.image?.imageName === imageDTO.image_name) {
+ dispatch(layerDeleted(l.id));
+ }
+ }
+ if (isInitialImageLayer(l)) {
+ if (l.image?.imageName === imageDTO.image_name) {
+ dispatch(layerDeleted(l.id));
+ }
+ }
+ });
+};
+
export const addRequestedSingleImageDeletionListener = (startAppListening: AppStartListening) => {
startAppListening({
actionCreator: imageDeletionConfirmed,
@@ -72,45 +151,9 @@ export const addRequestedSingleImageDeletionListener = (startAppListening: AppSt
}
imageDTOs.forEach((imageDTO) => {
- // reset control adapters that use the deleted images
- forEach(selectControlAdapterAll(getState().controlAdapters), (ca) => {
- if (
- ca.controlImage === imageDTO.image_name ||
- (isControlNetOrT2IAdapter(ca) && ca.processedControlImage === imageDTO.image_name)
- ) {
- dispatch(
- controlAdapterImageChanged({
- id: ca.id,
- controlImage: null,
- })
- );
- dispatch(
- controlAdapterProcessedImageChanged({
- id: ca.id,
- processedControlImage: null,
- })
- );
- }
- });
-
- // reset nodes that use the deleted images
- getState().nodes.nodes.forEach((node) => {
- if (!isInvocationNode(node)) {
- return;
- }
-
- forEach(node.data.inputs, (input) => {
- if (isImageFieldInputInstance(input) && input.value?.image_name === imageDTO.image_name) {
- dispatch(
- fieldImageValueChanged({
- nodeId: node.data.id,
- fieldName: input.name,
- value: undefined,
- })
- );
- }
- });
- });
+ deleteControlAdapterImages(state, dispatch, imageDTO);
+ deleteNodesImages(state, dispatch, imageDTO);
+ deleteControlLayerImages(state, dispatch, imageDTO);
});
// Delete from server
@@ -162,45 +205,9 @@ export const addRequestedSingleImageDeletionListener = (startAppListening: AppSt
}
imageDTOs.forEach((imageDTO) => {
- // reset control adapters that use the deleted images
- forEach(selectControlAdapterAll(getState().controlAdapters), (ca) => {
- if (
- ca.controlImage === imageDTO.image_name ||
- (isControlNetOrT2IAdapter(ca) && ca.processedControlImage === imageDTO.image_name)
- ) {
- dispatch(
- controlAdapterImageChanged({
- id: ca.id,
- controlImage: null,
- })
- );
- dispatch(
- controlAdapterProcessedImageChanged({
- id: ca.id,
- processedControlImage: null,
- })
- );
- }
- });
-
- // reset nodes that use the deleted images
- getState().nodes.nodes.forEach((node) => {
- if (!isInvocationNode(node)) {
- return;
- }
-
- forEach(node.data.inputs, (input) => {
- if (isImageFieldInputInstance(input) && input.value?.image_name === imageDTO.image_name) {
- dispatch(
- fieldImageValueChanged({
- nodeId: node.data.id,
- fieldName: input.name,
- value: undefined,
- })
- );
- }
- });
- });
+ deleteControlAdapterImages(state, dispatch, imageDTO);
+ deleteNodesImages(state, dispatch, imageDTO);
+ deleteControlLayerImages(state, dispatch, imageDTO);
});
} catch {
// no-op
diff --git a/invokeai/frontend/web/src/features/deleteImageModal/components/DeleteImageModal.tsx b/invokeai/frontend/web/src/features/deleteImageModal/components/DeleteImageModal.tsx
index e3ee0b3852..f4b7438dff 100644
--- a/invokeai/frontend/web/src/features/deleteImageModal/components/DeleteImageModal.tsx
+++ b/invokeai/frontend/web/src/features/deleteImageModal/components/DeleteImageModal.tsx
@@ -3,6 +3,7 @@ import { createMemoizedSelector } from 'app/store/createMemoizedSelector';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { selectCanvasSlice } from 'features/canvas/store/canvasSlice';
import { selectControlAdaptersSlice } from 'features/controlAdapters/store/controlAdaptersSlice';
+import { selectControlLayersSlice } from 'features/controlLayers/store/controlLayersSlice';
import { imageDeletionConfirmed } from 'features/deleteImageModal/store/actions';
import { getImageUsage, selectImageUsage } from 'features/deleteImageModal/store/selectors';
import {
@@ -12,7 +13,6 @@ import {
} from 'features/deleteImageModal/store/slice';
import type { ImageUsage } from 'features/deleteImageModal/store/types';
import { selectNodesSlice } from 'features/nodes/store/nodesSlice';
-import { selectGenerationSlice } from 'features/parameters/store/generationSlice';
import { setShouldConfirmOnDelete } from 'features/system/store/systemSlice';
import { some } from 'lodash-es';
import type { ChangeEvent } from 'react';
@@ -24,23 +24,24 @@ import ImageUsageMessage from './ImageUsageMessage';
const selectImageUsages = createMemoizedSelector(
[
selectDeleteImageModalSlice,
- selectGenerationSlice,
selectCanvasSlice,
selectNodesSlice,
selectControlAdaptersSlice,
+ selectControlLayersSlice,
selectImageUsage,
],
- (deleteImageModal, generation, canvas, nodes, controlAdapters, imagesUsage) => {
+ (deleteImageModal, canvas, nodes, controlAdapters, controlLayers, imagesUsage) => {
const { imagesToDelete } = deleteImageModal;
const allImageUsage = (imagesToDelete ?? []).map(({ image_name }) =>
- getImageUsage(generation, canvas, nodes, controlAdapters, image_name)
+ getImageUsage(canvas, nodes, controlAdapters, controlLayers.present, image_name)
);
const imageUsageSummary: ImageUsage = {
isCanvasImage: some(allImageUsage, (i) => i.isCanvasImage),
isNodesImage: some(allImageUsage, (i) => i.isNodesImage),
isControlImage: some(allImageUsage, (i) => i.isControlImage),
+ isControlLayerImage: some(allImageUsage, (i) => i.isControlLayerImage),
};
return {
diff --git a/invokeai/frontend/web/src/features/deleteImageModal/components/ImageUsageMessage.tsx b/invokeai/frontend/web/src/features/deleteImageModal/components/ImageUsageMessage.tsx
index ec613409e7..d76716d01d 100644
--- a/invokeai/frontend/web/src/features/deleteImageModal/components/ImageUsageMessage.tsx
+++ b/invokeai/frontend/web/src/features/deleteImageModal/components/ImageUsageMessage.tsx
@@ -29,9 +29,10 @@ const ImageUsageMessage = (props: Props) => {
<>
{topMessage}
- {imageUsage.isCanvasImage && {t('common.unifiedCanvas')}}
+ {imageUsage.isCanvasImage && {t('ui.tabs.canvasTab')}}
{imageUsage.isControlImage && {t('common.controlNet')}}
- {imageUsage.isNodesImage && {t('common.nodeEditor')}}
+ {imageUsage.isNodesImage && {t('ui.tabs.workflowsTab')}}
+ {imageUsage.isControlLayerImage && {t('ui.tabs.generationTab')}}
{bottomMessage}
>
diff --git a/invokeai/frontend/web/src/features/deleteImageModal/store/selectors.ts b/invokeai/frontend/web/src/features/deleteImageModal/store/selectors.ts
index b9540a3ecf..ce989de7b1 100644
--- a/invokeai/frontend/web/src/features/deleteImageModal/store/selectors.ts
+++ b/invokeai/frontend/web/src/features/deleteImageModal/store/selectors.ts
@@ -7,22 +7,28 @@ import {
} from 'features/controlAdapters/store/controlAdaptersSlice';
import type { ControlAdaptersState } from 'features/controlAdapters/store/types';
import { isControlNetOrT2IAdapter } from 'features/controlAdapters/store/types';
+import {
+ isControlAdapterLayer,
+ isInitialImageLayer,
+ isIPAdapterLayer,
+ isRegionalGuidanceLayer,
+ selectControlLayersSlice,
+} from 'features/controlLayers/store/controlLayersSlice';
+import type { ControlLayersState } from 'features/controlLayers/store/types';
import { selectDeleteImageModalSlice } from 'features/deleteImageModal/store/slice';
import { selectNodesSlice } from 'features/nodes/store/nodesSlice';
import type { NodesState } from 'features/nodes/store/types';
import { isImageFieldInputInstance } from 'features/nodes/types/field';
import { isInvocationNode } from 'features/nodes/types/invocation';
-import { selectGenerationSlice } from 'features/parameters/store/generationSlice';
-import type { GenerationState } from 'features/parameters/store/types';
import { some } from 'lodash-es';
import type { ImageUsage } from './types';
export const getImageUsage = (
- generation: GenerationState,
canvas: CanvasState,
nodes: NodesState,
controlAdapters: ControlAdaptersState,
+ controlLayers: ControlLayersState,
image_name: string
) => {
const isCanvasImage = canvas.layerState.objects.some((obj) => obj.kind === 'image' && obj.imageName === image_name);
@@ -38,10 +44,29 @@ export const getImageUsage = (
(ca) => ca.controlImage === image_name || (isControlNetOrT2IAdapter(ca) && ca.processedControlImage === image_name)
);
+ const isControlLayerImage = controlLayers.layers.some((l) => {
+ if (isRegionalGuidanceLayer(l)) {
+ return l.ipAdapters.some((ipa) => ipa.image?.imageName === image_name);
+ }
+ if (isControlAdapterLayer(l)) {
+ return (
+ l.controlAdapter.image?.imageName === image_name || l.controlAdapter.processedImage?.imageName === image_name
+ );
+ }
+ if (isIPAdapterLayer(l)) {
+ return l.ipAdapter.image?.imageName === image_name;
+ }
+ if (isInitialImageLayer(l)) {
+ return l.image?.imageName === image_name;
+ }
+ return false;
+ });
+
const imageUsage: ImageUsage = {
isCanvasImage,
isNodesImage,
isControlImage,
+ isControlLayerImage,
};
return imageUsage;
@@ -49,11 +74,11 @@ export const getImageUsage = (
export const selectImageUsage = createMemoizedSelector(
selectDeleteImageModalSlice,
- selectGenerationSlice,
selectCanvasSlice,
selectNodesSlice,
selectControlAdaptersSlice,
- (deleteImageModal, generation, canvas, nodes, controlAdapters) => {
+ selectControlLayersSlice,
+ (deleteImageModal, canvas, nodes, controlAdapters, controlLayers) => {
const { imagesToDelete } = deleteImageModal;
if (!imagesToDelete.length) {
@@ -61,7 +86,7 @@ export const selectImageUsage = createMemoizedSelector(
}
const imagesUsage = imagesToDelete.map((i) =>
- getImageUsage(generation, canvas, nodes, controlAdapters, i.image_name)
+ getImageUsage(canvas, nodes, controlAdapters, controlLayers.present, i.image_name)
);
return imagesUsage;
diff --git a/invokeai/frontend/web/src/features/deleteImageModal/store/types.ts b/invokeai/frontend/web/src/features/deleteImageModal/store/types.ts
index f0aaf7b097..2cc3dd90b4 100644
--- a/invokeai/frontend/web/src/features/deleteImageModal/store/types.ts
+++ b/invokeai/frontend/web/src/features/deleteImageModal/store/types.ts
@@ -9,4 +9,5 @@ export type ImageUsage = {
isCanvasImage: boolean;
isNodesImage: boolean;
isControlImage: boolean;
+ isControlLayerImage: boolean;
};
diff --git a/invokeai/frontend/web/src/features/gallery/components/Boards/DeleteBoardModal.tsx b/invokeai/frontend/web/src/features/gallery/components/Boards/DeleteBoardModal.tsx
index 5f01fd9f29..377636d0d0 100644
--- a/invokeai/frontend/web/src/features/gallery/components/Boards/DeleteBoardModal.tsx
+++ b/invokeai/frontend/web/src/features/gallery/components/Boards/DeleteBoardModal.tsx
@@ -15,11 +15,11 @@ import { createMemoizedSelector } from 'app/store/createMemoizedSelector';
import { useAppSelector } from 'app/store/storeHooks';
import { selectCanvasSlice } from 'features/canvas/store/canvasSlice';
import { selectControlAdaptersSlice } from 'features/controlAdapters/store/controlAdaptersSlice';
+import { selectControlLayersSlice } from 'features/controlLayers/store/controlLayersSlice';
import ImageUsageMessage from 'features/deleteImageModal/components/ImageUsageMessage';
import { getImageUsage } from 'features/deleteImageModal/store/selectors';
import type { ImageUsage } from 'features/deleteImageModal/store/types';
import { selectNodesSlice } from 'features/nodes/store/nodesSlice';
-import { selectGenerationSlice } from 'features/parameters/store/generationSlice';
import { some } from 'lodash-es';
import { memo, useCallback, useMemo, useRef } from 'react';
import { useTranslation } from 'react-i18next';
@@ -43,16 +43,17 @@ const DeleteBoardModal = (props: Props) => {
const selectImageUsageSummary = useMemo(
() =>
createMemoizedSelector(
- [selectGenerationSlice, selectCanvasSlice, selectNodesSlice, selectControlAdaptersSlice],
- (generation, canvas, nodes, controlAdapters) => {
+ [selectCanvasSlice, selectNodesSlice, selectControlAdaptersSlice, selectControlLayersSlice],
+ (canvas, nodes, controlAdapters, controlLayers) => {
const allImageUsage = (boardImageNames ?? []).map((imageName) =>
- getImageUsage(generation, canvas, nodes, controlAdapters, imageName)
+ getImageUsage(canvas, nodes, controlAdapters, controlLayers.present, imageName)
);
const imageUsageSummary: ImageUsage = {
isCanvasImage: some(allImageUsage, (i) => i.isCanvasImage),
isNodesImage: some(allImageUsage, (i) => i.isNodesImage),
isControlImage: some(allImageUsage, (i) => i.isControlImage),
+ isControlLayerImage: some(allImageUsage, (i) => i.isControlLayerImage),
};
return imageUsageSummary;