From 2e41af210912258f62d87a15595ecbb02602dc7b Mon Sep 17 00:00:00 2001 From: Mary Hipp Date: Thu, 15 Jun 2023 18:40:29 -0400 Subject: [PATCH] [half-baked] adding image to board modal --- .../frontend/web/src/app/components/App.tsx | 2 + .../web/src/app/components/InvokeAIUI.tsx | 14 +- .../app/contexts/AddImageToBoardContext.tsx | 151 ++++++++++++++++++ .../Boards/UpdateImageBoardModal.tsx | 85 ++++++++++ .../gallery/components/HoverableImage.tsx | 14 +- .../src/features/gallery/store/boardSlice.ts | 6 + .../frontend/web/src/services/thunks/board.ts | 12 ++ 7 files changed, 274 insertions(+), 10 deletions(-) create mode 100644 invokeai/frontend/web/src/app/contexts/AddImageToBoardContext.tsx create mode 100644 invokeai/frontend/web/src/features/gallery/components/Boards/UpdateImageBoardModal.tsx diff --git a/invokeai/frontend/web/src/app/components/App.tsx b/invokeai/frontend/web/src/app/components/App.tsx index ddc6dace27..a11d8d048c 100644 --- a/invokeai/frontend/web/src/app/components/App.tsx +++ b/invokeai/frontend/web/src/app/components/App.tsx @@ -23,6 +23,7 @@ import GlobalHotkeys from './GlobalHotkeys'; import Toaster from './Toaster'; import DeleteImageModal from 'features/gallery/components/DeleteImageModal'; import { requestCanvasRescale } from 'features/canvas/store/thunks/requestCanvasScale'; +import UpdateImageBoardModal from '../../features/gallery/components/Boards/UpdateImageBoardModal'; const DEFAULT_CONFIG = {}; @@ -143,6 +144,7 @@ const App = ({ + diff --git a/invokeai/frontend/web/src/app/components/InvokeAIUI.tsx b/invokeai/frontend/web/src/app/components/InvokeAIUI.tsx index 0537d1de2a..141e62652d 100644 --- a/invokeai/frontend/web/src/app/components/InvokeAIUI.tsx +++ b/invokeai/frontend/web/src/app/components/InvokeAIUI.tsx @@ -21,6 +21,8 @@ import { DeleteImageContext, DeleteImageContextProvider, } from 'app/contexts/DeleteImageContext'; +import UpdateImageBoardModal from '../../features/gallery/components/Boards/UpdateImageBoardModal'; +import { AddImageToBoardContextProvider } from '../contexts/AddImageToBoardContext'; const App = lazy(() => import('./App')); const ThemeLocaleProvider = lazy(() => import('./ThemeLocaleProvider')); @@ -76,11 +78,13 @@ const InvokeAIUI = ({ - + + + diff --git a/invokeai/frontend/web/src/app/contexts/AddImageToBoardContext.tsx b/invokeai/frontend/web/src/app/contexts/AddImageToBoardContext.tsx new file mode 100644 index 0000000000..cf541dca01 --- /dev/null +++ b/invokeai/frontend/web/src/app/contexts/AddImageToBoardContext.tsx @@ -0,0 +1,151 @@ +import { useDisclosure } from '@chakra-ui/react'; +import { createSelector } from '@reduxjs/toolkit'; +import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; +import { defaultSelectorOptions } from 'app/store/util/defaultMemoizeOptions'; +import { requestedImageDeletion } from 'features/gallery/store/actions'; +import { systemSelector } from 'features/system/store/systemSelectors'; +import { + PropsWithChildren, + createContext, + useCallback, + useEffect, + useState, +} from 'react'; +import { ImageDTO } from 'services/api'; +import { RootState } from 'app/store/store'; +import { canvasSelector } from 'features/canvas/store/canvasSelectors'; +import { controlNetSelector } from 'features/controlNet/store/controlNetSlice'; +import { nodesSelecter } from 'features/nodes/store/nodesSlice'; +import { generationSelector } from 'features/parameters/store/generationSelectors'; +import { some } from 'lodash-es'; +import { imageAddedToBoard } from '../../services/thunks/board'; + +export type ImageUsage = { + isInitialImage: boolean; + isCanvasImage: boolean; + isNodesImage: boolean; + isControlNetImage: boolean; +}; + +export const selectImageUsage = createSelector( + [ + generationSelector, + canvasSelector, + nodesSelecter, + controlNetSelector, + (state: RootState, image_name?: string) => image_name, + ], + (generation, canvas, nodes, controlNet, image_name) => { + const isInitialImage = generation.initialImage?.image_name === image_name; + + const isCanvasImage = canvas.layerState.objects.some( + (obj) => obj.kind === 'image' && obj.image.image_name === image_name + ); + + const isNodesImage = nodes.nodes.some((node) => { + return some( + node.data.inputs, + (input) => + input.type === 'image' && input.value?.image_name === image_name + ); + }); + + const isControlNetImage = some( + controlNet.controlNets, + (c) => + c.controlImage?.image_name === image_name || + c.processedControlImage?.image_name === image_name + ); + + const imageUsage: ImageUsage = { + isInitialImage, + isCanvasImage, + isNodesImage, + isControlNetImage, + }; + + return imageUsage; + }, + defaultSelectorOptions +); + +type AddImageToBoardContextValue = { + /** + * Whether the move image dialog is open. + */ + isOpen: boolean; + /** + * Closes the move image dialog. + */ + onClose: () => void; + /** + * The image pending movement + */ + image?: ImageDTO; + onClickAddToBoard: (image: ImageDTO) => void; + handleAddToBoard: (boardId: string) => void; +}; + +export const AddImageToBoardContext = + createContext({ + isOpen: false, + onClose: () => undefined, + onClickAddToBoard: () => undefined, + handleAddToBoard: () => undefined, + }); + +type Props = PropsWithChildren; + +export const AddImageToBoardContextProvider = (props: Props) => { + const [imageToMove, setImageToMove] = useState(); + const dispatch = useAppDispatch(); + const { isOpen, onOpen, onClose } = useDisclosure(); + + // Clean up after deleting or dismissing the modal + const closeAndClearImageToDelete = useCallback(() => { + setImageToMove(undefined); + onClose(); + }, [onClose]); + + const onClickAddToBoard = useCallback( + (image?: ImageDTO) => { + if (!image) { + return; + } + setImageToMove(image); + onOpen(); + }, + [setImageToMove, onOpen] + ); + + const handleAddToBoard = useCallback( + (boardId: string) => { + if (imageToMove) { + dispatch( + imageAddedToBoard({ + requestBody: { + board_id: boardId, + image_name: imageToMove.image_name, + }, + }) + ); + closeAndClearImageToDelete(); + } + }, + [closeAndClearImageToDelete, dispatch, imageToMove] + ); + + return ( + + {props.children} + + ); +}; diff --git a/invokeai/frontend/web/src/features/gallery/components/Boards/UpdateImageBoardModal.tsx b/invokeai/frontend/web/src/features/gallery/components/Boards/UpdateImageBoardModal.tsx new file mode 100644 index 0000000000..8a94764ab1 --- /dev/null +++ b/invokeai/frontend/web/src/features/gallery/components/Boards/UpdateImageBoardModal.tsx @@ -0,0 +1,85 @@ +import { + AlertDialog, + AlertDialogBody, + AlertDialogContent, + AlertDialogFooter, + AlertDialogHeader, + AlertDialogOverlay, + Box, + Divider, + Flex, + Select, + Text, +} from '@chakra-ui/react'; +import IAIButton from 'common/components/IAIButton'; + +import { memo, useContext, useRef, useState } from 'react'; +import { AddImageToBoardContext } from '../../../../app/contexts/AddImageToBoardContext'; +import { useSelector } from 'react-redux'; +import { selectBoardsAll } from '../../store/boardSlice'; +import IAISelect from '../../../../common/components/IAISelect'; + +const UpdateImageBoardModal = () => { + const boards = useSelector(selectBoardsAll); + const [selectedBoard, setSelectedBoard] = useState( + undefined + ); + + const { isOpen, onClose, handleAddToBoard, image } = useContext( + AddImageToBoardContext + ); + + const cancelRef = useRef(null); + + const currentBoard = boards.filter( + (board) => board.board_id === image?.board_id + )[0]; + + return ( + + + + + Move Image to Board + + + + + + + Moving this image to a board will remove it from its existing + board. + + setSelectedBoard(e.target.value)} + validValues={boards.map((board) => board.board_name)} + /> + + + + + Cancel + { + if (selectedBoard) handleAddToBoard(selectedBoard); + }} + ml={3} + > + Add to Board + + + + + + ); +}; + +export default memo(UpdateImageBoardModal); diff --git a/invokeai/frontend/web/src/features/gallery/components/HoverableImage.tsx b/invokeai/frontend/web/src/features/gallery/components/HoverableImage.tsx index d52ba89d8f..b21c62785b 100644 --- a/invokeai/frontend/web/src/features/gallery/components/HoverableImage.tsx +++ b/invokeai/frontend/web/src/features/gallery/components/HoverableImage.tsx @@ -34,6 +34,9 @@ import { useAppToaster } from 'app/components/Toaster'; import { ImageDTO } from 'services/api'; import { useDraggable } from '@dnd-kit/core'; import { DeleteImageContext } from 'app/contexts/DeleteImageContext'; +import { imageAddedToBoard } from '../../../services/thunks/board'; +import { setUpdateBoardModalOpen } from '../store/boardSlice'; +import { AddImageToBoardContext } from '../../../app/contexts/AddImageToBoardContext'; export const selector = createSelector( [gallerySelector, systemSelector, lightboxSelector, activeTabNameSelector], @@ -100,6 +103,7 @@ const HoverableImage = memo((props: HoverableImageProps) => { const isCanvasEnabled = useFeatureStatus('unifiedCanvas').isFeatureEnabled; const { onDelete } = useContext(DeleteImageContext); + const { onClickAddToBoard } = useContext(AddImageToBoardContext); const handleDelete = useCallback(() => { onDelete(image); }, [image, onDelete]); @@ -175,9 +179,9 @@ const HoverableImage = memo((props: HoverableImageProps) => { // dispatch(setIsLightboxOpen(true)); }; - const handleAddToFolder = useCallback(() => { - // dispatch(addImageToFolder(image)); - }, []); + const handleAddToBoard = useCallback(() => { + onClickAddToBoard(image); + }, [image, onClickAddToBoard]); const handleOpenInNewTab = () => { window.open(image.image_url, '_blank'); @@ -255,8 +259,8 @@ const HoverableImage = memo((props: HoverableImageProps) => { {t('parameters.sendToUnifiedCanvas')} )} - } onClickCapture={handleAddToFolder}> - Add to Folder + } onClickCapture={handleAddToBoard}> + Add to Board ) => { state.searchText = action.payload; }, + setUpdateBoardModalOpen: (state, action: PayloadAction) => { + state.updateBoardModalOpen = action.payload; + }, }, extraReducers: (builder) => { builder.addCase(receivedBoards.pending, (state) => { @@ -105,6 +110,7 @@ export const { boardRemoved, boardIdSelected, setBoardSearchText, + setUpdateBoardModalOpen, } = boardsSlice.actions; export const boardsSelector = (state: RootState) => state.boards; diff --git a/invokeai/frontend/web/src/services/thunks/board.ts b/invokeai/frontend/web/src/services/thunks/board.ts index a536a3fdb0..4535081e47 100644 --- a/invokeai/frontend/web/src/services/thunks/board.ts +++ b/invokeai/frontend/web/src/services/thunks/board.ts @@ -39,3 +39,15 @@ export const boardUpdated = createAppAsyncThunk( return response; } ); + +type ImageAddedToBoardArg = Parameters< + (typeof BoardsService)['createBoardImage'] +>[0]; + +export const imageAddedToBoard = createAppAsyncThunk( + 'api/imageAddedToBoard', + async (arg: ImageAddedToBoardArg) => { + const response = await BoardsService.createBoardImage(arg); + return response; + } +);