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;