mirror of
https://github.com/invoke-ai/InvokeAI
synced 2024-08-30 20:32:17 +00:00
feat(ui): refactor boards hierarchy
This commit is contained in:
parent
8fdc8a8da5
commit
0e3ca59e49
@ -156,14 +156,13 @@ export const addImageDroppedListener = () => {
|
|||||||
if (
|
if (
|
||||||
overData.actionType === 'MOVE_BOARD' &&
|
overData.actionType === 'MOVE_BOARD' &&
|
||||||
activeData.payloadType === 'IMAGE_DTO' &&
|
activeData.payloadType === 'IMAGE_DTO' &&
|
||||||
activeData.payload.imageDTO &&
|
activeData.payload.imageDTO
|
||||||
overData.context.boardId
|
|
||||||
) {
|
) {
|
||||||
const { imageDTO } = activeData.payload;
|
const { imageDTO } = activeData.payload;
|
||||||
const { boardId } = overData.context;
|
const { boardId } = overData.context;
|
||||||
|
|
||||||
// if the board is "No Board", this is a remove action
|
// image was droppe on the "NoBoardBoard"
|
||||||
if (boardId === 'no_board') {
|
if (!boardId) {
|
||||||
dispatch(
|
dispatch(
|
||||||
imagesApi.endpoints.removeImageFromBoard.initiate({
|
imagesApi.endpoints.removeImageFromBoard.initiate({
|
||||||
imageDTO,
|
imageDTO,
|
||||||
@ -172,12 +171,7 @@ export const addImageDroppedListener = () => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle adding image to batch
|
// image was dropped on a user board
|
||||||
if (boardId === 'batch') {
|
|
||||||
// TODO
|
|
||||||
}
|
|
||||||
|
|
||||||
// Otherwise, add the image to the board
|
|
||||||
dispatch(
|
dispatch(
|
||||||
imagesApi.endpoints.addImageToBoard.initiate({
|
imagesApi.endpoints.addImageToBoard.initiate({
|
||||||
imageDTO,
|
imageDTO,
|
||||||
|
@ -5,30 +5,30 @@ import { startAppListening } from '..';
|
|||||||
const moduleLog = log.child({ namespace: 'image' });
|
const moduleLog = log.child({ namespace: 'image' });
|
||||||
|
|
||||||
export const addImageUpdatedFulfilledListener = () => {
|
export const addImageUpdatedFulfilledListener = () => {
|
||||||
startAppListening({
|
// startAppListening({
|
||||||
matcher: imagesApi.endpoints.updateImage.matchFulfilled,
|
// matcher: imagesApi.endpoints.updateImage.matchFulfilled,
|
||||||
effect: (action, { dispatch, getState }) => {
|
// effect: (action, { dispatch, getState }) => {
|
||||||
moduleLog.debug(
|
// moduleLog.debug(
|
||||||
{
|
// {
|
||||||
data: {
|
// data: {
|
||||||
oldImage: action.meta.arg.originalArgs,
|
// oldImage: action.meta.arg.originalArgs,
|
||||||
updatedImage: action.payload,
|
// updatedImage: action.payload,
|
||||||
},
|
// },
|
||||||
},
|
// },
|
||||||
'Image updated'
|
// 'Image updated'
|
||||||
);
|
// );
|
||||||
},
|
// },
|
||||||
});
|
// });
|
||||||
};
|
};
|
||||||
|
|
||||||
export const addImageUpdatedRejectedListener = () => {
|
export const addImageUpdatedRejectedListener = () => {
|
||||||
startAppListening({
|
// startAppListening({
|
||||||
matcher: imagesApi.endpoints.updateImage.matchRejected,
|
// matcher: imagesApi.endpoints.updateImage.matchRejected,
|
||||||
effect: (action, { dispatch }) => {
|
// effect: (action, { dispatch }) => {
|
||||||
moduleLog.debug(
|
// moduleLog.debug(
|
||||||
{ data: action.meta.arg.originalArgs },
|
// { data: action.meta.arg.originalArgs },
|
||||||
'Image update failed'
|
// 'Image update failed'
|
||||||
);
|
// );
|
||||||
},
|
// },
|
||||||
});
|
// });
|
||||||
};
|
};
|
||||||
|
@ -16,6 +16,7 @@ import AddBoardButton from './AddBoardButton';
|
|||||||
import BoardsSearch from './BoardsSearch';
|
import BoardsSearch from './BoardsSearch';
|
||||||
import GalleryBoard from './GalleryBoard';
|
import GalleryBoard from './GalleryBoard';
|
||||||
import SystemBoardButton from './SystemBoardButton';
|
import SystemBoardButton from './SystemBoardButton';
|
||||||
|
import NoBoardBoard from './NoBoardBoard';
|
||||||
|
|
||||||
const selector = createSelector(
|
const selector = createSelector(
|
||||||
[stateSelector],
|
[stateSelector],
|
||||||
@ -42,10 +43,6 @@ const BoardsList = (props: Props) => {
|
|||||||
)
|
)
|
||||||
: boards;
|
: boards;
|
||||||
const [boardToDelete, setBoardToDelete] = useState<BoardDTO>();
|
const [boardToDelete, setBoardToDelete] = useState<BoardDTO>();
|
||||||
const [isSearching, setIsSearching] = useState(false);
|
|
||||||
const handleClickSearchIcon = useCallback(() => {
|
|
||||||
setIsSearching((v) => !v);
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@ -61,54 +58,7 @@ const BoardsList = (props: Props) => {
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Flex sx={{ gap: 2, alignItems: 'center' }}>
|
<Flex sx={{ gap: 2, alignItems: 'center' }}>
|
||||||
<AnimatePresence mode="popLayout">
|
<BoardsSearch />
|
||||||
{isSearching ? (
|
|
||||||
<motion.div
|
|
||||||
key="boards-search"
|
|
||||||
initial={{
|
|
||||||
opacity: 0,
|
|
||||||
}}
|
|
||||||
exit={{
|
|
||||||
opacity: 0,
|
|
||||||
}}
|
|
||||||
animate={{
|
|
||||||
opacity: 1,
|
|
||||||
transition: { duration: 0.1 },
|
|
||||||
}}
|
|
||||||
style={{ width: '100%' }}
|
|
||||||
>
|
|
||||||
<BoardsSearch setIsSearching={setIsSearching} />
|
|
||||||
</motion.div>
|
|
||||||
) : (
|
|
||||||
<motion.div
|
|
||||||
key="system-boards-select"
|
|
||||||
initial={{
|
|
||||||
opacity: 0,
|
|
||||||
}}
|
|
||||||
exit={{
|
|
||||||
opacity: 0,
|
|
||||||
}}
|
|
||||||
animate={{
|
|
||||||
opacity: 1,
|
|
||||||
transition: { duration: 0.1 },
|
|
||||||
}}
|
|
||||||
style={{ width: '100%' }}
|
|
||||||
>
|
|
||||||
<ButtonGroup sx={{ w: 'full', ps: 1.5 }} isAttached>
|
|
||||||
<SystemBoardButton board_id="images" />
|
|
||||||
<SystemBoardButton board_id="assets" />
|
|
||||||
<SystemBoardButton board_id="no_board" />
|
|
||||||
</ButtonGroup>
|
|
||||||
</motion.div>
|
|
||||||
)}
|
|
||||||
</AnimatePresence>
|
|
||||||
<IAIIconButton
|
|
||||||
aria-label="Search Boards"
|
|
||||||
size="sm"
|
|
||||||
isChecked={isSearching}
|
|
||||||
onClick={handleClickSearchIcon}
|
|
||||||
icon={<FaSearch />}
|
|
||||||
/>
|
|
||||||
<AddBoardButton />
|
<AddBoardButton />
|
||||||
</Flex>
|
</Flex>
|
||||||
<OverlayScrollbarsComponent
|
<OverlayScrollbarsComponent
|
||||||
@ -130,6 +80,9 @@ const BoardsList = (props: Props) => {
|
|||||||
maxH: 346,
|
maxH: 346,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
<GridItem sx={{ p: 1.5 }}>
|
||||||
|
<NoBoardBoard isSelected={selectedBoardId === undefined} />
|
||||||
|
</GridItem>
|
||||||
{filteredBoards &&
|
{filteredBoards &&
|
||||||
filteredBoards.map((board) => (
|
filteredBoards.map((board) => (
|
||||||
<GridItem key={board.board_id} sx={{ p: 1.5 }}>
|
<GridItem key={board.board_id} sx={{ p: 1.5 }}>
|
||||||
|
@ -28,12 +28,7 @@ const selector = createSelector(
|
|||||||
defaultSelectorOptions
|
defaultSelectorOptions
|
||||||
);
|
);
|
||||||
|
|
||||||
type Props = {
|
const BoardsSearch = () => {
|
||||||
setIsSearching: (isSearching: boolean) => void;
|
|
||||||
};
|
|
||||||
|
|
||||||
const BoardsSearch = (props: Props) => {
|
|
||||||
const { setIsSearching } = props;
|
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
const { searchText } = useAppSelector(selector);
|
const { searchText } = useAppSelector(selector);
|
||||||
const inputRef = useRef<HTMLInputElement>(null);
|
const inputRef = useRef<HTMLInputElement>(null);
|
||||||
@ -47,8 +42,7 @@ const BoardsSearch = (props: Props) => {
|
|||||||
|
|
||||||
const clearBoardSearch = useCallback(() => {
|
const clearBoardSearch = useCallback(() => {
|
||||||
dispatch(setBoardSearchText(''));
|
dispatch(setBoardSearchText(''));
|
||||||
setIsSearching(false);
|
}, [dispatch]);
|
||||||
}, [dispatch, setIsSearching]);
|
|
||||||
|
|
||||||
const handleKeydown = useCallback(
|
const handleKeydown = useCallback(
|
||||||
(e: KeyboardEvent<HTMLInputElement>) => {
|
(e: KeyboardEvent<HTMLInputElement>) => {
|
||||||
|
@ -24,6 +24,7 @@ import { useUpdateBoardMutation } from 'services/api/endpoints/boards';
|
|||||||
import { useGetImageDTOQuery } from 'services/api/endpoints/images';
|
import { useGetImageDTOQuery } from 'services/api/endpoints/images';
|
||||||
import { BoardDTO } from 'services/api/types';
|
import { BoardDTO } from 'services/api/types';
|
||||||
import BoardContextMenu from '../BoardContextMenu';
|
import BoardContextMenu from '../BoardContextMenu';
|
||||||
|
import { useBoardTotal } from 'services/api/hooks/useBoardTotal';
|
||||||
|
|
||||||
const AUTO_ADD_BADGE_STYLES: ChakraProps['sx'] = {
|
const AUTO_ADD_BADGE_STYLES: ChakraProps['sx'] = {
|
||||||
bg: 'accent.200',
|
bg: 'accent.200',
|
||||||
@ -64,6 +65,8 @@ const GalleryBoard = memo(
|
|||||||
board.cover_image_name ?? skipToken
|
board.cover_image_name ?? skipToken
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const { totalImages, totalAssets } = useBoardTotal(board.board_id);
|
||||||
|
|
||||||
const { board_name, board_id } = board;
|
const { board_name, board_id } = board;
|
||||||
const [localBoardName, setLocalBoardName] = useState(board_name);
|
const [localBoardName, setLocalBoardName] = useState(board_name);
|
||||||
|
|
||||||
@ -143,56 +146,48 @@ const GalleryBoard = memo(
|
|||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
borderRadius: 'base',
|
borderRadius: 'base',
|
||||||
cursor: 'pointer',
|
cursor: 'pointer',
|
||||||
|
bg: 'base.200',
|
||||||
|
_dark: {
|
||||||
|
bg: 'base.800',
|
||||||
|
},
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Flex
|
{coverImage?.thumbnail_url ? (
|
||||||
sx={{
|
<Image
|
||||||
w: 'full',
|
src={coverImage?.thumbnail_url}
|
||||||
h: 'full',
|
draggable={false}
|
||||||
justifyContent: 'center',
|
sx={{
|
||||||
alignItems: 'center',
|
objectFit: 'cover',
|
||||||
borderRadius: 'base',
|
w: 'full',
|
||||||
bg: 'base.200',
|
h: 'full',
|
||||||
_dark: {
|
maxH: 'full',
|
||||||
bg: 'base.800',
|
borderRadius: 'base',
|
||||||
},
|
borderBottomRadius: 'lg',
|
||||||
}}
|
}}
|
||||||
>
|
/>
|
||||||
{coverImage?.thumbnail_url ? (
|
) : (
|
||||||
<Image
|
<Flex
|
||||||
src={coverImage?.thumbnail_url}
|
sx={{
|
||||||
draggable={false}
|
w: 'full',
|
||||||
|
h: 'full',
|
||||||
|
justifyContent: 'center',
|
||||||
|
alignItems: 'center',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Icon
|
||||||
|
boxSize={12}
|
||||||
|
as={FaFolder}
|
||||||
sx={{
|
sx={{
|
||||||
maxW: 'full',
|
mt: -3,
|
||||||
maxH: 'full',
|
opacity: 0.7,
|
||||||
borderRadius: 'base',
|
color: 'base.500',
|
||||||
borderBottomRadius: 'lg',
|
_dark: {
|
||||||
|
color: 'base.500',
|
||||||
|
},
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
) : (
|
</Flex>
|
||||||
<Flex
|
)}
|
||||||
sx={{
|
|
||||||
w: 'full',
|
|
||||||
h: 'full',
|
|
||||||
justifyContent: 'center',
|
|
||||||
alignItems: 'center',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Icon
|
|
||||||
boxSize={12}
|
|
||||||
as={FaFolder}
|
|
||||||
sx={{
|
|
||||||
mt: -3,
|
|
||||||
opacity: 0.7,
|
|
||||||
color: 'base.500',
|
|
||||||
_dark: {
|
|
||||||
color: 'base.500',
|
|
||||||
},
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</Flex>
|
|
||||||
)}
|
|
||||||
</Flex>
|
|
||||||
<Flex
|
<Flex
|
||||||
sx={{
|
sx={{
|
||||||
position: 'absolute',
|
position: 'absolute',
|
||||||
@ -201,15 +196,8 @@ const GalleryBoard = memo(
|
|||||||
p: 1,
|
p: 1,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Badge
|
<Badge variant="solid" sx={BASE_BADGE_STYLES}>
|
||||||
variant="solid"
|
{totalImages}/{totalAssets}
|
||||||
sx={
|
|
||||||
isSelectedForAutoAdd
|
|
||||||
? AUTO_ADD_BADGE_STYLES
|
|
||||||
: BASE_BADGE_STYLES
|
|
||||||
}
|
|
||||||
>
|
|
||||||
{board.image_count}
|
|
||||||
</Badge>
|
</Badge>
|
||||||
</Flex>
|
</Flex>
|
||||||
<Box
|
<Box
|
||||||
|
@ -1,54 +1,123 @@
|
|||||||
import { Text } from '@chakra-ui/react';
|
import { Badge, Box, ChakraProps, Flex, Icon, Text } from '@chakra-ui/react';
|
||||||
import { MoveBoardDropData } from 'app/components/ImageDnd/typesafeDnd';
|
import { MoveBoardDropData } from 'app/components/ImageDnd/typesafeDnd';
|
||||||
import {
|
import { useAppDispatch } from 'app/store/storeHooks';
|
||||||
INITIAL_IMAGE_LIMIT,
|
import IAIDroppable from 'common/components/IAIDroppable';
|
||||||
boardIdSelected,
|
import { boardIdSelected } from 'features/gallery/store/gallerySlice';
|
||||||
} from 'features/gallery/store/gallerySlice';
|
import { memo, useCallback, useMemo } from 'react';
|
||||||
import { FaFolderOpen } from 'react-icons/fa';
|
import { FaFolder } from 'react-icons/fa';
|
||||||
import { useDispatch } from 'react-redux';
|
import { useBoardTotal } from 'services/api/hooks/useBoardTotal';
|
||||||
import {
|
|
||||||
ListImagesArgs,
|
|
||||||
useListImagesQuery,
|
|
||||||
} from 'services/api/endpoints/images';
|
|
||||||
import GenericBoard from './GenericBoard';
|
|
||||||
|
|
||||||
const baseQueryArg: ListImagesArgs = {
|
const BASE_BADGE_STYLES: ChakraProps['sx'] = {
|
||||||
board_id: 'none',
|
bg: 'base.500',
|
||||||
offset: 0,
|
color: 'whiteAlpha.900',
|
||||||
limit: INITIAL_IMAGE_LIMIT,
|
|
||||||
is_intermediate: false,
|
|
||||||
};
|
};
|
||||||
|
interface Props {
|
||||||
|
isSelected: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
const NoBoardBoard = ({ isSelected }: { isSelected: boolean }) => {
|
const NoBoardBoard = memo(({ isSelected }: Props) => {
|
||||||
const dispatch = useDispatch();
|
const dispatch = useAppDispatch();
|
||||||
|
const { totalImages, totalAssets } = useBoardTotal(undefined);
|
||||||
|
const handleSelectBoard = useCallback(() => {
|
||||||
|
dispatch(boardIdSelected(undefined));
|
||||||
|
}, [dispatch]);
|
||||||
|
|
||||||
const handleClick = () => {
|
const droppableData: MoveBoardDropData = useMemo(
|
||||||
dispatch(boardIdSelected('no_board'));
|
() => ({
|
||||||
};
|
id: 'no_board',
|
||||||
|
actionType: 'MOVE_BOARD',
|
||||||
const { total } = useListImagesQuery(baseQueryArg, {
|
context: { boardId: undefined },
|
||||||
selectFromResult: ({ data }) => ({ total: data?.total ?? 0 }),
|
}),
|
||||||
});
|
[]
|
||||||
|
);
|
||||||
// TODO: Do we support making 'images' 'assets? if yes, we need to handle this
|
|
||||||
const droppableData: MoveBoardDropData = {
|
|
||||||
id: 'all-images-board',
|
|
||||||
actionType: 'MOVE_BOARD',
|
|
||||||
context: { boardId: 'no_board' },
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<GenericBoard
|
<Box sx={{ w: 'full', h: 'full', touchAction: 'none', userSelect: 'none' }}>
|
||||||
board_id="no_board"
|
<Flex
|
||||||
droppableData={droppableData}
|
onClick={handleSelectBoard}
|
||||||
dropLabel={<Text fontSize="md">Move</Text>}
|
sx={{
|
||||||
onClick={handleClick}
|
position: 'relative',
|
||||||
isSelected={isSelected}
|
justifyContent: 'center',
|
||||||
icon={FaFolderOpen}
|
alignItems: 'center',
|
||||||
label="No Board"
|
aspectRatio: '1/1',
|
||||||
badgeCount={total}
|
borderRadius: 'base',
|
||||||
/>
|
cursor: 'pointer',
|
||||||
|
w: 'full',
|
||||||
|
h: 'full',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Flex
|
||||||
|
sx={{
|
||||||
|
w: 'full',
|
||||||
|
h: 'full',
|
||||||
|
justifyContent: 'center',
|
||||||
|
alignItems: 'center',
|
||||||
|
borderRadius: 'base',
|
||||||
|
bg: 'base.200',
|
||||||
|
_dark: {
|
||||||
|
bg: 'base.800',
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Flex
|
||||||
|
sx={{
|
||||||
|
w: 'full',
|
||||||
|
h: 'full',
|
||||||
|
justifyContent: 'center',
|
||||||
|
alignItems: 'center',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Icon
|
||||||
|
boxSize={12}
|
||||||
|
as={FaFolder}
|
||||||
|
sx={{
|
||||||
|
opacity: 0.7,
|
||||||
|
color: 'base.500',
|
||||||
|
_dark: {
|
||||||
|
color: 'base.500',
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Flex>
|
||||||
|
</Flex>
|
||||||
|
<Flex
|
||||||
|
sx={{
|
||||||
|
position: 'absolute',
|
||||||
|
insetInlineEnd: 0,
|
||||||
|
top: 0,
|
||||||
|
p: 1,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Badge variant="solid" sx={BASE_BADGE_STYLES}>
|
||||||
|
{totalImages}/{totalAssets}
|
||||||
|
</Badge>
|
||||||
|
</Flex>
|
||||||
|
<Box
|
||||||
|
className="selection-box"
|
||||||
|
sx={{
|
||||||
|
position: 'absolute',
|
||||||
|
top: 0,
|
||||||
|
insetInlineEnd: 0,
|
||||||
|
bottom: 0,
|
||||||
|
insetInlineStart: 0,
|
||||||
|
borderRadius: 'base',
|
||||||
|
transitionProperty: 'common',
|
||||||
|
transitionDuration: 'common',
|
||||||
|
shadow: isSelected ? 'selected.light' : undefined,
|
||||||
|
_dark: {
|
||||||
|
shadow: isSelected ? 'selected.dark' : undefined,
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<IAIDroppable
|
||||||
|
data={droppableData}
|
||||||
|
dropLabel={<Text fontSize="md">Move</Text>}
|
||||||
|
/>
|
||||||
|
</Flex>
|
||||||
|
</Box>
|
||||||
);
|
);
|
||||||
};
|
});
|
||||||
|
|
||||||
|
NoBoardBoard.displayName = 'HoverableBoard';
|
||||||
|
|
||||||
export default NoBoardBoard;
|
export default NoBoardBoard;
|
||||||
|
@ -54,44 +54,44 @@ const CurrentImagePreview = () => {
|
|||||||
shouldAntialiasProgressImage,
|
shouldAntialiasProgressImage,
|
||||||
} = useAppSelector(imagesSelector);
|
} = useAppSelector(imagesSelector);
|
||||||
|
|
||||||
const {
|
// const {
|
||||||
handlePrevImage,
|
// handlePrevImage,
|
||||||
handleNextImage,
|
// handleNextImage,
|
||||||
prevImageId,
|
// prevImageId,
|
||||||
nextImageId,
|
// nextImageId,
|
||||||
isOnLastImage,
|
// isOnLastImage,
|
||||||
handleLoadMoreImages,
|
// handleLoadMoreImages,
|
||||||
areMoreImagesAvailable,
|
// areMoreImagesAvailable,
|
||||||
isFetching,
|
// isFetching,
|
||||||
} = useNextPrevImage();
|
// } = useNextPrevImage();
|
||||||
|
|
||||||
useHotkeys(
|
// useHotkeys(
|
||||||
'left',
|
// 'left',
|
||||||
() => {
|
// () => {
|
||||||
handlePrevImage();
|
// handlePrevImage();
|
||||||
},
|
// },
|
||||||
[prevImageId]
|
// [prevImageId]
|
||||||
);
|
// );
|
||||||
|
|
||||||
useHotkeys(
|
// useHotkeys(
|
||||||
'right',
|
// 'right',
|
||||||
() => {
|
// () => {
|
||||||
if (isOnLastImage && areMoreImagesAvailable && !isFetching) {
|
// if (isOnLastImage && areMoreImagesAvailable && !isFetching) {
|
||||||
handleLoadMoreImages();
|
// handleLoadMoreImages();
|
||||||
return;
|
// return;
|
||||||
}
|
// }
|
||||||
if (!isOnLastImage) {
|
// if (!isOnLastImage) {
|
||||||
handleNextImage();
|
// handleNextImage();
|
||||||
}
|
// }
|
||||||
},
|
// },
|
||||||
[
|
// [
|
||||||
nextImageId,
|
// nextImageId,
|
||||||
isOnLastImage,
|
// isOnLastImage,
|
||||||
areMoreImagesAvailable,
|
// areMoreImagesAvailable,
|
||||||
handleLoadMoreImages,
|
// handleLoadMoreImages,
|
||||||
isFetching,
|
// isFetching,
|
||||||
]
|
// ]
|
||||||
);
|
// );
|
||||||
|
|
||||||
const {
|
const {
|
||||||
currentData: imageDTO,
|
currentData: imageDTO,
|
||||||
@ -213,7 +213,7 @@ const CurrentImagePreview = () => {
|
|||||||
pointerEvents: 'none',
|
pointerEvents: 'none',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<NextPrevImageButtons />
|
{/* <NextPrevImageButtons /> */}
|
||||||
</motion.div>
|
</motion.div>
|
||||||
)}
|
)}
|
||||||
</AnimatePresence>
|
</AnimatePresence>
|
||||||
|
@ -27,16 +27,24 @@ const GalleryBoardName = (props: Props) => {
|
|||||||
const { isOpen, onToggle } = props;
|
const { isOpen, onToggle } = props;
|
||||||
const { selectedBoardId } = useAppSelector(selector);
|
const { selectedBoardId } = useAppSelector(selector);
|
||||||
const boardName = useBoardName(selectedBoardId);
|
const boardName = useBoardName(selectedBoardId);
|
||||||
const numOfBoardImages = useBoardTotal(selectedBoardId);
|
const { totalImages, totalAssets } = useBoardTotal(selectedBoardId);
|
||||||
|
|
||||||
const formattedBoardName = useMemo(() => {
|
const formattedBoardName = useMemo(() => {
|
||||||
if (!boardName) return '';
|
if (!boardName) {
|
||||||
if (boardName && !numOfBoardImages) return boardName;
|
return '';
|
||||||
if (boardName.length > 20) {
|
|
||||||
return `${boardName.substring(0, 20)}... (${numOfBoardImages})`;
|
|
||||||
}
|
}
|
||||||
return `${boardName} (${numOfBoardImages})`;
|
|
||||||
}, [boardName, numOfBoardImages]);
|
if (boardName && (totalImages === undefined || totalAssets === undefined)) {
|
||||||
|
return boardName;
|
||||||
|
}
|
||||||
|
|
||||||
|
const count = `${totalImages}/${totalAssets}`;
|
||||||
|
|
||||||
|
if (boardName.length > 20) {
|
||||||
|
return `${boardName.substring(0, 20)}... (${count})`;
|
||||||
|
}
|
||||||
|
return `${boardName} (${count})`;
|
||||||
|
}, [boardName, totalAssets, totalImages]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Flex
|
<Flex
|
||||||
|
@ -1,23 +1,34 @@
|
|||||||
import { Box, Flex, VStack, useDisclosure } from '@chakra-ui/react';
|
import {
|
||||||
|
Box,
|
||||||
|
ButtonGroup,
|
||||||
|
Flex,
|
||||||
|
Spacer,
|
||||||
|
VStack,
|
||||||
|
useDisclosure,
|
||||||
|
} from '@chakra-ui/react';
|
||||||
import { createSelector } from '@reduxjs/toolkit';
|
import { createSelector } from '@reduxjs/toolkit';
|
||||||
import { stateSelector } from 'app/store/store';
|
import { stateSelector } from 'app/store/store';
|
||||||
import { 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';
|
||||||
import { memo, useRef } from 'react';
|
import { memo, useCallback, useRef } from 'react';
|
||||||
import BoardsList from './Boards/BoardsList/BoardsList';
|
import BoardsList from './Boards/BoardsList/BoardsList';
|
||||||
import GalleryBoardName from './GalleryBoardName';
|
import GalleryBoardName from './GalleryBoardName';
|
||||||
import GalleryPinButton from './GalleryPinButton';
|
import GalleryPinButton from './GalleryPinButton';
|
||||||
import GallerySettingsPopover from './GallerySettingsPopover';
|
import GallerySettingsPopover from './GallerySettingsPopover';
|
||||||
import BatchImageGrid from './ImageGrid/BatchImageGrid';
|
import BatchImageGrid from './ImageGrid/BatchImageGrid';
|
||||||
import GalleryImageGrid from './ImageGrid/GalleryImageGrid';
|
import GalleryImageGrid from './ImageGrid/GalleryImageGrid';
|
||||||
|
import IAIButton from 'common/components/IAIButton';
|
||||||
|
import { FaImages, FaServer } from 'react-icons/fa';
|
||||||
|
import { galleryViewChanged } from '../store/gallerySlice';
|
||||||
|
|
||||||
const selector = createSelector(
|
const selector = createSelector(
|
||||||
[stateSelector],
|
[stateSelector],
|
||||||
(state) => {
|
(state) => {
|
||||||
const { selectedBoardId } = state.gallery;
|
const { selectedBoardId, galleryView } = state.gallery;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
selectedBoardId,
|
selectedBoardId,
|
||||||
|
galleryView,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
defaultSelectorOptions
|
defaultSelectorOptions
|
||||||
@ -26,10 +37,19 @@ const selector = createSelector(
|
|||||||
const ImageGalleryContent = () => {
|
const ImageGalleryContent = () => {
|
||||||
const resizeObserverRef = useRef<HTMLDivElement>(null);
|
const resizeObserverRef = useRef<HTMLDivElement>(null);
|
||||||
const galleryGridRef = useRef<HTMLDivElement>(null);
|
const galleryGridRef = useRef<HTMLDivElement>(null);
|
||||||
const { selectedBoardId } = useAppSelector(selector);
|
const { selectedBoardId, galleryView } = useAppSelector(selector);
|
||||||
|
const dispatch = useAppDispatch();
|
||||||
const { isOpen: isBoardListOpen, onToggle: onToggleBoardList } =
|
const { isOpen: isBoardListOpen, onToggle: onToggleBoardList } =
|
||||||
useDisclosure();
|
useDisclosure();
|
||||||
|
|
||||||
|
const handleClickImages = useCallback(() => {
|
||||||
|
dispatch(galleryViewChanged('images'));
|
||||||
|
}, [dispatch]);
|
||||||
|
|
||||||
|
const handleClickAssets = useCallback(() => {
|
||||||
|
dispatch(galleryViewChanged('assets'));
|
||||||
|
}, [dispatch]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<VStack
|
<VStack
|
||||||
sx={{
|
sx={{
|
||||||
@ -48,11 +68,11 @@ const ImageGalleryContent = () => {
|
|||||||
gap: 2,
|
gap: 2,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<GallerySettingsPopover />
|
|
||||||
<GalleryBoardName
|
<GalleryBoardName
|
||||||
isOpen={isBoardListOpen}
|
isOpen={isBoardListOpen}
|
||||||
onToggle={onToggleBoardList}
|
onToggle={onToggleBoardList}
|
||||||
/>
|
/>
|
||||||
|
<GallerySettingsPopover />
|
||||||
<GalleryPinButton />
|
<GalleryPinButton />
|
||||||
</Flex>
|
</Flex>
|
||||||
<Box>
|
<Box>
|
||||||
@ -60,6 +80,39 @@ const ImageGalleryContent = () => {
|
|||||||
</Box>
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
<Flex ref={galleryGridRef} direction="column" gap={2} h="full" w="full">
|
<Flex ref={galleryGridRef} direction="column" gap={2} h="full" w="full">
|
||||||
|
<Flex
|
||||||
|
sx={{
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'space-between',
|
||||||
|
gap: 2,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<ButtonGroup isAttached sx={{ w: 'full' }}>
|
||||||
|
<IAIButton
|
||||||
|
leftIcon={<FaImages />}
|
||||||
|
size="sm"
|
||||||
|
isChecked={galleryView === 'images'}
|
||||||
|
onClick={handleClickImages}
|
||||||
|
sx={{
|
||||||
|
w: 'full',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Images
|
||||||
|
</IAIButton>
|
||||||
|
<IAIButton
|
||||||
|
leftIcon={<FaServer />}
|
||||||
|
size="sm"
|
||||||
|
isChecked={galleryView === 'assets'}
|
||||||
|
onClick={handleClickAssets}
|
||||||
|
sx={{
|
||||||
|
w: 'full',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Assets
|
||||||
|
</IAIButton>
|
||||||
|
</ButtonGroup>
|
||||||
|
</Flex>
|
||||||
|
|
||||||
{selectedBoardId === 'batch' ? (
|
{selectedBoardId === 'batch' ? (
|
||||||
<BatchImageGrid />
|
<BatchImageGrid />
|
||||||
) : (
|
) : (
|
||||||
|
@ -4,7 +4,6 @@ import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
|||||||
import {
|
import {
|
||||||
IMAGE_LIMIT,
|
IMAGE_LIMIT,
|
||||||
imageSelected,
|
imageSelected,
|
||||||
selectImagesById,
|
|
||||||
} from 'features/gallery/store/gallerySlice';
|
} from 'features/gallery/store/gallerySlice';
|
||||||
import { clamp, isEqual } from 'lodash-es';
|
import { clamp, isEqual } from 'lodash-es';
|
||||||
import { useCallback } from 'react';
|
import { useCallback } from 'react';
|
||||||
|
@ -2,11 +2,11 @@ import { createSelector } from '@reduxjs/toolkit';
|
|||||||
import { RootState } from 'app/store/store';
|
import { RootState } from 'app/store/store';
|
||||||
import { defaultSelectorOptions } from 'app/store/util/defaultMemoizeOptions';
|
import { defaultSelectorOptions } from 'app/store/util/defaultMemoizeOptions';
|
||||||
import { ListImagesArgs } from 'services/api/endpoints/images';
|
import { ListImagesArgs } from 'services/api/endpoints/images';
|
||||||
import { INITIAL_IMAGE_LIMIT } from './gallerySlice';
|
|
||||||
import {
|
import {
|
||||||
getBoardIdQueryParamForBoard,
|
ASSETS_CATEGORIES,
|
||||||
getCategoriesQueryParamForBoard,
|
IMAGE_CATEGORIES,
|
||||||
} from './util';
|
INITIAL_IMAGE_LIMIT,
|
||||||
|
} from './gallerySlice';
|
||||||
|
|
||||||
export const gallerySelector = (state: RootState) => state.gallery;
|
export const gallerySelector = (state: RootState) => state.gallery;
|
||||||
|
|
||||||
@ -19,14 +19,13 @@ export const selectLastSelectedImage = createSelector(
|
|||||||
export const selectListImagesBaseQueryArgs = createSelector(
|
export const selectListImagesBaseQueryArgs = createSelector(
|
||||||
[(state: RootState) => state],
|
[(state: RootState) => state],
|
||||||
(state) => {
|
(state) => {
|
||||||
const { selectedBoardId } = state.gallery;
|
const { selectedBoardId, galleryView } = state.gallery;
|
||||||
|
const categories =
|
||||||
const categories = getCategoriesQueryParamForBoard(selectedBoardId);
|
galleryView === 'images' ? IMAGE_CATEGORIES : ASSETS_CATEGORIES;
|
||||||
const board_id = getBoardIdQueryParamForBoard(selectedBoardId);
|
|
||||||
|
|
||||||
const listImagesBaseQueryArgs: ListImagesArgs = {
|
const listImagesBaseQueryArgs: ListImagesArgs = {
|
||||||
|
board_id: selectedBoardId ?? 'none',
|
||||||
categories,
|
categories,
|
||||||
board_id,
|
|
||||||
offset: 0,
|
offset: 0,
|
||||||
limit: INITIAL_IMAGE_LIMIT,
|
limit: INITIAL_IMAGE_LIMIT,
|
||||||
is_intermediate: false,
|
is_intermediate: false,
|
||||||
|
@ -14,13 +14,9 @@ export const ASSETS_CATEGORIES: ImageCategory[] = [
|
|||||||
export const INITIAL_IMAGE_LIMIT = 100;
|
export const INITIAL_IMAGE_LIMIT = 100;
|
||||||
export const IMAGE_LIMIT = 20;
|
export const IMAGE_LIMIT = 20;
|
||||||
|
|
||||||
// export type GalleryView = 'images' | 'assets';
|
export type GalleryView = 'images' | 'assets';
|
||||||
export type BoardId =
|
// export type BoardId = 'no_board' | (string & Record<never, never>);
|
||||||
| 'images'
|
export type BoardId = string | undefined;
|
||||||
| 'assets'
|
|
||||||
| 'no_board'
|
|
||||||
| 'batch'
|
|
||||||
| (string & Record<never, never>);
|
|
||||||
|
|
||||||
type GalleryState = {
|
type GalleryState = {
|
||||||
selection: string[];
|
selection: string[];
|
||||||
@ -28,6 +24,7 @@ type GalleryState = {
|
|||||||
autoAddBoardId: string | null;
|
autoAddBoardId: string | null;
|
||||||
galleryImageMinimumWidth: number;
|
galleryImageMinimumWidth: number;
|
||||||
selectedBoardId: BoardId;
|
selectedBoardId: BoardId;
|
||||||
|
galleryView: GalleryView;
|
||||||
batchImageNames: string[];
|
batchImageNames: string[];
|
||||||
isBatchEnabled: boolean;
|
isBatchEnabled: boolean;
|
||||||
};
|
};
|
||||||
@ -37,7 +34,8 @@ export const initialGalleryState: GalleryState = {
|
|||||||
shouldAutoSwitch: true,
|
shouldAutoSwitch: true,
|
||||||
autoAddBoardId: null,
|
autoAddBoardId: null,
|
||||||
galleryImageMinimumWidth: 96,
|
galleryImageMinimumWidth: 96,
|
||||||
selectedBoardId: 'images',
|
selectedBoardId: undefined,
|
||||||
|
galleryView: 'images',
|
||||||
batchImageNames: [],
|
batchImageNames: [],
|
||||||
isBatchEnabled: false,
|
isBatchEnabled: false,
|
||||||
};
|
};
|
||||||
@ -96,6 +94,7 @@ export const gallerySlice = createSlice({
|
|||||||
},
|
},
|
||||||
boardIdSelected: (state, action: PayloadAction<BoardId>) => {
|
boardIdSelected: (state, action: PayloadAction<BoardId>) => {
|
||||||
state.selectedBoardId = action.payload;
|
state.selectedBoardId = action.payload;
|
||||||
|
state.galleryView = 'images';
|
||||||
},
|
},
|
||||||
isBatchEnabledChanged: (state, action: PayloadAction<boolean>) => {
|
isBatchEnabledChanged: (state, action: PayloadAction<boolean>) => {
|
||||||
state.isBatchEnabled = action.payload;
|
state.isBatchEnabled = action.payload;
|
||||||
@ -128,6 +127,9 @@ export const gallerySlice = createSlice({
|
|||||||
autoAddBoardIdChanged: (state, action: PayloadAction<string | null>) => {
|
autoAddBoardIdChanged: (state, action: PayloadAction<string | null>) => {
|
||||||
state.autoAddBoardId = action.payload;
|
state.autoAddBoardId = action.payload;
|
||||||
},
|
},
|
||||||
|
galleryViewChanged: (state, action: PayloadAction<GalleryView>) => {
|
||||||
|
state.galleryView = action.payload;
|
||||||
|
},
|
||||||
},
|
},
|
||||||
extraReducers: (builder) => {
|
extraReducers: (builder) => {
|
||||||
builder.addMatcher(
|
builder.addMatcher(
|
||||||
@ -170,6 +172,7 @@ export const {
|
|||||||
imagesAddedToBatch,
|
imagesAddedToBatch,
|
||||||
imagesRemovedFromBatch,
|
imagesRemovedFromBatch,
|
||||||
autoAddBoardIdChanged,
|
autoAddBoardIdChanged,
|
||||||
|
galleryViewChanged,
|
||||||
} = gallerySlice.actions;
|
} = gallerySlice.actions;
|
||||||
|
|
||||||
export default gallerySlice.reducer;
|
export default gallerySlice.reducer;
|
||||||
|
@ -1,6 +1,11 @@
|
|||||||
import { SYSTEM_BOARDS } from 'services/api/endpoints/images';
|
import { ListImagesArgs, SYSTEM_BOARDS } from 'services/api/endpoints/images';
|
||||||
import { ASSETS_CATEGORIES, BoardId, IMAGE_CATEGORIES } from './gallerySlice';
|
import {
|
||||||
import { ImageCategory } from 'services/api/types';
|
ASSETS_CATEGORIES,
|
||||||
|
BoardId,
|
||||||
|
GalleryView,
|
||||||
|
IMAGE_CATEGORIES,
|
||||||
|
} from './gallerySlice';
|
||||||
|
import { ImageCategory, ImageDTO } from 'services/api/types';
|
||||||
import { isEqual } from 'lodash-es';
|
import { isEqual } from 'lodash-es';
|
||||||
|
|
||||||
export const getCategoriesQueryParamForBoard = (
|
export const getCategoriesQueryParamForBoard = (
|
||||||
@ -20,16 +25,11 @@ export const getCategoriesQueryParamForBoard = (
|
|||||||
|
|
||||||
export const getBoardIdQueryParamForBoard = (
|
export const getBoardIdQueryParamForBoard = (
|
||||||
board_id: BoardId
|
board_id: BoardId
|
||||||
): string | undefined => {
|
): string | null => {
|
||||||
if (board_id === 'no_board') {
|
if (board_id === undefined) {
|
||||||
return 'none';
|
return 'none';
|
||||||
}
|
}
|
||||||
|
|
||||||
// system boards besides 'no_board'
|
|
||||||
if (SYSTEM_BOARDS.includes(board_id)) {
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
// user boards
|
// user boards
|
||||||
return board_id;
|
return board_id;
|
||||||
};
|
};
|
||||||
@ -52,3 +52,10 @@ export const getBoardIdFromBoardAndCategoriesQueryParam = (
|
|||||||
|
|
||||||
return board_id ?? 'UNKNOWN_BOARD';
|
return board_id ?? 'UNKNOWN_BOARD';
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const getCategories = (imageDTO: ImageDTO) => {
|
||||||
|
if (IMAGE_CATEGORIES.includes(imageDTO.image_category)) {
|
||||||
|
return IMAGE_CATEGORIES;
|
||||||
|
}
|
||||||
|
return ASSETS_CATEGORIES;
|
||||||
|
};
|
||||||
|
@ -6,18 +6,17 @@ import {
|
|||||||
BoardId,
|
BoardId,
|
||||||
IMAGE_CATEGORIES,
|
IMAGE_CATEGORIES,
|
||||||
} from 'features/gallery/store/gallerySlice';
|
} from 'features/gallery/store/gallerySlice';
|
||||||
import { omit } from 'lodash-es';
|
|
||||||
import queryString from 'query-string';
|
import queryString from 'query-string';
|
||||||
import { ApiFullTagDescription, api } from '..';
|
import { ApiFullTagDescription, api } from '..';
|
||||||
import { components, paths } from '../schema';
|
import { components, paths } from '../schema';
|
||||||
import {
|
import {
|
||||||
ImageCategory,
|
ImageCategory,
|
||||||
ImageChanges,
|
|
||||||
ImageDTO,
|
ImageDTO,
|
||||||
OffsetPaginatedResults_ImageDTO_,
|
OffsetPaginatedResults_ImageDTO_,
|
||||||
PostUploadAction,
|
PostUploadAction,
|
||||||
} from '../types';
|
} from '../types';
|
||||||
import { getCacheAction } from './util';
|
import { getCacheAction, getIsImageInDateRange } from './util';
|
||||||
|
import { getCategories } from 'features/gallery/store/util';
|
||||||
|
|
||||||
export type ListImagesArgs = NonNullable<
|
export type ListImagesArgs = NonNullable<
|
||||||
paths['/api/v1/images/']['get']['parameters']['query']
|
paths['/api/v1/images/']['get']['parameters']['query']
|
||||||
@ -155,6 +154,42 @@ export const imagesApi = api.injectEndpoints({
|
|||||||
},
|
},
|
||||||
keepUnusedDataFor: 86400, // 24 hours
|
keepUnusedDataFor: 86400, // 24 hours
|
||||||
}),
|
}),
|
||||||
|
getBoardImagesTotal: build.query<number, string | undefined>({
|
||||||
|
query: (board_id) => ({
|
||||||
|
url: getListImagesUrl({
|
||||||
|
board_id: board_id ?? 'none',
|
||||||
|
categories: IMAGE_CATEGORIES,
|
||||||
|
is_intermediate: false,
|
||||||
|
limit: 0,
|
||||||
|
offset: 0,
|
||||||
|
}),
|
||||||
|
method: 'GET',
|
||||||
|
}),
|
||||||
|
providesTags: (result, error, arg) => [
|
||||||
|
{ type: 'BoardImagesTotal', id: arg ?? 'none' },
|
||||||
|
],
|
||||||
|
transformResponse: (response: OffsetPaginatedResults_ImageDTO_) => {
|
||||||
|
return response.total;
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
getBoardAssetsTotal: build.query<number, string | undefined>({
|
||||||
|
query: (board_id) => ({
|
||||||
|
url: getListImagesUrl({
|
||||||
|
board_id: board_id ?? 'none',
|
||||||
|
categories: ASSETS_CATEGORIES,
|
||||||
|
is_intermediate: false,
|
||||||
|
limit: 0,
|
||||||
|
offset: 0,
|
||||||
|
}),
|
||||||
|
method: 'GET',
|
||||||
|
}),
|
||||||
|
providesTags: (result, error, arg) => [
|
||||||
|
{ type: 'BoardAssetsTotal', id: arg ?? 'none' },
|
||||||
|
],
|
||||||
|
transformResponse: (response: OffsetPaginatedResults_ImageDTO_) => {
|
||||||
|
return response.total;
|
||||||
|
},
|
||||||
|
}),
|
||||||
clearIntermediates: build.mutation<number, void>({
|
clearIntermediates: build.mutation<number, void>({
|
||||||
query: () => ({ url: `images/clear-intermediates`, method: 'POST' }),
|
query: () => ({ url: `images/clear-intermediates`, method: 'POST' }),
|
||||||
invalidatesTags: ['IntermediatesCount'],
|
invalidatesTags: ['IntermediatesCount'],
|
||||||
@ -164,56 +199,42 @@ export const imagesApi = api.injectEndpoints({
|
|||||||
url: `images/${image_name}`,
|
url: `images/${image_name}`,
|
||||||
method: 'DELETE',
|
method: 'DELETE',
|
||||||
}),
|
}),
|
||||||
invalidatesTags: (result, error, arg) => [
|
invalidatesTags: (result, error, { board_id }) => [
|
||||||
{ type: 'Image', id: arg.image_name },
|
{ type: 'BoardImagesTotal', id: board_id ?? 'none' },
|
||||||
|
{ type: 'BoardAssetsTotal', id: board_id ?? 'none' },
|
||||||
],
|
],
|
||||||
async onQueryStarted(imageDTO, { dispatch, queryFulfilled }) {
|
async onQueryStarted(imageDTO, { dispatch, queryFulfilled }) {
|
||||||
/**
|
/**
|
||||||
* Cache changes for `deleteImage`:
|
* Cache changes for `deleteImage`:
|
||||||
* - *remove* from "All Images" / "All Assets"
|
* - NOT POSSIBLE: *remove* from getImageDTO
|
||||||
* - IF it has a board:
|
* - $cache = [board_id|no_board]/[images|assets]
|
||||||
* - THEN *remove* from it's own board
|
* - *remove* from $cache
|
||||||
* - ELSE *remove* from "No Board"
|
|
||||||
*/
|
*/
|
||||||
|
|
||||||
const { image_name, board_id, image_category } = imageDTO;
|
const { image_name, board_id } = imageDTO;
|
||||||
|
|
||||||
// Figure out the `listImages` caches that we need to update
|
// Store patches so we can undo if the query fails
|
||||||
// That means constructing the possible query args that are serialized into the cache key...
|
const patches: PatchCollection[] = [];
|
||||||
|
|
||||||
const removeFromCacheKeys: ListImagesArgs[] = [];
|
|
||||||
|
|
||||||
// determine `categories`, i.e. do we update "All Images" or "All Assets"
|
// determine `categories`, i.e. do we update "All Images" or "All Assets"
|
||||||
const categories = IMAGE_CATEGORIES.includes(image_category)
|
// $cache = [board_id|no_board]/[images|assets]
|
||||||
? IMAGE_CATEGORIES
|
const categories = getCategories(imageDTO);
|
||||||
: ASSETS_CATEGORIES;
|
|
||||||
|
|
||||||
// remove from "All Images"
|
// *remove* from $cache
|
||||||
removeFromCacheKeys.push({ categories });
|
patches.push(
|
||||||
|
dispatch(
|
||||||
if (board_id) {
|
imagesApi.util.updateQueryData(
|
||||||
// remove from it's own board
|
'listImages',
|
||||||
removeFromCacheKeys.push({ board_id });
|
{ board_id, categories },
|
||||||
} else {
|
(draft) => {
|
||||||
// remove from "No Board"
|
const oldTotal = draft.total;
|
||||||
removeFromCacheKeys.push({ board_id: 'none' });
|
const newState = imagesAdapter.removeOne(draft, image_name);
|
||||||
}
|
const delta = newState.total - oldTotal;
|
||||||
|
draft.total = draft.total + delta;
|
||||||
const patches: PatchCollection[] = [];
|
}
|
||||||
removeFromCacheKeys.forEach((cacheKey) => {
|
|
||||||
patches.push(
|
|
||||||
dispatch(
|
|
||||||
imagesApi.util.updateQueryData(
|
|
||||||
'listImages',
|
|
||||||
cacheKey,
|
|
||||||
(draft) => {
|
|
||||||
imagesAdapter.removeOne(draft, image_name);
|
|
||||||
draft.total = Math.max(draft.total - 1, 0);
|
|
||||||
}
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
);
|
)
|
||||||
});
|
);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await queryFulfilled;
|
await queryFulfilled;
|
||||||
@ -222,122 +243,165 @@ export const imagesApi = api.injectEndpoints({
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
updateImage: build.mutation<
|
/**
|
||||||
|
* Change an image's `is_intermediate` property.
|
||||||
|
*/
|
||||||
|
changeImageIsIntermediate: build.mutation<
|
||||||
ImageDTO,
|
ImageDTO,
|
||||||
{
|
{ imageDTO: ImageDTO; is_intermediate: boolean }
|
||||||
imageDTO: ImageDTO;
|
|
||||||
// For now, we will not allow image categories to change
|
|
||||||
changes: Omit<ImageChanges, 'image_category'>;
|
|
||||||
}
|
|
||||||
>({
|
>({
|
||||||
query: ({ imageDTO, changes }) => ({
|
query: ({ imageDTO, is_intermediate }) => ({
|
||||||
url: `images/${imageDTO.image_name}`,
|
url: `images/${imageDTO.image_name}`,
|
||||||
method: 'PATCH',
|
method: 'PATCH',
|
||||||
body: changes,
|
body: { is_intermediate },
|
||||||
}),
|
}),
|
||||||
invalidatesTags: (result, error, { imageDTO }) => [
|
invalidatesTags: (result, error, { imageDTO }) => [
|
||||||
{ type: 'Image', id: imageDTO.image_name },
|
{ type: 'BoardImagesTotal', id: imageDTO.board_id ?? 'none' },
|
||||||
|
{ type: 'BoardAssetsTotal', id: imageDTO.board_id ?? 'none' },
|
||||||
],
|
],
|
||||||
async onQueryStarted(
|
async onQueryStarted(
|
||||||
{ imageDTO: oldImageDTO, changes: _changes },
|
{ imageDTO, is_intermediate },
|
||||||
{ dispatch, queryFulfilled, getState }
|
{ dispatch, queryFulfilled, getState }
|
||||||
) {
|
) {
|
||||||
// let's be extra-sure we do not accidentally change categories
|
|
||||||
const changes = omit(_changes, 'image_category');
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Cache changes for "updateImage":
|
* Cache changes for `changeImageIsIntermediate`:
|
||||||
* - *update* "getImageDTO" cache
|
* - *update* getImageDTO
|
||||||
* - for "All Images" || "All Assets":
|
* - $cache = [board_id|no_board]/[images|assets]
|
||||||
* - IF it is not already in the cache
|
* - IF it is being changed to an intermediate:
|
||||||
* - THEN *add* it to "All Images" / "All Assets" and update the total
|
* - remove from $cache
|
||||||
* - ELSE *update* it
|
* - ELSE (it is being changed to a non-intermediate):
|
||||||
* - IF the image has a board:
|
* - IF it eligible for insertion into existing $cache:
|
||||||
* - THEN *update* it's own board
|
* - *upsert* to $cache
|
||||||
* - ELSE *update* the "No Board" board
|
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
// Store patches so we can undo if the query fails
|
||||||
const patches: PatchCollection[] = [];
|
const patches: PatchCollection[] = [];
|
||||||
const { image_name, board_id, image_category, is_intermediate } =
|
|
||||||
oldImageDTO;
|
|
||||||
|
|
||||||
const isChangingFromIntermediate = changes.is_intermediate === false;
|
// *update* getImageDTO
|
||||||
// do not add intermediates to gallery cache
|
|
||||||
if (is_intermediate && !isChangingFromIntermediate) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// determine `categories`, i.e. do we update "All Images" or "All Assets"
|
|
||||||
const categories = IMAGE_CATEGORIES.includes(image_category)
|
|
||||||
? IMAGE_CATEGORIES
|
|
||||||
: ASSETS_CATEGORIES;
|
|
||||||
|
|
||||||
// update `getImageDTO` cache
|
|
||||||
patches.push(
|
patches.push(
|
||||||
dispatch(
|
dispatch(
|
||||||
imagesApi.util.updateQueryData(
|
imagesApi.util.updateQueryData(
|
||||||
'getImageDTO',
|
'getImageDTO',
|
||||||
image_name,
|
imageDTO.image_name,
|
||||||
(draft) => {
|
(draft) => {
|
||||||
Object.assign(draft, changes);
|
Object.assign(draft, { is_intermediate });
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
// Update the "All Image" or "All Assets" board
|
// $cache = [board_id|no_board]/[images|assets]
|
||||||
const queryArgsToUpdate: ListImagesArgs[] = [{ categories }];
|
const categories = getCategories(imageDTO);
|
||||||
|
|
||||||
// IF the image has a board:
|
if (is_intermediate) {
|
||||||
if (board_id) {
|
// IF it is being changed to an intermediate:
|
||||||
// THEN update it's own board
|
// remove from $cache
|
||||||
queryArgsToUpdate.push({ board_id });
|
patches.push(
|
||||||
|
dispatch(
|
||||||
|
imagesApi.util.updateQueryData(
|
||||||
|
'listImages',
|
||||||
|
{ board_id: imageDTO.board_id, categories },
|
||||||
|
(draft) => {
|
||||||
|
const oldTotal = draft.total;
|
||||||
|
const newState = imagesAdapter.removeOne(
|
||||||
|
draft,
|
||||||
|
imageDTO.image_name
|
||||||
|
);
|
||||||
|
const delta = newState.total - oldTotal;
|
||||||
|
draft.total = draft.total + delta;
|
||||||
|
}
|
||||||
|
)
|
||||||
|
)
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
// ELSE update the "No Board" board
|
// ELSE (it is being changed to a non-intermediate):
|
||||||
queryArgsToUpdate.push({ board_id: 'none' });
|
const queryArgs = { board_id: imageDTO.board_id, categories };
|
||||||
}
|
|
||||||
|
|
||||||
queryArgsToUpdate.forEach((queryArg) => {
|
const currentCache = imagesApi.endpoints.listImages.select(queryArgs)(
|
||||||
const { data } = imagesApi.endpoints.listImages.select(queryArg)(
|
|
||||||
getState()
|
getState()
|
||||||
);
|
);
|
||||||
|
|
||||||
const cacheAction = getCacheAction(data, oldImageDTO);
|
// IF it eligible for insertion into existing $cache
|
||||||
|
// "eligible" means either:
|
||||||
|
// - The cache is fully populated, with all images in the db cached
|
||||||
|
// OR
|
||||||
|
// - The image's `created_at` is within the range of the cached images
|
||||||
|
|
||||||
if (['update', 'add'].includes(cacheAction)) {
|
const isCacheFullyPopulated =
|
||||||
|
currentCache.data &&
|
||||||
|
currentCache.data.ids.length >= currentCache.data.total;
|
||||||
|
|
||||||
|
const isInDateRange = getIsImageInDateRange(
|
||||||
|
currentCache.data,
|
||||||
|
imageDTO
|
||||||
|
);
|
||||||
|
|
||||||
|
if (isCacheFullyPopulated || isInDateRange) {
|
||||||
|
// *upsert* to $cache
|
||||||
patches.push(
|
patches.push(
|
||||||
dispatch(
|
dispatch(
|
||||||
imagesApi.util.updateQueryData(
|
imagesApi.util.updateQueryData(
|
||||||
'listImages',
|
'listImages',
|
||||||
queryArg,
|
queryArgs,
|
||||||
(draft) => {
|
(draft) => {
|
||||||
// One of the common changes is to make a canvas intermediate a non-intermediate,
|
const oldTotal = draft.total;
|
||||||
// i.e. save a canvas image to the gallery.
|
const newState = imagesAdapter.upsertOne(draft, imageDTO);
|
||||||
// If that was the change, need to add the image to the cache instead of updating
|
const delta = newState.total - oldTotal;
|
||||||
// the existing cache entry.
|
draft.total = draft.total + delta;
|
||||||
if (
|
|
||||||
changes.is_intermediate === false ||
|
|
||||||
cacheAction === 'add'
|
|
||||||
) {
|
|
||||||
// add it to the cache
|
|
||||||
imagesAdapter.addOne(draft, {
|
|
||||||
...oldImageDTO,
|
|
||||||
...changes,
|
|
||||||
});
|
|
||||||
draft.total += 1;
|
|
||||||
} else if (cacheAction === 'update') {
|
|
||||||
// just update it
|
|
||||||
imagesAdapter.updateOne(draft, {
|
|
||||||
id: image_name,
|
|
||||||
changes,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
});
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await queryFulfilled;
|
||||||
|
} catch {
|
||||||
|
patches.forEach((patchResult) => patchResult.undo());
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
/**
|
||||||
|
* Change an image's `session_id` association.
|
||||||
|
*/
|
||||||
|
changeImageSessionId: build.mutation<
|
||||||
|
ImageDTO,
|
||||||
|
{ imageDTO: ImageDTO; session_id: string }
|
||||||
|
>({
|
||||||
|
query: ({ imageDTO, session_id }) => ({
|
||||||
|
url: `images/${imageDTO.image_name}`,
|
||||||
|
method: 'PATCH',
|
||||||
|
body: { session_id },
|
||||||
|
}),
|
||||||
|
invalidatesTags: (result, error, { imageDTO }) => [
|
||||||
|
{ type: 'BoardImagesTotal', id: imageDTO.board_id ?? 'none' },
|
||||||
|
{ type: 'BoardAssetsTotal', id: imageDTO.board_id ?? 'none' },
|
||||||
|
],
|
||||||
|
async onQueryStarted(
|
||||||
|
{ imageDTO, session_id },
|
||||||
|
{ dispatch, queryFulfilled, getState }
|
||||||
|
) {
|
||||||
|
/**
|
||||||
|
* Cache changes for `changeImageSessionId`:
|
||||||
|
* - *update* getImageDTO
|
||||||
|
*/
|
||||||
|
|
||||||
|
// Store patches so we can undo if the query fails
|
||||||
|
const patches: PatchCollection[] = [];
|
||||||
|
|
||||||
|
// *update* getImageDTO
|
||||||
|
patches.push(
|
||||||
|
dispatch(
|
||||||
|
imagesApi.util.updateQueryData(
|
||||||
|
'getImageDTO',
|
||||||
|
imageDTO.image_name,
|
||||||
|
(draft) => {
|
||||||
|
Object.assign(draft, { session_id });
|
||||||
|
}
|
||||||
|
)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await queryFulfilled;
|
await queryFulfilled;
|
||||||
@ -375,6 +439,15 @@ export const imagesApi = api.injectEndpoints({
|
|||||||
{ dispatch, queryFulfilled }
|
{ dispatch, queryFulfilled }
|
||||||
) {
|
) {
|
||||||
try {
|
try {
|
||||||
|
/**
|
||||||
|
* NOTE: PESSIMISTIC UPDATE
|
||||||
|
* Cache changes for `uploadImage`:
|
||||||
|
* - IF the image is an intermediate:
|
||||||
|
* - BAIL OUT
|
||||||
|
* - *add* to `getImageDTO`
|
||||||
|
* - *add* to no_board/assets
|
||||||
|
*/
|
||||||
|
|
||||||
const { data: imageDTO } = await queryFulfilled;
|
const { data: imageDTO } = await queryFulfilled;
|
||||||
|
|
||||||
if (imageDTO.is_intermediate) {
|
if (imageDTO.is_intermediate) {
|
||||||
@ -382,21 +455,37 @@ export const imagesApi = api.injectEndpoints({
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// determine `categories`, i.e. do we update "All Images" or "All Assets"
|
// *add* to `getImageDTO`
|
||||||
const categories = IMAGE_CATEGORIES.includes(image_category)
|
dispatch(
|
||||||
? IMAGE_CATEGORIES
|
imagesApi.util.upsertQueryData(
|
||||||
: ASSETS_CATEGORIES;
|
'getImageDTO',
|
||||||
|
imageDTO.image_name,
|
||||||
|
imageDTO
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
const queryArg = { categories };
|
// *add* to no_board/assets
|
||||||
|
dispatch(
|
||||||
|
imagesApi.util.updateQueryData(
|
||||||
|
'listImages',
|
||||||
|
{ board_id: 'none', categories: ASSETS_CATEGORIES },
|
||||||
|
(draft) => {
|
||||||
|
const oldTotal = draft.total;
|
||||||
|
const newState = imagesAdapter.addOne(draft, imageDTO);
|
||||||
|
const delta = newState.total - oldTotal;
|
||||||
|
draft.total = draft.total + delta;
|
||||||
|
}
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
dispatch(
|
dispatch(
|
||||||
imagesApi.util.updateQueryData('listImages', queryArg, (draft) => {
|
imagesApi.util.invalidateTags([
|
||||||
imagesAdapter.addOne(draft, imageDTO);
|
{ type: 'BoardImagesTotal', id: imageDTO.board_id ?? 'none' },
|
||||||
draft.total = draft.total + 1;
|
{ type: 'BoardAssetsTotal', id: imageDTO.board_id ?? 'none' },
|
||||||
})
|
])
|
||||||
);
|
);
|
||||||
} catch {
|
} catch {
|
||||||
// no-op
|
// query failed, no action needed
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
@ -412,107 +501,122 @@ export const imagesApi = api.injectEndpoints({
|
|||||||
body: { board_id, image_name },
|
body: { board_id, image_name },
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
invalidatesTags: (result, error, arg) => [
|
invalidatesTags: (result, error, { board_id, imageDTO }) => [
|
||||||
{ type: 'BoardImage' },
|
{ type: 'BoardImage' },
|
||||||
{ type: 'Board', id: arg.board_id },
|
{ type: 'Board', id: board_id },
|
||||||
|
{ type: 'BoardImagesTotal', id: board_id },
|
||||||
|
{ type: 'BoardImagesTotal', id: imageDTO.board_id ?? 'none' },
|
||||||
|
{ type: 'BoardAssetsTotal', id: board_id },
|
||||||
|
{ type: 'BoardAssetsTotal', id: imageDTO.board_id ?? 'none' },
|
||||||
],
|
],
|
||||||
async onQueryStarted(
|
async onQueryStarted(
|
||||||
{ board_id, imageDTO: oldImageDTO },
|
{ board_id, imageDTO },
|
||||||
{ dispatch, queryFulfilled, getState }
|
{ dispatch, queryFulfilled, getState }
|
||||||
) {
|
) {
|
||||||
/**
|
/**
|
||||||
* Cache changes for `addImageToBoard`:
|
* Cache changes for `addImageToBoard`:
|
||||||
* - *update* the `getImageDTO` cache
|
* - *update* getImageDTO
|
||||||
* - *remove* from "No Board"
|
* - IF it has an old board_id:
|
||||||
* - IF the image has an old `board_id`:
|
* - THEN *remove* from old board_id/[images|assets]
|
||||||
* - THEN *remove* from it's old `board_id`
|
* - ELSE *remove* from no_board/[images|assets]
|
||||||
* - IF the image's `created_at` is within the range of the board's cached images
|
* - $cache = board_id/[images|assets]
|
||||||
* - OR the board cache has length of 0 or 1
|
* - IF it eligible for insertion into existing $cache:
|
||||||
* - THEN *add* it to new `board_id`
|
* - THEN *add* to $cache
|
||||||
*/
|
*/
|
||||||
|
|
||||||
const { image_name, board_id: old_board_id } = oldImageDTO;
|
|
||||||
|
|
||||||
// Figure out the `listImages` caches that we need to update
|
|
||||||
const removeFromQueryArgs: ListImagesArgs[] = [];
|
|
||||||
|
|
||||||
// remove from "No Board"
|
|
||||||
removeFromQueryArgs.push({ board_id: 'none' });
|
|
||||||
|
|
||||||
// remove from old board
|
|
||||||
if (old_board_id) {
|
|
||||||
removeFromQueryArgs.push({ board_id: old_board_id });
|
|
||||||
}
|
|
||||||
|
|
||||||
// Store all patch results in case we need to roll back
|
|
||||||
const patches: PatchCollection[] = [];
|
const patches: PatchCollection[] = [];
|
||||||
|
const categories = getCategories(imageDTO);
|
||||||
|
|
||||||
// Updated imageDTO with new board_id
|
// *update* getImageDTO
|
||||||
const newImageDTO = { ...oldImageDTO, board_id };
|
|
||||||
|
|
||||||
// Update getImageDTO cache
|
|
||||||
patches.push(
|
patches.push(
|
||||||
dispatch(
|
dispatch(
|
||||||
imagesApi.util.updateQueryData(
|
imagesApi.util.updateQueryData(
|
||||||
'getImageDTO',
|
'getImageDTO',
|
||||||
image_name,
|
imageDTO.image_name,
|
||||||
(draft) => {
|
(draft) => {
|
||||||
Object.assign(draft, newImageDTO);
|
Object.assign(draft, { board_id });
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
// Do the "Remove from" cache updates
|
if (imageDTO.board_id) {
|
||||||
removeFromQueryArgs.forEach((queryArgs) => {
|
// *remove* from old board_id/[images|assets]
|
||||||
|
patches.push(
|
||||||
|
dispatch(
|
||||||
|
imagesApi.util.updateQueryData(
|
||||||
|
'listImages',
|
||||||
|
{
|
||||||
|
board_id: imageDTO.board_id,
|
||||||
|
categories,
|
||||||
|
},
|
||||||
|
(draft) => {
|
||||||
|
const oldTotal = draft.total;
|
||||||
|
const newState = imagesAdapter.removeOne(
|
||||||
|
draft,
|
||||||
|
imageDTO.image_name
|
||||||
|
);
|
||||||
|
const delta = newState.total - oldTotal;
|
||||||
|
draft.total = draft.total + delta;
|
||||||
|
}
|
||||||
|
)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
// *remove* from no_board/[images|assets]
|
||||||
|
patches.push(
|
||||||
|
dispatch(
|
||||||
|
imagesApi.util.updateQueryData(
|
||||||
|
'listImages',
|
||||||
|
{
|
||||||
|
board_id: 'none',
|
||||||
|
categories,
|
||||||
|
},
|
||||||
|
(draft) => {
|
||||||
|
imagesAdapter.removeOne(draft, imageDTO.image_name);
|
||||||
|
}
|
||||||
|
)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// $cache = board_id/[images|assets]
|
||||||
|
const queryArgs = { board_id, categories };
|
||||||
|
const currentCache = imagesApi.endpoints.listImages.select(queryArgs)(
|
||||||
|
getState()
|
||||||
|
);
|
||||||
|
|
||||||
|
// IF it eligible for insertion into existing $cache
|
||||||
|
// "eligible" means either:
|
||||||
|
// - The cache is fully populated, with all images in the db cached
|
||||||
|
// OR
|
||||||
|
// - The image's `created_at` is within the range of the cached images
|
||||||
|
|
||||||
|
const isCacheFullyPopulated =
|
||||||
|
currentCache.data &&
|
||||||
|
currentCache.data.ids.length >= currentCache.data.total;
|
||||||
|
|
||||||
|
const isInDateRange = getIsImageInDateRange(
|
||||||
|
currentCache.data,
|
||||||
|
imageDTO
|
||||||
|
);
|
||||||
|
|
||||||
|
if (isCacheFullyPopulated || isInDateRange) {
|
||||||
|
// THEN *add* to $cache
|
||||||
patches.push(
|
patches.push(
|
||||||
dispatch(
|
dispatch(
|
||||||
imagesApi.util.updateQueryData(
|
imagesApi.util.updateQueryData(
|
||||||
'listImages',
|
'listImages',
|
||||||
queryArgs,
|
queryArgs,
|
||||||
(draft) => {
|
(draft) => {
|
||||||
// sanity check
|
const oldTotal = draft.total;
|
||||||
if (draft.ids.includes(image_name)) {
|
const newState = imagesAdapter.addOne(draft, imageDTO);
|
||||||
imagesAdapter.removeOne(draft, image_name);
|
const delta = newState.total - oldTotal;
|
||||||
draft.total = Math.max(draft.total - 1, 0);
|
draft.total = draft.total + delta;
|
||||||
}
|
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
});
|
|
||||||
|
|
||||||
// We only need to add to the cache if the board is not a system board
|
|
||||||
if (!SYSTEM_BOARDS.includes(board_id)) {
|
|
||||||
const queryArgs = { board_id };
|
|
||||||
const { data } = imagesApi.endpoints.listImages.select(queryArgs)(
|
|
||||||
getState()
|
|
||||||
);
|
|
||||||
|
|
||||||
const cacheAction = getCacheAction(data, oldImageDTO);
|
|
||||||
|
|
||||||
if (['add', 'update'].includes(cacheAction)) {
|
|
||||||
// Do the "Add to" cache updates
|
|
||||||
patches.push(
|
|
||||||
dispatch(
|
|
||||||
imagesApi.util.updateQueryData(
|
|
||||||
'listImages',
|
|
||||||
queryArgs,
|
|
||||||
(draft) => {
|
|
||||||
if (cacheAction === 'add') {
|
|
||||||
imagesAdapter.addOne(draft, newImageDTO);
|
|
||||||
draft.total += 1;
|
|
||||||
} else {
|
|
||||||
imagesAdapter.updateOne(draft, {
|
|
||||||
id: image_name,
|
|
||||||
changes: { board_id },
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@ -531,87 +635,98 @@ export const imagesApi = api.injectEndpoints({
|
|||||||
body: { board_id, image_name },
|
body: { board_id, image_name },
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
invalidatesTags: (result, error, arg) => [
|
invalidatesTags: (result, error, { imageDTO }) => [
|
||||||
{ type: 'BoardImage' },
|
{ type: 'BoardImage' },
|
||||||
{ type: 'Board', id: arg.imageDTO.board_id },
|
{ type: 'Board', id: imageDTO.board_id },
|
||||||
|
{ type: 'BoardImagesTotal', id: imageDTO.board_id },
|
||||||
|
{ type: 'BoardImagesTotal', id: 'none' },
|
||||||
|
{ type: 'BoardAssetsTotal', id: imageDTO.board_id },
|
||||||
|
{ type: 'BoardAssetsTotal', id: 'none' },
|
||||||
],
|
],
|
||||||
async onQueryStarted(
|
async onQueryStarted(
|
||||||
{ imageDTO },
|
{ imageDTO },
|
||||||
{ dispatch, queryFulfilled, getState }
|
{ dispatch, queryFulfilled, getState }
|
||||||
) {
|
) {
|
||||||
/**
|
/**
|
||||||
* Cache changes for `removeImageFromBoard`:
|
* Cache changes for removeImageFromBoard:
|
||||||
* - *update* `getImageDTO`
|
* - *update* getImageDTO
|
||||||
* - IF the image's `created_at` is within the range of the board's cached images
|
* - *remove* from board_id/[images|assets]
|
||||||
* - THEN *add* to "No Board"
|
* - $cache = no_board/[images|assets]
|
||||||
* - *remove* from `old_board_id`
|
* - IF it eligible for insertion into existing $cache:
|
||||||
|
* - THEN *upsert* to $cache
|
||||||
*/
|
*/
|
||||||
|
|
||||||
const { image_name, board_id: old_board_id } = imageDTO;
|
const categories = getCategories(imageDTO);
|
||||||
|
|
||||||
const patches: PatchCollection[] = [];
|
const patches: PatchCollection[] = [];
|
||||||
|
|
||||||
// Updated imageDTO with new board_id
|
// *update* getImageDTO
|
||||||
const newImageDTO = { ...imageDTO, board_id: undefined };
|
|
||||||
|
|
||||||
// Update getImageDTO cache
|
|
||||||
patches.push(
|
patches.push(
|
||||||
dispatch(
|
dispatch(
|
||||||
imagesApi.util.updateQueryData(
|
imagesApi.util.updateQueryData(
|
||||||
'getImageDTO',
|
'getImageDTO',
|
||||||
image_name,
|
imageDTO.image_name,
|
||||||
(draft) => {
|
(draft) => {
|
||||||
Object.assign(draft, newImageDTO);
|
Object.assign(draft, { board_id: undefined });
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
// Remove from old board
|
// *remove* from board_id/[images|assets]
|
||||||
if (old_board_id) {
|
patches.push(
|
||||||
const oldBoardQueryArgs = { board_id: old_board_id };
|
dispatch(
|
||||||
patches.push(
|
imagesApi.util.updateQueryData(
|
||||||
dispatch(
|
'listImages',
|
||||||
imagesApi.util.updateQueryData(
|
{
|
||||||
'listImages',
|
board_id: imageDTO.board_id,
|
||||||
oldBoardQueryArgs,
|
categories,
|
||||||
(draft) => {
|
},
|
||||||
// sanity check
|
(draft) => {
|
||||||
if (draft.ids.includes(image_name)) {
|
const oldTotal = draft.total;
|
||||||
imagesAdapter.removeOne(draft, image_name);
|
const newState = imagesAdapter.removeOne(
|
||||||
draft.total = Math.max(draft.total - 1, 0);
|
draft,
|
||||||
}
|
imageDTO.image_name
|
||||||
}
|
);
|
||||||
)
|
const delta = newState.total - oldTotal;
|
||||||
|
draft.total = draft.total + delta;
|
||||||
|
}
|
||||||
)
|
)
|
||||||
);
|
)
|
||||||
}
|
);
|
||||||
|
|
||||||
// Add to "No Board"
|
// $cache = no_board/[images|assets]
|
||||||
const noBoardQueryArgs = { board_id: 'none' };
|
const queryArgs = { board_id: 'none', categories };
|
||||||
const { data } = imagesApi.endpoints.listImages.select(
|
const currentCache = imagesApi.endpoints.listImages.select(queryArgs)(
|
||||||
noBoardQueryArgs
|
getState()
|
||||||
)(getState());
|
);
|
||||||
|
|
||||||
// Check if we need to make any cache changes
|
// IF it eligible for insertion into existing $cache
|
||||||
const cacheAction = getCacheAction(data, imageDTO);
|
// "eligible" means either:
|
||||||
|
// - The cache is fully populated, with all images in the db cached
|
||||||
|
// OR
|
||||||
|
// - The image's `created_at` is within the range of the cached images
|
||||||
|
|
||||||
if (['add', 'update'].includes(cacheAction)) {
|
const isCacheFullyPopulated =
|
||||||
|
currentCache.data &&
|
||||||
|
currentCache.data.ids.length >= currentCache.data.total;
|
||||||
|
|
||||||
|
const isInDateRange = getIsImageInDateRange(
|
||||||
|
currentCache.data,
|
||||||
|
imageDTO
|
||||||
|
);
|
||||||
|
|
||||||
|
if (isCacheFullyPopulated || isInDateRange) {
|
||||||
|
// THEN *upsert* to $cache
|
||||||
patches.push(
|
patches.push(
|
||||||
dispatch(
|
dispatch(
|
||||||
imagesApi.util.updateQueryData(
|
imagesApi.util.updateQueryData(
|
||||||
'listImages',
|
'listImages',
|
||||||
noBoardQueryArgs,
|
queryArgs,
|
||||||
(draft) => {
|
(draft) => {
|
||||||
if (cacheAction === 'add') {
|
const oldTotal = draft.total;
|
||||||
imagesAdapter.addOne(draft, imageDTO);
|
const newState = imagesAdapter.upsertOne(draft, imageDTO);
|
||||||
draft.total += 1;
|
const delta = newState.total - oldTotal;
|
||||||
} else {
|
draft.total = draft.total + delta;
|
||||||
imagesAdapter.updateOne(draft, {
|
|
||||||
id: image_name,
|
|
||||||
changes: { board_id: undefined },
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
@ -635,7 +750,9 @@ export const {
|
|||||||
useGetImageDTOQuery,
|
useGetImageDTOQuery,
|
||||||
useGetImageMetadataQuery,
|
useGetImageMetadataQuery,
|
||||||
useDeleteImageMutation,
|
useDeleteImageMutation,
|
||||||
useUpdateImageMutation,
|
// useUpdateImageMutation,
|
||||||
|
useGetBoardImagesTotalQuery,
|
||||||
|
useGetBoardAssetsTotalQuery,
|
||||||
useUploadImageMutation,
|
useUploadImageMutation,
|
||||||
useAddImageToBoardMutation,
|
useAddImageToBoardMutation,
|
||||||
useRemoveImageFromBoardMutation,
|
useRemoveImageFromBoardMutation,
|
||||||
|
@ -4,19 +4,8 @@ import { useListAllBoardsQuery } from '../endpoints/boards';
|
|||||||
export const useBoardName = (board_id: BoardId | null | undefined) => {
|
export const useBoardName = (board_id: BoardId | null | undefined) => {
|
||||||
const { boardName } = useListAllBoardsQuery(undefined, {
|
const { boardName } = useListAllBoardsQuery(undefined, {
|
||||||
selectFromResult: ({ data }) => {
|
selectFromResult: ({ data }) => {
|
||||||
let boardName = '';
|
const selectedBoard = data?.find((b) => b.board_id === board_id);
|
||||||
if (board_id === 'images') {
|
const boardName = selectedBoard?.board_name || 'Uncategorized';
|
||||||
boardName = 'Images';
|
|
||||||
} else if (board_id === 'assets') {
|
|
||||||
boardName = 'Assets';
|
|
||||||
} else if (board_id === 'no_board') {
|
|
||||||
boardName = 'No Board';
|
|
||||||
} else if (board_id === 'batch') {
|
|
||||||
boardName = 'Batch';
|
|
||||||
} else {
|
|
||||||
const selectedBoard = data?.find((b) => b.board_id === board_id);
|
|
||||||
boardName = selectedBoard?.board_name || 'Unknown Board';
|
|
||||||
}
|
|
||||||
|
|
||||||
return { boardName };
|
return { boardName };
|
||||||
},
|
},
|
||||||
|
@ -1,53 +1,45 @@
|
|||||||
import { skipToken } from '@reduxjs/toolkit/dist/query';
|
|
||||||
import {
|
import {
|
||||||
ASSETS_CATEGORIES,
|
|
||||||
BoardId,
|
BoardId,
|
||||||
IMAGE_CATEGORIES,
|
|
||||||
INITIAL_IMAGE_LIMIT,
|
INITIAL_IMAGE_LIMIT,
|
||||||
} from 'features/gallery/store/gallerySlice';
|
} from 'features/gallery/store/gallerySlice';
|
||||||
import { useMemo } from 'react';
|
import {
|
||||||
import { ListImagesArgs, useListImagesQuery } from '../endpoints/images';
|
ListImagesArgs,
|
||||||
|
useGetBoardAssetsTotalQuery,
|
||||||
|
useGetBoardImagesTotalQuery,
|
||||||
|
} from '../endpoints/images';
|
||||||
|
|
||||||
const baseQueryArg: ListImagesArgs = {
|
const baseQueryArgs: ListImagesArgs = {
|
||||||
offset: 0,
|
offset: 0,
|
||||||
limit: INITIAL_IMAGE_LIMIT,
|
limit: INITIAL_IMAGE_LIMIT,
|
||||||
is_intermediate: false,
|
is_intermediate: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
const imagesQueryArg: ListImagesArgs = {
|
export const useBoardTotal = (board_id: BoardId) => {
|
||||||
categories: IMAGE_CATEGORIES,
|
const { data: totalImages } = useGetBoardImagesTotalQuery(board_id);
|
||||||
...baseQueryArg,
|
const { data: totalAssets } = useGetBoardAssetsTotalQuery(board_id);
|
||||||
};
|
// const imagesQueryArg = useMemo(() => {
|
||||||
|
// const categories = IMAGE_CATEGORIES;
|
||||||
const assetsQueryArg: ListImagesArgs = {
|
// return { board_id, categories, ...baseQueryArgs };
|
||||||
categories: ASSETS_CATEGORIES,
|
// }, [board_id]);
|
||||||
...baseQueryArg,
|
|
||||||
};
|
// const assetsQueryArg = useMemo(() => {
|
||||||
|
// const categories = ASSETS_CATEGORIES;
|
||||||
const noBoardQueryArg: ListImagesArgs = {
|
// return { board_id, categories, ...baseQueryArgs };
|
||||||
board_id: 'none',
|
// }, [board_id]);
|
||||||
...baseQueryArg,
|
|
||||||
};
|
// const { total: totalImages } = useListImagesQuery(
|
||||||
|
// imagesQueryArg ?? skipToken,
|
||||||
export const useBoardTotal = (board_id: BoardId | null | undefined) => {
|
// {
|
||||||
const queryArg = useMemo(() => {
|
// selectFromResult: ({ currentData }) => ({ total: currentData?.total }),
|
||||||
if (!board_id) {
|
// }
|
||||||
return;
|
// );
|
||||||
}
|
|
||||||
if (board_id === 'images') {
|
// const { total: totalAssets } = useListImagesQuery(
|
||||||
return imagesQueryArg;
|
// assetsQueryArg ?? skipToken,
|
||||||
} else if (board_id === 'assets') {
|
// {
|
||||||
return assetsQueryArg;
|
// selectFromResult: ({ currentData }) => ({ total: currentData?.total }),
|
||||||
} else if (board_id === 'no_board') {
|
// }
|
||||||
return noBoardQueryArg;
|
// );
|
||||||
} else {
|
|
||||||
return { board_id, ...baseQueryArg };
|
return { totalImages, totalAssets };
|
||||||
}
|
|
||||||
}, [board_id]);
|
|
||||||
|
|
||||||
const { total } = useListImagesQuery(queryArg ?? skipToken, {
|
|
||||||
selectFromResult: ({ currentData }) => ({ total: currentData?.total }),
|
|
||||||
});
|
|
||||||
|
|
||||||
return total;
|
|
||||||
};
|
};
|
||||||
|
@ -10,6 +10,8 @@ import { $authToken, $baseUrl } from 'services/api/client';
|
|||||||
|
|
||||||
export const tagTypes = [
|
export const tagTypes = [
|
||||||
'Board',
|
'Board',
|
||||||
|
'BoardImagesTotal',
|
||||||
|
'BoardAssetsTotal',
|
||||||
'Image',
|
'Image',
|
||||||
'ImageNameList',
|
'ImageNameList',
|
||||||
'ImageList',
|
'ImageList',
|
||||||
|
Loading…
x
Reference in New Issue
Block a user