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 { 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 { useAppDispatch } from 'app/store/storeHooks';
import { PropsWithChildren, createContext, useCallback, 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 = {
@ -27,48 +11,6 @@ export type ImageUsage = {
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.

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 { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { defaultSelectorOptions } from 'app/store/util/defaultMemoizeOptions';
@ -14,19 +23,25 @@ import AddBoardButton from './AddBoardButton';
import AllImagesBoard from './AllImagesBoard';
import { searchBoardsSelector } from '../../store/boardSelectors';
import { useSelector } from 'react-redux';
import IAICollapse from '../../../../common/components/IAICollapse';
import { CloseIcon } from '@chakra-ui/icons';
const selector = createSelector(
[selectBoardsAll, boardsSelector],
(boards, boardsState) => {
return { boards, selectedBoardId: boardsState.selectedBoardId };
const selectedBoard = boards.find(
(board) => board.board_id === boardsState.selectedBoardId
);
return { selectedBoard, searchText: boardsState.searchText };
},
defaultSelectorOptions
);
const BoardsList = () => {
const dispatch = useAppDispatch();
const { selectedBoardId } = useAppSelector(selector);
const { selectedBoard, searchText } = useAppSelector(selector);
const filteredBoards = useSelector(searchBoardsSelector);
const { isOpen, onToggle } = useDisclosure();
const [searchMode, setSearchMode] = useState(false);
@ -34,52 +49,68 @@ const BoardsList = () => {
setSearchMode(searchTerm.length > 0);
dispatch(setBoardSearchText(searchTerm));
};
const clearBoardSearch = () => {
setSearchMode(false);
dispatch(setBoardSearchText(''));
};
return (
<OverlayScrollbarsComponent
defer
style={{ height: '100%', width: '100%' }}
options={{
scrollbars: {
visibility: 'auto',
autoHide: 'move',
autoHideDelay: 1300,
theme: 'os-theme-dark',
},
}}
>
<Box margin="1rem 0">
<Input
placeholder="Search Boards..."
onChange={(e) => {
handleBoardSearch(e.target.value);
<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
defer
style={{ height: '100%', width: '100%' }}
options={{
scrollbars: {
visibility: 'auto',
autoHide: 'move',
autoHideDelay: 1300,
theme: 'os-theme-dark',
},
}}
/>
</Box>
<Grid
className="list-container"
sx={{
gap: 2,
gridTemplateRows: '5rem 5rem',
gridAutoFlow: 'column dense',
gridAutoColumns: '4rem',
}}
>
{!searchMode && (
<>
<AddBoardButton />
<AllImagesBoard isSelected={selectedBoardId === null} />
</>
)}
{filteredBoards.map((board) => (
<HoverableBoard
key={board.board_id}
board={board}
isSelected={selectedBoardId === board.board_id}
/>
))}
</Grid>
</OverlayScrollbarsComponent>
>
<Grid
className="list-container"
sx={{
gap: 2,
gridTemplateRows: '5rem 5rem',
gridAutoFlow: 'column dense',
gridAutoColumns: '4rem',
}}
>
{!searchMode && (
<>
<AddBoardButton />
<AllImagesBoard isSelected={!selectedBoard} />
</>
)}
{filteredBoards.map((board) => (
<HoverableBoard
key={board.board_id}
board={board}
isSelected={selectedBoard?.board_id === board.board_id}
/>
))}
</Grid>
</OverlayScrollbarsComponent>
</>
</IAICollapse>
);
};

View File

@ -4,19 +4,34 @@ import {
EditableInput,
EditablePreview,
Flex,
Icon,
Image,
MenuItem,
MenuList,
} from '@chakra-ui/react';
import { useAppDispatch } from 'app/store/storeHooks';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
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 { BoardDTO } from 'services/api';
import { BoardDTO, ImageDTO } from 'services/api';
import { IAIImageFallback } from 'common/components/IAIImageFallback';
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 {
board: BoardDTO;
@ -25,6 +40,7 @@ interface HoverableBoardProps {
const HoverableBoard = memo(({ board, isSelected }: HoverableBoardProps) => {
const dispatch = useAppDispatch();
const { images } = useAppSelector(selector);
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 (
<Box sx={{ touchAction: 'none' }}>
<ContextMenu<HTMLDivElement>
@ -91,19 +124,12 @@ const HoverableBoard = memo(({ board, isSelected }: HoverableBoardProps) => {
overflow: 'hidden',
}}
>
{cover_image_url ? (
<Image
loading="lazy"
objectFit="cover"
draggable={false}
rounded="md"
src={cover_image_url}
fallback={<IAIImageFallback />}
sx={{}}
/>
) : (
<Icon boxSize={8} color="base.700" as={FaFolder} />
)}
<IAIDndImage
image={cover_image_url ? images[0] : undefined}
onDrop={handleDrop}
fallback={<IAIImageFallback sx={{ bg: 'none' }} />}
isUploadDisabled={true}
/>
</Flex>
<Editable

View File

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

View File

@ -240,39 +240,10 @@ const ImageGalleryContent = () => {
icon={<FaServer />}
/>
</ButtonGroup>
{selectedBoard && (
<Flex>
<Text>{selectedBoard.board_name}</Text>
</Flex>
)}
<Flex>
<Text>{selectedBoard ? selectedBoard.board_name : 'All Images'}</Text>
</Flex>
<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
triggerComponent={
<IAIIconButton

View File

@ -7,7 +7,7 @@ import type { ImageMetadata } from './ImageMetadata';
import type { ResourceOrigin } from './ResourceOrigin';
/**
* Deserialized image record, enriched for the frontend with URLs.
* Deserialized image record, enriched for the frontend.
*/
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.
*/
metadata?: ImageMetadata;
/**
* The id of the board the image belongs to, if one exists.
*/
board_id?: string;
};