diff --git a/invokeai/frontend/web/src/app/contexts/AddImageToBoardContext.tsx b/invokeai/frontend/web/src/app/contexts/AddImageToBoardContext.tsx
index da3dcb2239..d29c1c8a48 100644
--- a/invokeai/frontend/web/src/app/contexts/AddImageToBoardContext.tsx
+++ b/invokeai/frontend/web/src/app/contexts/AddImageToBoardContext.tsx
@@ -3,6 +3,7 @@ import { useAppDispatch } from 'app/store/storeHooks';
import { PropsWithChildren, createContext, useCallback, useState } from 'react';
import { ImageDTO } from 'services/api';
import { imageAddedToBoard } from '../../services/thunks/board';
+import { useAddImageToBoardMutation } from 'services/apiSlice';
export type ImageUsage = {
isInitialImage: boolean;
@@ -43,6 +44,8 @@ export const AddImageToBoardContextProvider = (props: Props) => {
const dispatch = useAppDispatch();
const { isOpen, onOpen, onClose } = useDisclosure();
+ const [addImageToBoard, result] = useAddImageToBoardMutation();
+
// Clean up after deleting or dismissing the modal
const closeAndClearImageToDelete = useCallback(() => {
setImageToMove(undefined);
@@ -63,18 +66,14 @@ export const AddImageToBoardContextProvider = (props: Props) => {
const handleAddToBoard = useCallback(
(boardId: string) => {
if (imageToMove) {
- dispatch(
- imageAddedToBoard({
- requestBody: {
- board_id: boardId,
- image_name: imageToMove.image_name,
- },
- })
- );
+ addImageToBoard({
+ board_id: boardId,
+ image_name: imageToMove.image_name,
+ });
closeAndClearImageToDelete();
}
},
- [closeAndClearImageToDelete, dispatch, imageToMove]
+ [addImageToBoard, closeAndClearImageToDelete, imageToMove]
);
return (
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 8c073e81d6..15fd48fbb2 100644
--- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/index.ts
+++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/index.ts
@@ -73,6 +73,10 @@ import { addImageCategoriesChangedListener } from './listeners/imageCategoriesCh
import { addControlNetImageProcessedListener } from './listeners/controlNetImageProcessed';
import { addControlNetAutoProcessListener } from './listeners/controlNetAutoProcess';
import { addUpdateImageUrlsOnConnectListener } from './listeners/updateImageUrlsOnConnect';
+import {
+ addImageAddedToBoardFulfilledListener,
+ addImageAddedToBoardRejectedListener,
+} from './listeners/imageAddedToBoard';
export const listenerMiddleware = createListenerMiddleware();
@@ -183,3 +187,7 @@ addControlNetAutoProcessListener();
// Update image URLs on connect
addUpdateImageUrlsOnConnectListener();
+
+// Boards
+addImageAddedToBoardFulfilledListener();
+addImageAddedToBoardRejectedListener();
diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/imageAddedToBoard.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/imageAddedToBoard.ts
new file mode 100644
index 0000000000..0f404cab68
--- /dev/null
+++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/imageAddedToBoard.ts
@@ -0,0 +1,40 @@
+import { log } from 'app/logging/useLogger';
+import { startAppListening } from '..';
+import { imageMetadataReceived } from 'services/thunks/image';
+import { api } from 'services/apiSlice';
+
+const moduleLog = log.child({ namespace: 'boards' });
+
+export const addImageAddedToBoardFulfilledListener = () => {
+ startAppListening({
+ matcher: api.endpoints.addImageToBoard.matchFulfilled,
+ effect: (action, { getState, dispatch }) => {
+ const { board_id, image_name } = action.meta.arg.originalArgs;
+
+ moduleLog.debug(
+ { data: { board_id, image_name } },
+ 'Image added to board'
+ );
+
+ dispatch(
+ imageMetadataReceived({
+ imageName: image_name,
+ })
+ );
+ },
+ });
+};
+
+export const addImageAddedToBoardRejectedListener = () => {
+ startAppListening({
+ matcher: api.endpoints.addImageToBoard.matchRejected,
+ effect: (action, { getState, dispatch }) => {
+ const { board_id, image_name } = action.meta.arg.originalArgs;
+
+ moduleLog.debug(
+ { data: { board_id, image_name } },
+ 'Problem adding image to board'
+ );
+ },
+ });
+};
diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/imageDeleted.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/imageDeleted.ts
index 4c0c057242..9792137bbe 100644
--- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/imageDeleted.ts
+++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/imageDeleted.ts
@@ -13,6 +13,7 @@ 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 { api } from 'services/apiSlice';
const moduleLog = log.child({ namespace: 'addRequestedImageDeletionListener' });
@@ -22,7 +23,7 @@ const moduleLog = log.child({ namespace: 'addRequestedImageDeletionListener' });
export const addRequestedImageDeletionListener = () => {
startAppListening({
actionCreator: requestedImageDeletion,
- effect: (action, { dispatch, getState }) => {
+ effect: async (action, { dispatch, getState, condition }) => {
const { image, imageUsage } = action.payload;
const { image_name } = image;
@@ -30,7 +31,7 @@ export const addRequestedImageDeletionListener = () => {
const state = getState();
const selectedImage = state.gallery.selectedImage;
- if (selectedImage && selectedImage.image_name === image_name) {
+ if (selectedImage && selectedImage === image_name) {
const ids = selectImagesIds(state);
const entities = selectImagesEntities(state);
@@ -51,7 +52,7 @@ export const addRequestedImageDeletionListener = () => {
const newSelectedImage = entities[newSelectedImageId];
if (newSelectedImageId) {
- dispatch(imageSelected(newSelectedImage));
+ dispatch(imageSelected(newSelectedImageId));
} else {
dispatch(imageSelected());
}
@@ -79,7 +80,19 @@ export const addRequestedImageDeletionListener = () => {
dispatch(imageRemoved(image_name));
// Delete from server
- dispatch(imageDeleted({ imageName: image_name }));
+ const { requestId } = dispatch(imageDeleted({ imageName: image_name }));
+
+ // Wait for successful deletion, then trigger boards to re-fetch
+ const wasImageDeleted = await condition(
+ (action) => action.meta.requestId === requestId,
+ 30000
+ );
+
+ if (wasImageDeleted) {
+ dispatch(
+ api.util.invalidateTags([{ type: 'Board', id: image.board_id }])
+ );
+ }
},
});
};
diff --git a/invokeai/frontend/web/src/app/store/store.ts b/invokeai/frontend/web/src/app/store/store.ts
index 4032db3159..a9011f9356 100644
--- a/invokeai/frontend/web/src/app/store/store.ts
+++ b/invokeai/frontend/web/src/app/store/store.ts
@@ -33,6 +33,7 @@ import { actionsDenylist } from './middleware/devtools/actionsDenylist';
import { serialize } from './enhancers/reduxRemember/serialize';
import { unserialize } from './enhancers/reduxRemember/unserialize';
import { LOCALSTORAGE_PREFIX } from './constants';
+import { api } from 'services/apiSlice';
const allReducers = {
canvas: canvasReducer,
@@ -49,6 +50,7 @@ const allReducers = {
images: imagesReducer,
controlNet: controlNetReducer,
boards: boardsReducer,
+ [api.reducerPath]: api.reducer,
// session: sessionReducer,
};
@@ -87,6 +89,7 @@ export const store = configureStore({
immutableCheck: false,
serializableCheck: false,
})
+ .concat(api.middleware)
.concat(dynamicMiddlewares)
.prepend(listenerMiddleware.middleware),
devTools: {
diff --git a/invokeai/frontend/web/src/features/gallery/components/Boards/AddBoardButton.tsx b/invokeai/frontend/web/src/features/gallery/components/Boards/AddBoardButton.tsx
index d8828fe736..284e6558ac 100644
--- a/invokeai/frontend/web/src/features/gallery/components/Boards/AddBoardButton.tsx
+++ b/invokeai/frontend/web/src/features/gallery/components/Boards/AddBoardButton.tsx
@@ -1,19 +1,20 @@
-import { Flex, Icon, Text } from '@chakra-ui/react';
+import { Flex, Icon, Spinner, Text } from '@chakra-ui/react';
import { useCallback } from 'react';
import { FaPlus } from 'react-icons/fa';
-import { useAppDispatch } from '../../../../app/store/storeHooks';
-import { boardCreated } from '../../../../services/thunks/board';
+import { useCreateBoardMutation } from 'services/apiSlice';
+
+const DEFAULT_BOARD_NAME = 'My Board';
const AddBoardButton = () => {
- const dispatch = useAppDispatch();
+ const [createBoard, { isLoading }] = useCreateBoardMutation();
const handleCreateBoard = useCallback(() => {
- dispatch(boardCreated({ requestBody: 'My Board' }));
- }, [dispatch]);
+ createBoard(DEFAULT_BOARD_NAME);
+ }, [createBoard]);
return (
{
aspectRatio: '1/1',
}}
>
-
+ {isLoading ? (
+
+ ) : (
+
+ )}
New Board
diff --git a/invokeai/frontend/web/src/features/gallery/components/Boards/BoardsList.tsx b/invokeai/frontend/web/src/features/gallery/components/Boards/BoardsList.tsx
index 1f84d3be0e..be849e625e 100644
--- a/invokeai/frontend/web/src/features/gallery/components/Boards/BoardsList.tsx
+++ b/invokeai/frontend/web/src/features/gallery/components/Boards/BoardsList.tsx
@@ -25,6 +25,7 @@ import { searchBoardsSelector } from '../../store/boardSelectors';
import { useSelector } from 'react-redux';
import IAICollapse from '../../../../common/components/IAICollapse';
import { CloseIcon } from '@chakra-ui/icons';
+import { useListBoardsQuery } from 'services/apiSlice';
const selector = createSelector(
[selectBoardsAll, boardsSelector],
@@ -40,9 +41,17 @@ const selector = createSelector(
const BoardsList = () => {
const dispatch = useAppDispatch();
const { selectedBoard, searchText } = useAppSelector(selector);
- const filteredBoards = useSelector(searchBoardsSelector);
+ // const filteredBoards = useSelector(searchBoardsSelector);
const { isOpen, onToggle } = useDisclosure();
+ const { data } = useListBoardsQuery({ offset: 0, limit: 8 });
+
+ const filteredBoards = searchText
+ ? data?.items.filter((board) =>
+ board.board_name.toLowerCase().includes(searchText.toLowerCase())
+ )
+ : data.items;
+
const [searchMode, setSearchMode] = useState(false);
const handleBoardSearch = (searchTerm: string) => {
@@ -100,13 +109,14 @@ const BoardsList = () => {
>
)}
- {filteredBoards.map((board) => (
-
- ))}
+ {filteredBoards &&
+ filteredBoards.map((board) => (
+
+ ))}
>
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 fdde7528cb..7ae864f55b 100644
--- a/invokeai/frontend/web/src/features/gallery/components/Boards/HoverableBoard.tsx
+++ b/invokeai/frontend/web/src/features/gallery/components/Boards/HoverableBoard.tsx
@@ -26,6 +26,10 @@ import IAIDndImage from '../../../../common/components/IAIDndImage';
import { defaultSelectorOptions } from '../../../../app/store/util/defaultMemoizeOptions';
import { createSelector } from '@reduxjs/toolkit';
import { RootState } from '../../../../app/store/store';
+import {
+ useDeleteBoardMutation,
+ useUpdateBoardMutation,
+} from 'services/apiSlice';
const coverImageSelector = (imageName: string | undefined) =>
createSelector(
@@ -59,19 +63,20 @@ const HoverableBoard = memo(({ board, isSelected }: HoverableBoardProps) => {
dispatch(boardIdSelected(board_id));
}, [board_id, dispatch]);
- const handleDeleteBoard = useCallback(() => {
- dispatch(boardDeleted(board_id));
- }, [board_id, dispatch]);
+ const [updateBoard, { isLoading: isUpdateBoardLoading }] =
+ useUpdateBoardMutation();
+
+ const [deleteBoard, { isLoading: isDeleteBoardLoading }] =
+ useDeleteBoardMutation();
const handleUpdateBoardName = (newBoardName: string) => {
- dispatch(
- boardUpdated({
- boardId: board_id,
- requestBody: { board_name: newBoardName },
- })
- );
+ updateBoard({ board_id, changes: { board_name: newBoardName } });
};
+ const handleDeleteBoard = useCallback(() => {
+ deleteBoard(board_id);
+ }, [board_id, deleteBoard]);
+
const handleDrop = useCallback(
(droppedImage: ImageDTO) => {
if (droppedImage.board_id === board_id) {
diff --git a/invokeai/frontend/web/src/features/gallery/components/Boards/UpdateImageBoardModal.tsx b/invokeai/frontend/web/src/features/gallery/components/Boards/UpdateImageBoardModal.tsx
index 9136e23e03..edd4d215af 100644
--- a/invokeai/frontend/web/src/features/gallery/components/Boards/UpdateImageBoardModal.tsx
+++ b/invokeai/frontend/web/src/features/gallery/components/Boards/UpdateImageBoardModal.tsx
@@ -9,6 +9,7 @@ import {
Divider,
Flex,
Select,
+ Spinner,
Text,
} from '@chakra-ui/react';
import IAIButton from 'common/components/IAIButton';
@@ -19,9 +20,11 @@ import { useSelector } from 'react-redux';
import { selectBoardsAll } from '../../store/boardSlice';
import IAISelect from '../../../../common/components/IAISelect';
import IAIMantineSelect from 'common/components/IAIMantineSelect';
+import { useListAllBoardsQuery } from 'services/apiSlice';
const UpdateImageBoardModal = () => {
- const boards = useSelector(selectBoardsAll);
+ // const boards = useSelector(selectBoardsAll);
+ const { data: boards, isFetching } = useListAllBoardsQuery();
const { isOpen, onClose, handleAddToBoard, image } = useContext(
AddImageToBoardContext
);
@@ -29,9 +32,9 @@ const UpdateImageBoardModal = () => {
const cancelRef = useRef(null);
- const currentBoard = boards.filter(
+ const currentBoard = boards?.find(
(board) => board.board_id === image?.board_id
- )[0];
+ );
return (
{
{currentBoard.board_name} to
)}
- setSelectedBoard(v)}
- value={selectedBoard}
- data={boards.map((board) => ({
- label: board.board_name,
- value: board.board_id,
- }))}
- />
+ {isFetching ? (
+
+ ) : (
+ setSelectedBoard(v)}
+ value={selectedBoard}
+ data={(boards ?? []).map((board) => ({
+ label: board.board_name,
+ value: board.board_id,
+ }))}
+ />
+ )}
@@ -73,7 +80,9 @@ const UpdateImageBoardModal = () => {
isDisabled={!selectedBoard}
colorScheme="accent"
onClick={() => {
- if (selectedBoard) handleAddToBoard(selectedBoard);
+ if (selectedBoard) {
+ handleAddToBoard(selectedBoard);
+ }
}}
ml={3}
>
diff --git a/invokeai/frontend/web/src/features/gallery/components/CurrentImageButtons.tsx b/invokeai/frontend/web/src/features/gallery/components/CurrentImageButtons.tsx
index a5eaeb4c71..169a965be0 100644
--- a/invokeai/frontend/web/src/features/gallery/components/CurrentImageButtons.tsx
+++ b/invokeai/frontend/web/src/features/gallery/components/CurrentImageButtons.tsx
@@ -51,9 +51,12 @@ import { useAppToaster } from 'app/components/Toaster';
import { setInitialCanvasImage } from 'features/canvas/store/canvasSlice';
import { DeleteImageContext } from 'app/contexts/DeleteImageContext';
import { DeleteImageButton } from './DeleteImageModal';
+import { selectImagesById } from '../store/imagesSlice';
+import { RootState } from 'app/store/store';
const currentImageButtonsSelector = createSelector(
[
+ (state: RootState) => state,
systemSelector,
gallerySelector,
postprocessingSelector,
@@ -61,7 +64,7 @@ const currentImageButtonsSelector = createSelector(
lightboxSelector,
activeTabNameSelector,
],
- (system, gallery, postprocessing, ui, lightbox, activeTabName) => {
+ (state, system, gallery, postprocessing, ui, lightbox, activeTabName) => {
const {
isProcessing,
isConnected,
@@ -81,6 +84,8 @@ const currentImageButtonsSelector = createSelector(
shouldShowProgressInViewer,
} = ui;
+ const imageDTO = selectImagesById(state, gallery.selectedImage ?? '');
+
const { selectedImage } = gallery;
return {
@@ -97,10 +102,10 @@ const currentImageButtonsSelector = createSelector(
activeTabName,
isLightboxOpen,
shouldHidePreview,
- image: selectedImage,
- seed: selectedImage?.metadata?.seed,
- prompt: selectedImage?.metadata?.positive_conditioning,
- negativePrompt: selectedImage?.metadata?.negative_conditioning,
+ image: imageDTO,
+ seed: imageDTO?.metadata?.seed,
+ prompt: imageDTO?.metadata?.positive_conditioning,
+ negativePrompt: imageDTO?.metadata?.negative_conditioning,
shouldShowProgressInViewer,
};
},
diff --git a/invokeai/frontend/web/src/features/gallery/components/CurrentImagePreview.tsx b/invokeai/frontend/web/src/features/gallery/components/CurrentImagePreview.tsx
index c591206a27..649cae7682 100644
--- a/invokeai/frontend/web/src/features/gallery/components/CurrentImagePreview.tsx
+++ b/invokeai/frontend/web/src/features/gallery/components/CurrentImagePreview.tsx
@@ -15,6 +15,8 @@ import { imageSelected } from '../store/gallerySlice';
import IAIDndImage from 'common/components/IAIDndImage';
import { ImageDTO } from 'services/api';
import { IAIImageFallback } from 'common/components/IAIImageFallback';
+import { RootState } from 'app/store/store';
+import { selectImagesById } from '../store/imagesSlice';
export const imagesSelector = createSelector(
[uiSelector, gallerySelector, systemSelector],
@@ -29,7 +31,7 @@ export const imagesSelector = createSelector(
return {
shouldShowImageDetails,
shouldHidePreview,
- image: selectedImage,
+ selectedImage,
progressImage,
shouldShowProgressInViewer,
shouldAntialiasProgressImage,
@@ -45,11 +47,16 @@ export const imagesSelector = createSelector(
const CurrentImagePreview = () => {
const {
shouldShowImageDetails,
- image,
+ selectedImage,
progressImage,
shouldShowProgressInViewer,
shouldAntialiasProgressImage,
} = useAppSelector(imagesSelector);
+
+ const image = useAppSelector((state: RootState) =>
+ selectImagesById(state, selectedImage ?? '')
+ );
+
const dispatch = useAppDispatch();
const handleDrop = useCallback(
@@ -57,7 +64,7 @@ const CurrentImagePreview = () => {
if (droppedImage.image_name === image?.image_name) {
return;
}
- dispatch(imageSelected(droppedImage));
+ dispatch(imageSelected(droppedImage.image_name));
},
[dispatch, image?.image_name]
);
diff --git a/invokeai/frontend/web/src/features/gallery/components/HoverableImage.tsx b/invokeai/frontend/web/src/features/gallery/components/HoverableImage.tsx
index b21c62785b..86ec3436f0 100644
--- a/invokeai/frontend/web/src/features/gallery/components/HoverableImage.tsx
+++ b/invokeai/frontend/web/src/features/gallery/components/HoverableImage.tsx
@@ -72,17 +72,10 @@ interface HoverableImageProps {
isSelected: boolean;
}
-const memoEqualityCheck = (
- prev: HoverableImageProps,
- next: HoverableImageProps
-) =>
- prev.image.image_name === next.image.image_name &&
- prev.isSelected === next.isSelected;
-
/**
* Gallery image component with delete/use all/use seed buttons on hover.
*/
-const HoverableImage = memo((props: HoverableImageProps) => {
+const HoverableImage = (props: HoverableImageProps) => {
const dispatch = useAppDispatch();
const {
activeTabName,
@@ -121,7 +114,7 @@ const HoverableImage = memo((props: HoverableImageProps) => {
const handleMouseOut = () => setIsHovered(false);
const handleSelectImage = useCallback(() => {
- dispatch(imageSelected(image));
+ dispatch(imageSelected(image.image_name));
}, [image, dispatch]);
// Recall parameters handlers
@@ -260,7 +253,7 @@ const HoverableImage = memo((props: HoverableImageProps) => {
)}
} onClickCapture={handleAddToBoard}>
- Add to Board
+ {image.board_id ? 'Change Board' : 'Add to Board'}