diff --git a/invokeai/frontend/web/src/app/components/App.tsx b/invokeai/frontend/web/src/app/components/App.tsx
index c93bd8791c..5b3cf5925f 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 4d83a407c0..7259f6105d 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 0000000000..dd50ce15a5
--- /dev/null
+++ b/invokeai/frontend/web/src/app/contexts/DeleteBoardImagesContext.tsx
@@ -0,0 +1,101 @@
+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';
+
+export type ImageUsage = {
+ isInitialImage: boolean;
+ isCanvasImage: boolean;
+ isNodesImage: boolean;
+ isControlNetImage: boolean;
+};
+
+type DeleteBoardImagesContextValue = {
+ /**
+ * Whether the move image dialog is open.
+ */
+ isOpen: boolean;
+ /**
+ * Closes the move image dialog.
+ */
+ onClose: () => void;
+ /**
+ * The image pending movement
+ */
+ 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 [deleteBoardAndImages] = useDeleteBoardAndImagesMutation();
+ 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) {
+ deleteBoardAndImages(boardId);
+ closeAndClearBoardToDelete();
+ }
+ },
+ [deleteBoardAndImages, closeAndClearBoardToDelete, boardToDelete]
+ );
+
+ const handleDeleteBoardOnly = useCallback(
+ (boardId: string) => {
+ if (boardToDelete) {
+ deleteBoard(boardId);
+ closeAndClearBoardToDelete();
+ }
+ },
+ [deleteBoard, closeAndClearBoardToDelete, boardToDelete]
+ );
+
+ return (
+
+ {props.children}
+
+ );
+};
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 0000000000..345a95b846
--- /dev/null
+++ b/invokeai/frontend/web/src/features/gallery/components/Boards/DeleteBoardImagesModal.tsx
@@ -0,0 +1,78 @@
+import {
+ AlertDialog,
+ AlertDialogBody,
+ AlertDialogContent,
+ AlertDialogFooter,
+ AlertDialogHeader,
+ AlertDialogOverlay,
+ Divider,
+ Flex,
+ Text,
+} 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';
+
+const DeleteBoardImagesModal = () => {
+ const { t } = useTranslation();
+
+ const {
+ isOpen,
+ onClose,
+ board,
+ handleDeleteBoardImages,
+ handleDeleteBoardOnly,
+ } = 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 ba43f792bf..535cab1b15 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/services/api/endpoints/boards.ts b/invokeai/frontend/web/src/services/api/endpoints/boards.ts
index 9816d88eb9..64ab21075d 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;