Skip to content

Commit 723d68e

Browse files
Mary Hipppsychedelicious
authored andcommitted
add image usage for board images and listener to handle actual deletion
1 parent ba67e57 commit 723d68e

File tree

6 files changed

+213
-13
lines changed

6 files changed

+213
-13
lines changed

invokeai/frontend/web/src/app/contexts/DeleteBoardImagesContext.tsx

Lines changed: 81 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,76 @@ import { useDisclosure } from '@chakra-ui/react';
22
import { PropsWithChildren, createContext, useCallback, useState } from 'react';
33
import { BoardDTO } from 'services/api/types';
44
import { useDeleteBoardMutation } from '../../services/api/endpoints/boards';
5+
import { defaultSelectorOptions } from '../store/util/defaultMemoizeOptions';
6+
import { createSelector } from '@reduxjs/toolkit';
7+
import { some } from 'lodash-es';
8+
import { canvasSelector } from '../../features/canvas/store/canvasSelectors';
9+
import { controlNetSelector } from '../../features/controlNet/store/controlNetSlice';
10+
import { selectImagesById } from '../../features/gallery/store/imagesSlice';
11+
import { nodesSelector } from '../../features/nodes/store/nodesSlice';
12+
import { generationSelector } from '../../features/parameters/store/generationSelectors';
13+
import { RootState } from '../store/store';
14+
import { useAppDispatch, useAppSelector } from '../store/storeHooks';
15+
import { ImageUsage } from './DeleteImageContext';
16+
import { requestedBoardImagesDeletion } from '../../features/gallery/store/actions';
517

6-
export type ImageUsage = {
7-
isInitialImage: boolean;
8-
isCanvasImage: boolean;
9-
isNodesImage: boolean;
10-
isControlNetImage: boolean;
11-
};
18+
export const selectBoardImagesUsage = createSelector(
19+
[
20+
(state: RootState) => state,
21+
generationSelector,
22+
canvasSelector,
23+
nodesSelector,
24+
controlNetSelector,
25+
(state: RootState, board_id?: string) => board_id,
26+
],
27+
(state, generation, canvas, nodes, controlNet, board_id) => {
28+
const initialImage = generation.initialImage
29+
? selectImagesById(state, generation.initialImage.imageName)
30+
: undefined;
31+
const isInitialImage = initialImage?.board_id === board_id;
32+
33+
const isCanvasImage = canvas.layerState.objects.some((obj) => {
34+
if (obj.kind === 'image') {
35+
const image = selectImagesById(state, obj.imageName);
36+
return image?.board_id === board_id;
37+
}
38+
return false;
39+
});
40+
41+
const isNodesImage = nodes.nodes.some((node) => {
42+
return some(node.data.inputs, (input) => {
43+
if (input.type === 'image' && input.value) {
44+
const image = selectImagesById(state, input.value.image_name);
45+
return image?.board_id === board_id;
46+
}
47+
return false;
48+
});
49+
});
50+
51+
const isControlNetImage = some(controlNet.controlNets, (c) => {
52+
const controlImage = c.controlImage
53+
? selectImagesById(state, c.controlImage)
54+
: undefined;
55+
const processedControlImage = c.processedControlImage
56+
? selectImagesById(state, c.processedControlImage)
57+
: undefined;
58+
return (
59+
controlImage?.board_id === board_id ||
60+
processedControlImage?.board_id === board_id
61+
);
62+
});
63+
64+
const imageUsage: ImageUsage = {
65+
isInitialImage,
66+
isCanvasImage,
67+
isNodesImage,
68+
isControlNetImage,
69+
};
70+
71+
return imageUsage;
72+
},
73+
defaultSelectorOptions
74+
);
1275

