[half-baked] adding image to board modal

This commit is contained in:
Mary Hipp 2023-06-15 18:40:29 -04:00 committed by psychedelicious
parent bd29e5e655
commit 2e41af2109
7 changed files with 274 additions and 10 deletions

View File

@ -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 = ({
</Portal>
</Grid>
<DeleteImageModal />
<UpdateImageBoardModal />
<Toaster />
<GlobalHotkeys />
</>

View File

@ -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 = ({
<ThemeLocaleProvider>
<ImageDndContext>
<DeleteImageContextProvider>
<App
config={config}
headerComponent={headerComponent}
setIsReady={setIsReady}
/>
<AddImageToBoardContextProvider>
<App
config={config}
headerComponent={headerComponent}
setIsReady={setIsReady}
/>
</AddImageToBoardContextProvider>
</DeleteImageContextProvider>
</ImageDndContext>
</ThemeLocaleProvider>

View File

@ -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<AddImageToBoardContextValue>({
isOpen: false,
onClose: () => undefined,
onClickAddToBoard: () => undefined,
handleAddToBoard: () => undefined,
});
type Props = PropsWithChildren;
export const AddImageToBoardContextProvider = (props: Props) => {
const [imageToMove, setImageToMove] = useState<ImageDTO>();
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 (
<AddImageToBoardContext.Provider
value={{
isOpen,
image: imageToMove,
onClose: closeAndClearImageToDelete,
onClickAddToBoard,
handleAddToBoard,
}}
>
{props.children}
</AddImageToBoardContext.Provider>
);
};

View File

@ -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<string | undefined>(
undefined
);
const { isOpen, onClose, handleAddToBoard, image } = useContext(
AddImageToBoardContext
);
const cancelRef = useRef<HTMLButtonElement>(null);
const currentBoard = boards.filter(
(board) => board.board_id === image?.board_id
)[0];
return (
<AlertDialog
isOpen={isOpen}
leastDestructiveRef={cancelRef}
onClose={onClose}
isCentered
>
<AlertDialogOverlay>
<AlertDialogContent>
<AlertDialogHeader fontSize="lg" fontWeight="bold">
Move Image to Board
</AlertDialogHeader>
<AlertDialogBody>
<Box>
<Flex direction="column" gap={3}>
<Text>
Moving this image to a board will remove it from its existing
board.
</Text>
<IAISelect
placeholder="Select Board"
onChange={(e) => setSelectedBoard(e.target.value)}
validValues={boards.map((board) => board.board_name)}
/>
</Flex>
</Box>
</AlertDialogBody>
<AlertDialogFooter>
<IAIButton onClick={onClose}>Cancel</IAIButton>
<IAIButton
isDisabled={!selectedBoard}
colorScheme="accent"
onClick={() => {
if (selectedBoard) handleAddToBoard(selectedBoard);
}}
ml={3}
>
Add to Board
</IAIButton>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialogOverlay>
</AlertDialog>
);
};
export default memo(UpdateImageBoardModal);

View File

@ -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')}
</MenuItem>
)}
<MenuItem icon={<FaFolder />} onClickCapture={handleAddToFolder}>
Add to Folder
<MenuItem icon={<FaFolder />} onClickCapture={handleAddToBoard}>
Add to Board
</MenuItem>
<MenuItem
sx={{ color: 'error.300' }}

View File

@ -27,6 +27,7 @@ type AdditionalBoardsState = {
isLoading: boolean;
selectedBoardId: EntityId | null;
searchText?: string;
updateBoardModalOpen: boolean;
};
export const initialBoardsState =
@ -36,6 +37,7 @@ export const initialBoardsState =
total: 0,
isLoading: false,
selectedBoardId: null,
updateBoardModalOpen: false,
});
export type BoardsState = typeof initialBoardsState;
@ -59,6 +61,9 @@ const boardsSlice = createSlice({
setBoardSearchText: (state, action: PayloadAction<string>) => {
state.searchText = action.payload;
},
setUpdateBoardModalOpen: (state, action: PayloadAction<boolean>) => {
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;

View File

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