mirror of
https://github.com/invoke-ai/InvokeAI
synced 2024-08-30 20:32:17 +00:00
add image usage for board images and listener to handle actual deletion
This commit is contained in:
parent
ba67e57a7e
commit
723d68e496
@ -2,13 +2,76 @@ import { useDisclosure } from '@chakra-ui/react';
|
|||||||
import { PropsWithChildren, createContext, useCallback, useState } from 'react';
|
import { PropsWithChildren, createContext, useCallback, useState } from 'react';
|
||||||
import { BoardDTO } from 'services/api/types';
|
import { BoardDTO } from 'services/api/types';
|
||||||
import { useDeleteBoardMutation } from '../../services/api/endpoints/boards';
|
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 type ImageUsage = {
|
export const selectBoardImagesUsage = createSelector(
|
||||||
isInitialImage: boolean;
|
[
|
||||||
isCanvasImage: boolean;
|
(state: RootState) => state,
|
||||||
isNodesImage: boolean;
|
generationSelector,
|
||||||
isControlNetImage: boolean;
|
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 = {
|
type DeleteBoardImagesContextValue = {
|
||||||
/**
|
/**
|
||||||
@ -19,9 +82,7 @@ type DeleteBoardImagesContextValue = {
|
|||||||
* Closes the move image dialog.
|
* Closes the move image dialog.
|
||||||
*/
|
*/
|
||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
/**
|
imagesUsage?: ImageUsage;
|
||||||
* The image pending movement
|
|
||||||
*/
|
|
||||||
board?: BoardDTO;
|
board?: BoardDTO;
|
||||||
onClickDeleteBoardImages: (board: BoardDTO) => void;
|
onClickDeleteBoardImages: (board: BoardDTO) => void;
|
||||||
handleDeleteBoardImages: (boardId: string) => void;
|
handleDeleteBoardImages: (boardId: string) => void;
|
||||||
@ -42,8 +103,13 @@ type Props = PropsWithChildren;
|
|||||||
export const DeleteBoardImagesContextProvider = (props: Props) => {
|
export const DeleteBoardImagesContextProvider = (props: Props) => {
|
||||||
const [boardToDelete, setBoardToDelete] = useState<BoardDTO>();
|
const [boardToDelete, setBoardToDelete] = useState<BoardDTO>();
|
||||||
const { isOpen, onOpen, onClose } = useDisclosure();
|
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 [deleteBoardAndImages] = useDeleteBoardAndImagesMutation();
|
|
||||||
const [deleteBoard] = useDeleteBoardMutation();
|
const [deleteBoard] = useDeleteBoardMutation();
|
||||||
|
|
||||||
// Clean up after deleting or dismissing the modal
|
// Clean up after deleting or dismissing the modal
|
||||||
@ -67,11 +133,13 @@ export const DeleteBoardImagesContextProvider = (props: Props) => {
|
|||||||
const handleDeleteBoardImages = useCallback(
|
const handleDeleteBoardImages = useCallback(
|
||||||
(boardId: string) => {
|
(boardId: string) => {
|
||||||
if (boardToDelete) {
|
if (boardToDelete) {
|
||||||
deleteBoardAndImages(boardId);
|
dispatch(
|
||||||
|
requestedBoardImagesDeletion({ board: boardToDelete, imagesUsage })
|
||||||
|
);
|
||||||
closeAndClearBoardToDelete();
|
closeAndClearBoardToDelete();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[deleteBoardAndImages, closeAndClearBoardToDelete, boardToDelete]
|
[dispatch, closeAndClearBoardToDelete, boardToDelete, imagesUsage]
|
||||||
);
|
);
|
||||||
|
|
||||||
const handleDeleteBoardOnly = useCallback(
|
const handleDeleteBoardOnly = useCallback(
|
||||||
@ -93,6 +161,7 @@ export const DeleteBoardImagesContextProvider = (props: Props) => {
|
|||||||
onClickDeleteBoardImages,
|
onClickDeleteBoardImages,
|
||||||
handleDeleteBoardImages,
|
handleDeleteBoardImages,
|
||||||
handleDeleteBoardOnly,
|
handleDeleteBoardOnly,
|
||||||
|
imagesUsage,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{props.children}
|
{props.children}
|
||||||
|
@ -83,6 +83,7 @@ import {
|
|||||||
addImageRemovedFromBoardRejectedListener,
|
addImageRemovedFromBoardRejectedListener,
|
||||||
} from './listeners/imageRemovedFromBoard';
|
} from './listeners/imageRemovedFromBoard';
|
||||||
import { addReceivedOpenAPISchemaListener } from './listeners/receivedOpenAPISchema';
|
import { addReceivedOpenAPISchemaListener } from './listeners/receivedOpenAPISchema';
|
||||||
|
import { addRequestedBoardImageDeletionListener } from './listeners/boardImagesDeleted';
|
||||||
|
|
||||||
export const listenerMiddleware = createListenerMiddleware();
|
export const listenerMiddleware = createListenerMiddleware();
|
||||||
|
|
||||||
@ -124,6 +125,7 @@ addRequestedImageDeletionListener();
|
|||||||
addImageDeletedPendingListener();
|
addImageDeletedPendingListener();
|
||||||
addImageDeletedFulfilledListener();
|
addImageDeletedFulfilledListener();
|
||||||
addImageDeletedRejectedListener();
|
addImageDeletedRejectedListener();
|
||||||
|
addRequestedBoardImageDeletionListener();
|
||||||
|
|
||||||
// Image metadata
|
// Image metadata
|
||||||
addImageMetadataReceivedFulfilledListener();
|
addImageMetadataReceivedFulfilledListener();
|
||||||
|
@ -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 },
|
||||||
|
])
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
@ -7,12 +7,46 @@ import {
|
|||||||
AlertDialogOverlay,
|
AlertDialogOverlay,
|
||||||
Divider,
|
Divider,
|
||||||
Flex,
|
Flex,
|
||||||
|
ListItem,
|
||||||
Text,
|
Text,
|
||||||
|
UnorderedList,
|
||||||
} from '@chakra-ui/react';
|
} from '@chakra-ui/react';
|
||||||
import IAIButton from 'common/components/IAIButton';
|
import IAIButton from 'common/components/IAIButton';
|
||||||
import { memo, useContext, useRef } from 'react';
|
import { memo, useContext, useRef } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { DeleteBoardImagesContext } from '../../../../app/contexts/DeleteBoardImagesContext';
|
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 (
|
||||||
|
<>
|
||||||
|
<Text>
|
||||||
|
An image from this board is currently in use in the following features:
|
||||||
|
</Text>
|
||||||
|
<UnorderedList sx={{ paddingInlineStart: 6 }}>
|
||||||
|
{imagesUsage.isInitialImage && <ListItem>Image to Image</ListItem>}
|
||||||
|
{imagesUsage.isCanvasImage && <ListItem>Unified Canvas</ListItem>}
|
||||||
|
{imagesUsage.isControlNetImage && <ListItem>ControlNet</ListItem>}
|
||||||
|
{imagesUsage.isNodesImage && <ListItem>Node Editor</ListItem>}
|
||||||
|
</UnorderedList>
|
||||||
|
<Text>
|
||||||
|
If you delete images from this board, those features will immediately be
|
||||||
|
reset.
|
||||||
|
</Text>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
const DeleteBoardImagesModal = () => {
|
const DeleteBoardImagesModal = () => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
@ -23,6 +57,7 @@ const DeleteBoardImagesModal = () => {
|
|||||||
board,
|
board,
|
||||||
handleDeleteBoardImages,
|
handleDeleteBoardImages,
|
||||||
handleDeleteBoardOnly,
|
handleDeleteBoardOnly,
|
||||||
|
imagesUsage,
|
||||||
} = useContext(DeleteBoardImagesContext);
|
} = useContext(DeleteBoardImagesContext);
|
||||||
|
|
||||||
const cancelRef = useRef<HTMLButtonElement>(null);
|
const cancelRef = useRef<HTMLButtonElement>(null);
|
||||||
@ -43,6 +78,7 @@ const DeleteBoardImagesModal = () => {
|
|||||||
|
|
||||||
<AlertDialogBody>
|
<AlertDialogBody>
|
||||||
<Flex direction="column" gap={3}>
|
<Flex direction="column" gap={3}>
|
||||||
|
<BoardImageInUseMessage imagesUsage={imagesUsage} />
|
||||||
<Divider />
|
<Divider />
|
||||||
<Text>{t('common.areYouSure')}</Text>
|
<Text>{t('common.areYouSure')}</Text>
|
||||||
<Text fontWeight="bold">
|
<Text fontWeight="bold">
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import { createAction } from '@reduxjs/toolkit';
|
import { createAction } from '@reduxjs/toolkit';
|
||||||
import { ImageUsage } from 'app/contexts/DeleteImageContext';
|
import { ImageUsage } from 'app/contexts/DeleteImageContext';
|
||||||
import { ImageDTO } from 'services/api/types';
|
import { ImageDTO, BoardDTO } from 'services/api/types';
|
||||||
|
|
||||||
export type RequestedImageDeletionArg = {
|
export type RequestedImageDeletionArg = {
|
||||||
image: ImageDTO;
|
image: ImageDTO;
|
||||||
@ -11,6 +11,16 @@ export const requestedImageDeletion = createAction<RequestedImageDeletionArg>(
|
|||||||
'gallery/requestedImageDeletion'
|
'gallery/requestedImageDeletion'
|
||||||
);
|
);
|
||||||
|
|
||||||
|
export type RequestedBoardImagesDeletionArg = {
|
||||||
|
board: BoardDTO;
|
||||||
|
imagesUsage: ImageUsage;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const requestedBoardImagesDeletion =
|
||||||
|
createAction<RequestedBoardImagesDeletionArg>(
|
||||||
|
'gallery/requestedBoardImagesDeletion'
|
||||||
|
);
|
||||||
|
|
||||||
export const sentImageToCanvas = createAction('gallery/sentImageToCanvas');
|
export const sentImageToCanvas = createAction('gallery/sentImageToCanvas');
|
||||||
|
|
||||||
export const sentImageToImg2Img = createAction('gallery/sentImageToImg2Img');
|
export const sentImageToImg2Img = createAction('gallery/sentImageToImg2Img');
|
||||||
|
@ -60,6 +60,9 @@ const imagesSlice = createSlice({
|
|||||||
imageRemoved: (state, action: PayloadAction<string>) => {
|
imageRemoved: (state, action: PayloadAction<string>) => {
|
||||||
imagesAdapter.removeOne(state, action.payload);
|
imagesAdapter.removeOne(state, action.payload);
|
||||||
},
|
},
|
||||||
|
imagesRemoved: (state, action: PayloadAction<string[]>) => {
|
||||||
|
imagesAdapter.removeMany(state, action.payload);
|
||||||
|
},
|
||||||
imageCategoriesChanged: (state, action: PayloadAction<ImageCategory[]>) => {
|
imageCategoriesChanged: (state, action: PayloadAction<ImageCategory[]>) => {
|
||||||
state.categories = action.payload;
|
state.categories = action.payload;
|
||||||
},
|
},
|
||||||
@ -117,6 +120,7 @@ export const {
|
|||||||
imageUpserted,
|
imageUpserted,
|
||||||
imageUpdatedOne,
|
imageUpdatedOne,
|
||||||
imageRemoved,
|
imageRemoved,
|
||||||
|
imagesRemoved,
|
||||||
imageCategoriesChanged,
|
imageCategoriesChanged,
|
||||||
} = imagesSlice.actions;
|
} = imagesSlice.actions;
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user