feat(ui): wip boards via rtk-query

This commit is contained in:
psychedelicious 2023-06-21 00:07:24 +10:00
parent 661a94b3de
commit cfda128e06
20 changed files with 356 additions and 129 deletions

View File

@ -3,6 +3,7 @@ import { useAppDispatch } from 'app/store/storeHooks';
import { PropsWithChildren, createContext, useCallback, useState } from 'react'; import { PropsWithChildren, createContext, useCallback, useState } from 'react';
import { ImageDTO } from 'services/api'; import { ImageDTO } from 'services/api';
import { imageAddedToBoard } from '../../services/thunks/board'; import { imageAddedToBoard } from '../../services/thunks/board';
import { useAddImageToBoardMutation } from 'services/apiSlice';
export type ImageUsage = { export type ImageUsage = {
isInitialImage: boolean; isInitialImage: boolean;
@ -43,6 +44,8 @@ export const AddImageToBoardContextProvider = (props: Props) => {
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
const { isOpen, onOpen, onClose } = useDisclosure(); const { isOpen, onOpen, onClose } = useDisclosure();
const [addImageToBoard, result] = useAddImageToBoardMutation();
// Clean up after deleting or dismissing the modal // Clean up after deleting or dismissing the modal
const closeAndClearImageToDelete = useCallback(() => { const closeAndClearImageToDelete = useCallback(() => {
setImageToMove(undefined); setImageToMove(undefined);
@ -63,18 +66,14 @@ export const AddImageToBoardContextProvider = (props: Props) => {
const handleAddToBoard = useCallback( const handleAddToBoard = useCallback(
(boardId: string) => { (boardId: string) => {
if (imageToMove) { if (imageToMove) {
dispatch( addImageToBoard({
imageAddedToBoard({ board_id: boardId,
requestBody: { image_name: imageToMove.image_name,
board_id: boardId, });
image_name: imageToMove.image_name,
},
})
);
closeAndClearImageToDelete(); closeAndClearImageToDelete();
} }
}, },
[closeAndClearImageToDelete, dispatch, imageToMove] [addImageToBoard, closeAndClearImageToDelete, imageToMove]
); );
return ( return (

View File

@ -73,6 +73,10 @@ import { addImageCategoriesChangedListener } from './listeners/imageCategoriesCh
import { addControlNetImageProcessedListener } from './listeners/controlNetImageProcessed'; import { addControlNetImageProcessedListener } from './listeners/controlNetImageProcessed';
import { addControlNetAutoProcessListener } from './listeners/controlNetAutoProcess'; import { addControlNetAutoProcessListener } from './listeners/controlNetAutoProcess';
import { addUpdateImageUrlsOnConnectListener } from './listeners/updateImageUrlsOnConnect'; import { addUpdateImageUrlsOnConnectListener } from './listeners/updateImageUrlsOnConnect';
import {
addImageAddedToBoardFulfilledListener,
addImageAddedToBoardRejectedListener,
} from './listeners/imageAddedToBoard';
export const listenerMiddleware = createListenerMiddleware(); export const listenerMiddleware = createListenerMiddleware();
@ -183,3 +187,7 @@ addControlNetAutoProcessListener();
// Update image URLs on connect // Update image URLs on connect
addUpdateImageUrlsOnConnectListener(); addUpdateImageUrlsOnConnectListener();
// Boards
addImageAddedToBoardFulfilledListener();
addImageAddedToBoardRejectedListener();

View File

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

View File

@ -13,6 +13,7 @@ import { resetCanvas } from 'features/canvas/store/canvasSlice';
import { controlNetReset } from 'features/controlNet/store/controlNetSlice'; import { controlNetReset } from 'features/controlNet/store/controlNetSlice';
import { clearInitialImage } from 'features/parameters/store/generationSlice'; import { clearInitialImage } from 'features/parameters/store/generationSlice';
import { nodeEditorReset } from 'features/nodes/store/nodesSlice'; import { nodeEditorReset } from 'features/nodes/store/nodesSlice';
import { api } from 'services/apiSlice';
const moduleLog = log.child({ namespace: 'addRequestedImageDeletionListener' }); const moduleLog = log.child({ namespace: 'addRequestedImageDeletionListener' });
@ -22,7 +23,7 @@ const moduleLog = log.child({ namespace: 'addRequestedImageDeletionListener' });
export const addRequestedImageDeletionListener = () => { export const addRequestedImageDeletionListener = () => {
startAppListening({ startAppListening({
actionCreator: requestedImageDeletion, actionCreator: requestedImageDeletion,
effect: (action, { dispatch, getState }) => { effect: async (action, { dispatch, getState, condition }) => {
const { image, imageUsage } = action.payload; const { image, imageUsage } = action.payload;
const { image_name } = image; const { image_name } = image;
@ -30,7 +31,7 @@ export const addRequestedImageDeletionListener = () => {
const state = getState(); const state = getState();
const selectedImage = state.gallery.selectedImage; const selectedImage = state.gallery.selectedImage;
if (selectedImage && selectedImage.image_name === image_name) { if (selectedImage && selectedImage === image_name) {
const ids = selectImagesIds(state); const ids = selectImagesIds(state);
const entities = selectImagesEntities(state); const entities = selectImagesEntities(state);
@ -51,7 +52,7 @@ export const addRequestedImageDeletionListener = () => {
const newSelectedImage = entities[newSelectedImageId]; const newSelectedImage = entities[newSelectedImageId];
if (newSelectedImageId) { if (newSelectedImageId) {
dispatch(imageSelected(newSelectedImage)); dispatch(imageSelected(newSelectedImageId));
} else { } else {
dispatch(imageSelected()); dispatch(imageSelected());
} }
@ -79,7 +80,19 @@ export const addRequestedImageDeletionListener = () => {
dispatch(imageRemoved(image_name)); dispatch(imageRemoved(image_name));
// Delete from server // 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 }])
);
}
}, },
}); });
}; };

View File

@ -33,6 +33,7 @@ import { actionsDenylist } from './middleware/devtools/actionsDenylist';
import { serialize } from './enhancers/reduxRemember/serialize'; import { serialize } from './enhancers/reduxRemember/serialize';
import { unserialize } from './enhancers/reduxRemember/unserialize'; import { unserialize } from './enhancers/reduxRemember/unserialize';
import { LOCALSTORAGE_PREFIX } from './constants'; import { LOCALSTORAGE_PREFIX } from './constants';
import { api } from 'services/apiSlice';
const allReducers = { const allReducers = {
canvas: canvasReducer, canvas: canvasReducer,
@ -49,6 +50,7 @@ const allReducers = {
images: imagesReducer, images: imagesReducer,
controlNet: controlNetReducer, controlNet: controlNetReducer,
boards: boardsReducer, boards: boardsReducer,
[api.reducerPath]: api.reducer,
// session: sessionReducer, // session: sessionReducer,
}; };
@ -87,6 +89,7 @@ export const store = configureStore({
immutableCheck: false, immutableCheck: false,
serializableCheck: false, serializableCheck: false,
}) })
.concat(api.middleware)
.concat(dynamicMiddlewares) .concat(dynamicMiddlewares)
.prepend(listenerMiddleware.middleware), .prepend(listenerMiddleware.middleware),
devTools: { devTools: {

View File

@ -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 { useCallback } from 'react';
import { FaPlus } from 'react-icons/fa'; import { FaPlus } from 'react-icons/fa';
import { useAppDispatch } from '../../../../app/store/storeHooks'; import { useCreateBoardMutation } from 'services/apiSlice';
import { boardCreated } from '../../../../services/thunks/board';
const DEFAULT_BOARD_NAME = 'My Board';
const AddBoardButton = () => { const AddBoardButton = () => {
const dispatch = useAppDispatch(); const [createBoard, { isLoading }] = useCreateBoardMutation();
const handleCreateBoard = useCallback(() => { const handleCreateBoard = useCallback(() => {
dispatch(boardCreated({ requestBody: 'My Board' })); createBoard(DEFAULT_BOARD_NAME);
}, [dispatch]); }, [createBoard]);
return ( return (
<Flex <Flex
onClick={handleCreateBoard} onClick={isLoading ? undefined : handleCreateBoard}
sx={{ sx={{
flexDir: 'column', flexDir: 'column',
justifyContent: 'space-between', justifyContent: 'space-between',
@ -36,7 +37,11 @@ const AddBoardButton = () => {
aspectRatio: '1/1', aspectRatio: '1/1',
}} }}
> >
<Icon boxSize={8} color="base.700" as={FaPlus} /> {isLoading ? (
<Spinner />
) : (
<Icon boxSize={8} color="base.700" as={FaPlus} />
)}
</Flex> </Flex>
<Text sx={{ color: 'base.200', fontSize: 'xs' }}>New Board</Text> <Text sx={{ color: 'base.200', fontSize: 'xs' }}>New Board</Text>
</Flex> </Flex>

View File

@ -25,6 +25,7 @@ import { searchBoardsSelector } from '../../store/boardSelectors';
import { useSelector } from 'react-redux'; import { useSelector } from 'react-redux';
import IAICollapse from '../../../../common/components/IAICollapse'; import IAICollapse from '../../../../common/components/IAICollapse';
import { CloseIcon } from '@chakra-ui/icons'; import { CloseIcon } from '@chakra-ui/icons';
import { useListBoardsQuery } from 'services/apiSlice';
const selector = createSelector( const selector = createSelector(
[selectBoardsAll, boardsSelector], [selectBoardsAll, boardsSelector],
@ -40,9 +41,17 @@ const selector = createSelector(
const BoardsList = () => { const BoardsList = () => {
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
const { selectedBoard, searchText } = useAppSelector(selector); const { selectedBoard, searchText } = useAppSelector(selector);
const filteredBoards = useSelector(searchBoardsSelector); // const filteredBoards = useSelector(searchBoardsSelector);
const { isOpen, onToggle } = useDisclosure(); 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 [searchMode, setSearchMode] = useState(false);
const handleBoardSearch = (searchTerm: string) => { const handleBoardSearch = (searchTerm: string) => {
@ -100,13 +109,14 @@ const BoardsList = () => {
<AllImagesBoard isSelected={!selectedBoard} /> <AllImagesBoard isSelected={!selectedBoard} />
</> </>
)} )}
{filteredBoards.map((board) => ( {filteredBoards &&
<HoverableBoard filteredBoards.map((board) => (
key={board.board_id} <HoverableBoard
board={board} key={board.board_id}
isSelected={selectedBoard?.board_id === board.board_id} board={board}
/> isSelected={selectedBoard?.board_id === board.board_id}
))} />
))}
</Grid> </Grid>
</OverlayScrollbarsComponent> </OverlayScrollbarsComponent>
</> </>

View File

@ -26,6 +26,10 @@ import IAIDndImage from '../../../../common/components/IAIDndImage';
import { defaultSelectorOptions } from '../../../../app/store/util/defaultMemoizeOptions'; import { defaultSelectorOptions } from '../../../../app/store/util/defaultMemoizeOptions';
import { createSelector } from '@reduxjs/toolkit'; import { createSelector } from '@reduxjs/toolkit';
import { RootState } from '../../../../app/store/store'; import { RootState } from '../../../../app/store/store';
import {
useDeleteBoardMutation,
useUpdateBoardMutation,
} from 'services/apiSlice';
const coverImageSelector = (imageName: string | undefined) => const coverImageSelector = (imageName: string | undefined) =>
createSelector( createSelector(
@ -59,19 +63,20 @@ const HoverableBoard = memo(({ board, isSelected }: HoverableBoardProps) => {
dispatch(boardIdSelected(board_id)); dispatch(boardIdSelected(board_id));
}, [board_id, dispatch]); }, [board_id, dispatch]);
const handleDeleteBoard = useCallback(() => { const [updateBoard, { isLoading: isUpdateBoardLoading }] =
dispatch(boardDeleted(board_id)); useUpdateBoardMutation();
}, [board_id, dispatch]);
const [deleteBoard, { isLoading: isDeleteBoardLoading }] =
useDeleteBoardMutation();
const handleUpdateBoardName = (newBoardName: string) => { const handleUpdateBoardName = (newBoardName: string) => {
dispatch( updateBoard({ board_id, changes: { board_name: newBoardName } });
boardUpdated({
boardId: board_id,
requestBody: { board_name: newBoardName },
})
);
}; };
const handleDeleteBoard = useCallback(() => {
deleteBoard(board_id);
}, [board_id, deleteBoard]);
const handleDrop = useCallback( const handleDrop = useCallback(
(droppedImage: ImageDTO) => { (droppedImage: ImageDTO) => {
if (droppedImage.board_id === board_id) { if (droppedImage.board_id === board_id) {

View File

@ -9,6 +9,7 @@ import {
Divider, Divider,
Flex, Flex,
Select, Select,
Spinner,
Text, Text,
} from '@chakra-ui/react'; } from '@chakra-ui/react';
import IAIButton from 'common/components/IAIButton'; import IAIButton from 'common/components/IAIButton';
@ -19,9 +20,11 @@ import { useSelector } from 'react-redux';
import { selectBoardsAll } from '../../store/boardSlice'; import { selectBoardsAll } from '../../store/boardSlice';
import IAISelect from '../../../../common/components/IAISelect'; import IAISelect from '../../../../common/components/IAISelect';
import IAIMantineSelect from 'common/components/IAIMantineSelect'; import IAIMantineSelect from 'common/components/IAIMantineSelect';
import { useListAllBoardsQuery } from 'services/apiSlice';
const UpdateImageBoardModal = () => { const UpdateImageBoardModal = () => {
const boards = useSelector(selectBoardsAll); // const boards = useSelector(selectBoardsAll);
const { data: boards, isFetching } = useListAllBoardsQuery();
const { isOpen, onClose, handleAddToBoard, image } = useContext( const { isOpen, onClose, handleAddToBoard, image } = useContext(
AddImageToBoardContext AddImageToBoardContext
); );
@ -29,9 +32,9 @@ const UpdateImageBoardModal = () => {
const cancelRef = useRef<HTMLButtonElement>(null); const cancelRef = useRef<HTMLButtonElement>(null);
const currentBoard = boards.filter( const currentBoard = boards?.find(
(board) => board.board_id === image?.board_id (board) => board.board_id === image?.board_id
)[0]; );
return ( return (
<AlertDialog <AlertDialog
@ -55,15 +58,19 @@ const UpdateImageBoardModal = () => {
<strong>{currentBoard.board_name}</strong> to <strong>{currentBoard.board_name}</strong> to
</Text> </Text>
)} )}
<IAIMantineSelect {isFetching ? (
placeholder="Select Board" <Spinner />
onChange={(v) => setSelectedBoard(v)} ) : (
value={selectedBoard} <IAIMantineSelect
data={boards.map((board) => ({ placeholder="Select Board"
label: board.board_name, onChange={(v) => setSelectedBoard(v)}
value: board.board_id, value={selectedBoard}
}))} data={(boards ?? []).map((board) => ({
/> label: board.board_name,
value: board.board_id,
}))}
/>
)}
</Flex> </Flex>
</Box> </Box>
</AlertDialogBody> </AlertDialogBody>
@ -73,7 +80,9 @@ const UpdateImageBoardModal = () => {
isDisabled={!selectedBoard} isDisabled={!selectedBoard}
colorScheme="accent" colorScheme="accent"
onClick={() => { onClick={() => {
if (selectedBoard) handleAddToBoard(selectedBoard); if (selectedBoard) {
handleAddToBoard(selectedBoard);
}
}} }}
ml={3} ml={3}
> >

View File

@ -51,9 +51,12 @@ import { useAppToaster } from 'app/components/Toaster';
import { setInitialCanvasImage } from 'features/canvas/store/canvasSlice'; import { setInitialCanvasImage } from 'features/canvas/store/canvasSlice';
import { DeleteImageContext } from 'app/contexts/DeleteImageContext'; import { DeleteImageContext } from 'app/contexts/DeleteImageContext';
import { DeleteImageButton } from './DeleteImageModal'; import { DeleteImageButton } from './DeleteImageModal';
import { selectImagesById } from '../store/imagesSlice';
import { RootState } from 'app/store/store';
const currentImageButtonsSelector = createSelector( const currentImageButtonsSelector = createSelector(
[ [
(state: RootState) => state,
systemSelector, systemSelector,
gallerySelector, gallerySelector,
postprocessingSelector, postprocessingSelector,
@ -61,7 +64,7 @@ const currentImageButtonsSelector = createSelector(
lightboxSelector, lightboxSelector,
activeTabNameSelector, activeTabNameSelector,
], ],
(system, gallery, postprocessing, ui, lightbox, activeTabName) => { (state, system, gallery, postprocessing, ui, lightbox, activeTabName) => {
const { const {
isProcessing, isProcessing,
isConnected, isConnected,
@ -81,6 +84,8 @@ const currentImageButtonsSelector = createSelector(
shouldShowProgressInViewer, shouldShowProgressInViewer,
} = ui; } = ui;
const imageDTO = selectImagesById(state, gallery.selectedImage ?? '');
const { selectedImage } = gallery; const { selectedImage } = gallery;
return { return {
@ -97,10 +102,10 @@ const currentImageButtonsSelector = createSelector(
activeTabName, activeTabName,
isLightboxOpen, isLightboxOpen,
shouldHidePreview, shouldHidePreview,
image: selectedImage, image: imageDTO,
seed: selectedImage?.metadata?.seed, seed: imageDTO?.metadata?.seed,
prompt: selectedImage?.metadata?.positive_conditioning, prompt: imageDTO?.metadata?.positive_conditioning,
negativePrompt: selectedImage?.metadata?.negative_conditioning, negativePrompt: imageDTO?.metadata?.negative_conditioning,
shouldShowProgressInViewer, shouldShowProgressInViewer,
}; };
}, },

View File

@ -15,6 +15,8 @@ import { imageSelected } from '../store/gallerySlice';
import IAIDndImage from 'common/components/IAIDndImage'; import IAIDndImage from 'common/components/IAIDndImage';
import { ImageDTO } from 'services/api'; import { ImageDTO } from 'services/api';
import { IAIImageFallback } from 'common/components/IAIImageFallback'; import { IAIImageFallback } from 'common/components/IAIImageFallback';
import { RootState } from 'app/store/store';
import { selectImagesById } from '../store/imagesSlice';
export const imagesSelector = createSelector( export const imagesSelector = createSelector(
[uiSelector, gallerySelector, systemSelector], [uiSelector, gallerySelector, systemSelector],
@ -29,7 +31,7 @@ export const imagesSelector = createSelector(
return { return {
shouldShowImageDetails, shouldShowImageDetails,
shouldHidePreview, shouldHidePreview,
image: selectedImage, selectedImage,
progressImage, progressImage,
shouldShowProgressInViewer, shouldShowProgressInViewer,
shouldAntialiasProgressImage, shouldAntialiasProgressImage,
@ -45,11 +47,16 @@ export const imagesSelector = createSelector(
const CurrentImagePreview = () => { const CurrentImagePreview = () => {
const { const {
shouldShowImageDetails, shouldShowImageDetails,
image, selectedImage,
progressImage, progressImage,
shouldShowProgressInViewer, shouldShowProgressInViewer,
shouldAntialiasProgressImage, shouldAntialiasProgressImage,
} = useAppSelector(imagesSelector); } = useAppSelector(imagesSelector);
const image = useAppSelector((state: RootState) =>
selectImagesById(state, selectedImage ?? '')
);
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
const handleDrop = useCallback( const handleDrop = useCallback(
@ -57,7 +64,7 @@ const CurrentImagePreview = () => {
if (droppedImage.image_name === image?.image_name) { if (droppedImage.image_name === image?.image_name) {
return; return;
} }
dispatch(imageSelected(droppedImage)); dispatch(imageSelected(droppedImage.image_name));
}, },
[dispatch, image?.image_name] [dispatch, image?.image_name]
); );

View File

@ -72,17 +72,10 @@ interface HoverableImageProps {
isSelected: boolean; 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. * 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 dispatch = useAppDispatch();
const { const {
activeTabName, activeTabName,
@ -121,7 +114,7 @@ const HoverableImage = memo((props: HoverableImageProps) => {
const handleMouseOut = () => setIsHovered(false); const handleMouseOut = () => setIsHovered(false);
const handleSelectImage = useCallback(() => { const handleSelectImage = useCallback(() => {
dispatch(imageSelected(image)); dispatch(imageSelected(image.image_name));
}, [image, dispatch]); }, [image, dispatch]);
// Recall parameters handlers // Recall parameters handlers
@ -260,7 +253,7 @@ const HoverableImage = memo((props: HoverableImageProps) => {
</MenuItem> </MenuItem>
)} )}
<MenuItem icon={<FaFolder />} onClickCapture={handleAddToBoard}> <MenuItem icon={<FaFolder />} onClickCapture={handleAddToBoard}>
Add to Board {image.board_id ? 'Change Board' : 'Add to Board'}
</MenuItem> </MenuItem>
<MenuItem <MenuItem
sx={{ color: 'error.300' }} sx={{ color: 'error.300' }}
@ -357,8 +350,6 @@ const HoverableImage = memo((props: HoverableImageProps) => {
</ContextMenu> </ContextMenu>
</Box> </Box>
); );
}, memoEqualityCheck); };
HoverableImage.displayName = 'HoverableImage'; export default memo(HoverableImage);
export default HoverableImage;

View File

@ -201,12 +201,6 @@ const ImageGalleryContent = () => {
dispatch(setGalleryView('boards')); dispatch(setGalleryView('boards'));
}, [dispatch]); }, [dispatch]);
const [newBoardName, setNewBoardName] = useState('');
const handleCreateNewBoard = () => {
dispatch(boardCreated({ requestBody: newBoardName }));
};
return ( return (
<Flex <Flex
sx={{ sx={{
@ -323,9 +317,7 @@ const ImageGalleryContent = () => {
<HoverableImage <HoverableImage
key={`${item.image_name}-${item.thumbnail_url}`} key={`${item.image_name}-${item.thumbnail_url}`}
image={item} image={item}
isSelected={ isSelected={selectedImage === item?.image_name}
selectedImage?.image_name === item?.image_name
}
/> />
</Flex> </Flex>
)} )}
@ -344,9 +336,7 @@ const ImageGalleryContent = () => {
<HoverableImage <HoverableImage
key={`${item.image_name}-${item.thumbnail_url}`} key={`${item.image_name}-${item.thumbnail_url}`}
image={item} image={item}
isSelected={ isSelected={selectedImage === item?.image_name}
selectedImage?.image_name === item?.image_name
}
/> />
)} )}
/> />

View File

@ -93,19 +93,11 @@ type ImageMetadataViewerProps = {
image: ImageDTO; image: ImageDTO;
}; };
// TODO: I don't know if this is needed.
const memoEqualityCheck = (
prev: ImageMetadataViewerProps,
next: ImageMetadataViewerProps
) => prev.image.image_name === next.image.image_name;
// TODO: Show more interesting information in this component.
/** /**
* Image metadata viewer overlays currently selected image and provides * Image metadata viewer overlays currently selected image and provides
* access to any of its metadata for use in processing. * access to any of its metadata for use in processing.
*/ */
const ImageMetadataViewer = memo(({ image }: ImageMetadataViewerProps) => { const ImageMetadataViewer = ({ image }: ImageMetadataViewerProps) => {
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
const { const {
recallBothPrompts, recallBothPrompts,
@ -333,8 +325,6 @@ const ImageMetadataViewer = memo(({ image }: ImageMetadataViewerProps) => {
</Flex> </Flex>
</Flex> </Flex>
); );
}, memoEqualityCheck); };
ImageMetadataViewer.displayName = 'ImageMetadataViewer'; export default memo(ImageMetadataViewer);
export default ImageMetadataViewer;

View File

@ -42,7 +42,7 @@ export const nextPrevImageButtonsSelector = createSelector(
} }
const currentImageIndex = filteredImageIds.findIndex( const currentImageIndex = filteredImageIds.findIndex(
(i) => i === selectedImage.image_name (i) => i === selectedImage
); );
const nextImageIndex = clamp( const nextImageIndex = clamp(
@ -71,6 +71,8 @@ export const nextPrevImageButtonsSelector = createSelector(
!isNaN(currentImageIndex) && currentImageIndex === imagesLength - 1, !isNaN(currentImageIndex) && currentImageIndex === imagesLength - 1,
nextImage, nextImage,
prevImage, prevImage,
nextImageId,
prevImageId,
}; };
}, },
{ {
@ -84,7 +86,7 @@ const NextPrevImageButtons = () => {
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
const { t } = useTranslation(); const { t } = useTranslation();
const { isOnFirstImage, isOnLastImage, nextImage, prevImage } = const { isOnFirstImage, isOnLastImage, nextImageId, prevImageId } =
useAppSelector(nextPrevImageButtonsSelector); useAppSelector(nextPrevImageButtonsSelector);
const [shouldShowNextPrevButtons, setShouldShowNextPrevButtons] = const [shouldShowNextPrevButtons, setShouldShowNextPrevButtons] =
@ -99,19 +101,19 @@ const NextPrevImageButtons = () => {
}, []); }, []);
const handlePrevImage = useCallback(() => { const handlePrevImage = useCallback(() => {
dispatch(imageSelected(prevImage)); dispatch(imageSelected(prevImageId));
}, [dispatch, prevImage]); }, [dispatch, prevImageId]);
const handleNextImage = useCallback(() => { const handleNextImage = useCallback(() => {
dispatch(imageSelected(nextImage)); dispatch(imageSelected(nextImageId));
}, [dispatch, nextImage]); }, [dispatch, nextImageId]);
useHotkeys( useHotkeys(
'left', 'left',
() => { () => {
handlePrevImage(); handlePrevImage();
}, },
[prevImage] [prevImageId]
); );
useHotkeys( useHotkeys(
@ -119,7 +121,7 @@ const NextPrevImageButtons = () => {
() => { () => {
handleNextImage(); handleNextImage();
}, },
[nextImage] [nextImageId]
); );
return ( return (

View File

@ -7,7 +7,7 @@ import { imageUrlsReceived } from 'services/thunks/image';
type GalleryImageObjectFitType = 'contain' | 'cover'; type GalleryImageObjectFitType = 'contain' | 'cover';
export interface GalleryState { export interface GalleryState {
selectedImage?: ImageDTO; selectedImage?: string;
galleryImageMinimumWidth: number; galleryImageMinimumWidth: number;
galleryImageObjectFit: GalleryImageObjectFitType; galleryImageObjectFit: GalleryImageObjectFitType;
shouldAutoSwitchToNewImages: boolean; shouldAutoSwitchToNewImages: boolean;
@ -27,7 +27,7 @@ export const gallerySlice = createSlice({
name: 'gallery', name: 'gallery',
initialState: initialGalleryState, initialState: initialGalleryState,
reducers: { reducers: {
imageSelected: (state, action: PayloadAction<ImageDTO | undefined>) => { imageSelected: (state, action: PayloadAction<string | undefined>) => {
state.selectedImage = action.payload; state.selectedImage = action.payload;
// TODO: if the user selects an image, disable the auto switch? // TODO: if the user selects an image, disable the auto switch?
// state.shouldAutoSwitchToNewImages = false; // state.shouldAutoSwitchToNewImages = false;
@ -63,17 +63,17 @@ export const gallerySlice = createSlice({
state.shouldAutoSwitchToNewImages && state.shouldAutoSwitchToNewImages &&
action.payload.image_category === 'general' action.payload.image_category === 'general'
) { ) {
state.selectedImage = action.payload; state.selectedImage = action.payload.image_name;
} }
}); });
builder.addCase(imageUrlsReceived.fulfilled, (state, action) => { // builder.addCase(imageUrlsReceived.fulfilled, (state, action) => {
const { image_name, image_url, thumbnail_url } = action.payload; // const { image_name, image_url, thumbnail_url } = action.payload;
if (state.selectedImage?.image_name === image_name) { // if (state.selectedImage?.image_name === image_name) {
state.selectedImage.image_url = image_url; // state.selectedImage.image_url = image_url;
state.selectedImage.thumbnail_url = thumbnail_url; // state.selectedImage.thumbnail_url = thumbnail_url;
} // }
}); // });
}, },
}); });

View File

@ -23,13 +23,9 @@ export type BoardDTO = {
*/ */
updated_at: string; updated_at: string;
/** /**
* The name of the cover image of the board. * The name of the board's cover image.
*/ */
cover_image_name?: string; cover_image_name?: string;
/**
* The URL of the thumbnail of the board's cover image.
*/
cover_image_url?: string;
/** /**
* The number of images in the board. * The number of images in the board.
*/ */

View File

@ -17,13 +17,18 @@ export class BoardsService {
/** /**
* List Boards * List Boards
* Gets a list of boards * Gets a list of boards
* @returns OffsetPaginatedResults_BoardDTO_ Successful Response * @returns any Successful Response
* @throws ApiError * @throws ApiError
*/ */
public static listBoards({ public static listBoards({
all,
offset, offset,
limit = 10, limit,
}: { }: {
/**
* Whether to list all boards
*/
all?: boolean,
/** /**
* The page offset * The page offset
*/ */
@ -32,11 +37,12 @@ export class BoardsService {
* The number of boards per page * The number of boards per page
*/ */
limit?: number, limit?: number,
}): CancelablePromise<OffsetPaginatedResults_BoardDTO_> { }): CancelablePromise<(OffsetPaginatedResults_BoardDTO_ | Array<BoardDTO>)> {
return __request(OpenAPI, { return __request(OpenAPI, {
method: 'GET', method: 'GET',
url: '/api/v1/boards/', url: '/api/v1/boards/',
query: { query: {
'all': all,
'offset': offset, 'offset': offset,
'limit': limit, 'limit': limit,
}, },
@ -53,15 +59,19 @@ export class BoardsService {
* @throws ApiError * @throws ApiError
*/ */
public static createBoard({ public static createBoard({
requestBody, boardName,
}: { }: {
requestBody: string, /**
* The name of the board to create
*/
boardName: string,
}): CancelablePromise<BoardDTO> { }): CancelablePromise<BoardDTO> {
return __request(OpenAPI, { return __request(OpenAPI, {
method: 'POST', method: 'POST',
url: '/api/v1/boards/', url: '/api/v1/boards/',
body: requestBody, query: {
mediaType: 'application/json', 'board_name': boardName,
},
errors: { errors: {
422: `Validation Error`, 422: `Validation Error`,
}, },

View File

@ -0,0 +1,144 @@
import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react';
import { BoardDTO } from './api/models/BoardDTO';
import { OffsetPaginatedResults_BoardDTO_ } from './api/models/OffsetPaginatedResults_BoardDTO_';
import { BoardChanges } from './api/models/BoardChanges';
import { OffsetPaginatedResults_ImageDTO_ } from './api/models/OffsetPaginatedResults_ImageDTO_';
type ListBoardsArg = { offset: number; limit: number };
type UpdateBoardArg = { board_id: string; changes: BoardChanges };
type AddImageToBoardArg = { board_id: string; image_name: string };
type RemoveImageFromBoardArg = { board_id: string; image_name: string };
type ListBoardImagesArg = { board_id: string; offset: number; limit: number };
export const api = createApi({
baseQuery: fetchBaseQuery({ baseUrl: 'http://localhost:5173/api/v1/' }),
reducerPath: 'api',
tagTypes: ['Board'],
endpoints: (build) => ({
/**
* Boards Queries
*/
listBoards: build.query<OffsetPaginatedResults_BoardDTO_, ListBoardsArg>({
query: (arg) => ({ url: 'boards/', params: arg }),
providesTags: (result, error, arg) => {
if (!result) {
// Provide the broad 'Board' tag until there is a response
return ['Board'];
}
// Provide the broad 'Board' tab, and individual tags for each board
return [
...result.items.map(({ board_id }) => ({
type: 'Board' as const,
id: board_id,
})),
'Board',
];
},
}),
listAllBoards: build.query<Array<BoardDTO>, void>({
query: () => ({
url: 'boards/',
params: { all: true },
}),
providesTags: (result, error, arg) => {
if (!result) {
// Provide the broad 'Board' tag until there is a response
return ['Board'];
}
// Provide the broad 'Board' tab, and individual tags for each board
return [
...result.map(({ board_id }) => ({
type: 'Board' as const,
id: board_id,
})),
'Board',
];
},
}),
/**
* Boards Mutations
*/
createBoard: build.mutation<BoardDTO, string>({
query: (board_name) => ({
url: `boards/`,
method: 'POST',
params: { board_name },
}),
invalidatesTags: ['Board'],
}),
updateBoard: build.mutation<BoardDTO, UpdateBoardArg>({
query: ({ board_id, changes }) => ({
url: `boards/${board_id}`,
method: 'PATCH',
body: changes,
}),
invalidatesTags: (result, error, arg) => [
{ type: 'Board', id: arg.board_id },
],
}),
deleteBoard: build.mutation<void, string>({
query: (board_id) => ({ url: `boards/${board_id}`, method: 'DELETE' }),
invalidatesTags: (result, error, arg) => [{ type: 'Board', id: arg }],
}),
/**
* Board Images Queries
*/
listBoardImages: build.query<
OffsetPaginatedResults_ImageDTO_,
ListBoardImagesArg
>({
query: ({ board_id, offset, limit }) => ({
url: `board_images/${board_id}`,
method: 'DELETE',
body: { offset, limit },
}),
}),
/**
* Board Images Mutations
*/
addImageToBoard: build.mutation<void, AddImageToBoardArg>({
query: ({ board_id, image_name }) => ({
url: `board_images/`,
method: 'POST',
body: { board_id, image_name },
}),
invalidatesTags: ['Board'],
// invalidatesTags: (result, error, arg) => [
// { type: 'Board', id: arg.board_id },
// ],
}),
removeImageFromBoard: build.mutation<void, RemoveImageFromBoardArg>({
query: ({ board_id, image_name }) => ({
url: `board_images/`,
method: 'DELETE',
body: { board_id, image_name },
}),
invalidatesTags: (result, error, arg) => [
{ type: 'Board', id: arg.board_id },
],
}),
}),
});
export const {
useListBoardsQuery,
useListAllBoardsQuery,
useCreateBoardMutation,
useUpdateBoardMutation,
useDeleteBoardMutation,
useAddImageToBoardMutation,
useRemoveImageFromBoardMutation,
useListBoardImagesQuery,
} = api;

View File

@ -42,7 +42,7 @@ export const boardUpdated = createAppAsyncThunk(
type ImageAddedToBoardArg = Parameters< type ImageAddedToBoardArg = Parameters<
(typeof BoardsService)['createBoardImage'] (typeof BoardsService)['createBoardImage']
>[0]; >[0]['requestBody'];
export const imageAddedToBoard = createAppAsyncThunk( export const imageAddedToBoard = createAppAsyncThunk(
'api/imageAddedToBoard', 'api/imageAddedToBoard',