1376
type DeleteBoardImagesContextValue = {
1477
/**
@@ -19,9 +82,7 @@ type DeleteBoardImagesContextValue = {
1982
* Closes the move image dialog.
2083
*/
2184
onClose: () => void;
22-
/**
23-
* The image pending movement
24-
*/
85+
imagesUsage?: ImageUsage;
2586
board?: BoardDTO;
2687
onClickDeleteBoardImages: (board: BoardDTO) => void;
2788
handleDeleteBoardImages: (boardId: string) => void;
@@ -42,8 +103,13 @@ type Props = PropsWithChildren;
42103
export const DeleteBoardImagesContextProvider = (props: Props) => {
43104
const [boardToDelete, setBoardToDelete] = useState<BoardDTO>();
44105
const { isOpen, onOpen, onClose } = useDisclosure();
106+
const dispatch = useAppDispatch();
107+
108+
// Check where the board images to be deleted are used (eg init image, controlnet, etc.)
109+
const imagesUsage = useAppSelector((state) =>
110+
selectBoardImagesUsage(state, boardToDelete?.board_id)
111+
);
45112

46-
const [deleteBoardAndImages] = useDeleteBoardAndImagesMutation();
47113
const [deleteBoard] = useDeleteBoardMutation();
48114

49115
// Clean up after deleting or dismissing the modal
@@ -67,11 +133,13 @@ export const DeleteBoardImagesContextProvider = (props: Props) => {
67133
const handleDeleteBoardImages = useCallback(
68134
(boardId: string) => {
69135
if (boardToDelete) {
70-
deleteBoardAndImages(boardId);
136+
dispatch(
137+
requestedBoardImagesDeletion({ board: boardToDelete, imagesUsage })
138+
);
71139
closeAndClearBoardToDelete();
72140
}
73141
},
74-
[deleteBoardAndImages, closeAndClearBoardToDelete, boardToDelete]
142+
[dispatch, closeAndClearBoardToDelete, boardToDelete, imagesUsage]
75143
);
76144

77145
const handleDeleteBoardOnly = useCallback(
@@ -93,6 +161,7 @@ export const DeleteBoardImagesContextProvider = (props: Props) => {
93161
onClickDeleteBoardImages,
94162
handleDeleteBoardImages,
95163
handleDeleteBoardOnly,
164+
imagesUsage,
96165
}}
97166
>
98167
{props.children}

invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,7 @@ import {
8383
addImageRemovedFromBoardRejectedListener,
8484
} from './listeners/imageRemovedFromBoard';
8585
import { addReceivedOpenAPISchemaListener } from './listeners/receivedOpenAPISchema';
86+
import { addRequestedBoardImageDeletionListener } from './listeners/boardImagesDeleted';
8687

8788
export const listenerMiddleware = createListenerMiddleware();
8889

@@ -124,6 +125,7 @@ addRequestedImageDeletionListener();
124125
addImageDeletedPendingListener();
125126
addImageDeletedFulfilledListener();
126127
addImageDeletedRejectedListener();
128+
addRequestedBoardImageDeletionListener();
127129

128130
// Image metadata
129131
addImageMetadataReceivedFulfilledListener();
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
import { requestedBoardImagesDeletion } from 'features/gallery/store/actions';
2+
import { startAppListening } from '..';
3+
import { imageSelected } from 'features/gallery/store/gallerySlice';
4+
import {
5+
imagesRemoved,
6+
selectImagesAll,
7+
selectImagesById,
8+
} from 'features/gallery/store/imagesSlice';
9+
import { resetCanvas } from 'features/canvas/store/canvasSlice';
10+
import { controlNetReset } from 'features/controlNet/store/controlNetSlice';
11+
import { clearInitialImage } from 'features/parameters/store/generationSlice';
12+
import { nodeEditorReset } from 'features/nodes/store/nodesSlice';
13+
import { LIST_TAG, api } from 'services/api';
14+
import { boardsApi } from '../../../../../services/api/endpoints/boards';
15+
16+
export const addRequestedBoardImageDeletionListener = () => {
17+
startAppListening({
18+
actionCreator: requestedBoardImagesDeletion,
19+
effect: async (action, { dispatch, getState, condition }) => {
20+
const { board, imagesUsage } = action.payload;
21+
22+
const { board_id } = board;
23+
24+
const state = getState();
25+
const selectedImage = state.gallery.selectedImage
26+
? selectImagesById(state, state.gallery.selectedImage)
27+
: undefined;
28+
29+
if (selectedImage && selectedImage.board_id === board_id) {
30+
dispatch(imageSelected());
31+
}
32+
33+
// We need to reset the features where the board images are in use - none of these work if their image(s) don't exist
34+
35+
if (imagesUsage.isCanvasImage) {
36+
dispatch(resetCanvas());
37+
}
38+
39+
if (imagesUsage.isControlNetImage) {
40+
dispatch(controlNetReset());
41+
}
42+
43+
if (imagesUsage.isInitialImage) {
44+
dispatch(clearInitialImage());
45+
}
46+
47+
if (imagesUsage.isNodesImage) {
48+
dispatch(nodeEditorReset());
49+
}
50+
51+
// Preemptively remove from gallery
52+
const images = selectImagesAll(state).reduce((acc: string[], img) => {
53+
if (img.board_id === board_id) {
54+
acc.push(img.image_name);
55+
}
56+
return acc;
57+
}, []);
58+
dispatch(imagesRemoved(images));
59+
60+
// Delete from server
61+
dispatch(boardsApi.endpoints.deleteBoardAndImages.initiate(board_id));
62+
const result =
63+
boardsApi.endpoints.deleteBoardAndImages.select(board_id)(state);
64+
const { isSuccess } = result;
65+
66+
// Wait for successful deletion, then trigger boards to re-fetch
67+
const wasBoardDeleted = await condition(() => !!isSuccess, 30000);
68+
69+
if (wasBoardDeleted) {
70+
dispatch(
71+
api.util.invalidateTags([
72+
{ type: 'Board', id: board_id },
73+
{ type: 'Image', id: LIST_TAG },
74+
])
75+
);
76+
}
77+
},
78+
});
79+
};

invokeai/frontend/web/src/features/gallery/components/Boards/DeleteBoardImagesModal.tsx

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,12 +7,46 @@ import {
77
AlertDialogOverlay,
88
Divider,
99
Flex,
10+
ListItem,
1011
Text,
12+
UnorderedList,
1113
} from '@chakra-ui/react';
1214
import IAIButton from 'common/components/IAIButton';
1315
import { memo, useContext, useRef } from 'react';
1416
import { useTranslation } from 'react-i18next';
1517
import { DeleteBoardImagesContext } from '../../../../app/contexts/DeleteBoardImagesContext';
18+
import { some } from 'lodash-es';
19+
import { ImageUsage } from '../../../../app/contexts/DeleteImageContext';
20+
21+
const BoardImageInUseMessage = (props: { imagesUsage?: ImageUsage }) => {
22+
const { imagesUsage } = props;
23+
24+
if (!imagesUsage) {
25+
return null;
26+
}
27+
28+
if (!some(imagesUsage)) {
29+
return null;
30+
}
31+
32+
return (
33+
<>
34+
<Text>
35+
An image from this board is currently in use in the following features:
36+
</Text>
37+
<UnorderedList sx={{ paddingInlineStart: 6 }}>
38+
{imagesUsage.isInitialImage && <ListItem>Image to Image</ListItem>}
39+
{imagesUsage.isCanvasImage && <ListItem>Unified Canvas</ListItem>}
40+
{imagesUsage.isControlNetImage && <ListItem>ControlNet</ListItem>}
41+
{imagesUsage.isNodesImage && <ListItem>Node Editor</ListItem>}
42+
</UnorderedList>
43+
<Text>
44+
If you delete images from this board, those features will immediately be
45+
reset.
46+
</Text>
47+
</>
48+
);
49+
};
1650

1751
const DeleteBoardImagesModal = () => {
1852
const { t } = useTranslation();
@@ -23,6 +57,7 @@ const DeleteBoardImagesModal = () => {
2357
board,
2458
handleDeleteBoardImages,
2559
handleDeleteBoardOnly,
60+
imagesUsage,
2661
} = useContext(DeleteBoardImagesContext);
2762

2863
const cancelRef = useRef<HTMLButtonElement>(null);
@@ -43,6 +78,7 @@ const DeleteBoardImagesModal = () => {
4378

4479
<AlertDialogBody>
4580
<Flex direction="column" gap={3}>
81+
<BoardImageInUseMessage imagesUsage={imagesUsage} />
4682
<Divider />
4783
<Text>{t('common.areYouSure')}</Text>
4884
<Text fontWeight="bold">

invokeai/frontend/web/src/features/gallery/store/actions.ts

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { createAction } from '@reduxjs/toolkit';
22
import { ImageUsage } from 'app/contexts/DeleteImageContext';
3-
import { ImageDTO } from 'services/api/types';
3+
import { ImageDTO, BoardDTO } from 'services/api/types';
44

55
export type RequestedImageDeletionArg = {
66
image: ImageDTO;
@@ -11,6 +11,16 @@ export const requestedImageDeletion = createAction<RequestedImageDeletionArg>(
1111
'gallery/requestedImageDeletion'
1212
);
1313

14+
export type RequestedBoardImagesDeletionArg = {
15+
board: BoardDTO;
16+
imagesUsage: ImageUsage;
17+
};
18+
19+
export const requestedBoardImagesDeletion =
20+
createAction<RequestedBoardImagesDeletionArg>(
21+
'gallery/requestedBoardImagesDeletion'
22+
);
23+
1424
export const sentImageToCanvas = createAction('gallery/sentImageToCanvas');
1525

1626
export const sentImageToImg2Img = createAction('gallery/sentImageToImg2Img');

invokeai/frontend/web/src/features/gallery/store/imagesSlice.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,9 @@ const imagesSlice = createSlice({
6060
imageRemoved: (state, action: PayloadAction<string>) => {
6161
imagesAdapter.removeOne(state, action.payload);
6262
},
63+
imagesRemoved: (state, action: PayloadAction<string[]>) => {
64+
imagesAdapter.removeMany(state, action.payload);
65+
},
6366
imageCategoriesChanged: (state, action: PayloadAction<ImageCategory[]>) => {
6467
state.categories = action.payload;
6568
},
@@ -117,6 +120,7 @@ export const {
117120
imageUpserted,
118121
imageUpdatedOne,
119122
imageRemoved,
123+
imagesRemoved,
120124
imageCategoriesChanged,
121125
} = imagesSlice.actions;
122126

0 commit comments

Comments
 (0)