mirror of
https://github.com/invoke-ai/InvokeAI
synced 2024-08-30 20:32:17 +00:00
ui: boards 2: electric boogaloo (#3869)
## What type of PR is this? (check all applicable) - [x] Refactor - [ ] Feature - [ ] Bug Fix - [ ] Optimization - [ ] Documentation Update - [ ] Community Node Submission ## Have you discussed this change with the InvokeAI team? - [x] Yes - [ ] No, because: ## Description Revised boards logic and UI ## Related Tickets & Documents <!-- For pull requests that relate or close an issue, please include them below. For example having the text: "closes #1234" would connect the current pull request to issue 1234. And when we merge the pull request, Github will automatically close the issue. --> - Related Issue # discord convos - Closes # ## QA Instructions, Screenshots, Recordings <!-- Please provide steps on how to test changes, any hardware or software specifications as well as any other pertinent information. --> ## Added/updated tests? - [ ] Yes - [x] No : n/a ## [optional] Are there any post deployment tasks we need to perform?
This commit is contained in:
commit
fba4085939
@ -175,9 +175,7 @@ export const isValidDrop = (
|
||||
const destinationBoard = overData.context.boardId;
|
||||
|
||||
const isSameBoard = currentBoard === destinationBoard;
|
||||
const isDestinationValid = !currentBoard
|
||||
? destinationBoard !== 'no_board'
|
||||
: true;
|
||||
const isDestinationValid = !currentBoard ? destinationBoard : true;
|
||||
|
||||
return !isSameBoard && isDestinationValid;
|
||||
}
|
||||
|
@ -1,20 +1,20 @@
|
||||
import { log } from 'app/logging/useLogger';
|
||||
import {
|
||||
ASSETS_CATEGORIES,
|
||||
IMAGE_CATEGORIES,
|
||||
boardIdSelected,
|
||||
galleryViewChanged,
|
||||
imageSelected,
|
||||
} from 'features/gallery/store/gallerySlice';
|
||||
import {
|
||||
getBoardIdQueryParamForBoard,
|
||||
getCategoriesQueryParamForBoard,
|
||||
} from 'features/gallery/store/util';
|
||||
import { imagesApi } from 'services/api/endpoints/images';
|
||||
import { startAppListening } from '..';
|
||||
import { isAnyOf } from '@reduxjs/toolkit';
|
||||
|
||||
const moduleLog = log.child({ namespace: 'boards' });
|
||||
|
||||
export const addBoardIdSelectedListener = () => {
|
||||
startAppListening({
|
||||
actionCreator: boardIdSelected,
|
||||
matcher: isAnyOf(boardIdSelected, galleryViewChanged),
|
||||
effect: async (
|
||||
action,
|
||||
{ getState, dispatch, condition, cancelActiveListeners }
|
||||
@ -22,12 +22,21 @@ export const addBoardIdSelectedListener = () => {
|
||||
// Cancel any in-progress instances of this listener, we don't want to select an image from a previous board
|
||||
cancelActiveListeners();
|
||||
|
||||
const _board_id = action.payload;
|
||||
// when a board is selected, we need to wait until the board has loaded *some* images, then select the first one
|
||||
const state = getState();
|
||||
|
||||
const categories = getCategoriesQueryParamForBoard(_board_id);
|
||||
const board_id = getBoardIdQueryParamForBoard(_board_id);
|
||||
const queryArgs = { board_id, categories };
|
||||
const board_id = boardIdSelected.match(action)
|
||||
? action.payload
|
||||
: state.gallery.selectedBoardId;
|
||||
|
||||
const galleryView = galleryViewChanged.match(action)
|
||||
? action.payload
|
||||
: state.gallery.galleryView;
|
||||
|
||||
// when a board is selected, we need to wait until the board has loaded *some* images, then select the first one
|
||||
const categories =
|
||||
galleryView === 'images' ? IMAGE_CATEGORIES : ASSETS_CATEGORIES;
|
||||
|
||||
const queryArgs = { board_id: board_id ?? 'none', categories };
|
||||
|
||||
// wait until the board has some images - maybe it already has some from a previous fetch
|
||||
// must use getState() to ensure we do not have stale state
|
||||
|
@ -156,14 +156,13 @@ export const addImageDroppedListener = () => {
|
||||
if (
|
||||
overData.actionType === 'MOVE_BOARD' &&
|
||||
activeData.payloadType === 'IMAGE_DTO' &&
|
||||
activeData.payload.imageDTO &&
|
||||
overData.context.boardId
|
||||
activeData.payload.imageDTO
|
||||
) {
|
||||
const { imageDTO } = activeData.payload;
|
||||
const { boardId } = overData.context;
|
||||
|
||||
// if the board is "No Board", this is a remove action
|
||||
if (boardId === 'no_board') {
|
||||
// image was droppe on the "NoBoardBoard"
|
||||
if (!boardId) {
|
||||
dispatch(
|
||||
imagesApi.endpoints.removeImageFromBoard.initiate({
|
||||
imageDTO,
|
||||
@ -172,12 +171,7 @@ export const addImageDroppedListener = () => {
|
||||
return;
|
||||
}
|
||||
|
||||
// Handle adding image to batch
|
||||
if (boardId === 'batch') {
|
||||
// TODO
|
||||
}
|
||||
|
||||
// Otherwise, add the image to the board
|
||||
// image was dropped on a user board
|
||||
dispatch(
|
||||
imagesApi.endpoints.addImageToBoard.initiate({
|
||||
imageDTO,
|
||||
|
@ -5,30 +5,30 @@ import { startAppListening } from '..';
|
||||
const moduleLog = log.child({ namespace: 'image' });
|
||||
|
||||
export const addImageUpdatedFulfilledListener = () => {
|
||||
startAppListening({
|
||||
matcher: imagesApi.endpoints.updateImage.matchFulfilled,
|
||||
effect: (action, { dispatch, getState }) => {
|
||||
moduleLog.debug(
|
||||
{
|
||||
data: {
|
||||
oldImage: action.meta.arg.originalArgs,
|
||||
updatedImage: action.payload,
|
||||
},
|
||||
},
|
||||
'Image updated'
|
||||
);
|
||||
},
|
||||
});
|
||||
// startAppListening({
|
||||
// matcher: imagesApi.endpoints.updateImage.matchFulfilled,
|
||||
// effect: (action, { dispatch, getState }) => {
|
||||
// moduleLog.debug(
|
||||
// {
|
||||
// data: {
|
||||
// oldImage: action.meta.arg.originalArgs,
|
||||
// updatedImage: action.payload,
|
||||
// },
|
||||
// },
|
||||
// 'Image updated'
|
||||
// );
|
||||
// },
|
||||
// });
|
||||
};
|
||||
|
||||
export const addImageUpdatedRejectedListener = () => {
|
||||
startAppListening({
|
||||
matcher: imagesApi.endpoints.updateImage.matchRejected,
|
||||
effect: (action, { dispatch }) => {
|
||||
moduleLog.debug(
|
||||
{ data: action.meta.arg.originalArgs },
|
||||
'Image update failed'
|
||||
);
|
||||
},
|
||||
});
|
||||
// startAppListening({
|
||||
// matcher: imagesApi.endpoints.updateImage.matchRejected,
|
||||
// effect: (action, { dispatch }) => {
|
||||
// moduleLog.debug(
|
||||
// { data: action.meta.arg.originalArgs },
|
||||
// 'Image update failed'
|
||||
// );
|
||||
// },
|
||||
// });
|
||||
};
|
||||
|
@ -3,6 +3,7 @@ import { addImageToStagingArea } from 'features/canvas/store/canvasSlice';
|
||||
import {
|
||||
IMAGE_CATEGORIES,
|
||||
boardIdSelected,
|
||||
galleryViewChanged,
|
||||
imageSelected,
|
||||
} from 'features/gallery/store/gallerySlice';
|
||||
import { progressImageSet } from 'features/system/store/systemSlice';
|
||||
@ -55,34 +56,6 @@ export const addInvocationCompleteEventListener = () => {
|
||||
}
|
||||
|
||||
if (!imageDTO.is_intermediate) {
|
||||
// update the cache for 'All Images'
|
||||
dispatch(
|
||||
imagesApi.util.updateQueryData(
|
||||
'listImages',
|
||||
{
|
||||
categories: IMAGE_CATEGORIES,
|
||||
},
|
||||
(draft) => {
|
||||
imagesAdapter.addOne(draft, imageDTO);
|
||||
draft.total = draft.total + 1;
|
||||
}
|
||||
)
|
||||
);
|
||||
|
||||
// update the cache for 'No Board'
|
||||
dispatch(
|
||||
imagesApi.util.updateQueryData(
|
||||
'listImages',
|
||||
{
|
||||
board_id: 'none',
|
||||
},
|
||||
(draft) => {
|
||||
imagesAdapter.addOne(draft, imageDTO);
|
||||
draft.total = draft.total + 1;
|
||||
}
|
||||
)
|
||||
);
|
||||
|
||||
const { autoAddBoardId } = gallery;
|
||||
|
||||
// add image to the board if auto-add is enabled
|
||||
@ -93,8 +66,31 @@ export const addInvocationCompleteEventListener = () => {
|
||||
imageDTO,
|
||||
})
|
||||
);
|
||||
} else {
|
||||
// add to no board board
|
||||
// update the cache for 'No Board'
|
||||
dispatch(
|
||||
imagesApi.util.updateQueryData(
|
||||
'listImages',
|
||||
{
|
||||
board_id: 'none',
|
||||
categories: IMAGE_CATEGORIES,
|
||||
},
|
||||
(draft) => {
|
||||
imagesAdapter.addOne(draft, imageDTO);
|
||||
draft.total = draft.total + 1;
|
||||
}
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
dispatch(
|
||||
imagesApi.util.invalidateTags([
|
||||
{ type: 'BoardImagesTotal', id: autoAddBoardId ?? 'none' },
|
||||
{ type: 'BoardAssetsTotal', id: autoAddBoardId ?? 'none' },
|
||||
])
|
||||
);
|
||||
|
||||
const { selectedBoardId, shouldAutoSwitch } = gallery;
|
||||
|
||||
// If auto-switch is enabled, select the new image
|
||||
@ -102,8 +98,9 @@ export const addInvocationCompleteEventListener = () => {
|
||||
// if auto-add is enabled, switch the board as the image comes in
|
||||
if (autoAddBoardId && autoAddBoardId !== selectedBoardId) {
|
||||
dispatch(boardIdSelected(autoAddBoardId));
|
||||
dispatch(galleryViewChanged('images'));
|
||||
} else if (!autoAddBoardId) {
|
||||
dispatch(boardIdSelected('images'));
|
||||
dispatch(galleryViewChanged('images'));
|
||||
}
|
||||
dispatch(imageSelected(imageDTO.image_name));
|
||||
}
|
||||
|
@ -0,0 +1,23 @@
|
||||
import { Badge, Flex } from '@chakra-ui/react';
|
||||
|
||||
const AutoAddIcon = () => {
|
||||
return (
|
||||
<Flex
|
||||
sx={{
|
||||
position: 'absolute',
|
||||
insetInlineStart: 0,
|
||||
top: 0,
|
||||
p: 1,
|
||||
}}
|
||||
>
|
||||
<Badge
|
||||
variant="solid"
|
||||
sx={{ fontSize: 10, bg: 'accent.400', _dark: { bg: 'accent.500' } }}
|
||||
>
|
||||
auto
|
||||
</Badge>
|
||||
</Flex>
|
||||
);
|
||||
};
|
||||
|
||||
export default AutoAddIcon;
|
@ -52,7 +52,7 @@ const BoardAutoAddSelect = () => {
|
||||
return;
|
||||
}
|
||||
|
||||
dispatch(autoAddBoardIdChanged(v === 'none' ? null : v));
|
||||
dispatch(autoAddBoardIdChanged(v === 'none' ? undefined : v));
|
||||
},
|
||||
[dispatch]
|
||||
);
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { Box, MenuItem, MenuList } from '@chakra-ui/react';
|
||||
import { MenuItem, MenuList } from '@chakra-ui/react';
|
||||
import { useAppDispatch } from 'app/store/storeHooks';
|
||||
import { ContextMenu, ContextMenuProps } from 'chakra-ui-contextmenu';
|
||||
import { boardIdSelected } from 'features/gallery/store/gallerySlice';
|
||||
@ -7,11 +7,11 @@ import { FaFolder } from 'react-icons/fa';
|
||||
import { BoardDTO } from 'services/api/types';
|
||||
import { menuListMotionProps } from 'theme/components/menu';
|
||||
import GalleryBoardContextMenuItems from './GalleryBoardContextMenuItems';
|
||||
import SystemBoardContextMenuItems from './SystemBoardContextMenuItems';
|
||||
import NoBoardContextMenuItems from './NoBoardContextMenuItems';
|
||||
|
||||
type Props = {
|
||||
board?: BoardDTO;
|
||||
board_id: string;
|
||||
board_id?: string;
|
||||
children: ContextMenuProps<HTMLDivElement>['children'];
|
||||
setBoardToDelete?: (board?: BoardDTO) => void;
|
||||
};
|
||||
@ -19,9 +19,11 @@ type Props = {
|
||||
const BoardContextMenu = memo(
|
||||
({ board, board_id, setBoardToDelete, children }: Props) => {
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
const handleSelectBoard = useCallback(() => {
|
||||
dispatch(boardIdSelected(board?.board_id ?? board_id));
|
||||
}, [board?.board_id, board_id, dispatch]);
|
||||
|
||||
return (
|
||||
<ContextMenu<HTMLDivElement>
|
||||
menuProps={{ size: 'sm', isLazy: true }}
|
||||
@ -37,7 +39,7 @@ const BoardContextMenu = memo(
|
||||
<MenuItem icon={<FaFolder />} onClickCapture={handleSelectBoard}>
|
||||
Select Board
|
||||
</MenuItem>
|
||||
{!board && <SystemBoardContextMenuItems board_id={board_id} />}
|
||||
{!board && <NoBoardContextMenuItems />}
|
||||
{board && (
|
||||
<GalleryBoardContextMenuItems
|
||||
board={board}
|
||||
|
@ -16,6 +16,7 @@ import AddBoardButton from './AddBoardButton';
|
||||
import BoardsSearch from './BoardsSearch';
|
||||
import GalleryBoard from './GalleryBoard';
|
||||
import SystemBoardButton from './SystemBoardButton';
|
||||
import NoBoardBoard from './NoBoardBoard';
|
||||
|
||||
const selector = createSelector(
|
||||
[stateSelector],
|
||||
@ -42,10 +43,6 @@ const BoardsList = (props: Props) => {
|
||||
)
|
||||
: boards;
|
||||
const [boardToDelete, setBoardToDelete] = useState<BoardDTO>();
|
||||
const [isSearching, setIsSearching] = useState(false);
|
||||
const handleClickSearchIcon = useCallback(() => {
|
||||
setIsSearching((v) => !v);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<>
|
||||
@ -61,54 +58,7 @@ const BoardsList = (props: Props) => {
|
||||
}}
|
||||
>
|
||||
<Flex sx={{ gap: 2, alignItems: 'center' }}>
|
||||
<AnimatePresence mode="popLayout">
|
||||
{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 />}
|
||||
/>
|
||||
<BoardsSearch />
|
||||
<AddBoardButton />
|
||||
</Flex>
|
||||
<OverlayScrollbarsComponent
|
||||
@ -130,6 +80,9 @@ const BoardsList = (props: Props) => {
|
||||
maxH: 346,
|
||||
}}
|
||||
>
|
||||
<GridItem sx={{ p: 1.5 }}>
|
||||
<NoBoardBoard isSelected={selectedBoardId === undefined} />
|
||||
</GridItem>
|
||||
{filteredBoards &&
|
||||
filteredBoards.map((board) => (
|
||||
<GridItem key={board.board_id} sx={{ p: 1.5 }}>
|
||||
|
@ -28,12 +28,7 @@ const selector = createSelector(
|
||||
defaultSelectorOptions
|
||||
);
|
||||
|
||||
type Props = {
|
||||
setIsSearching: (isSearching: boolean) => void;
|
||||
};
|
||||
|
||||
const BoardsSearch = (props: Props) => {
|
||||
const { setIsSearching } = props;
|
||||
const BoardsSearch = () => {
|
||||
const dispatch = useAppDispatch();
|
||||
const { searchText } = useAppSelector(selector);
|
||||
const inputRef = useRef<HTMLInputElement>(null);
|
||||
@ -47,8 +42,7 @@ const BoardsSearch = (props: Props) => {
|
||||
|
||||
const clearBoardSearch = useCallback(() => {
|
||||
dispatch(setBoardSearchText(''));
|
||||
setIsSearching(false);
|
||||
}, [dispatch, setIsSearching]);
|
||||
}, [dispatch]);
|
||||
|
||||
const handleKeydown = useCallback(
|
||||
(e: KeyboardEvent<HTMLInputElement>) => {
|
||||
|
@ -19,17 +19,14 @@ import { defaultSelectorOptions } from 'app/store/util/defaultMemoizeOptions';
|
||||
import IAIDroppable from 'common/components/IAIDroppable';
|
||||
import { boardIdSelected } from 'features/gallery/store/gallerySlice';
|
||||
import { memo, useCallback, useMemo, useState } from 'react';
|
||||
import { FaFolder } from 'react-icons/fa';
|
||||
import { FaUser } from 'react-icons/fa';
|
||||
import { useUpdateBoardMutation } from 'services/api/endpoints/boards';
|
||||
import { useGetImageDTOQuery } from 'services/api/endpoints/images';
|
||||
import { useBoardTotal } from 'services/api/hooks/useBoardTotal';
|
||||
import { BoardDTO } from 'services/api/types';
|
||||
import AutoAddIcon from '../AutoAddIcon';
|
||||
import BoardContextMenu from '../BoardContextMenu';
|
||||
|
||||
const AUTO_ADD_BADGE_STYLES: ChakraProps['sx'] = {
|
||||
bg: 'accent.200',
|
||||
color: 'blackAlpha.900',
|
||||
};
|
||||
|
||||
const BASE_BADGE_STYLES: ChakraProps['sx'] = {
|
||||
bg: 'base.500',
|
||||
color: 'whiteAlpha.900',
|
||||
@ -64,6 +61,8 @@ const GalleryBoard = memo(
|
||||
board.cover_image_name ?? skipToken
|
||||
);
|
||||
|
||||
const { totalImages, totalAssets } = useBoardTotal(board.board_id);
|
||||
|
||||
const { board_name, board_id } = board;
|
||||
const [localBoardName, setLocalBoardName] = useState(board_name);
|
||||
|
||||
@ -143,56 +142,48 @@ const GalleryBoard = memo(
|
||||
alignItems: 'center',
|
||||
borderRadius: 'base',
|
||||
cursor: 'pointer',
|
||||
bg: 'base.200',
|
||||
_dark: {
|
||||
bg: 'base.800',
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Flex
|
||||
sx={{
|
||||
w: 'full',
|
||||
h: 'full',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
borderRadius: 'base',
|
||||
bg: 'base.200',
|
||||
_dark: {
|
||||
bg: 'base.800',
|
||||
},
|
||||
}}
|
||||
>
|
||||
{coverImage?.thumbnail_url ? (
|
||||
<Image
|
||||
src={coverImage?.thumbnail_url}
|
||||
draggable={false}
|
||||
{coverImage?.thumbnail_url ? (
|
||||
<Image
|
||||
src={coverImage?.thumbnail_url}
|
||||
draggable={false}
|
||||
sx={{
|
||||
objectFit: 'cover',
|
||||
w: 'full',
|
||||
h: 'full',
|
||||
maxH: 'full',
|
||||
borderRadius: 'base',
|
||||
borderBottomRadius: 'lg',
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<Flex
|
||||
sx={{
|
||||
w: 'full',
|
||||
h: 'full',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
}}
|
||||
>
|
||||
<Icon
|
||||
boxSize={12}
|
||||
as={FaUser}
|
||||
sx={{
|
||||
maxW: 'full',
|
||||
maxH: 'full',
|
||||
borderRadius: 'base',
|
||||
borderBottomRadius: 'lg',
|
||||
mt: -3,
|
||||
opacity: 0.7,
|
||||
color: 'base.500',
|
||||
_dark: {
|
||||
color: 'base.500',
|
||||
},
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<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={{
|
||||
position: 'absolute',
|
||||
@ -201,17 +192,11 @@ const GalleryBoard = memo(
|
||||
p: 1,
|
||||
}}
|
||||
>
|
||||
<Badge
|
||||
variant="solid"
|
||||
sx={
|
||||
isSelectedForAutoAdd
|
||||
? AUTO_ADD_BADGE_STYLES
|
||||
: BASE_BADGE_STYLES
|
||||
}
|
||||
>
|
||||
{board.image_count}
|
||||
<Badge variant="solid" sx={BASE_BADGE_STYLES}>
|
||||
{totalImages}/{totalAssets}
|
||||
</Badge>
|
||||
</Flex>
|
||||
{isSelectedForAutoAdd && <AutoAddIcon />}
|
||||
<Box
|
||||
className="selection-box"
|
||||
sx={{
|
||||
|
@ -1,54 +1,145 @@
|
||||
import { Text } from '@chakra-ui/react';
|
||||
import { Badge, Box, ChakraProps, Flex, Icon, Text } from '@chakra-ui/react';
|
||||
import { createSelector } from '@reduxjs/toolkit';
|
||||
import { MoveBoardDropData } from 'app/components/ImageDnd/typesafeDnd';
|
||||
import {
|
||||
INITIAL_IMAGE_LIMIT,
|
||||
boardIdSelected,
|
||||
} from 'features/gallery/store/gallerySlice';
|
||||
import { FaFolderOpen } from 'react-icons/fa';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import {
|
||||
ListImagesArgs,
|
||||
useListImagesQuery,
|
||||
} from 'services/api/endpoints/images';
|
||||
import GenericBoard from './GenericBoard';
|
||||
import { stateSelector } from 'app/store/store';
|
||||
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
||||
import { defaultSelectorOptions } from 'app/store/util/defaultMemoizeOptions';
|
||||
import IAIDroppable from 'common/components/IAIDroppable';
|
||||
import { boardIdSelected } from 'features/gallery/store/gallerySlice';
|
||||
import { memo, useCallback, useMemo } from 'react';
|
||||
import { FaFolder } from 'react-icons/fa';
|
||||
import { useBoardTotal } from 'services/api/hooks/useBoardTotal';
|
||||
import AutoAddIcon from '../AutoAddIcon';
|
||||
import BoardContextMenu from '../BoardContextMenu';
|
||||
|
||||
const baseQueryArg: ListImagesArgs = {
|
||||
board_id: 'none',
|
||||
offset: 0,
|
||||
limit: INITIAL_IMAGE_LIMIT,
|
||||
is_intermediate: false,
|
||||
const BASE_BADGE_STYLES: ChakraProps['sx'] = {
|
||||
bg: 'base.500',
|
||||
color: 'whiteAlpha.900',
|
||||
};
|
||||
interface Props {
|
||||
isSelected: boolean;
|
||||
}
|
||||
|
||||
const NoBoardBoard = ({ isSelected }: { isSelected: boolean }) => {
|
||||
const dispatch = useDispatch();
|
||||
const selector = createSelector(
|
||||
stateSelector,
|
||||
({ gallery }) => {
|
||||
const { autoAddBoardId } = gallery;
|
||||
return { autoAddBoardId };
|
||||
},
|
||||
defaultSelectorOptions
|
||||
);
|
||||
|
||||
const handleClick = () => {
|
||||
dispatch(boardIdSelected('no_board'));
|
||||
};
|
||||
const NoBoardBoard = memo(({ isSelected }: Props) => {
|
||||
const dispatch = useAppDispatch();
|
||||
const { totalImages, totalAssets } = useBoardTotal(undefined);
|
||||
const { autoAddBoardId } = useAppSelector(selector);
|
||||
const handleSelectBoard = useCallback(() => {
|
||||
dispatch(boardIdSelected(undefined));
|
||||
}, [dispatch]);
|
||||
|
||||
const { total } = useListImagesQuery(baseQueryArg, {
|
||||
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' },
|
||||
};
|
||||
const droppableData: MoveBoardDropData = useMemo(
|
||||
() => ({
|
||||
id: 'no_board',
|
||||
actionType: 'MOVE_BOARD',
|
||||
context: { boardId: undefined },
|
||||
}),
|
||||
[]
|
||||
);
|
||||
|
||||
return (
|
||||
<GenericBoard
|
||||
board_id="no_board"
|
||||
droppableData={droppableData}
|
||||
dropLabel={<Text fontSize="md">Move</Text>}
|
||||
onClick={handleClick}
|
||||
isSelected={isSelected}
|
||||
icon={FaFolderOpen}
|
||||
label="No Board"
|
||||
badgeCount={total}
|
||||
/>
|
||||
<Box sx={{ w: 'full', h: 'full', touchAction: 'none', userSelect: 'none' }}>
|
||||
<Flex
|
||||
sx={{
|
||||
position: 'relative',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
aspectRatio: '1/1',
|
||||
borderRadius: 'base',
|
||||
w: 'full',
|
||||
h: 'full',
|
||||
}}
|
||||
>
|
||||
<BoardContextMenu>
|
||||
{(ref) => (
|
||||
<Flex
|
||||
ref={ref}
|
||||
onClick={handleSelectBoard}
|
||||
sx={{
|
||||
w: 'full',
|
||||
h: 'full',
|
||||
position: 'relative',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
borderRadius: 'base',
|
||||
cursor: 'pointer',
|
||||
bg: 'base.300',
|
||||
_dark: {
|
||||
bg: 'base.900',
|
||||
},
|
||||
}}
|
||||
>
|
||||
<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
|
||||
sx={{
|
||||
position: 'absolute',
|
||||
insetInlineEnd: 0,
|
||||
top: 0,
|
||||
p: 1,
|
||||
}}
|
||||
>
|
||||
<Badge variant="solid" sx={BASE_BADGE_STYLES}>
|
||||
{totalImages}/{totalAssets}
|
||||
</Badge>
|
||||
</Flex>
|
||||
{!autoAddBoardId && <AutoAddIcon />}
|
||||
<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>
|
||||
)}
|
||||
</BoardContextMenu>
|
||||
</Flex>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
});
|
||||
|
||||
NoBoardBoard.displayName = 'HoverableBoard';
|
||||
|
||||
export default NoBoardBoard;
|
||||
|
@ -5,7 +5,7 @@ import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
||||
import { defaultSelectorOptions } from 'app/store/util/defaultMemoizeOptions';
|
||||
import { autoAddBoardIdChanged } from 'features/gallery/store/gallerySlice';
|
||||
import { memo, useCallback, useMemo } from 'react';
|
||||
import { FaMinus, FaPlus, FaTrash } from 'react-icons/fa';
|
||||
import { FaPlus, FaTrash } from 'react-icons/fa';
|
||||
import { BoardDTO } from 'services/api/types';
|
||||
|
||||
type Props = {
|
||||
@ -42,7 +42,7 @@ const GalleryBoardContextMenuItems = ({ board, setBoardToDelete }: Props) => {
|
||||
|
||||
const handleToggleAutoAdd = useCallback(() => {
|
||||
dispatch(
|
||||
autoAddBoardIdChanged(isSelectedForAutoAdd ? null : board.board_id)
|
||||
autoAddBoardIdChanged(isSelectedForAutoAdd ? undefined : board.board_id)
|
||||
);
|
||||
}, [board.board_id, dispatch, isSelectedForAutoAdd]);
|
||||
|
||||
@ -59,16 +59,15 @@ const GalleryBoardContextMenuItems = ({ board, setBoardToDelete }: Props) => {
|
||||
</MenuItem> */}
|
||||
</>
|
||||
)}
|
||||
<MenuItem
|
||||
icon={isSelectedForAutoAdd ? <FaMinus /> : <FaPlus />}
|
||||
onClickCapture={handleToggleAutoAdd}
|
||||
>
|
||||
{isSelectedForAutoAdd ? 'Disable Auto-Add' : 'Auto-Add to this Board'}
|
||||
</MenuItem>
|
||||
{!isSelectedForAutoAdd && (
|
||||
<MenuItem icon={<FaPlus />} onClick={handleToggleAutoAdd}>
|
||||
Auto-add to this Board
|
||||
</MenuItem>
|
||||
)}
|
||||
<MenuItem
|
||||
sx={{ color: 'error.600', _dark: { color: 'error.300' } }}
|
||||
icon={<FaTrash />}
|
||||
onClickCapture={handleDelete}
|
||||
onClick={handleDelete}
|
||||
>
|
||||
Delete Board
|
||||
</MenuItem>
|
||||
|
@ -0,0 +1,28 @@
|
||||
import { MenuItem } from '@chakra-ui/react';
|
||||
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
||||
import { autoAddBoardIdChanged } from 'features/gallery/store/gallerySlice';
|
||||
import { memo, useCallback } from 'react';
|
||||
import { FaPlus } from 'react-icons/fa';
|
||||
|
||||
const NoBoardContextMenuItems = () => {
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
const autoAddBoardId = useAppSelector(
|
||||
(state) => state.gallery.autoAddBoardId
|
||||
);
|
||||
const handleDisableAutoAdd = useCallback(() => {
|
||||
dispatch(autoAddBoardIdChanged(undefined));
|
||||
}, [dispatch]);
|
||||
|
||||
return (
|
||||
<>
|
||||
{autoAddBoardId && (
|
||||
<MenuItem icon={<FaPlus />} onClick={handleDisableAutoAdd}>
|
||||
Auto-add to this Board
|
||||
</MenuItem>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default memo(NoBoardContextMenuItems);
|
@ -1,12 +0,0 @@
|
||||
import { BoardId } from 'features/gallery/store/gallerySlice';
|
||||
import { memo } from 'react';
|
||||
|
||||
type Props = {
|
||||
board_id: BoardId;
|
||||
};
|
||||
|
||||
const SystemBoardContextMenuItems = ({ board_id }: Props) => {
|
||||
return <></>;
|
||||
};
|
||||
|
||||
export default memo(SystemBoardContextMenuItems);
|
@ -1,5 +1,5 @@
|
||||
import { ChevronUpIcon } from '@chakra-ui/icons';
|
||||
import { Box, Button, Flex, Spacer, Text } from '@chakra-ui/react';
|
||||
import { Button, Flex, Text } from '@chakra-ui/react';
|
||||
import { createSelector } from '@reduxjs/toolkit';
|
||||
import { stateSelector } from 'app/store/store';
|
||||
import { useAppSelector } from 'app/store/storeHooks';
|
||||
@ -27,52 +27,60 @@ const GalleryBoardName = (props: Props) => {
|
||||
const { isOpen, onToggle } = props;
|
||||
const { selectedBoardId } = useAppSelector(selector);
|
||||
const boardName = useBoardName(selectedBoardId);
|
||||
const numOfBoardImages = useBoardTotal(selectedBoardId);
|
||||
const { totalImages, totalAssets } = useBoardTotal(selectedBoardId);
|
||||
|
||||
const formattedBoardName = useMemo(() => {
|
||||
if (!boardName) return '';
|
||||
if (boardName && !numOfBoardImages) return boardName;
|
||||
if (boardName.length > 20) {
|
||||
return `${boardName.substring(0, 20)}... (${numOfBoardImages})`;
|
||||
if (!boardName) {
|
||||
return '';
|
||||
}
|
||||
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 (
|
||||
<Flex
|
||||
as={Button}
|
||||
onClick={onToggle}
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
// variant="ghost"
|
||||
sx={{
|
||||
position: 'relative',
|
||||
gap: 2,
|
||||
w: 'full',
|
||||
justifyContent: 'center',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
px: 2,
|
||||
_hover: {
|
||||
bg: 'base.100',
|
||||
_dark: { bg: 'base.800' },
|
||||
},
|
||||
// bg: 'base.100',
|
||||
// _dark: { bg: 'base.800' },
|
||||
// _hover: {
|
||||
// bg: 'base.200',
|
||||
// _dark: { bg: 'base.700' },
|
||||
// },
|
||||
}}
|
||||
>
|
||||
<Spacer />
|
||||
<Box position="relative">
|
||||
<Text
|
||||
noOfLines={1}
|
||||
sx={{
|
||||
fontWeight: 600,
|
||||
color: 'base.800',
|
||||
_dark: {
|
||||
color: 'base.200',
|
||||
},
|
||||
}}
|
||||
>
|
||||
{formattedBoardName}
|
||||
</Text>
|
||||
</Box>
|
||||
<Spacer />
|
||||
<Text
|
||||
noOfLines={1}
|
||||
sx={{
|
||||
fontWeight: 600,
|
||||
w: '100%',
|
||||
textAlign: 'center',
|
||||
color: 'base.800',
|
||||
_dark: {
|
||||
color: 'base.200',
|
||||
},
|
||||
}}
|
||||
>
|
||||
{formattedBoardName}
|
||||
</Text>
|
||||
<ChevronUpIcon
|
||||
sx={{
|
||||
transform: isOpen ? 'rotate(0deg)' : 'rotate(180deg)',
|
||||
|
@ -35,6 +35,8 @@ import {
|
||||
import { ImageDTO } from 'services/api/types';
|
||||
import { AddImageToBoardContext } from '../../../../app/contexts/AddImageToBoardContext';
|
||||
import { sentImageToCanvas, sentImageToImg2Img } from '../../store/actions';
|
||||
import { useDebounce } from 'use-debounce';
|
||||
import { skipToken } from '@reduxjs/toolkit/dist/query';
|
||||
|
||||
type SingleSelectionMenuItemsProps = {
|
||||
imageDTO: ImageDTO;
|
||||
@ -70,7 +72,16 @@ const SingleSelectionMenuItems = (props: SingleSelectionMenuItemsProps) => {
|
||||
|
||||
const { onClickAddToBoard } = useContext(AddImageToBoardContext);
|
||||
|
||||
const { currentData } = useGetImageMetadataQuery(imageDTO.image_name);
|
||||
const [debouncedMetadataQueryArg, debounceState] = useDebounce(
|
||||
imageDTO.image_name,
|
||||
500
|
||||
);
|
||||
|
||||
const { currentData } = useGetImageMetadataQuery(
|
||||
debounceState.isPending()
|
||||
? skipToken
|
||||
: debouncedMetadataQueryArg ?? skipToken
|
||||
);
|
||||
|
||||
const { isClipboardAPIAvailable, copyImageToClipboard } =
|
||||
useCopyImageToClipboard();
|
||||
|
@ -1,23 +1,37 @@
|
||||
import { Box, Flex, VStack, useDisclosure } from '@chakra-ui/react';
|
||||
import {
|
||||
Box,
|
||||
ButtonGroup,
|
||||
Flex,
|
||||
Spacer,
|
||||
Tab,
|
||||
TabList,
|
||||
Tabs,
|
||||
VStack,
|
||||
useDisclosure,
|
||||
} from '@chakra-ui/react';
|
||||
import { createSelector } from '@reduxjs/toolkit';
|
||||
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 { memo, useRef } from 'react';
|
||||
import { memo, useCallback, useRef } from 'react';
|
||||
import BoardsList from './Boards/BoardsList/BoardsList';
|
||||
import GalleryBoardName from './GalleryBoardName';
|
||||
import GalleryPinButton from './GalleryPinButton';
|
||||
import GallerySettingsPopover from './GallerySettingsPopover';
|
||||
import BatchImageGrid from './ImageGrid/BatchImageGrid';
|
||||
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(
|
||||
[stateSelector],
|
||||
(state) => {
|
||||
const { selectedBoardId } = state.gallery;
|
||||
const { selectedBoardId, galleryView } = state.gallery;
|
||||
|
||||
return {
|
||||
selectedBoardId,
|
||||
galleryView,
|
||||
};
|
||||
},
|
||||
defaultSelectorOptions
|
||||
@ -26,10 +40,19 @@ const selector = createSelector(
|
||||
const ImageGalleryContent = () => {
|
||||
const resizeObserverRef = 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 } =
|
||||
useDisclosure();
|
||||
|
||||
const handleClickImages = useCallback(() => {
|
||||
dispatch(galleryViewChanged('images'));
|
||||
}, [dispatch]);
|
||||
|
||||
const handleClickAssets = useCallback(() => {
|
||||
dispatch(galleryViewChanged('assets'));
|
||||
}, [dispatch]);
|
||||
|
||||
return (
|
||||
<VStack
|
||||
sx={{
|
||||
@ -48,11 +71,11 @@ const ImageGalleryContent = () => {
|
||||
gap: 2,
|
||||
}}
|
||||
>
|
||||
<GallerySettingsPopover />
|
||||
<GalleryBoardName
|
||||
isOpen={isBoardListOpen}
|
||||
onToggle={onToggleBoardList}
|
||||
/>
|
||||
<GallerySettingsPopover />
|
||||
<GalleryPinButton />
|
||||
</Flex>
|
||||
<Box>
|
||||
@ -60,6 +83,36 @@ const ImageGalleryContent = () => {
|
||||
</Box>
|
||||
</Box>
|
||||
<Flex ref={galleryGridRef} direction="column" gap={2} h="full" w="full">
|
||||
<Flex
|
||||
sx={{
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between',
|
||||
gap: 2,
|
||||
}}
|
||||
>
|
||||
<Tabs
|
||||
index={galleryView === 'images' ? 0 : 1}
|
||||
variant="line"
|
||||
size="sm"
|
||||
sx={{ w: 'full' }}
|
||||
>
|
||||
<TabList>
|
||||
<Tab
|
||||
onClick={handleClickImages}
|
||||
sx={{ borderTopRadius: 'base', w: 'full' }}
|
||||
>
|
||||
Images
|
||||
</Tab>
|
||||
<Tab
|
||||
onClick={handleClickAssets}
|
||||
sx={{ borderTopRadius: 'base', w: 'full' }}
|
||||
>
|
||||
Assets
|
||||
</Tab>
|
||||
</TabList>
|
||||
</Tabs>
|
||||
</Flex>
|
||||
|
||||
{selectedBoardId === 'batch' ? (
|
||||
<BatchImageGrid />
|
||||
) : (
|
||||
|
@ -19,6 +19,7 @@ import GalleryImage from './GalleryImage';
|
||||
import ImageGridItemContainer from './ImageGridItemContainer';
|
||||
import ImageGridListContainer from './ImageGridListContainer';
|
||||
import { selectListImagesBaseQueryArgs } from 'features/gallery/store/gallerySelectors';
|
||||
import { useBoardTotal } from 'services/api/hooks/useBoardTotal';
|
||||
|
||||
const overlayScrollbarsConfig: UseOverlayScrollbarsParams = {
|
||||
defer: true,
|
||||
@ -40,7 +41,10 @@ const GalleryImageGrid = () => {
|
||||
const [initialize, osInstance] = useOverlayScrollbars(
|
||||
overlayScrollbarsConfig
|
||||
);
|
||||
|
||||
const selectedBoardId = useAppSelector(
|
||||
(state) => state.gallery.selectedBoardId
|
||||
);
|
||||
const { currentViewTotal } = useBoardTotal(selectedBoardId);
|
||||
const queryArgs = useAppSelector(selectListImagesBaseQueryArgs);
|
||||
|
||||
const { currentData, isFetching, isSuccess, isError } =
|
||||
@ -49,19 +53,23 @@ const GalleryImageGrid = () => {
|
||||
const [listImages] = useLazyListImagesQuery();
|
||||
|
||||
const areMoreAvailable = useMemo(() => {
|
||||
if (!currentData) {
|
||||
if (!currentData || !currentViewTotal) {
|
||||
return false;
|
||||
}
|
||||
return currentData.ids.length < currentData.total;
|
||||
}, [currentData]);
|
||||
return currentData.ids.length < currentViewTotal;
|
||||
}, [currentData, currentViewTotal]);
|
||||
|
||||
const handleLoadMoreImages = useCallback(() => {
|
||||
if (!areMoreAvailable) {
|
||||
return;
|
||||
}
|
||||
|
||||
listImages({
|
||||
...queryArgs,
|
||||
offset: currentData?.ids.length ?? 0,
|
||||
limit: IMAGE_LIMIT,
|
||||
});
|
||||
}, [listImages, queryArgs, currentData?.ids.length]);
|
||||
}, [areMoreAvailable, listImages, queryArgs, currentData?.ids.length]);
|
||||
|
||||
useEffect(() => {
|
||||
// Initialize the gallery's custom scrollbar
|
||||
|
@ -4,7 +4,6 @@ import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
||||
import {
|
||||
IMAGE_LIMIT,
|
||||
imageSelected,
|
||||
selectImagesById,
|
||||
} from 'features/gallery/store/gallerySlice';
|
||||
import { clamp, isEqual } from 'lodash-es';
|
||||
import { useCallback } from 'react';
|
||||
@ -53,8 +52,8 @@ export const nextPrevImageButtonsSelector = createSelector(
|
||||
|
||||
const prevImageIndex = clamp(currentImageIndex - 1, 0, images.length - 1);
|
||||
|
||||
const nextImageId = images[nextImageIndex].image_name;
|
||||
const prevImageId = images[prevImageIndex].image_name;
|
||||
const nextImageId = images[nextImageIndex]?.image_name;
|
||||
const prevImageId = images[prevImageIndex]?.image_name;
|
||||
|
||||
const nextImage = selectors.selectById(data, nextImageId);
|
||||
const prevImage = selectors.selectById(data, prevImageId);
|
||||
@ -65,7 +64,7 @@ export const nextPrevImageButtonsSelector = createSelector(
|
||||
isOnFirstImage: currentImageIndex === 0,
|
||||
isOnLastImage:
|
||||
!isNaN(currentImageIndex) && currentImageIndex === imagesLength - 1,
|
||||
areMoreImagesAvailable: data?.total ?? 0 > imagesLength,
|
||||
areMoreImagesAvailable: (data?.total ?? 0) > imagesLength,
|
||||
isFetching: status === 'pending',
|
||||
nextImage,
|
||||
prevImage,
|
||||
|
@ -2,11 +2,11 @@ import { createSelector } from '@reduxjs/toolkit';
|
||||
import { RootState } from 'app/store/store';
|
||||
import { defaultSelectorOptions } from 'app/store/util/defaultMemoizeOptions';
|
||||
import { ListImagesArgs } from 'services/api/endpoints/images';
|
||||
import { INITIAL_IMAGE_LIMIT } from './gallerySlice';
|
||||
import {
|
||||
getBoardIdQueryParamForBoard,
|
||||
getCategoriesQueryParamForBoard,
|
||||
} from './util';
|
||||
ASSETS_CATEGORIES,
|
||||
IMAGE_CATEGORIES,
|
||||
INITIAL_IMAGE_LIMIT,
|
||||
} from './gallerySlice';
|
||||
|
||||
export const gallerySelector = (state: RootState) => state.gallery;
|
||||
|
||||
@ -19,14 +19,13 @@ export const selectLastSelectedImage = createSelector(
|
||||
export const selectListImagesBaseQueryArgs = createSelector(
|
||||
[(state: RootState) => state],
|
||||
(state) => {
|
||||
const { selectedBoardId } = state.gallery;
|
||||
|
||||
const categories = getCategoriesQueryParamForBoard(selectedBoardId);
|
||||
const board_id = getBoardIdQueryParamForBoard(selectedBoardId);
|
||||
const { selectedBoardId, galleryView } = state.gallery;
|
||||
const categories =
|
||||
galleryView === 'images' ? IMAGE_CATEGORIES : ASSETS_CATEGORIES;
|
||||
|
||||
const listImagesBaseQueryArgs: ListImagesArgs = {
|
||||
board_id: selectedBoardId ?? 'none',
|
||||
categories,
|
||||
board_id,
|
||||
offset: 0,
|
||||
limit: INITIAL_IMAGE_LIMIT,
|
||||
is_intermediate: false,
|
||||
|
@ -14,20 +14,17 @@ export const ASSETS_CATEGORIES: ImageCategory[] = [
|
||||
export const INITIAL_IMAGE_LIMIT = 100;
|
||||
export const IMAGE_LIMIT = 20;
|
||||
|
||||
// export type GalleryView = 'images' | 'assets';
|
||||
export type BoardId =
|
||||
| 'images'
|
||||
| 'assets'
|
||||
| 'no_board'
|
||||
| 'batch'
|
||||
| (string & Record<never, never>);
|
||||
export type GalleryView = 'images' | 'assets';
|
||||
// export type BoardId = 'no_board' | (string & Record<never, never>);
|
||||
export type BoardId = string | undefined;
|
||||
|
||||
type GalleryState = {
|
||||
selection: string[];
|
||||
shouldAutoSwitch: boolean;
|
||||
autoAddBoardId: string | null;
|
||||
autoAddBoardId: string | undefined;
|
||||
galleryImageMinimumWidth: number;
|
||||
selectedBoardId: BoardId;
|
||||
galleryView: GalleryView;
|
||||
batchImageNames: string[];
|
||||
isBatchEnabled: boolean;
|
||||
};
|
||||
@ -35,9 +32,10 @@ type GalleryState = {
|
||||
export const initialGalleryState: GalleryState = {
|
||||
selection: [],
|
||||
shouldAutoSwitch: true,
|
||||
autoAddBoardId: null,
|
||||
autoAddBoardId: undefined,
|
||||
galleryImageMinimumWidth: 96,
|
||||
selectedBoardId: 'images',
|
||||
selectedBoardId: undefined,
|
||||
galleryView: 'images',
|
||||
batchImageNames: [],
|
||||
isBatchEnabled: false,
|
||||
};
|
||||
@ -96,6 +94,7 @@ export const gallerySlice = createSlice({
|
||||
},
|
||||
boardIdSelected: (state, action: PayloadAction<BoardId>) => {
|
||||
state.selectedBoardId = action.payload;
|
||||
state.galleryView = 'images';
|
||||
},
|
||||
isBatchEnabledChanged: (state, action: PayloadAction<boolean>) => {
|
||||
state.isBatchEnabled = action.payload;
|
||||
@ -125,9 +124,15 @@ export const gallerySlice = createSlice({
|
||||
state.batchImageNames = [];
|
||||
state.selection = [];
|
||||
},
|
||||
autoAddBoardIdChanged: (state, action: PayloadAction<string | null>) => {
|
||||
autoAddBoardIdChanged: (
|
||||
state,
|
||||
action: PayloadAction<string | undefined>
|
||||
) => {
|
||||
state.autoAddBoardId = action.payload;
|
||||
},
|
||||
galleryViewChanged: (state, action: PayloadAction<GalleryView>) => {
|
||||
state.galleryView = action.payload;
|
||||
},
|
||||
},
|
||||
extraReducers: (builder) => {
|
||||
builder.addMatcher(
|
||||
@ -135,10 +140,11 @@ export const gallerySlice = createSlice({
|
||||
(state, action) => {
|
||||
const deletedBoardId = action.meta.arg.originalArgs;
|
||||
if (deletedBoardId === state.selectedBoardId) {
|
||||
state.selectedBoardId = 'images';
|
||||
state.selectedBoardId = undefined;
|
||||
state.galleryView = 'images';
|
||||
}
|
||||
if (deletedBoardId === state.autoAddBoardId) {
|
||||
state.autoAddBoardId = null;
|
||||
state.autoAddBoardId = undefined;
|
||||
}
|
||||
}
|
||||
);
|
||||
@ -151,7 +157,7 @@ export const gallerySlice = createSlice({
|
||||
}
|
||||
|
||||
if (!boards.map((b) => b.board_id).includes(state.autoAddBoardId)) {
|
||||
state.autoAddBoardId = null;
|
||||
state.autoAddBoardId = undefined;
|
||||
}
|
||||
}
|
||||
);
|
||||
@ -170,6 +176,7 @@ export const {
|
||||
imagesAddedToBatch,
|
||||
imagesRemovedFromBatch,
|
||||
autoAddBoardIdChanged,
|
||||
galleryViewChanged,
|
||||
} = gallerySlice.actions;
|
||||
|
||||
export default gallerySlice.reducer;
|
||||
|
@ -1,6 +1,11 @@
|
||||
import { SYSTEM_BOARDS } from 'services/api/endpoints/images';
|
||||
import { ASSETS_CATEGORIES, BoardId, IMAGE_CATEGORIES } from './gallerySlice';
|
||||
import { ImageCategory } from 'services/api/types';
|
||||
import { ListImagesArgs, SYSTEM_BOARDS } from 'services/api/endpoints/images';
|
||||
import {
|
||||
ASSETS_CATEGORIES,
|
||||
BoardId,
|
||||
GalleryView,
|
||||
IMAGE_CATEGORIES,
|
||||
} from './gallerySlice';
|
||||
import { ImageCategory, ImageDTO } from 'services/api/types';
|
||||
import { isEqual } from 'lodash-es';
|
||||
|
||||
export const getCategoriesQueryParamForBoard = (
|
||||
@ -20,16 +25,11 @@ export const getCategoriesQueryParamForBoard = (
|
||||
|
||||
export const getBoardIdQueryParamForBoard = (
|
||||
board_id: BoardId
|
||||
): string | undefined => {
|
||||
if (board_id === 'no_board') {
|
||||
): string | null => {
|
||||
if (board_id === undefined) {
|
||||
return 'none';
|
||||
}
|
||||
|
||||
// system boards besides 'no_board'
|
||||
if (SYSTEM_BOARDS.includes(board_id)) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
// user boards
|
||||
return board_id;
|
||||
};
|
||||
@ -52,3 +52,10 @@ export const getBoardIdFromBoardAndCategoriesQueryParam = (
|
||||
|
||||
return board_id ?? 'UNKNOWN_BOARD';
|
||||
};
|
||||
|
||||
export const getCategories = (imageDTO: ImageDTO) => {
|
||||
if (IMAGE_CATEGORIES.includes(imageDTO.image_category)) {
|
||||
return IMAGE_CATEGORIES;
|
||||
}
|
||||
return ASSETS_CATEGORIES;
|
||||
};
|
||||
|
@ -1,52 +1,36 @@
|
||||
import { ImageDTO, OffsetPaginatedResults_ImageDTO_ } from 'services/api/types';
|
||||
import { ApiFullTagDescription, LIST_TAG, api } from '..';
|
||||
import { paths } from '../schema';
|
||||
import { BoardId } from 'features/gallery/store/gallerySlice';
|
||||
|
||||
type ListBoardImagesArg =
|
||||
paths['/api/v1/board_images/{board_id}']['get']['parameters']['path'] &
|
||||
paths['/api/v1/board_images/{board_id}']['get']['parameters']['query'];
|
||||
|
||||
type AddImageToBoardArg =
|
||||
paths['/api/v1/board_images/']['post']['requestBody']['content']['application/json'];
|
||||
|
||||
type RemoveImageFromBoardArg =
|
||||
paths['/api/v1/board_images/']['delete']['requestBody']['content']['application/json'];
|
||||
import { api } from '..';
|
||||
|
||||
export const boardImagesApi = api.injectEndpoints({
|
||||
endpoints: (build) => ({
|
||||
/**
|
||||
* Board Images Queries
|
||||
*/
|
||||
|
||||
listBoardImages: build.query<
|
||||
OffsetPaginatedResults_ImageDTO_,
|
||||
ListBoardImagesArg
|
||||
>({
|
||||
query: ({ board_id, offset, limit }) => ({
|
||||
url: `board_images/${board_id}`,
|
||||
method: 'GET',
|
||||
}),
|
||||
providesTags: (result, error, arg) => {
|
||||
// any list of boardimages
|
||||
const tags: ApiFullTagDescription[] = [
|
||||
{ type: 'BoardImage', id: `${arg.board_id}_${LIST_TAG}` },
|
||||
];
|
||||
|
||||
if (result) {
|
||||
// and individual tags for each boardimage
|
||||
tags.push(
|
||||
...result.items.map(({ board_id, image_name }) => ({
|
||||
type: 'BoardImage' as const,
|
||||
id: `${board_id}_${image_name}`,
|
||||
}))
|
||||
);
|
||||
}
|
||||
|
||||
return tags;
|
||||
},
|
||||
}),
|
||||
// listBoardImages: build.query<
|
||||
// OffsetPaginatedResults_ImageDTO_,
|
||||
// ListBoardImagesArg
|
||||
// >({
|
||||
// query: ({ board_id, offset, limit }) => ({
|
||||
// url: `board_images/${board_id}`,
|
||||
// method: 'GET',
|
||||
// }),
|
||||
// providesTags: (result, error, arg) => {
|
||||
// // any list of boardimages
|
||||
// const tags: ApiFullTagDescription[] = [
|
||||
// { type: 'BoardImage', id: `${arg.board_id}_${LIST_TAG}` },
|
||||
// ];
|
||||
// if (result) {
|
||||
// // and individual tags for each boardimage
|
||||
// tags.push(
|
||||
// ...result.items.map(({ board_id, image_name }) => ({
|
||||
// type: 'BoardImage' as const,
|
||||
// id: `${board_id}_${image_name}`,
|
||||
// }))
|
||||
// );
|
||||
// }
|
||||
// return tags;
|
||||
// },
|
||||
// }),
|
||||
}),
|
||||
});
|
||||
|
||||
export const { useListBoardImagesQuery } = boardImagesApi;
|
||||
// export const { useListBoardImagesQuery } = boardImagesApi;
|
||||
|
@ -109,10 +109,25 @@ export const boardsApi = api.injectEndpoints({
|
||||
|
||||
deleteBoard: build.mutation<DeleteBoardResult, string>({
|
||||
query: (board_id) => ({ url: `boards/${board_id}`, method: 'DELETE' }),
|
||||
invalidatesTags: (result, error, arg) => [
|
||||
{ type: 'Board', id: arg },
|
||||
invalidatesTags: (result, error, board_id) => [
|
||||
{ type: 'Board', id: LIST_TAG },
|
||||
// invalidate the 'No Board' cache
|
||||
{ type: 'ImageList', id: getListImagesUrl({ board_id: 'none' }) },
|
||||
{
|
||||
type: 'ImageList',
|
||||
id: getListImagesUrl({
|
||||
board_id: 'none',
|
||||
categories: IMAGE_CATEGORIES,
|
||||
}),
|
||||
},
|
||||
{
|
||||
type: 'ImageList',
|
||||
id: getListImagesUrl({
|
||||
board_id: 'none',
|
||||
categories: ASSETS_CATEGORIES,
|
||||
}),
|
||||
},
|
||||
{ type: 'BoardImagesTotal', id: 'none' },
|
||||
{ type: 'BoardAssetsTotal', id: 'none' },
|
||||
],
|
||||
async onQueryStarted(board_id, { dispatch, queryFulfilled, getState }) {
|
||||
/**
|
||||
@ -167,24 +182,14 @@ export const boardsApi = api.injectEndpoints({
|
||||
'listImages',
|
||||
queryArgs,
|
||||
(draft) => {
|
||||
const oldCount = imagesAdapter
|
||||
.getSelectors()
|
||||
.selectTotal(draft);
|
||||
const oldTotal = draft.total;
|
||||
const newState = imagesAdapter.updateMany(draft, updates);
|
||||
const newCount = imagesAdapter
|
||||
.getSelectors()
|
||||
.selectTotal(newState);
|
||||
draft.total = Math.max(
|
||||
draft.total - (oldCount - newCount),
|
||||
0
|
||||
);
|
||||
const delta = newState.total - oldTotal;
|
||||
draft.total = draft.total + delta;
|
||||
}
|
||||
)
|
||||
);
|
||||
});
|
||||
|
||||
// after deleting a board, select the 'All Images' board
|
||||
dispatch(boardIdSelected('images'));
|
||||
} catch {
|
||||
//no-op
|
||||
}
|
||||
@ -197,9 +202,24 @@ export const boardsApi = api.injectEndpoints({
|
||||
method: 'DELETE',
|
||||
params: { include_images: true },
|
||||
}),
|
||||
invalidatesTags: (result, error, arg) => [
|
||||
{ type: 'Board', id: arg },
|
||||
{ type: 'ImageList', id: getListImagesUrl({ board_id: 'none' }) },
|
||||
invalidatesTags: (result, error, board_id) => [
|
||||
{ type: 'Board', id: LIST_TAG },
|
||||
{
|
||||
type: 'ImageList',
|
||||
id: getListImagesUrl({
|
||||
board_id: 'none',
|
||||
categories: IMAGE_CATEGORIES,
|
||||
}),
|
||||
},
|
||||
{
|
||||
type: 'ImageList',
|
||||
id: getListImagesUrl({
|
||||
board_id: 'none',
|
||||
categories: ASSETS_CATEGORIES,
|
||||
}),
|
||||
},
|
||||
{ type: 'BoardImagesTotal', id: 'none' },
|
||||
{ type: 'BoardAssetsTotal', id: 'none' },
|
||||
],
|
||||
async onQueryStarted(board_id, { dispatch, queryFulfilled, getState }) {
|
||||
/**
|
||||
@ -231,27 +251,17 @@ export const boardsApi = api.injectEndpoints({
|
||||
'listImages',
|
||||
queryArgs,
|
||||
(draft) => {
|
||||
const oldCount = imagesAdapter
|
||||
.getSelectors()
|
||||
.selectTotal(draft);
|
||||
const oldTotal = draft.total;
|
||||
const newState = imagesAdapter.removeMany(
|
||||
draft,
|
||||
deleted_images
|
||||
);
|
||||
const newCount = imagesAdapter
|
||||
.getSelectors()
|
||||
.selectTotal(newState);
|
||||
draft.total = Math.max(
|
||||
draft.total - (oldCount - newCount),
|
||||
0
|
||||
);
|
||||
const delta = newState.total - oldTotal;
|
||||
draft.total = draft.total + delta;
|
||||
}
|
||||
)
|
||||
);
|
||||
});
|
||||
|
||||
// after deleting a board, select the 'All Images' board
|
||||
dispatch(boardIdSelected('images'));
|
||||
} catch {
|
||||
//no-op
|
||||
}
|
||||
|
@ -6,18 +6,17 @@ import {
|
||||
BoardId,
|
||||
IMAGE_CATEGORIES,
|
||||
} from 'features/gallery/store/gallerySlice';
|
||||
import { omit } from 'lodash-es';
|
||||
import { getCategories } from 'features/gallery/store/util';
|
||||
import queryString from 'query-string';
|
||||
import { ApiFullTagDescription, api } from '..';
|
||||
import { components, paths } from '../schema';
|
||||
import {
|
||||
ImageCategory,
|
||||
ImageChanges,
|
||||
ImageDTO,
|
||||
OffsetPaginatedResults_ImageDTO_,
|
||||
PostUploadAction,
|
||||
} from '../types';
|
||||
import { getCacheAction } from './util';
|
||||
import { getIsImageInDateRange } from './util';
|
||||
|
||||
export type ListImagesArgs = NonNullable<
|
||||
paths['/api/v1/images/']['get']['parameters']['query']
|
||||
@ -155,6 +154,42 @@ export const imagesApi = api.injectEndpoints({
|
||||
},
|
||||
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>({
|
||||
query: () => ({ url: `images/clear-intermediates`, method: 'POST' }),
|
||||
invalidatesTags: ['IntermediatesCount'],
|
||||
@ -164,56 +199,42 @@ export const imagesApi = api.injectEndpoints({
|
||||
url: `images/${image_name}`,
|
||||
method: 'DELETE',
|
||||
}),
|
||||
invalidatesTags: (result, error, arg) => [
|
||||
{ type: 'Image', id: arg.image_name },
|
||||
invalidatesTags: (result, error, { board_id }) => [
|
||||
{ type: 'BoardImagesTotal', id: board_id ?? 'none' },
|
||||
{ type: 'BoardAssetsTotal', id: board_id ?? 'none' },
|
||||
],
|
||||
async onQueryStarted(imageDTO, { dispatch, queryFulfilled }) {
|
||||
/**
|
||||
* Cache changes for `deleteImage`:
|
||||
* - *remove* from "All Images" / "All Assets"
|
||||
* - IF it has a board:
|
||||
* - THEN *remove* from it's own board
|
||||
* - ELSE *remove* from "No Board"
|
||||
* - NOT POSSIBLE: *remove* from getImageDTO
|
||||
* - $cache = [board_id|no_board]/[images|assets]
|
||||
* - *remove* from $cache
|
||||
*/
|
||||
|
||||
const { image_name, board_id, image_category } = imageDTO;
|
||||
const { image_name, board_id } = imageDTO;
|
||||
|
||||
// Figure out the `listImages` caches that we need to update
|
||||
// That means constructing the possible query args that are serialized into the cache key...
|
||||
|
||||
const removeFromCacheKeys: ListImagesArgs[] = [];
|
||||
// Store patches so we can undo if the query fails
|
||||
const patches: PatchCollection[] = [];
|
||||
|
||||
// determine `categories`, i.e. do we update "All Images" or "All Assets"
|
||||
const categories = IMAGE_CATEGORIES.includes(image_category)
|
||||
? IMAGE_CATEGORIES
|
||||
: ASSETS_CATEGORIES;
|
||||
// $cache = [board_id|no_board]/[images|assets]
|
||||
const categories = getCategories(imageDTO);
|
||||
|
||||
// remove from "All Images"
|
||||
removeFromCacheKeys.push({ categories });
|
||||
|
||||
if (board_id) {
|
||||
// remove from it's own board
|
||||
removeFromCacheKeys.push({ board_id });
|
||||
} else {
|
||||
// remove from "No Board"
|
||||
removeFromCacheKeys.push({ board_id: 'none' });
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
)
|
||||
// *remove* from $cache
|
||||
patches.push(
|
||||
dispatch(
|
||||
imagesApi.util.updateQueryData(
|
||||
'listImages',
|
||||
{ board_id: board_id ?? 'none', categories },
|
||||
(draft) => {
|
||||
const oldTotal = draft.total;
|
||||
const newState = imagesAdapter.removeOne(draft, image_name);
|
||||
const delta = newState.total - oldTotal;
|
||||
draft.total = draft.total + delta;
|
||||
}
|
||||
)
|
||||
);
|
||||
});
|
||||
)
|
||||
);
|
||||
|
||||
try {
|
||||
await queryFulfilled;
|
||||
@ -222,122 +243,168 @@ export const imagesApi = api.injectEndpoints({
|
||||
}
|
||||
},
|
||||
}),
|
||||
updateImage: build.mutation<
|
||||
/**
|
||||
* Change an image's `is_intermediate` property.
|
||||
*/
|
||||
changeImageIsIntermediate: build.mutation<
|
||||
ImageDTO,
|
||||
{
|
||||
imageDTO: ImageDTO;
|
||||
// For now, we will not allow image categories to change
|
||||
changes: Omit<ImageChanges, 'image_category'>;
|
||||
}
|
||||
{ imageDTO: ImageDTO; is_intermediate: boolean }
|
||||
>({
|
||||
query: ({ imageDTO, changes }) => ({
|
||||
query: ({ imageDTO, is_intermediate }) => ({
|
||||
url: `images/${imageDTO.image_name}`,
|
||||
method: 'PATCH',
|
||||
body: changes,
|
||||
body: { is_intermediate },
|
||||
}),
|
||||
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(
|
||||
{ imageDTO: oldImageDTO, changes: _changes },
|
||||
{ imageDTO, is_intermediate },
|
||||
{ dispatch, queryFulfilled, getState }
|
||||
) {
|
||||
// let's be extra-sure we do not accidentally change categories
|
||||
const changes = omit(_changes, 'image_category');
|
||||
|
||||
/**
|
||||
* Cache changes for "updateImage":
|
||||
* - *update* "getImageDTO" cache
|
||||
* - for "All Images" || "All Assets":
|
||||
* - IF it is not already in the cache
|
||||
* - THEN *add* it to "All Images" / "All Assets" and update the total
|
||||
* - ELSE *update* it
|
||||
* - IF the image has a board:
|
||||
* - THEN *update* it's own board
|
||||
* - ELSE *update* the "No Board" board
|
||||
* Cache changes for `changeImageIsIntermediate`:
|
||||
* - *update* getImageDTO
|
||||
* - $cache = [board_id|no_board]/[images|assets]
|
||||
* - IF it is being changed to an intermediate:
|
||||
* - remove from $cache
|
||||
* - ELSE (it is being changed to a non-intermediate):
|
||||
* - IF it eligible for insertion into existing $cache:
|
||||
* - *upsert* to $cache
|
||||
*/
|
||||
|
||||
// Store patches so we can undo if the query fails
|
||||
const patches: PatchCollection[] = [];
|
||||
const { image_name, board_id, image_category, is_intermediate } =
|
||||
oldImageDTO;
|
||||
|
||||
const isChangingFromIntermediate = changes.is_intermediate === false;
|
||||
// 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
|
||||
// *update* getImageDTO
|
||||
patches.push(
|
||||
dispatch(
|
||||
imagesApi.util.updateQueryData(
|
||||
'getImageDTO',
|
||||
image_name,
|
||||
imageDTO.image_name,
|
||||
(draft) => {
|
||||
Object.assign(draft, changes);
|
||||
Object.assign(draft, { is_intermediate });
|
||||
}
|
||||
)
|
||||
)
|
||||
);
|
||||
|
||||
// Update the "All Image" or "All Assets" board
|
||||
const queryArgsToUpdate: ListImagesArgs[] = [{ categories }];
|
||||
// $cache = [board_id|no_board]/[images|assets]
|
||||
const categories = getCategories(imageDTO);
|
||||
|
||||
// IF the image has a board:
|
||||
if (board_id) {
|
||||
// THEN update it's own board
|
||||
queryArgsToUpdate.push({ board_id });
|
||||
if (is_intermediate) {
|
||||
// IF it is being changed to an intermediate:
|
||||
// remove from $cache
|
||||
patches.push(
|
||||
dispatch(
|
||||
imagesApi.util.updateQueryData(
|
||||
'listImages',
|
||||
{ board_id: imageDTO.board_id ?? 'none', 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 update the "No Board" board
|
||||
queryArgsToUpdate.push({ board_id: 'none' });
|
||||
}
|
||||
// ELSE (it is being changed to a non-intermediate):
|
||||
const queryArgs = {
|
||||
board_id: imageDTO.board_id ?? 'none',
|
||||
categories,
|
||||
};
|
||||
|
||||
queryArgsToUpdate.forEach((queryArg) => {
|
||||
const { data } = imagesApi.endpoints.listImages.select(queryArg)(
|
||||
const currentCache = imagesApi.endpoints.listImages.select(queryArgs)(
|
||||
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(
|
||||
dispatch(
|
||||
imagesApi.util.updateQueryData(
|
||||
'listImages',
|
||||
queryArg,
|
||||
queryArgs,
|
||||
(draft) => {
|
||||
// One of the common changes is to make a canvas intermediate a non-intermediate,
|
||||
// i.e. save a canvas image to the gallery.
|
||||
// If that was the change, need to add the image to the cache instead of updating
|
||||
// the existing cache entry.
|
||||
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,
|
||||
});
|
||||
}
|
||||
const oldTotal = draft.total;
|
||||
const newState = imagesAdapter.upsertOne(draft, imageDTO);
|
||||
const delta = newState.total - oldTotal;
|
||||
draft.total = draft.total + delta;
|
||||
}
|
||||
)
|
||||
)
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
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 {
|
||||
await queryFulfilled;
|
||||
@ -375,6 +442,15 @@ export const imagesApi = api.injectEndpoints({
|
||||
{ dispatch, queryFulfilled }
|
||||
) {
|
||||
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;
|
||||
|
||||
if (imageDTO.is_intermediate) {
|
||||
@ -382,21 +458,37 @@ export const imagesApi = api.injectEndpoints({
|
||||
return;
|
||||
}
|
||||
|
||||
// determine `categories`, i.e. do we update "All Images" or "All Assets"
|
||||
const categories = IMAGE_CATEGORIES.includes(image_category)
|
||||
? IMAGE_CATEGORIES
|
||||
: ASSETS_CATEGORIES;
|
||||
// *add* to `getImageDTO`
|
||||
dispatch(
|
||||
imagesApi.util.upsertQueryData(
|
||||
'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(
|
||||
imagesApi.util.updateQueryData('listImages', queryArg, (draft) => {
|
||||
imagesAdapter.addOne(draft, imageDTO);
|
||||
draft.total = draft.total + 1;
|
||||
})
|
||||
imagesApi.util.invalidateTags([
|
||||
{ type: 'BoardImagesTotal', id: imageDTO.board_id ?? 'none' },
|
||||
{ type: 'BoardAssetsTotal', id: imageDTO.board_id ?? 'none' },
|
||||
])
|
||||
);
|
||||
} catch {
|
||||
// no-op
|
||||
// query failed, no action needed
|
||||
}
|
||||
},
|
||||
}),
|
||||
@ -412,107 +504,103 @@ export const imagesApi = api.injectEndpoints({
|
||||
body: { board_id, image_name },
|
||||
};
|
||||
},
|
||||
invalidatesTags: (result, error, arg) => [
|
||||
{ type: 'BoardImage' },
|
||||
{ type: 'Board', id: arg.board_id },
|
||||
invalidatesTags: (result, error, { board_id, imageDTO }) => [
|
||||
{ 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(
|
||||
{ board_id, imageDTO: oldImageDTO },
|
||||
{ board_id, imageDTO },
|
||||
{ dispatch, queryFulfilled, getState }
|
||||
) {
|
||||
/**
|
||||
* Cache changes for `addImageToBoard`:
|
||||
* - *update* the `getImageDTO` cache
|
||||
* - *remove* from "No Board"
|
||||
* - IF the image has an old `board_id`:
|
||||
* - THEN *remove* from it's old `board_id`
|
||||
* - IF the image's `created_at` is within the range of the board's cached images
|
||||
* - OR the board cache has length of 0 or 1
|
||||
* - THEN *add* it to new `board_id`
|
||||
* - *update* getImageDTO
|
||||
* - IF it has an old board_id:
|
||||
* - THEN *remove* from old board_id/[images|assets]
|
||||
* - ELSE *remove* from no_board/[images|assets]
|
||||
* - $cache = board_id/[images|assets]
|
||||
* - IF it eligible for insertion into existing $cache:
|
||||
* - 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 categories = getCategories(imageDTO);
|
||||
|
||||
// Updated imageDTO with new board_id
|
||||
const newImageDTO = { ...oldImageDTO, board_id };
|
||||
|
||||
// Update getImageDTO cache
|
||||
// *update* getImageDTO
|
||||
patches.push(
|
||||
dispatch(
|
||||
imagesApi.util.updateQueryData(
|
||||
'getImageDTO',
|
||||
image_name,
|
||||
imageDTO.image_name,
|
||||
(draft) => {
|
||||
Object.assign(draft, newImageDTO);
|
||||
Object.assign(draft, { board_id });
|
||||
}
|
||||
)
|
||||
)
|
||||
);
|
||||
|
||||
// Do the "Remove from" cache updates
|
||||
removeFromQueryArgs.forEach((queryArgs) => {
|
||||
// *remove* from [no_board|board_id]/[images|assets]
|
||||
patches.push(
|
||||
dispatch(
|
||||
imagesApi.util.updateQueryData(
|
||||
'listImages',
|
||||
{
|
||||
board_id: imageDTO.board_id ?? 'none',
|
||||
categories,
|
||||
},
|
||||
(draft) => {
|
||||
const oldTotal = draft.total;
|
||||
const newState = imagesAdapter.removeOne(
|
||||
draft,
|
||||
imageDTO.image_name
|
||||
);
|
||||
const delta = newState.total - oldTotal;
|
||||
draft.total = draft.total + delta;
|
||||
}
|
||||
)
|
||||
)
|
||||
);
|
||||
|
||||
// $cache = board_id/[images|assets]
|
||||
const queryArgs = { board_id: board_id ?? 'none', 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(
|
||||
dispatch(
|
||||
imagesApi.util.updateQueryData(
|
||||
'listImages',
|
||||
queryArgs,
|
||||
(draft) => {
|
||||
// sanity check
|
||||
if (draft.ids.includes(image_name)) {
|
||||
imagesAdapter.removeOne(draft, image_name);
|
||||
draft.total = Math.max(draft.total - 1, 0);
|
||||
}
|
||||
const oldTotal = draft.total;
|
||||
const newState = imagesAdapter.addOne(draft, imageDTO);
|
||||
const delta = newState.total - oldTotal;
|
||||
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 {
|
||||
@ -531,87 +619,97 @@ export const imagesApi = api.injectEndpoints({
|
||||
body: { board_id, image_name },
|
||||
};
|
||||
},
|
||||
invalidatesTags: (result, error, arg) => [
|
||||
{ type: 'BoardImage' },
|
||||
{ type: 'Board', id: arg.imageDTO.board_id },
|
||||
invalidatesTags: (result, error, { imageDTO }) => [
|
||||
{ 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(
|
||||
{ imageDTO },
|
||||
{ dispatch, queryFulfilled, getState }
|
||||
) {
|
||||
/**
|
||||
* Cache changes for `removeImageFromBoard`:
|
||||
* - *update* `getImageDTO`
|
||||
* - IF the image's `created_at` is within the range of the board's cached images
|
||||
* - THEN *add* to "No Board"
|
||||
* - *remove* from `old_board_id`
|
||||
* Cache changes for removeImageFromBoard:
|
||||
* - *update* getImageDTO
|
||||
* - *remove* from board_id/[images|assets]
|
||||
* - $cache = no_board/[images|assets]
|
||||
* - 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[] = [];
|
||||
|
||||
// Updated imageDTO with new board_id
|
||||
const newImageDTO = { ...imageDTO, board_id: undefined };
|
||||
|
||||
// Update getImageDTO cache
|
||||
// *update* getImageDTO
|
||||
patches.push(
|
||||
dispatch(
|
||||
imagesApi.util.updateQueryData(
|
||||
'getImageDTO',
|
||||
image_name,
|
||||
imageDTO.image_name,
|
||||
(draft) => {
|
||||
Object.assign(draft, newImageDTO);
|
||||
Object.assign(draft, { board_id: undefined });
|
||||
}
|
||||
)
|
||||
)
|
||||
);
|
||||
|
||||
// Remove from old board
|
||||
if (old_board_id) {
|
||||
const oldBoardQueryArgs = { board_id: old_board_id };
|
||||
patches.push(
|
||||
dispatch(
|
||||
imagesApi.util.updateQueryData(
|
||||
'listImages',
|
||||
oldBoardQueryArgs,
|
||||
(draft) => {
|
||||
// sanity check
|
||||
if (draft.ids.includes(image_name)) {
|
||||
imagesAdapter.removeOne(draft, image_name);
|
||||
draft.total = Math.max(draft.total - 1, 0);
|
||||
}
|
||||
}
|
||||
)
|
||||
// *remove* from board_id/[images|assets]
|
||||
patches.push(
|
||||
dispatch(
|
||||
imagesApi.util.updateQueryData(
|
||||
'listImages',
|
||||
{
|
||||
board_id: imageDTO.board_id ?? 'none',
|
||||
categories,
|
||||
},
|
||||
(draft) => {
|
||||
const oldTotal = draft.total;
|
||||
const newState = imagesAdapter.removeOne(
|
||||
draft,
|
||||
imageDTO.image_name
|
||||
);
|
||||
const delta = newState.total - oldTotal;
|
||||
draft.total = draft.total + delta;
|
||||
}
|
||||
)
|
||||
);
|
||||
}
|
||||
)
|
||||
);
|
||||
|
||||
// Add to "No Board"
|
||||
const noBoardQueryArgs = { board_id: 'none' };
|
||||
const { data } = imagesApi.endpoints.listImages.select(
|
||||
noBoardQueryArgs
|
||||
)(getState());
|
||||
// $cache = no_board/[images|assets]
|
||||
const queryArgs = { board_id: 'none', categories };
|
||||
const currentCache = imagesApi.endpoints.listImages.select(queryArgs)(
|
||||
getState()
|
||||
);
|
||||
|
||||
// Check if we need to make any cache changes
|
||||
const cacheAction = getCacheAction(data, imageDTO);
|
||||
// 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 (['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(
|
||||
dispatch(
|
||||
imagesApi.util.updateQueryData(
|
||||
'listImages',
|
||||
noBoardQueryArgs,
|
||||
queryArgs,
|
||||
(draft) => {
|
||||
if (cacheAction === 'add') {
|
||||
imagesAdapter.addOne(draft, imageDTO);
|
||||
draft.total += 1;
|
||||
} else {
|
||||
imagesAdapter.updateOne(draft, {
|
||||
id: image_name,
|
||||
changes: { board_id: undefined },
|
||||
});
|
||||
}
|
||||
const oldTotal = draft.total;
|
||||
const newState = imagesAdapter.upsertOne(draft, imageDTO);
|
||||
const delta = newState.total - oldTotal;
|
||||
draft.total = draft.total + delta;
|
||||
}
|
||||
)
|
||||
)
|
||||
@ -635,7 +733,9 @@ export const {
|
||||
useGetImageDTOQuery,
|
||||
useGetImageMetadataQuery,
|
||||
useDeleteImageMutation,
|
||||
useUpdateImageMutation,
|
||||
// useUpdateImageMutation,
|
||||
useGetBoardImagesTotalQuery,
|
||||
useGetBoardAssetsTotalQuery,
|
||||
useUploadImageMutation,
|
||||
useAddImageToBoardMutation,
|
||||
useRemoveImageFromBoardMutation,
|
||||
|
@ -25,27 +25,27 @@ export const getIsImageInDateRange = (
|
||||
return false;
|
||||
};
|
||||
|
||||
/**
|
||||
* Determines the action we should take when an image may need to be added or updated in a cache.
|
||||
*/
|
||||
export const getCacheAction = (
|
||||
data: ImageCache | undefined,
|
||||
imageDTO: ImageDTO
|
||||
): 'add' | 'update' | 'none' => {
|
||||
const isInDateRange = getIsImageInDateRange(data, imageDTO);
|
||||
const isCacheFullyPopulated = data && data.total === data.ids.length;
|
||||
const shouldUpdateCache =
|
||||
Boolean(isInDateRange) || Boolean(isCacheFullyPopulated);
|
||||
// /**
|
||||
// * Determines the action we should take when an image may need to be added or updated in a cache.
|
||||
// */
|
||||
// export const getCacheAction = (
|
||||
// data: ImageCache | undefined,
|
||||
// imageDTO: ImageDTO
|
||||
// ): 'add' | 'update' | 'none' => {
|
||||
// const isInDateRange = getIsImageInDateRange(data, imageDTO);
|
||||
// const isCacheFullyPopulated = data && data.total === data.ids.length;
|
||||
// const shouldUpdateCache =
|
||||
// Boolean(isInDateRange) || Boolean(isCacheFullyPopulated);
|
||||
|
||||
const isImageInCache = data && data.ids.includes(imageDTO.image_name);
|
||||
// const isImageInCache = data && data.ids.includes(imageDTO.image_name);
|
||||
|
||||
if (shouldUpdateCache && isImageInCache) {
|
||||
return 'update';
|
||||
}
|
||||
// if (shouldUpdateCache && isImageInCache) {
|
||||
// return 'update';
|
||||
// }
|
||||
|
||||
if (shouldUpdateCache && !isImageInCache) {
|
||||
return 'add';
|
||||
}
|
||||
// if (shouldUpdateCache && !isImageInCache) {
|
||||
// return 'add';
|
||||
// }
|
||||
|
||||
return 'none';
|
||||
};
|
||||
// return 'none';
|
||||
// };
|
||||
|
@ -4,19 +4,8 @@ import { useListAllBoardsQuery } from '../endpoints/boards';
|
||||
export const useBoardName = (board_id: BoardId | null | undefined) => {
|
||||
const { boardName } = useListAllBoardsQuery(undefined, {
|
||||
selectFromResult: ({ data }) => {
|
||||
let boardName = '';
|
||||
if (board_id === 'images') {
|
||||
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';
|
||||
}
|
||||
const selectedBoard = data?.find((b) => b.board_id === board_id);
|
||||
const boardName = selectedBoard?.board_name || 'Uncategorized';
|
||||
|
||||
return { boardName };
|
||||
},
|
||||
|
@ -1,53 +1,21 @@
|
||||
import { skipToken } from '@reduxjs/toolkit/dist/query';
|
||||
import {
|
||||
ASSETS_CATEGORIES,
|
||||
BoardId,
|
||||
IMAGE_CATEGORIES,
|
||||
INITIAL_IMAGE_LIMIT,
|
||||
} from 'features/gallery/store/gallerySlice';
|
||||
import { useAppSelector } from 'app/store/storeHooks';
|
||||
import { BoardId } from 'features/gallery/store/gallerySlice';
|
||||
import { useMemo } from 'react';
|
||||
import { ListImagesArgs, useListImagesQuery } from '../endpoints/images';
|
||||
import {
|
||||
useGetBoardAssetsTotalQuery,
|
||||
useGetBoardImagesTotalQuery,
|
||||
} from '../endpoints/images';
|
||||
|
||||
const baseQueryArg: ListImagesArgs = {
|
||||
offset: 0,
|
||||
limit: INITIAL_IMAGE_LIMIT,
|
||||
is_intermediate: false,
|
||||
};
|
||||
|
||||
const imagesQueryArg: ListImagesArgs = {
|
||||
categories: IMAGE_CATEGORIES,
|
||||
...baseQueryArg,
|
||||
};
|
||||
|
||||
const assetsQueryArg: ListImagesArgs = {
|
||||
categories: ASSETS_CATEGORIES,
|
||||
...baseQueryArg,
|
||||
};
|
||||
|
||||
const noBoardQueryArg: ListImagesArgs = {
|
||||
board_id: 'none',
|
||||
...baseQueryArg,
|
||||
};
|
||||
|
||||
export const useBoardTotal = (board_id: BoardId | null | undefined) => {
|
||||
const queryArg = useMemo(() => {
|
||||
if (!board_id) {
|
||||
return;
|
||||
}
|
||||
if (board_id === 'images') {
|
||||
return imagesQueryArg;
|
||||
} else if (board_id === 'assets') {
|
||||
return assetsQueryArg;
|
||||
} else if (board_id === 'no_board') {
|
||||
return noBoardQueryArg;
|
||||
} else {
|
||||
return { board_id, ...baseQueryArg };
|
||||
}
|
||||
}, [board_id]);
|
||||
|
||||
const { total } = useListImagesQuery(queryArg ?? skipToken, {
|
||||
selectFromResult: ({ currentData }) => ({ total: currentData?.total }),
|
||||
});
|
||||
|
||||
return total;
|
||||
export const useBoardTotal = (board_id: BoardId) => {
|
||||
const galleryView = useAppSelector((state) => state.gallery.galleryView);
|
||||
|
||||
const { data: totalImages } = useGetBoardImagesTotalQuery(board_id);
|
||||
const { data: totalAssets } = useGetBoardAssetsTotalQuery(board_id);
|
||||
|
||||
const currentViewTotal = useMemo(
|
||||
() => (galleryView === 'images' ? totalImages : totalAssets),
|
||||
[galleryView, totalAssets, totalImages]
|
||||
);
|
||||
|
||||
return { totalImages, totalAssets, currentViewTotal };
|
||||
};
|
||||
|
@ -10,6 +10,8 @@ import { $authToken, $baseUrl } from 'services/api/client';
|
||||
|
||||
export const tagTypes = [
|
||||
'Board',
|
||||
'BoardImagesTotal',
|
||||
'BoardAssetsTotal',
|
||||
'Image',
|
||||
'ImageNameList',
|
||||
'ImageList',
|
||||
|
Loading…
Reference in New Issue
Block a user