diff --git a/invokeai/app/api/routers/boards.py b/invokeai/app/api/routers/boards.py
index 55cd7c8ca2c..94d8667ae44 100644
--- a/invokeai/app/api/routers/boards.py
+++ b/invokeai/app/api/routers/boards.py
@@ -5,6 +5,7 @@
from invokeai.app.services.image_record_storage import OffsetPaginatedResults
from invokeai.app.services.models.board_record import BoardDTO
+
from ..dependencies import ApiDependencies
boards_router = APIRouter(prefix="/v1/boards", tags=["boards"])
@@ -71,11 +72,19 @@ async def update_board(
@boards_router.delete("/{board_id}", operation_id="delete_board")
async def delete_board(
board_id: str = Path(description="The id of board to delete"),
+ include_images: Optional[bool] = Query(
+ description="Permanently delete all images on the board", default=False
+ ),
) -> None:
"""Deletes a board"""
-
try:
- ApiDependencies.invoker.services.boards.delete(board_id=board_id)
+ if include_images is True:
+ ApiDependencies.invoker.services.images.delete_images_on_board(
+ board_id=board_id
+ )
+ ApiDependencies.invoker.services.boards.delete(board_id=board_id)
+ else:
+ ApiDependencies.invoker.services.boards.delete(board_id=board_id)
except Exception as e:
# TODO: Does this need any exception handling at all?
pass
diff --git a/invokeai/app/services/image_file_storage.py b/invokeai/app/services/image_file_storage.py
index b90b9b2f8b4..f30499ea265 100644
--- a/invokeai/app/services/image_file_storage.py
+++ b/invokeai/app/services/image_file_storage.py
@@ -85,8 +85,10 @@ def __init__(self, output_folder: str | Path):
self.__cache_ids = Queue()
self.__max_cache_size = 10 # TODO: get this from config
- self.__output_folder: Path = output_folder if isinstance(output_folder, Path) else Path(output_folder)
- self.__thumbnails_folder = self.__output_folder / 'thumbnails'
+ self.__output_folder: Path = (
+ output_folder if isinstance(output_folder, Path) else Path(output_folder)
+ )
+ self.__thumbnails_folder = self.__output_folder / "thumbnails"
# Validate required output folders at launch
self.__validate_storage_folders()
@@ -94,7 +96,7 @@ def __init__(self, output_folder: str | Path):
def get(self, image_name: str) -> PILImageType:
try:
image_path = self.get_path(image_name)
-
+
cache_item = self.__get_cache(image_path)
if cache_item:
return cache_item
@@ -155,7 +157,7 @@ def delete(self, image_name: str) -> None:
# TODO: make this a bit more flexible for e.g. cloud storage
def get_path(self, image_name: str, thumbnail: bool = False) -> Path:
path = self.__output_folder / image_name
-
+
if thumbnail:
thumbnail_name = get_thumbnail_name(image_name)
path = self.__thumbnails_folder / thumbnail_name
@@ -166,7 +168,7 @@ def validate_path(self, path: str | Path) -> bool:
"""Validates the path given for an image or thumbnail."""
path = path if isinstance(path, Path) else Path(path)
return path.exists()
-
+
def __validate_storage_folders(self) -> None:
"""Checks if the required output folders exist and create them if they don't"""
folders: list[Path] = [self.__output_folder, self.__thumbnails_folder]
@@ -179,7 +181,9 @@ def __get_cache(self, image_name: Path) -> PILImageType | None:
def __set_cache(self, image_name: Path, image: PILImageType):
if not image_name in self.__cache:
self.__cache[image_name] = image
- self.__cache_ids.put(image_name) # TODO: this should refresh position for LRU cache
+ self.__cache_ids.put(
+ image_name
+ ) # TODO: this should refresh position for LRU cache
if len(self.__cache) > self.__max_cache_size:
cache_id = self.__cache_ids.get()
if cache_id in self.__cache:
diff --git a/invokeai/app/services/image_record_storage.py b/invokeai/app/services/image_record_storage.py
index c34d2ca5c82..066e6f8d5fb 100644
--- a/invokeai/app/services/image_record_storage.py
+++ b/invokeai/app/services/image_record_storage.py
@@ -94,6 +94,11 @@ def delete(self, image_name: str) -> None:
"""Deletes an image record."""
pass
+ @abstractmethod
+ def delete_many(self, image_names: list[str]) -> None:
+ """Deletes many image records."""
+ pass
+
@abstractmethod
def save(
self,
@@ -385,6 +390,25 @@ def delete(self, image_name: str) -> None:
finally:
self._lock.release()
+ def delete_many(self, image_names: list[str]) -> None:
+ try:
+ placeholders = ",".join("?" for _ in image_names)
+
+ self._lock.acquire()
+
+ # Construct the SQLite query with the placeholders
+ query = f"DELETE FROM images WHERE image_name IN ({placeholders})"
+
+ # Execute the query with the list of IDs as parameters
+ self._cursor.execute(query, image_names)
+
+ self._conn.commit()
+ except sqlite3.Error as e:
+ self._conn.rollback()
+ raise ImageRecordDeleteException from e
+ finally:
+ self._lock.release()
+
def save(
self,
image_name: str,
diff --git a/invokeai/app/services/images.py b/invokeai/app/services/images.py
index 542f874f1d7..aeb5e520d8c 100644
--- a/invokeai/app/services/images.py
+++ b/invokeai/app/services/images.py
@@ -112,6 +112,11 @@ def delete(self, image_name: str):
"""Deletes an image."""
pass
+ @abstractmethod
+ def delete_images_on_board(self, board_id: str):
+ """Deletes all images on a board."""
+ pass
+
class ImageServiceDependencies:
"""Service dependencies for the ImageService."""
@@ -341,6 +346,28 @@ def delete(self, image_name: str):
self._services.logger.error("Problem deleting image record and file")
raise e
+ def delete_images_on_board(self, board_id: str):
+ try:
+ images = self._services.board_image_records.get_images_for_board(board_id)
+ image_name_list = list(
+ map(
+ lambda r: r.image_name,
+ images.items,
+ )
+ )
+ for image_name in image_name_list:
+ self._services.image_files.delete(image_name)
+ self._services.image_records.delete_many(image_name_list)
+ except ImageRecordDeleteException:
+ self._services.logger.error(f"Failed to delete image records")
+ raise
+ except ImageFileDeleteException:
+ self._services.logger.error(f"Failed to delete image files")
+ raise
+ except Exception as e:
+ self._services.logger.error("Problem deleting image records and files")
+ raise e
+
def _get_metadata(
self, session_id: Optional[str] = None, node_id: Optional[str] = None
) -> Union[ImageMetadata, None]:
diff --git a/invokeai/frontend/web/src/app/components/App.tsx b/invokeai/frontend/web/src/app/components/App.tsx
index c93bd8791cc..5b3cf5925f8 100644
--- a/invokeai/frontend/web/src/app/components/App.tsx
+++ b/invokeai/frontend/web/src/app/components/App.tsx
@@ -25,6 +25,7 @@ import DeleteImageModal from 'features/gallery/components/DeleteImageModal';
import { requestCanvasRescale } from 'features/canvas/store/thunks/requestCanvasScale';
import UpdateImageBoardModal from '../../features/gallery/components/Boards/UpdateImageBoardModal';
import { useListModelsQuery } from 'services/api/endpoints/models';
+import DeleteBoardImagesModal from '../../features/gallery/components/Boards/DeleteBoardImagesModal';
const DEFAULT_CONFIG = {};
@@ -158,6 +159,7 @@ const App = ({
+
>
diff --git a/invokeai/frontend/web/src/app/components/InvokeAIUI.tsx b/invokeai/frontend/web/src/app/components/InvokeAIUI.tsx
index 4d83a407c0e..7259f6105dd 100644
--- a/invokeai/frontend/web/src/app/components/InvokeAIUI.tsx
+++ b/invokeai/frontend/web/src/app/components/InvokeAIUI.tsx
@@ -24,6 +24,7 @@ import {
import UpdateImageBoardModal from '../../features/gallery/components/Boards/UpdateImageBoardModal';
import { AddImageToBoardContextProvider } from '../contexts/AddImageToBoardContext';
import { $authToken, $baseUrl } from 'services/api/client';
+import { DeleteBoardImagesContextProvider } from '../contexts/DeleteBoardImagesContext';
const App = lazy(() => import('./App'));
const ThemeLocaleProvider = lazy(() => import('./ThemeLocaleProvider'));
@@ -86,11 +87,13 @@ const InvokeAIUI = ({
-
+
+
+
diff --git a/invokeai/frontend/web/src/app/contexts/DeleteBoardImagesContext.tsx b/invokeai/frontend/web/src/app/contexts/DeleteBoardImagesContext.tsx
new file mode 100644
index 00000000000..38c89bfcf9d
--- /dev/null
+++ b/invokeai/frontend/web/src/app/contexts/DeleteBoardImagesContext.tsx
@@ -0,0 +1,170 @@
+import { useDisclosure } from '@chakra-ui/react';
+import { PropsWithChildren, createContext, useCallback, useState } from 'react';
+import { BoardDTO } from 'services/api/types';
+import { useDeleteBoardMutation } from '../../services/api/endpoints/boards';
+import { defaultSelectorOptions } from '../store/util/defaultMemoizeOptions';
+import { createSelector } from '@reduxjs/toolkit';
+import { some } from 'lodash-es';
+import { canvasSelector } from '../../features/canvas/store/canvasSelectors';
+import { controlNetSelector } from '../../features/controlNet/store/controlNetSlice';
+import { selectImagesById } from '../../features/gallery/store/imagesSlice';
+import { nodesSelector } from '../../features/nodes/store/nodesSlice';
+import { generationSelector } from '../../features/parameters/store/generationSelectors';
+import { RootState } from '../store/store';
+import { useAppDispatch, useAppSelector } from '../store/storeHooks';
+import { ImageUsage } from './DeleteImageContext';
+import { requestedBoardImagesDeletion } from '../../features/gallery/store/actions';
+
+export const selectBoardImagesUsage = createSelector(
+ [
+ (state: RootState) => state,
+ generationSelector,
+ canvasSelector,
+ nodesSelector,
+ controlNetSelector,
+ (state: RootState, board_id?: string) => board_id,
+ ],
+ (state, generation, canvas, nodes, controlNet, board_id) => {
+ const initialImage = generation.initialImage
+ ? selectImagesById(state, generation.initialImage.imageName)
+ : undefined;
+ const isInitialImage = initialImage?.board_id === board_id;
+
+ const isCanvasImage = canvas.layerState.objects.some((obj) => {
+ if (obj.kind === 'image') {
+ const image = selectImagesById(state, obj.imageName);
+ return image?.board_id === board_id;
+ }
+ return false;
+ });
+
+ const isNodesImage = nodes.nodes.some((node) => {
+ return some(node.data.inputs, (input) => {
+ if (input.type === 'image' && input.value) {
+ const image = selectImagesById(state, input.value.image_name);
+ return image?.board_id === board_id;
+ }
+ return false;
+ });
+ });
+
+ const isControlNetImage = some(controlNet.controlNets, (c) => {
+ const controlImage = c.controlImage
+ ? selectImagesById(state, c.controlImage)
+ : undefined;
+ const processedControlImage = c.processedControlImage
+ ? selectImagesById(state, c.processedControlImage)
+ : undefined;
+ return (
+ controlImage?.board_id === board_id ||
+ processedControlImage?.board_id === board_id
+ );
+ });
+
+ const imageUsage: ImageUsage = {
+ isInitialImage,
+ isCanvasImage,
+ isNodesImage,
+ isControlNetImage,
+ };
+
+ return imageUsage;
+ },
+ defaultSelectorOptions
+);
+
+type DeleteBoardImagesContextValue = {
+ /**
+ * Whether the move image dialog is open.
+ */
+ isOpen: boolean;
+ /**
+ * Closes the move image dialog.
+ */
+ onClose: () => void;
+ imagesUsage?: ImageUsage;
+ board?: BoardDTO;
+ onClickDeleteBoardImages: (board: BoardDTO) => void;
+ handleDeleteBoardImages: (boardId: string) => void;
+ handleDeleteBoardOnly: (boardId: string) => void;
+};
+
+export const DeleteBoardImagesContext =
+ createContext({
+ isOpen: false,
+ onClose: () => undefined,
+ onClickDeleteBoardImages: () => undefined,
+ handleDeleteBoardImages: () => undefined,
+ handleDeleteBoardOnly: () => undefined,
+ });
+
+type Props = PropsWithChildren;
+
+export const DeleteBoardImagesContextProvider = (props: Props) => {
+ const [boardToDelete, setBoardToDelete] = useState();
+ const { isOpen, onOpen, onClose } = useDisclosure();
+ const dispatch = useAppDispatch();
+
+ // Check where the board images to be deleted are used (eg init image, controlnet, etc.)
+ const imagesUsage = useAppSelector((state) =>
+ selectBoardImagesUsage(state, boardToDelete?.board_id)
+ );
+
+ const [deleteBoard] = useDeleteBoardMutation();
+
+ // Clean up after deleting or dismissing the modal
+ const closeAndClearBoardToDelete = useCallback(() => {
+ setBoardToDelete(undefined);
+ onClose();
+ }, [onClose]);
+
+ const onClickDeleteBoardImages = useCallback(
+ (board?: BoardDTO) => {
+ console.log({ board });
+ if (!board) {
+ return;
+ }
+ setBoardToDelete(board);
+ onOpen();
+ },
+ [setBoardToDelete, onOpen]
+ );
+
+ const handleDeleteBoardImages = useCallback(
+ (boardId: string) => {
+ if (boardToDelete) {
+ dispatch(
+ requestedBoardImagesDeletion({ board: boardToDelete, imagesUsage })
+ );
+ closeAndClearBoardToDelete();
+ }
+ },
+ [dispatch, closeAndClearBoardToDelete, boardToDelete, imagesUsage]
+ );
+
+ const handleDeleteBoardOnly = useCallback(
+ (boardId: string) => {
+ if (boardToDelete) {
+ deleteBoard(boardId);
+ closeAndClearBoardToDelete();
+ }
+ },
+ [deleteBoard, closeAndClearBoardToDelete, boardToDelete]
+ );
+
+ return (
+
+ {props.children}
+
+ );
+};
diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/index.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/index.ts
index a5fde1d0c27..a36141fafce 100644
--- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/index.ts
+++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/index.ts
@@ -83,6 +83,7 @@ import {
addImageRemovedFromBoardRejectedListener,
} from './listeners/imageRemovedFromBoard';
import { addReceivedOpenAPISchemaListener } from './listeners/receivedOpenAPISchema';
+import { addRequestedBoardImageDeletionListener } from './listeners/boardImagesDeleted';
export const listenerMiddleware = createListenerMiddleware();
@@ -124,6 +125,7 @@ addRequestedImageDeletionListener();
addImageDeletedPendingListener();
addImageDeletedFulfilledListener();
addImageDeletedRejectedListener();
+addRequestedBoardImageDeletionListener();
// Image metadata
addImageMetadataReceivedFulfilledListener();
diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/boardImagesDeleted.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/boardImagesDeleted.ts
new file mode 100644
index 00000000000..c4d3c5f0ba6
--- /dev/null
+++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/boardImagesDeleted.ts
@@ -0,0 +1,79 @@
+import { requestedBoardImagesDeletion } from 'features/gallery/store/actions';
+import { startAppListening } from '..';
+import { imageSelected } from 'features/gallery/store/gallerySlice';
+import {
+ imagesRemoved,
+ selectImagesAll,
+ selectImagesById,
+} from 'features/gallery/store/imagesSlice';
+import { resetCanvas } from 'features/canvas/store/canvasSlice';
+import { controlNetReset } from 'features/controlNet/store/controlNetSlice';
+import { clearInitialImage } from 'features/parameters/store/generationSlice';
+import { nodeEditorReset } from 'features/nodes/store/nodesSlice';
+import { LIST_TAG, api } from 'services/api';
+import { boardsApi } from '../../../../../services/api/endpoints/boards';
+
+export const addRequestedBoardImageDeletionListener = () => {
+ startAppListening({
+ actionCreator: requestedBoardImagesDeletion,
+ effect: async (action, { dispatch, getState, condition }) => {
+ const { board, imagesUsage } = action.payload;
+
+ const { board_id } = board;
+
+ const state = getState();
+ const selectedImage = state.gallery.selectedImage
+ ? selectImagesById(state, state.gallery.selectedImage)
+ : undefined;
+
+ if (selectedImage && selectedImage.board_id === board_id) {
+ dispatch(imageSelected());
+ }
+
+ // We need to reset the features where the board images are in use - none of these work if their image(s) don't exist
+
+ if (imagesUsage.isCanvasImage) {
+ dispatch(resetCanvas());
+ }
+
+ if (imagesUsage.isControlNetImage) {
+ dispatch(controlNetReset());
+ }
+
+ if (imagesUsage.isInitialImage) {
+ dispatch(clearInitialImage());
+ }
+
+ if (imagesUsage.isNodesImage) {
+ dispatch(nodeEditorReset());
+ }
+
+ // Preemptively remove from gallery
+ const images = selectImagesAll(state).reduce((acc: string[], img) => {
+ if (img.board_id === board_id) {
+ acc.push(img.image_name);
+ }
+ return acc;
+ }, []);
+ dispatch(imagesRemoved(images));
+
+ // Delete from server
+ dispatch(boardsApi.endpoints.deleteBoardAndImages.initiate(board_id));
+ const result =
+ boardsApi.endpoints.deleteBoardAndImages.select(board_id)(state);
+ const { isSuccess } = result;
+
+ // Wait for successful deletion, then trigger boards to re-fetch
+ const wasBoardDeleted = await condition(() => !!isSuccess, 30000);
+
+ if (wasBoardDeleted) {
+ dispatch(
+ api.util.invalidateTags([
+ { type: 'Board', id: board_id },
+ { type: 'Image', id: LIST_TAG },
+ ])
+ );
+ }
+ },
+ });
+};
diff --git a/invokeai/frontend/web/src/features/gallery/components/Boards/DeleteBoardImagesModal.tsx b/invokeai/frontend/web/src/features/gallery/components/Boards/DeleteBoardImagesModal.tsx
new file mode 100644
index 00000000000..736d72f862c
--- /dev/null
+++ b/invokeai/frontend/web/src/features/gallery/components/Boards/DeleteBoardImagesModal.tsx
@@ -0,0 +1,114 @@
+import {
+ AlertDialog,
+ AlertDialogBody,
+ AlertDialogContent,
+ AlertDialogFooter,
+ AlertDialogHeader,
+ AlertDialogOverlay,
+ Divider,
+ Flex,
+ ListItem,
+ Text,
+ UnorderedList,
+} from '@chakra-ui/react';
+import IAIButton from 'common/components/IAIButton';
+import { memo, useContext, useRef } from 'react';
+import { useTranslation } from 'react-i18next';
+import { DeleteBoardImagesContext } from '../../../../app/contexts/DeleteBoardImagesContext';
+import { some } from 'lodash-es';
+import { ImageUsage } from '../../../../app/contexts/DeleteImageContext';
+
+const BoardImageInUseMessage = (props: { imagesUsage?: ImageUsage }) => {
+ const { imagesUsage } = props;
+
+ if (!imagesUsage) {
+ return null;
+ }
+
+ if (!some(imagesUsage)) {
+ return null;
+ }
+
+ return (
+ <>
+
+ An image from this board is currently in use in the following features:
+
+
+ {imagesUsage.isInitialImage && Image to Image}
+ {imagesUsage.isCanvasImage && Unified Canvas}
+ {imagesUsage.isControlNetImage && ControlNet}
+ {imagesUsage.isNodesImage && Node Editor}
+
+
+ If you delete images from this board, those features will immediately be
+ reset.
+
+ >
+ );
+};
+
+const DeleteBoardImagesModal = () => {
+ const { t } = useTranslation();
+
+ const {
+ isOpen,
+ onClose,
+ board,
+ handleDeleteBoardImages,
+ handleDeleteBoardOnly,
+ imagesUsage,
+ } = useContext(DeleteBoardImagesContext);
+
+ const cancelRef = useRef(null);
+
+ return (
+
+
+ {board && (
+
+
+ Delete Board
+
+
+
+
+
+
+ {t('common.areYouSure')}
+
+ This board has {board.image_count} image(s) that will be
+ deleted.
+
+
+
+
+
+ Cancel
+
+ handleDeleteBoardOnly(board.board_id)}
+ >
+ Delete Board Only
+
+ handleDeleteBoardImages(board.board_id)}
+ >
+ Delete Board and Images
+
+
+
+ )}
+
+
+ );
+};
+
+export default memo(DeleteBoardImagesModal);
diff --git a/invokeai/frontend/web/src/features/gallery/components/Boards/HoverableBoard.tsx b/invokeai/frontend/web/src/features/gallery/components/Boards/HoverableBoard.tsx
index ba43f792bf3..535cab1b15b 100644
--- a/invokeai/frontend/web/src/features/gallery/components/Boards/HoverableBoard.tsx
+++ b/invokeai/frontend/web/src/features/gallery/components/Boards/HoverableBoard.tsx
@@ -11,7 +11,7 @@ import {
} from '@chakra-ui/react';
import { useAppDispatch } from 'app/store/storeHooks';
-import { memo, useCallback } from 'react';
+import { memo, useCallback, useContext } from 'react';
import { FaFolder, FaTrash } from 'react-icons/fa';
import { ContextMenu } from 'chakra-ui-contextmenu';
import { BoardDTO, ImageDTO } from 'services/api/types';
@@ -29,6 +29,7 @@ import { useDroppable } from '@dnd-kit/core';
import { AnimatePresence } from 'framer-motion';
import IAIDropOverlay from 'common/components/IAIDropOverlay';
import { SelectedItemOverlay } from '../SelectedItemOverlay';
+import { DeleteBoardImagesContext } from '../../../../app/contexts/DeleteBoardImagesContext';
interface HoverableBoardProps {
board: BoardDTO;
@@ -44,6 +45,8 @@ const HoverableBoard = memo(({ board, isSelected }: HoverableBoardProps) => {
const { board_name, board_id } = board;
+ const { onClickDeleteBoardImages } = useContext(DeleteBoardImagesContext);
+
const handleSelectBoard = useCallback(() => {
dispatch(boardIdSelected(board_id));
}, [board_id, dispatch]);
@@ -65,6 +68,11 @@ const HoverableBoard = memo(({ board, isSelected }: HoverableBoardProps) => {
deleteBoard(board_id);
}, [board_id, deleteBoard]);
+ const handleDeleteBoardAndImages = useCallback(() => {
+ console.log({ board });
+ onClickDeleteBoardImages(board);
+ }, [board, onClickDeleteBoardImages]);
+
const handleDrop = useCallback(
(droppedImage: ImageDTO) => {
if (droppedImage.board_id === board_id) {
@@ -92,6 +100,15 @@ const HoverableBoard = memo(({ board, isSelected }: HoverableBoardProps) => {
menuProps={{ size: 'sm', isLazy: true }}
renderMenu={() => (
+ {board.image_count > 0 && (
+ }
+ onClickCapture={handleDeleteBoardAndImages}
+ >
+ Delete Board and Images
+
+ )}
}
diff --git a/invokeai/frontend/web/src/features/gallery/store/actions.ts b/invokeai/frontend/web/src/features/gallery/store/actions.ts
index aa767b14224..42347781204 100644
--- a/invokeai/frontend/web/src/features/gallery/store/actions.ts
+++ b/invokeai/frontend/web/src/features/gallery/store/actions.ts
@@ -1,6 +1,6 @@
import { createAction } from '@reduxjs/toolkit';
import { ImageUsage } from 'app/contexts/DeleteImageContext';
-import { ImageDTO } from 'services/api/types';
+import { ImageDTO, BoardDTO } from 'services/api/types';
export type RequestedImageDeletionArg = {
image: ImageDTO;
@@ -11,6 +11,16 @@ export const requestedImageDeletion = createAction(
'gallery/requestedImageDeletion'
);
+export type RequestedBoardImagesDeletionArg = {
+ board: BoardDTO;
+ imagesUsage: ImageUsage;
+};
+
+export const requestedBoardImagesDeletion =
+ createAction(
+ 'gallery/requestedBoardImagesDeletion'
+ );
+
export const sentImageToCanvas = createAction('gallery/sentImageToCanvas');
export const sentImageToImg2Img = createAction('gallery/sentImageToImg2Img');
diff --git a/invokeai/frontend/web/src/features/gallery/store/imagesSlice.ts b/invokeai/frontend/web/src/features/gallery/store/imagesSlice.ts
index 4bdaa796cd3..8041ffd5c58 100644
--- a/invokeai/frontend/web/src/features/gallery/store/imagesSlice.ts
+++ b/invokeai/frontend/web/src/features/gallery/store/imagesSlice.ts
@@ -60,6 +60,9 @@ const imagesSlice = createSlice({
imageRemoved: (state, action: PayloadAction) => {
imagesAdapter.removeOne(state, action.payload);
},
+ imagesRemoved: (state, action: PayloadAction) => {
+ imagesAdapter.removeMany(state, action.payload);
+ },
imageCategoriesChanged: (state, action: PayloadAction) => {
state.categories = action.payload;
},
@@ -117,6 +120,7 @@ export const {
imageUpserted,
imageUpdatedOne,
imageRemoved,
+ imagesRemoved,
imageCategoriesChanged,
} = imagesSlice.actions;
diff --git a/invokeai/frontend/web/src/services/api/endpoints/boards.ts b/invokeai/frontend/web/src/services/api/endpoints/boards.ts
index 9816d88eb94..64ab21075d0 100644
--- a/invokeai/frontend/web/src/services/api/endpoints/boards.ts
+++ b/invokeai/frontend/web/src/services/api/endpoints/boards.ts
@@ -82,11 +82,14 @@ export const boardsApi = api.injectEndpoints({
{ type: 'Board', id: arg.board_id },
],
}),
-
deleteBoard: build.mutation({
query: (board_id) => ({ url: `boards/${board_id}`, method: 'DELETE' }),
invalidatesTags: (result, error, arg) => [{ type: 'Board', id: arg }],
}),
+ deleteBoardAndImages: build.mutation({
+ query: (board_id) => ({ url: `boards/${board_id}`, method: 'DELETE', params: { include_images: true } }),
+ invalidatesTags: (result, error, arg) => [{ type: 'Board', id: arg }, { type: 'Image', id: LIST_TAG }],
+ }),
}),
});
@@ -96,4 +99,5 @@ export const {
useCreateBoardMutation,
useUpdateBoardMutation,
useDeleteBoardMutation,
+ useDeleteBoardAndImagesMutation
} = boardsApi;