feat(ui): refactor boards hierarchy

This commit is contained in:
psychedelicious 2023-07-21 13:48:15 +10:00
parent 8fdc8a8da5
commit 0e3ca59e49
17 changed files with 739 additions and 572 deletions

View File

@ -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,

View File

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

View File

@ -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 }}>

View File

@ -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>) => {

View File

@ -24,6 +24,7 @@ import { useUpdateBoardMutation } from 'services/api/endpoints/boards';
import { useGetImageDTOQuery } from 'services/api/endpoints/images';
import { BoardDTO } from 'services/api/types';
import BoardContextMenu from '../BoardContextMenu';
import { useBoardTotal } from 'services/api/hooks/useBoardTotal';
const AUTO_ADD_BADGE_STYLES: ChakraProps['sx'] = {
bg: 'accent.200',
@ -64,6 +65,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 +146,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={FaFolder}
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,15 +196,8 @@ 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>
<Box

View File

@ -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 {
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 { useAppDispatch } from 'app/store/storeHooks';
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';
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 NoBoardBoard = memo(({ isSelected }: Props) => {
const dispatch = useAppDispatch();
const { totalImages, totalAssets } = useBoardTotal(undefined);
const handleSelectBoard = useCallback(() => {
dispatch(boardIdSelected(undefined));
}, [dispatch]);
const handleClick = () => {
dispatch(boardIdSelected('no_board'));
};
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
onClick={handleSelectBoard}
sx={{
position: 'relative',
justifyContent: 'center',
alignItems: 'center',
aspectRatio: '1/1',
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;

View File

@ -54,44 +54,44 @@ const CurrentImagePreview = () => {
shouldAntialiasProgressImage,
} = useAppSelector(imagesSelector);
const {
handlePrevImage,
handleNextImage,
prevImageId,
nextImageId,
isOnLastImage,
handleLoadMoreImages,
areMoreImagesAvailable,
isFetching,
} = useNextPrevImage();
// const {
// handlePrevImage,
// handleNextImage,
// prevImageId,
// nextImageId,
// isOnLastImage,
// handleLoadMoreImages,
// areMoreImagesAvailable,
// isFetching,
// } = useNextPrevImage();
useHotkeys(
'left',
() => {
handlePrevImage();
},
[prevImageId]
);
// useHotkeys(
// 'left',
// () => {
// handlePrevImage();
// },
// [prevImageId]
// );
useHotkeys(
'right',
() => {
if (isOnLastImage && areMoreImagesAvailable && !isFetching) {
handleLoadMoreImages();
return;
}
if (!isOnLastImage) {
handleNextImage();
}
},
[
nextImageId,
isOnLastImage,
areMoreImagesAvailable,
handleLoadMoreImages,
isFetching,
]
);
// useHotkeys(
// 'right',
// () => {
// if (isOnLastImage && areMoreImagesAvailable && !isFetching) {
// handleLoadMoreImages();
// return;
// }
// if (!isOnLastImage) {
// handleNextImage();
// }
// },
// [
// nextImageId,
// isOnLastImage,
// areMoreImagesAvailable,
// handleLoadMoreImages,
// isFetching,
// ]
// );
const {
currentData: imageDTO,
@ -213,7 +213,7 @@ const CurrentImagePreview = () => {
pointerEvents: 'none',
}}
>
<NextPrevImageButtons />
{/* <NextPrevImageButtons /> */}
</motion.div>
)}
</AnimatePresence>

View File

@ -27,16 +27,24 @@ 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

View File

@ -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 { 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 +37,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 +68,11 @@ const ImageGalleryContent = () => {
gap: 2,
}}
>
<GallerySettingsPopover />
<GalleryBoardName
isOpen={isBoardListOpen}
onToggle={onToggleBoardList}
/>
<GallerySettingsPopover />
<GalleryPinButton />
</Flex>
<Box>
@ -60,6 +80,39 @@ const ImageGalleryContent = () => {
</Box>
</Box>
<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' ? (
<BatchImageGrid />
) : (

View File

@ -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';

View File

@ -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,

View File

@ -14,13 +14,9 @@ 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[];
@ -28,6 +24,7 @@ type GalleryState = {
autoAddBoardId: string | null;
galleryImageMinimumWidth: number;
selectedBoardId: BoardId;
galleryView: GalleryView;
batchImageNames: string[];
isBatchEnabled: boolean;
};
@ -37,7 +34,8 @@ export const initialGalleryState: GalleryState = {
shouldAutoSwitch: true,
autoAddBoardId: null,
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;
@ -128,6 +127,9 @@ export const gallerySlice = createSlice({
autoAddBoardIdChanged: (state, action: PayloadAction<string | null>) => {
state.autoAddBoardId = action.payload;
},
galleryViewChanged: (state, action: PayloadAction<GalleryView>) => {
state.galleryView = action.payload;
},
},
extraReducers: (builder) => {
builder.addMatcher(
@ -170,6 +172,7 @@ export const {
imagesAddedToBatch,
imagesRemovedFromBatch,
autoAddBoardIdChanged,
galleryViewChanged,
} = gallerySlice.actions;
export default gallerySlice.reducer;

View File

@ -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;
};

View File

@ -6,18 +6,17 @@ import {
BoardId,
IMAGE_CATEGORIES,
} from 'features/gallery/store/gallerySlice';
import { omit } from 'lodash-es';
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 { getCacheAction, getIsImageInDateRange } from './util';
import { getCategories } from 'features/gallery/store/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, 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,165 @@ 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, 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, 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 +439,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 +455,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 +501,122 @@ export const imagesApi = api.injectEndpoints({
body: { board_id, image_name },
};
},
invalidatesTags: (result, error, arg) => [
invalidatesTags: (result, error, { board_id, imageDTO }) => [
{ 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(
{ 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) => {
if (imageDTO.board_id) {
// *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(
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 +635,98 @@ export const imagesApi = api.injectEndpoints({
body: { board_id, image_name },
};
},
invalidatesTags: (result, error, arg) => [
invalidatesTags: (result, error, { imageDTO }) => [
{ 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(
{ 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,
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 +750,9 @@ export const {
useGetImageDTOQuery,
useGetImageMetadataQuery,
useDeleteImageMutation,
useUpdateImageMutation,
// useUpdateImageMutation,
useGetBoardImagesTotalQuery,
useGetBoardAssetsTotalQuery,
useUploadImageMutation,
useAddImageToBoardMutation,
useRemoveImageFromBoardMutation,

View File

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

View File

@ -1,53 +1,45 @@
import { skipToken } from '@reduxjs/toolkit/dist/query';
import {
ASSETS_CATEGORIES,
BoardId,
IMAGE_CATEGORIES,
INITIAL_IMAGE_LIMIT,
} from 'features/gallery/store/gallerySlice';
import { useMemo } from 'react';
import { ListImagesArgs, useListImagesQuery } from '../endpoints/images';
import {
ListImagesArgs,
useGetBoardAssetsTotalQuery,
useGetBoardImagesTotalQuery,
} from '../endpoints/images';
const baseQueryArg: ListImagesArgs = {
const baseQueryArgs: 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 { data: totalImages } = useGetBoardImagesTotalQuery(board_id);
const { data: totalAssets } = useGetBoardAssetsTotalQuery(board_id);
// const imagesQueryArg = useMemo(() => {
// const categories = IMAGE_CATEGORIES;
// return { board_id, categories, ...baseQueryArgs };
// }, [board_id]);
// const assetsQueryArg = useMemo(() => {
// const categories = ASSETS_CATEGORIES;
// return { board_id, categories, ...baseQueryArgs };
// }, [board_id]);
// const { total: totalImages } = useListImagesQuery(
// imagesQueryArg ?? skipToken,
// {
// selectFromResult: ({ currentData }) => ({ total: currentData?.total }),
// }
// );
// const { total: totalAssets } = useListImagesQuery(
// assetsQueryArg ?? skipToken,
// {
// selectFromResult: ({ currentData }) => ({ total: currentData?.total }),
// }
// );
return { totalImages, totalAssets };
};

View File

@ -10,6 +10,8 @@ import { $authToken, $baseUrl } from 'services/api/client';
export const tagTypes = [
'Board',
'BoardImagesTotal',
'BoardAssetsTotal',
'Image',
'ImageNameList',
'ImageList',