add image usage for board images and listener to handle actual deletion

This commit is contained in:
Mary Hipp 2023-06-28 11:43:04 -04:00 committed by psychedelicious
parent ba67e57a7e
commit 723d68e496
6 changed files with 213 additions and 13 deletions

View File

@ -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}

View File

@ -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();

View File

@ -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 },
])
);
}
},
});
};

View File

@ -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">

View File

@ -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');

View File

@ -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;