drag and drop to move image to board, a bit of board list UI

This commit is contained in:
Mary Hipp 2023-06-16 13:06:59 -04:00 committed by psychedelicious
parent 95b9c8e505
commit f9f3c91a83
6 changed files with 139 additions and 164 deletions

View File

@ -1,23 +1,7 @@
import { useDisclosure } from '@chakra-ui/react'; import { useDisclosure } from '@chakra-ui/react';
import { createSelector } from '@reduxjs/toolkit'; import { useAppDispatch } from 'app/store/storeHooks';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import { PropsWithChildren, createContext, useCallback, useState } from 'react';
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 { 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'; import { imageAddedToBoard } from '../../services/thunks/board';
export type ImageUsage = { export type ImageUsage = {
@ -27,48 +11,6 @@ export type ImageUsage = {
isControlNetImage: 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 = { type AddImageToBoardContextValue = {
/** /**
* Whether the move image dialog is open. * Whether the move image dialog is open.

View File

@ -1,4 +1,13 @@
import { Box, Grid, Input, Spacer } from '@chakra-ui/react'; import {
Box,
Divider,
Grid,
Input,
InputGroup,
InputRightElement,
Spacer,
useDisclosure,
} from '@chakra-ui/react';
import { createSelector } from '@reduxjs/toolkit'; import { createSelector } from '@reduxjs/toolkit';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { defaultSelectorOptions } from 'app/store/util/defaultMemoizeOptions'; import { defaultSelectorOptions } from 'app/store/util/defaultMemoizeOptions';
@ -14,19 +23,25 @@ import AddBoardButton from './AddBoardButton';
import AllImagesBoard from './AllImagesBoard'; import AllImagesBoard from './AllImagesBoard';
import { searchBoardsSelector } from '../../store/boardSelectors'; import { searchBoardsSelector } from '../../store/boardSelectors';
import { useSelector } from 'react-redux'; import { useSelector } from 'react-redux';
import IAICollapse from '../../../../common/components/IAICollapse';
import { CloseIcon } from '@chakra-ui/icons';
const selector = createSelector( const selector = createSelector(
[selectBoardsAll, boardsSelector], [selectBoardsAll, boardsSelector],
(boards, boardsState) => { (boards, boardsState) => {
return { boards, selectedBoardId: boardsState.selectedBoardId }; const selectedBoard = boards.find(
(board) => board.board_id === boardsState.selectedBoardId
);
return { selectedBoard, searchText: boardsState.searchText };
}, },
defaultSelectorOptions defaultSelectorOptions
); );
const BoardsList = () => { const BoardsList = () => {
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
const { selectedBoardId } = useAppSelector(selector); const { selectedBoard, searchText } = useAppSelector(selector);
const filteredBoards = useSelector(searchBoardsSelector); const filteredBoards = useSelector(searchBoardsSelector);
const { isOpen, onToggle } = useDisclosure();
const [searchMode, setSearchMode] = useState(false); const [searchMode, setSearchMode] = useState(false);
@ -34,8 +49,30 @@ const BoardsList = () => {
setSearchMode(searchTerm.length > 0); setSearchMode(searchTerm.length > 0);
dispatch(setBoardSearchText(searchTerm)); dispatch(setBoardSearchText(searchTerm));
}; };
const clearBoardSearch = () => {
setSearchMode(false);
dispatch(setBoardSearchText(''));
};
return ( return (
<IAICollapse label="Select Board" isOpen={isOpen} onToggle={onToggle}>
<>
<Box marginBottom="1rem">
<InputGroup>
<Input
placeholder="Search Boards..."
value={searchText}
onChange={(e) => {
handleBoardSearch(e.target.value);
}}
/>
{searchText && searchText.length && (
<InputRightElement>
<CloseIcon onClick={clearBoardSearch} cursor="pointer" />
</InputRightElement>
)}
</InputGroup>
</Box>
<OverlayScrollbarsComponent <OverlayScrollbarsComponent
defer defer
style={{ height: '100%', width: '100%' }} style={{ height: '100%', width: '100%' }}
@ -48,14 +85,6 @@ const BoardsList = () => {
}, },
}} }}
> >
<Box margin="1rem 0">
<Input
placeholder="Search Boards..."
onChange={(e) => {
handleBoardSearch(e.target.value);
}}
/>
</Box>
<Grid <Grid
className="list-container" className="list-container"
sx={{ sx={{
@ -68,18 +97,20 @@ const BoardsList = () => {
{!searchMode && ( {!searchMode && (
<> <>
<AddBoardButton /> <AddBoardButton />
<AllImagesBoard isSelected={selectedBoardId === null} /> <AllImagesBoard isSelected={!selectedBoard} />
</> </>
)} )}
{filteredBoards.map((board) => ( {filteredBoards.map((board) => (
<HoverableBoard <HoverableBoard
key={board.board_id} key={board.board_id}
board={board} board={board}
isSelected={selectedBoardId === board.board_id} isSelected={selectedBoard?.board_id === board.board_id}
/> />
))} ))}
</Grid> </Grid>
</OverlayScrollbarsComponent> </OverlayScrollbarsComponent>
</>
</IAICollapse>
); );
}; };

View File

@ -4,19 +4,34 @@ import {
EditableInput, EditableInput,
EditablePreview, EditablePreview,
Flex, Flex,
Icon,
Image,
MenuItem, MenuItem,
MenuList, MenuList,
} from '@chakra-ui/react'; } from '@chakra-ui/react';
import { useAppDispatch } from 'app/store/storeHooks';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { memo, useCallback } from 'react'; import { memo, useCallback } from 'react';
import { FaFolder, FaTrash } from 'react-icons/fa'; import { FaTrash } from 'react-icons/fa';
import { ContextMenu } from 'chakra-ui-contextmenu'; import { ContextMenu } from 'chakra-ui-contextmenu';
import { BoardDTO } from 'services/api'; import { BoardDTO, ImageDTO } from 'services/api';
import { IAIImageFallback } from 'common/components/IAIImageFallback'; import { IAIImageFallback } from 'common/components/IAIImageFallback';
import { boardIdSelected } from 'features/gallery/store/boardSlice'; import { boardIdSelected } from 'features/gallery/store/boardSlice';
import { boardDeleted, boardUpdated } from '../../../../services/thunks/board'; import {
boardDeleted,
boardUpdated,
imageAddedToBoard,
} from '../../../../services/thunks/board';
import { selectImagesAll } from '../../store/imagesSlice';
import IAIDndImage from '../../../../common/components/IAIDndImage';
import { defaultSelectorOptions } from '../../../../app/store/util/defaultMemoizeOptions';
import { createSelector } from '@reduxjs/toolkit';
const selector = createSelector(
[selectImagesAll],
(images) => {
return { images };
},
defaultSelectorOptions
);
interface HoverableBoardProps { interface HoverableBoardProps {
board: BoardDTO; board: BoardDTO;
@ -25,6 +40,7 @@ interface HoverableBoardProps {
const HoverableBoard = memo(({ board, isSelected }: HoverableBoardProps) => { const HoverableBoard = memo(({ board, isSelected }: HoverableBoardProps) => {
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
const { images } = useAppSelector(selector);
const { board_name, board_id, cover_image_url } = board; const { board_name, board_id, cover_image_url } = board;
@ -45,6 +61,23 @@ const HoverableBoard = memo(({ board, isSelected }: HoverableBoardProps) => {
); );
}; };
const handleDrop = useCallback(
(droppedImage: ImageDTO) => {
if (droppedImage.board_id === board_id) {
return;
}
dispatch(
imageAddedToBoard({
requestBody: {
board_id,
image_name: droppedImage.image_name,
},
})
);
},
[board_id, dispatch]
);
return ( return (
<Box sx={{ touchAction: 'none' }}> <Box sx={{ touchAction: 'none' }}>
<ContextMenu<HTMLDivElement> <ContextMenu<HTMLDivElement>
@ -91,19 +124,12 @@ const HoverableBoard = memo(({ board, isSelected }: HoverableBoardProps) => {
overflow: 'hidden', overflow: 'hidden',
}} }}
> >
{cover_image_url ? ( <IAIDndImage
<Image image={cover_image_url ? images[0] : undefined}
loading="lazy" onDrop={handleDrop}
objectFit="cover" fallback={<IAIImageFallback sx={{ bg: 'none' }} />}
draggable={false} isUploadDisabled={true}
rounded="md"
src={cover_image_url}
fallback={<IAIImageFallback />}
sx={{}}
/> />
) : (
<Icon boxSize={8} color="base.700" as={FaFolder} />
)}
</Flex> </Flex>
<Editable <Editable

View File

@ -22,11 +22,10 @@ import IAIMantineSelect from 'common/components/IAIMantineSelect';
const UpdateImageBoardModal = () => { const UpdateImageBoardModal = () => {
const boards = useSelector(selectBoardsAll); const boards = useSelector(selectBoardsAll);
const [selectedBoard, setSelectedBoard] = useState<string | null>(null);
const { isOpen, onClose, handleAddToBoard, image } = useContext( const { isOpen, onClose, handleAddToBoard, image } = useContext(
AddImageToBoardContext AddImageToBoardContext
); );
const [selectedBoard, setSelectedBoard] = useState<string | null>();
const cancelRef = useRef<HTMLButtonElement>(null); const cancelRef = useRef<HTMLButtonElement>(null);
@ -50,10 +49,12 @@ const UpdateImageBoardModal = () => {
<AlertDialogBody> <AlertDialogBody>
<Box> <Box>
<Flex direction="column" gap={3}> <Flex direction="column" gap={3}>
{currentBoard && (
<Text> <Text>
Moving this image to a board will remove it from its existing Moving this image from{' '}
board. <strong>{currentBoard.board_name}</strong> to
</Text> </Text>
)}
<IAIMantineSelect <IAIMantineSelect
placeholder="Select Board" placeholder="Select Board"
onChange={(v) => setSelectedBoard(v)} onChange={(v) => setSelectedBoard(v)}

View File

@ -240,39 +240,10 @@ const ImageGalleryContent = () => {
icon={<FaServer />} icon={<FaServer />}
/> />
</ButtonGroup> </ButtonGroup>
{selectedBoard && (
<Flex> <Flex>
<Text>{selectedBoard.board_name}</Text> <Text>{selectedBoard ? selectedBoard.board_name : 'All Images'}</Text>
</Flex> </Flex>
)}
<Flex gap={2}> <Flex gap={2}>
{/* <IAIPopover
triggerComponent={
<IAIIconButton
tooltip="Add Board"
aria-label="Add Board"
size="sm"
icon={<FaPlus />}
/>
}
>
<Flex direction="column" gap={2}>
<IAIInput
label="Board Name"
placeholder="Board Name"
value={newBoardName}
onChange={(e) => setNewBoardName(e.target.value)}
/>
<IAIButton
size="sm"
onClick={handleCreateNewBoard}
disabled={true}
isLoading={false}
>
Create
</IAIButton>
</Flex>
</IAIPopover> */}
<IAIPopover <IAIPopover
triggerComponent={ triggerComponent={
<IAIIconButton <IAIIconButton

View File

@ -7,7 +7,7 @@ import type { ImageMetadata } from './ImageMetadata';
import type { ResourceOrigin } from './ResourceOrigin'; import type { ResourceOrigin } from './ResourceOrigin';
/** /**
* Deserialized image record, enriched for the frontend with URLs. * Deserialized image record, enriched for the frontend.
*/ */
export type ImageDTO = { export type ImageDTO = {
/** /**
@ -66,5 +66,9 @@ export type ImageDTO = {
* A limited subset of the image's generation metadata. Retrieve the image's session for full metadata. * A limited subset of the image's generation metadata. Retrieve the image's session for full metadata.
*/ */
metadata?: ImageMetadata; metadata?: ImageMetadata;
/**
* The id of the board the image belongs to, if one exists.
*/
board_id?: string;
}; };