feat(ui): remove all calls to getBoardImagesTotals/getBoardAssetsTotals

This caused a crapload of network requests any time an image was generated.

The counts are necessary to handle the logic for inserting images into existing image list caches; we have to keep track of the counts.

Replace tag invalidation with manual cache updates in all cases, except the initial request (which is necessary to get the initial image counts).

One subtle change is to make the counts an object instead of a number. This is required for `immer` to handle draft states. This should be raised as a bug with RTK Query, as no error is thrown when attempting to update a primitive immer draft.
This commit is contained in:
psychedelicious
2023-10-04 20:32:04 +11:00
parent fbb61f2334
commit 2acc93eb8e
7 changed files with 370 additions and 173 deletions

View File

@ -8,6 +8,7 @@ import {
} from 'features/gallery/store/gallerySlice'; } from 'features/gallery/store/gallerySlice';
import { IMAGE_CATEGORIES } from 'features/gallery/store/types'; import { IMAGE_CATEGORIES } from 'features/gallery/store/types';
import { CANVAS_OUTPUT } from 'features/nodes/util/graphBuilders/constants'; import { CANVAS_OUTPUT } from 'features/nodes/util/graphBuilders/constants';
import { boardsApi } from 'services/api/endpoints/boards';
import { imagesApi } from 'services/api/endpoints/images'; import { imagesApi } from 'services/api/endpoints/images';
import { isImageOutput } from 'services/api/guards'; import { isImageOutput } from 'services/api/guards';
import { imagesAdapter } from 'services/api/util'; import { imagesAdapter } from 'services/api/util';
@ -70,11 +71,21 @@ export const addInvocationCompleteEventListener = () => {
) )
); );
// update the total images for the board
dispatch(
boardsApi.util.updateQueryData(
'getBoardImagesTotal',
imageDTO.board_id ?? 'none',
(draft) => {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
draft.total += 1;
}
)
);
dispatch( dispatch(
imagesApi.util.invalidateTags([ imagesApi.util.invalidateTags([
{ type: 'BoardImagesTotal', id: imageDTO.board_id }, { type: 'Board', id: imageDTO.board_id ?? 'none' },
{ type: 'BoardAssetsTotal', id: imageDTO.board_id },
{ type: 'Board', id: imageDTO.board_id },
]) ])
); );

View File

@ -77,12 +77,12 @@ const GalleryBoard = ({
const { data: imagesTotal } = useGetBoardImagesTotalQuery(board.board_id); const { data: imagesTotal } = useGetBoardImagesTotalQuery(board.board_id);
const { data: assetsTotal } = useGetBoardAssetsTotalQuery(board.board_id); const { data: assetsTotal } = useGetBoardAssetsTotalQuery(board.board_id);
const tooltip = useMemo(() => { const tooltip = useMemo(() => {
if (!imagesTotal || !assetsTotal) { if (!imagesTotal?.total || !assetsTotal?.total) {
return undefined; return undefined;
} }
return `${imagesTotal} image${ return `${imagesTotal.total} image${imagesTotal.total > 1 ? 's' : ''}, ${
imagesTotal > 1 ? 's' : '' assetsTotal.total
}, ${assetsTotal} asset${assetsTotal > 1 ? 's' : ''}`; } asset${assetsTotal.total > 1 ? 's' : ''}`;
}, [assetsTotal, imagesTotal]); }, [assetsTotal, imagesTotal]);
const { currentData: coverImage } = useGetImageDTOQuery( const { currentData: coverImage } = useGetImageDTOQuery(

View File

@ -1,4 +1,4 @@
import { Box, Flex, Image, Text } from '@chakra-ui/react'; import { Box, Flex, Image, Text, Tooltip } from '@chakra-ui/react';
import { createSelector } from '@reduxjs/toolkit'; import { createSelector } from '@reduxjs/toolkit';
import { stateSelector } from 'app/store/store'; import { stateSelector } from 'app/store/store';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
@ -15,6 +15,10 @@ import { memo, useCallback, useMemo, useState } from 'react';
import { useBoardName } from 'services/api/hooks/useBoardName'; import { useBoardName } from 'services/api/hooks/useBoardName';
import AutoAddIcon from '../AutoAddIcon'; import AutoAddIcon from '../AutoAddIcon';
import BoardContextMenu from '../BoardContextMenu'; import BoardContextMenu from '../BoardContextMenu';
import {
useGetBoardAssetsTotalQuery,
useGetBoardImagesTotalQuery,
} from 'services/api/endpoints/boards';
interface Props { interface Props {
isSelected: boolean; isSelected: boolean;
@ -41,6 +45,17 @@ const NoBoardBoard = memo(({ isSelected }: Props) => {
}, [dispatch, autoAssignBoardOnClick]); }, [dispatch, autoAssignBoardOnClick]);
const [isHovered, setIsHovered] = useState(false); const [isHovered, setIsHovered] = useState(false);
const { data: imagesTotal } = useGetBoardImagesTotalQuery('none');
const { data: assetsTotal } = useGetBoardAssetsTotalQuery('none');
const tooltip = useMemo(() => {
if (!imagesTotal?.total || !assetsTotal?.total) {
return undefined;
}
return `${imagesTotal.total} image${imagesTotal.total > 1 ? 's' : ''}, ${
assetsTotal.total
} asset${assetsTotal.total > 1 ? 's' : ''}`;
}, [assetsTotal, imagesTotal]);
const handleMouseOver = useCallback(() => { const handleMouseOver = useCallback(() => {
setIsHovered(true); setIsHovered(true);
}, []); }, []);
@ -74,77 +89,82 @@ const NoBoardBoard = memo(({ isSelected }: Props) => {
> >
<BoardContextMenu board_id="none"> <BoardContextMenu board_id="none">
{(ref) => ( {(ref) => (
<Flex <Tooltip label={tooltip} openDelay={1000} hasArrow>
ref={ref}
onClick={handleSelectBoard}
sx={{
w: 'full',
h: 'full',
position: 'relative',
justifyContent: 'center',
alignItems: 'center',
borderRadius: 'base',
cursor: 'pointer',
bg: 'base.200',
_dark: {
bg: 'base.800',
},
}}
>
<Flex <Flex
ref={ref}
onClick={handleSelectBoard}
sx={{ sx={{
w: 'full', w: 'full',
h: 'full', h: 'full',
position: 'relative',
justifyContent: 'center', justifyContent: 'center',
alignItems: 'center', alignItems: 'center',
borderRadius: 'base',
cursor: 'pointer',
bg: 'base.200',
_dark: {
bg: 'base.800',
},
}} }}
> >
<Image <Flex
src={InvokeAILogoImage}
alt="invoke-ai-logo"
sx={{ sx={{
opacity: 0.4, w: 'full',
filter: 'grayscale(1)', h: 'full',
mt: -6, justifyContent: 'center',
w: 16, alignItems: 'center',
h: 16,
minW: 16,
minH: 16,
userSelect: 'none',
}} }}
>
<Image
src={InvokeAILogoImage}
alt="invoke-ai-logo"
sx={{
opacity: 0.4,
filter: 'grayscale(1)',
mt: -6,
w: 16,
h: 16,
minW: 16,
minH: 16,
userSelect: 'none',
}}
/>
</Flex>
{autoAddBoardId === 'none' && <AutoAddIcon />}
<Flex
sx={{
position: 'absolute',
bottom: 0,
left: 0,
p: 1,
justifyContent: 'center',
alignItems: 'center',
w: 'full',
maxW: 'full',
borderBottomRadius: 'base',
bg: isSelected ? 'accent.400' : 'base.500',
color: isSelected ? 'base.50' : 'base.100',
_dark: {
bg: isSelected ? 'accent.500' : 'base.600',
color: isSelected ? 'base.50' : 'base.100',
},
lineHeight: 'short',
fontSize: 'xs',
fontWeight: isSelected ? 700 : 500,
}}
>
{boardName}
</Flex>
<SelectionOverlay
isSelected={isSelected}
isHovered={isHovered}
/>
<IAIDroppable
data={droppableData}
dropLabel={<Text fontSize="md">Move</Text>}
/> />
</Flex> </Flex>
{autoAddBoardId === 'none' && <AutoAddIcon />} </Tooltip>
<Flex
sx={{
position: 'absolute',
bottom: 0,
left: 0,
p: 1,
justifyContent: 'center',
alignItems: 'center',
w: 'full',
maxW: 'full',
borderBottomRadius: 'base',
bg: isSelected ? 'accent.400' : 'base.500',
color: isSelected ? 'base.50' : 'base.100',
_dark: {
bg: isSelected ? 'accent.500' : 'base.600',
color: isSelected ? 'base.50' : 'base.100',
},
lineHeight: 'short',
fontSize: 'xs',
fontWeight: isSelected ? 700 : 500,
}}
>
{boardName}
</Flex>
<SelectionOverlay isSelected={isSelected} isHovered={isHovered} />
<IAIDroppable
data={droppableData}
dropLabel={<Text fontSize="md">Move</Text>}
/>
</Flex>
)} )}
</BoardContextMenu> </BoardContextMenu>
</Flex> </Flex>

View File

@ -20,7 +20,7 @@ export const nextPrevImageButtonsSelector = createSelector(
const { data, status } = const { data, status } =
imagesApi.endpoints.listImages.select(baseQueryArgs)(state); imagesApi.endpoints.listImages.select(baseQueryArgs)(state);
const { data: total } = const { data: totalsData } =
state.gallery.galleryView === 'images' state.gallery.galleryView === 'images'
? boardsApi.endpoints.getBoardImagesTotal.select( ? boardsApi.endpoints.getBoardImagesTotal.select(
baseQueryArgs.board_id ?? 'none' baseQueryArgs.board_id ?? 'none'
@ -34,7 +34,7 @@ export const nextPrevImageButtonsSelector = createSelector(
const isFetching = status === 'pending'; const isFetching = status === 'pending';
if (!data || !lastSelectedImage || total === 0) { if (!data || !lastSelectedImage || totalsData?.total === 0) {
return { return {
isFetching, isFetching,
queryArgs: baseQueryArgs, queryArgs: baseQueryArgs,
@ -74,7 +74,7 @@ export const nextPrevImageButtonsSelector = createSelector(
return { return {
loadedImagesCount: images.length, loadedImagesCount: images.length,
currentImageIndex, currentImageIndex,
areMoreImagesAvailable: (total ?? 0) > imagesLength, areMoreImagesAvailable: (totalsData?.total ?? 0) > imagesLength,
isFetching: status === 'pending', isFetching: status === 'pending',
nextImage, nextImage,
prevImage, prevImage,

View File

@ -70,7 +70,7 @@ export const boardsApi = api.injectEndpoints({
keepUnusedDataFor: 0, keepUnusedDataFor: 0,
}), }),
getBoardImagesTotal: build.query<number, string | undefined>({ getBoardImagesTotal: build.query<{ total: number }, string | undefined>({
query: (board_id) => ({ query: (board_id) => ({
url: getListImagesUrl({ url: getListImagesUrl({
board_id: board_id ?? 'none', board_id: board_id ?? 'none',
@ -85,11 +85,11 @@ export const boardsApi = api.injectEndpoints({
{ type: 'BoardImagesTotal', id: arg ?? 'none' }, { type: 'BoardImagesTotal', id: arg ?? 'none' },
], ],
transformResponse: (response: OffsetPaginatedResults_ImageDTO_) => { transformResponse: (response: OffsetPaginatedResults_ImageDTO_) => {
return response.total; return { total: response.total };
}, },
}), }),
getBoardAssetsTotal: build.query<number, string | undefined>({ getBoardAssetsTotal: build.query<{ total: number }, string | undefined>({
query: (board_id) => ({ query: (board_id) => ({
url: getListImagesUrl({ url: getListImagesUrl({
board_id: board_id ?? 'none', board_id: board_id ?? 'none',
@ -104,7 +104,7 @@ export const boardsApi = api.injectEndpoints({
{ type: 'BoardAssetsTotal', id: arg ?? 'none' }, { type: 'BoardAssetsTotal', id: arg ?? 'none' },
], ],
transformResponse: (response: OffsetPaginatedResults_ImageDTO_) => { transformResponse: (response: OffsetPaginatedResults_ImageDTO_) => {
return response.total; return { total: response.total };
}, },
}), }),

View File

@ -103,6 +103,9 @@ export const imagesApi = api.injectEndpoints({
query: () => ({ url: getListImagesUrl({ is_intermediate: true }) }), query: () => ({ url: getListImagesUrl({ is_intermediate: true }) }),
providesTags: ['IntermediatesCount'], providesTags: ['IntermediatesCount'],
transformResponse: (response: OffsetPaginatedResults_ImageDTO_) => { transformResponse: (response: OffsetPaginatedResults_ImageDTO_) => {
// TODO: This is storing a primitive value in the cache. `immer` cannot track state changes, so
// attempts to use manual cache updates on this value will fail. This should be changed into an
// object.
return response.total; return response.total;
}, },
}), }),
@ -191,35 +194,51 @@ export const imagesApi = api.injectEndpoints({
url: `images/i/${image_name}`, url: `images/i/${image_name}`,
method: 'DELETE', method: 'DELETE',
}), }),
invalidatesTags: (result, error, { board_id }) => [
{ type: 'BoardImagesTotal', id: board_id ?? 'none' },
{ type: 'BoardAssetsTotal', id: board_id ?? 'none' },
],
async onQueryStarted(imageDTO, { dispatch, queryFulfilled }) { async onQueryStarted(imageDTO, { dispatch, queryFulfilled }) {
/** /**
* Cache changes for `deleteImage`: * Cache changes for `deleteImage`:
* - NOT POSSIBLE: *remove* from getImageDTO * - NOT POSSIBLE: *remove* from getImageDTO
* - $cache = [board_id|no_board]/[images|assets] * - $cache = [board_id|no_board]/[images|assets]
* - *remove* from $cache * - *remove* from $cache
* - decrement the image's board's total
*/ */
const { image_name, board_id } = imageDTO; const { image_name, board_id } = imageDTO;
const isAsset = ASSETS_CATEGORIES.includes(imageDTO.image_category);
const queryArg = { const queryArg = {
board_id: board_id ?? 'none', board_id: board_id ?? 'none',
categories: getCategories(imageDTO), categories: getCategories(imageDTO),
}; };
const patch = dispatch( const patches: PatchCollection[] = [];
imagesApi.util.updateQueryData('listImages', queryArg, (draft) => {
imagesAdapter.removeOne(draft, image_name); patches.push(
}) dispatch(
imagesApi.util.updateQueryData('listImages', queryArg, (draft) => {
imagesAdapter.removeOne(draft, image_name);
})
)
); );
patches.push(
dispatch(
boardsApi.util.updateQueryData(
isAsset ? 'getBoardAssetsTotal' : 'getBoardImagesTotal',
imageDTO.board_id ?? 'none',
(draft) => {
draft.total = Math.max(draft.total - 1, 0);
}
)
)
); // decrement the image board's total
try { try {
await queryFulfilled; await queryFulfilled;
} catch { } catch {
patch.undo(); patches.forEach((patch) => {
patch.undo();
});
} }
}, },
}), }),
@ -237,18 +256,11 @@ export const imagesApi = api.injectEndpoints({
}, },
}; };
}, },
invalidatesTags: (result, error, { imageDTOs }) => {
// for now, assume bulk delete is all on one board
const boardId = imageDTOs[0]?.board_id;
return [
{ type: 'BoardImagesTotal', id: boardId ?? 'none' },
{ type: 'BoardAssetsTotal', id: boardId ?? 'none' },
];
},
async onQueryStarted({ imageDTOs }, { dispatch, queryFulfilled }) { async onQueryStarted({ imageDTOs }, { dispatch, queryFulfilled }) {
/** /**
* Cache changes for `deleteImages`: * Cache changes for `deleteImages`:
* - *remove* the deleted images from their boards * - *remove* the deleted images from their boards
* - decrement the images' board's totals
* *
* Unfortunately we cannot do an optimistic update here due to how immer handles patching * Unfortunately we cannot do an optimistic update here due to how immer handles patching
* arrays. You have to undo *all* patches, else the entity adapter's `ids` array is borked. * arrays. You have to undo *all* patches, else the entity adapter's `ids` array is borked.
@ -279,6 +291,21 @@ export const imagesApi = api.injectEndpoints({
} }
) )
); );
const isAsset = ASSETS_CATEGORIES.includes(
imageDTO.image_category
);
// decrement the image board's total
dispatch(
boardsApi.util.updateQueryData(
isAsset ? 'getBoardAssetsTotal' : 'getBoardImagesTotal',
imageDTO.board_id ?? 'none',
(draft) => {
draft.total = Math.max(draft.total - 1, 0);
}
)
);
} }
}); });
} catch { } catch {
@ -298,10 +325,6 @@ export const imagesApi = api.injectEndpoints({
method: 'PATCH', method: 'PATCH',
body: { is_intermediate }, body: { is_intermediate },
}), }),
invalidatesTags: (result, error, { imageDTO }) => [
{ type: 'BoardImagesTotal', id: imageDTO.board_id ?? 'none' },
{ type: 'BoardAssetsTotal', id: imageDTO.board_id ?? 'none' },
],
async onQueryStarted( async onQueryStarted(
{ imageDTO, is_intermediate }, { imageDTO, is_intermediate },
{ dispatch, queryFulfilled, getState } { dispatch, queryFulfilled, getState }
@ -312,9 +335,11 @@ export const imagesApi = api.injectEndpoints({
* - $cache = [board_id|no_board]/[images|assets] * - $cache = [board_id|no_board]/[images|assets]
* - IF it is being changed to an intermediate: * - IF it is being changed to an intermediate:
* - remove from $cache * - remove from $cache
* - decrement the image's board's total
* - ELSE (it is being changed to a non-intermediate): * - ELSE (it is being changed to a non-intermediate):
* - IF it eligible for insertion into existing $cache: * - IF it eligible for insertion into existing $cache:
* - *upsert* to $cache * - *upsert* to $cache
* - increment the image's board's total
*/ */
// Store patches so we can undo if the query fails // Store patches so we can undo if the query fails
@ -335,6 +360,7 @@ export const imagesApi = api.injectEndpoints({
// $cache = [board_id|no_board]/[images|assets] // $cache = [board_id|no_board]/[images|assets]
const categories = getCategories(imageDTO); const categories = getCategories(imageDTO);
const isAsset = ASSETS_CATEGORIES.includes(imageDTO.image_category);
if (is_intermediate) { if (is_intermediate) {
// IF it is being changed to an intermediate: // IF it is being changed to an intermediate:
@ -350,8 +376,35 @@ export const imagesApi = api.injectEndpoints({
) )
) )
); );
// decrement the image board's total
patches.push(
dispatch(
boardsApi.util.updateQueryData(
isAsset ? 'getBoardAssetsTotal' : 'getBoardImagesTotal',
imageDTO.board_id ?? 'none',
(draft) => {
draft.total = Math.max(draft.total - 1, 0);
}
)
)
);
} else { } else {
// ELSE (it is being changed to a non-intermediate): // ELSE (it is being changed to a non-intermediate):
// increment the image board's total
patches.push(
dispatch(
boardsApi.util.updateQueryData(
isAsset ? 'getBoardAssetsTotal' : 'getBoardImagesTotal',
imageDTO.board_id ?? 'none',
(draft) => {
draft.total += 1;
}
)
)
);
const queryArgs = { const queryArgs = {
board_id: imageDTO.board_id ?? 'none', board_id: imageDTO.board_id ?? 'none',
categories, categories,
@ -361,9 +414,7 @@ export const imagesApi = api.injectEndpoints({
getState() getState()
); );
const { data: total } = IMAGE_CATEGORIES.includes( const { data } = IMAGE_CATEGORIES.includes(imageDTO.image_category)
imageDTO.image_category
)
? boardsApi.endpoints.getBoardImagesTotal.select( ? boardsApi.endpoints.getBoardImagesTotal.select(
imageDTO.board_id ?? 'none' imageDTO.board_id ?? 'none'
)(getState()) )(getState())
@ -378,7 +429,8 @@ export const imagesApi = api.injectEndpoints({
// - The image's `created_at` is within the range of the cached images // - The image's `created_at` is within the range of the cached images
const isCacheFullyPopulated = const isCacheFullyPopulated =
currentCache.data && currentCache.data.ids.length >= (total ?? 0); currentCache.data &&
currentCache.data.ids.length >= (data?.total ?? 0);
const isInDateRange = getIsImageInDateRange( const isInDateRange = getIsImageInDateRange(
currentCache.data, currentCache.data,
@ -420,10 +472,6 @@ export const imagesApi = api.injectEndpoints({
method: 'PATCH', method: 'PATCH',
body: { session_id }, body: { session_id },
}), }),
invalidatesTags: (result, error, { imageDTO }) => [
{ type: 'BoardImagesTotal', id: imageDTO.board_id ?? 'none' },
{ type: 'BoardAssetsTotal', id: imageDTO.board_id ?? 'none' },
],
async onQueryStarted( async onQueryStarted(
{ imageDTO, session_id }, { imageDTO, session_id },
{ dispatch, queryFulfilled } { dispatch, queryFulfilled }
@ -535,9 +583,7 @@ export const imagesApi = api.injectEndpoints({
queryArgs queryArgs
)(getState()); )(getState());
const { data: previousTotal } = IMAGE_CATEGORIES.includes( const { data } = IMAGE_CATEGORIES.includes(imageDTO.image_category)
imageDTO.image_category
)
? boardsApi.endpoints.getBoardImagesTotal.select( ? boardsApi.endpoints.getBoardImagesTotal.select(
boardId ?? 'none' boardId ?? 'none'
)(getState()) )(getState())
@ -547,10 +593,10 @@ export const imagesApi = api.injectEndpoints({
const isCacheFullyPopulated = const isCacheFullyPopulated =
currentCache.data && currentCache.data &&
currentCache.data.ids.length >= (previousTotal ?? 0); currentCache.data.ids.length >= (data?.total ?? 0);
const isInDateRange = const isInDateRange =
(previousTotal || 0) >= IMAGE_LIMIT (data?.total ?? 0) >= IMAGE_LIMIT
? getIsImageInDateRange(currentCache.data, imageDTO) ? getIsImageInDateRange(currentCache.data, imageDTO)
: true; : true;
@ -652,9 +698,7 @@ export const imagesApi = api.injectEndpoints({
queryArgs queryArgs
)(getState()); )(getState());
const { data: previousTotal } = IMAGE_CATEGORIES.includes( const { data } = IMAGE_CATEGORIES.includes(imageDTO.image_category)
imageDTO.image_category
)
? boardsApi.endpoints.getBoardImagesTotal.select( ? boardsApi.endpoints.getBoardImagesTotal.select(
boardId ?? 'none' boardId ?? 'none'
)(getState()) )(getState())
@ -664,10 +708,10 @@ export const imagesApi = api.injectEndpoints({
const isCacheFullyPopulated = const isCacheFullyPopulated =
currentCache.data && currentCache.data &&
currentCache.data.ids.length >= (previousTotal ?? 0); currentCache.data.ids.length >= (data?.total ?? 0);
const isInDateRange = const isInDateRange =
(previousTotal || 0) >= IMAGE_LIMIT (data?.total ?? 0) >= IMAGE_LIMIT
? getIsImageInDateRange(currentCache.data, imageDTO) ? getIsImageInDateRange(currentCache.data, imageDTO)
: true; : true;
@ -736,6 +780,7 @@ export const imagesApi = api.injectEndpoints({
* - BAIL OUT * - BAIL OUT
* - *add* to `getImageDTO` * - *add* to `getImageDTO`
* - *add* to no_board/assets * - *add* to no_board/assets
* - update the image's board's assets total
*/ */
const { data: imageDTO } = await queryFulfilled; const { data: imageDTO } = await queryFulfilled;
@ -770,11 +815,15 @@ export const imagesApi = api.injectEndpoints({
) )
); );
// increment new board's total
dispatch( dispatch(
imagesApi.util.invalidateTags([ boardsApi.util.updateQueryData(
{ type: 'BoardImagesTotal', id: imageDTO.board_id ?? 'none' }, 'getBoardAssetsTotal',
{ type: 'BoardAssetsTotal', id: imageDTO.board_id ?? 'none' }, imageDTO.board_id ?? 'none',
]) (draft) => {
draft.total += 1;
}
)
); );
} catch { } catch {
// query failed, no action needed // query failed, no action needed
@ -801,8 +850,6 @@ export const imagesApi = api.injectEndpoints({
categories: ASSETS_CATEGORIES, categories: ASSETS_CATEGORIES,
}), }),
}, },
{ type: 'BoardImagesTotal', id: 'none' },
{ type: 'BoardAssetsTotal', id: 'none' },
], ],
async onQueryStarted(board_id, { dispatch, queryFulfilled }) { async onQueryStarted(board_id, { dispatch, queryFulfilled }) {
/** /**
@ -815,6 +862,7 @@ export const imagesApi = api.injectEndpoints({
* have access to the deleted images DTOs - only the names, and a network request * have access to the deleted images DTOs - only the names, and a network request
* for all of a board's DTOs could be very large. Instead, we invalidate the 'No Board' * for all of a board's DTOs could be very large. Instead, we invalidate the 'No Board'
* cache. * cache.
* - set the board's totals to zero
*/ */
try { try {
@ -834,6 +882,28 @@ export const imagesApi = api.injectEndpoints({
); );
}); });
// set the board's asset total to 0 (feels unnecessary since we are deleting it?)
dispatch(
boardsApi.util.updateQueryData(
'getBoardAssetsTotal',
board_id,
(draft) => {
draft.total = 0;
}
)
);
// set the board's images total to 0 (feels unnecessary since we are deleting it?)
dispatch(
boardsApi.util.updateQueryData(
'getBoardImagesTotal',
board_id,
(draft) => {
draft.total = 0;
}
)
);
// update 'All Images' & 'All Assets' caches // update 'All Images' & 'All Assets' caches
const queryArgsToUpdate = [ const queryArgsToUpdate = [
{ {
@ -890,8 +960,6 @@ export const imagesApi = api.injectEndpoints({
categories: ASSETS_CATEGORIES, categories: ASSETS_CATEGORIES,
}), }),
}, },
{ type: 'BoardImagesTotal', id: 'none' },
{ type: 'BoardAssetsTotal', id: 'none' },
], ],
async onQueryStarted(board_id, { dispatch, queryFulfilled }) { async onQueryStarted(board_id, { dispatch, queryFulfilled }) {
/** /**
@ -901,6 +969,7 @@ export const imagesApi = api.injectEndpoints({
* Instead, we rely on the UI to remove all components that use the deleted images. * Instead, we rely on the UI to remove all components that use the deleted images.
* - Remove every image in the 'All Images' cache that has the board_id * - Remove every image in the 'All Images' cache that has the board_id
* - Remove every image in the 'All Assets' cache that has the board_id * - Remove every image in the 'All Assets' cache that has the board_id
* - set the board's totals to zero
*/ */
try { try {
@ -928,6 +997,28 @@ export const imagesApi = api.injectEndpoints({
) )
); );
}); });
// set the board's asset total to 0 (feels unnecessary since we are deleting it?)
dispatch(
boardsApi.util.updateQueryData(
'getBoardAssetsTotal',
board_id,
(draft) => {
draft.total = 0;
}
)
);
// set the board's images total to 0 (feels unnecessary since we are deleting it?)
dispatch(
boardsApi.util.updateQueryData(
'getBoardImagesTotal',
board_id,
(draft) => {
draft.total = 0;
}
)
);
} catch { } catch {
//no-op //no-op
} }
@ -945,15 +1036,9 @@ export const imagesApi = api.injectEndpoints({
body: { board_id, image_name }, body: { board_id, image_name },
}; };
}, },
invalidatesTags: (result, error, { board_id, imageDTO }) => [ invalidatesTags: (result, error, { board_id }) => [
// refresh the board itself // refresh the board itself
{ type: 'Board', id: board_id }, { type: 'Board', id: board_id },
// update old board totals
{ type: 'BoardImagesTotal', id: board_id },
{ type: 'BoardAssetsTotal', id: board_id },
// update new board totals
{ type: 'BoardImagesTotal', id: imageDTO.board_id ?? 'none' },
{ type: 'BoardAssetsTotal', id: imageDTO.board_id ?? 'none' },
], ],
async onQueryStarted( async onQueryStarted(
{ board_id, imageDTO }, { board_id, imageDTO },
@ -970,11 +1055,13 @@ export const imagesApi = api.injectEndpoints({
* - $cache = board_id/[images|assets] * - $cache = board_id/[images|assets]
* - IF it eligible for insertion into existing $cache: * - IF it eligible for insertion into existing $cache:
* - THEN *add* to $cache * - THEN *add* to $cache
* - decrement both old board's total
* - increment the new board's total
*/ */
const patches: PatchCollection[] = []; const patches: PatchCollection[] = [];
const categories = getCategories(imageDTO); const categories = getCategories(imageDTO);
const isAsset = ASSETS_CATEGORIES.includes(imageDTO.image_category);
// *update* getImageDTO // *update* getImageDTO
patches.push( patches.push(
dispatch( dispatch(
@ -1005,6 +1092,32 @@ export const imagesApi = api.injectEndpoints({
) )
); );
// decrement old board's total
patches.push(
dispatch(
boardsApi.util.updateQueryData(
isAsset ? 'getBoardAssetsTotal' : 'getBoardImagesTotal',
imageDTO.board_id ?? 'none',
(draft) => {
draft.total = Math.max(draft.total - 1, 0);
}
)
)
);
// increment new board's total
patches.push(
dispatch(
boardsApi.util.updateQueryData(
isAsset ? 'getBoardAssetsTotal' : 'getBoardImagesTotal',
board_id ?? 'none',
(draft) => {
draft.total += 1;
}
)
)
);
// $cache = board_id/[images|assets] // $cache = board_id/[images|assets]
const queryArgs = { board_id: board_id ?? 'none', categories }; const queryArgs = { board_id: board_id ?? 'none', categories };
const currentCache = imagesApi.endpoints.listImages.select(queryArgs)( const currentCache = imagesApi.endpoints.listImages.select(queryArgs)(
@ -1017,9 +1130,7 @@ export const imagesApi = api.injectEndpoints({
// OR // OR
// - The image's `created_at` is within the range of the cached images // - The image's `created_at` is within the range of the cached images
const { data: total } = IMAGE_CATEGORIES.includes( const { data } = IMAGE_CATEGORIES.includes(imageDTO.image_category)
imageDTO.image_category
)
? boardsApi.endpoints.getBoardImagesTotal.select( ? boardsApi.endpoints.getBoardImagesTotal.select(
imageDTO.board_id ?? 'none' imageDTO.board_id ?? 'none'
)(getState()) )(getState())
@ -1028,7 +1139,8 @@ export const imagesApi = api.injectEndpoints({
)(getState()); )(getState());
const isCacheFullyPopulated = const isCacheFullyPopulated =
currentCache.data && currentCache.data.ids.length >= (total ?? 0); currentCache.data &&
currentCache.data.ids.length >= (data?.total ?? 0);
const isInDateRange = getIsImageInDateRange( const isInDateRange = getIsImageInDateRange(
currentCache.data, currentCache.data,
@ -1072,12 +1184,6 @@ export const imagesApi = api.injectEndpoints({
return [ return [
// invalidate the image's old board // invalidate the image's old board
{ type: 'Board', id: board_id ?? 'none' }, { type: 'Board', id: board_id ?? 'none' },
// update old board totals
{ type: 'BoardImagesTotal', id: board_id ?? 'none' },
{ type: 'BoardAssetsTotal', id: board_id ?? 'none' },
// update the no_board totals
{ type: 'BoardImagesTotal', id: 'none' },
{ type: 'BoardAssetsTotal', id: 'none' },
]; ];
}, },
async onQueryStarted( async onQueryStarted(
@ -1091,10 +1197,13 @@ export const imagesApi = api.injectEndpoints({
* - $cache = no_board/[images|assets] * - $cache = no_board/[images|assets]
* - IF it eligible for insertion into existing $cache: * - IF it eligible for insertion into existing $cache:
* - THEN *upsert* to $cache * - THEN *upsert* to $cache
* - decrement old board's total
* - increment the new board's total (no board)
*/ */
const categories = getCategories(imageDTO); const categories = getCategories(imageDTO);
const patches: PatchCollection[] = []; const patches: PatchCollection[] = [];
const isAsset = ASSETS_CATEGORIES.includes(imageDTO.image_category);
// *update* getImageDTO // *update* getImageDTO
patches.push( patches.push(
@ -1125,6 +1234,32 @@ export const imagesApi = api.injectEndpoints({
) )
); );
// decrement old board's total
patches.push(
dispatch(
boardsApi.util.updateQueryData(
isAsset ? 'getBoardAssetsTotal' : 'getBoardImagesTotal',
imageDTO.board_id ?? 'none',
(draft) => {
draft.total = Math.max(draft.total - 1, 0);
}
)
)
);
// increment new board's total (no board)
patches.push(
dispatch(
boardsApi.util.updateQueryData(
isAsset ? 'getBoardAssetsTotal' : 'getBoardImagesTotal',
'none',
(draft) => {
draft.total += 1;
}
)
)
);
// $cache = no_board/[images|assets] // $cache = no_board/[images|assets]
const queryArgs = { board_id: 'none', categories }; const queryArgs = { board_id: 'none', categories };
const currentCache = imagesApi.endpoints.listImages.select(queryArgs)( const currentCache = imagesApi.endpoints.listImages.select(queryArgs)(
@ -1137,9 +1272,7 @@ export const imagesApi = api.injectEndpoints({
// OR // OR
// - The image's `created_at` is within the range of the cached images // - The image's `created_at` is within the range of the cached images
const { data: total } = IMAGE_CATEGORIES.includes( const { data } = IMAGE_CATEGORIES.includes(imageDTO.image_category)
imageDTO.image_category
)
? boardsApi.endpoints.getBoardImagesTotal.select( ? boardsApi.endpoints.getBoardImagesTotal.select(
imageDTO.board_id ?? 'none' imageDTO.board_id ?? 'none'
)(getState()) )(getState())
@ -1148,7 +1281,8 @@ export const imagesApi = api.injectEndpoints({
)(getState()); )(getState());
const isCacheFullyPopulated = const isCacheFullyPopulated =
currentCache.data && currentCache.data.ids.length >= (total ?? 0); currentCache.data &&
currentCache.data.ids.length >= (data?.total ?? 0);
const isInDateRange = getIsImageInDateRange( const isInDateRange = getIsImageInDateRange(
currentCache.data, currentCache.data,
@ -1192,21 +1326,10 @@ export const imagesApi = api.injectEndpoints({
board_id, board_id,
}, },
}), }),
invalidatesTags: (result, error, { imageDTOs, board_id }) => { invalidatesTags: (result, error, { board_id }) => {
//assume all images are being moved from one board for now
const oldBoardId = imageDTOs[0]?.board_id;
return [ return [
// update the destination board // update the destination board
{ type: 'Board', id: board_id ?? 'none' }, { type: 'Board', id: board_id ?? 'none' },
// update new board totals
{ type: 'BoardImagesTotal', id: board_id ?? 'none' },
{ type: 'BoardAssetsTotal', id: board_id ?? 'none' },
// update old board totals
{ type: 'BoardImagesTotal', id: oldBoardId ?? 'none' },
{ type: 'BoardAssetsTotal', id: oldBoardId ?? 'none' },
// update the no_board totals
{ type: 'BoardImagesTotal', id: 'none' },
{ type: 'BoardAssetsTotal', id: 'none' },
]; ];
}, },
async onQueryStarted( async onQueryStarted(
@ -1222,6 +1345,8 @@ export const imagesApi = api.injectEndpoints({
* - *update* getImageDTO for each image * - *update* getImageDTO for each image
* - *add* to board_id/[images|assets] * - *add* to board_id/[images|assets]
* - *remove* from [old_board_id|no_board]/[images|assets] * - *remove* from [old_board_id|no_board]/[images|assets]
* - decrement old board's totals for each image
* - increment new board's totals for each image
*/ */
added_image_names.forEach((image_name) => { added_image_names.forEach((image_name) => {
@ -1230,7 +1355,8 @@ export const imagesApi = api.injectEndpoints({
'getImageDTO', 'getImageDTO',
image_name, image_name,
(draft) => { (draft) => {
draft.board_id = new_board_id; draft.board_id =
new_board_id === 'none' ? undefined : new_board_id;
} }
) )
); );
@ -1243,6 +1369,7 @@ export const imagesApi = api.injectEndpoints({
const categories = getCategories(imageDTO); const categories = getCategories(imageDTO);
const old_board_id = imageDTO.board_id; const old_board_id = imageDTO.board_id;
const isAsset = ASSETS_CATEGORIES.includes(imageDTO.image_category);
// remove from the old board // remove from the old board
dispatch( dispatch(
@ -1255,6 +1382,28 @@ export const imagesApi = api.injectEndpoints({
) )
); );
// decrement old board's total
dispatch(
boardsApi.util.updateQueryData(
isAsset ? 'getBoardAssetsTotal' : 'getBoardImagesTotal',
old_board_id ?? 'none',
(draft) => {
draft.total = Math.max(draft.total - 1, 0);
}
)
);
// increment new board's total
dispatch(
boardsApi.util.updateQueryData(
isAsset ? 'getBoardAssetsTotal' : 'getBoardImagesTotal',
new_board_id ?? 'none',
(draft) => {
draft.total += 1;
}
)
);
const queryArgs = { const queryArgs = {
board_id: new_board_id, board_id: new_board_id,
categories, categories,
@ -1264,9 +1413,7 @@ export const imagesApi = api.injectEndpoints({
queryArgs queryArgs
)(getState()); )(getState());
const { data: previousTotal } = IMAGE_CATEGORIES.includes( const { data } = IMAGE_CATEGORIES.includes(imageDTO.image_category)
imageDTO.image_category
)
? boardsApi.endpoints.getBoardImagesTotal.select( ? boardsApi.endpoints.getBoardImagesTotal.select(
new_board_id ?? 'none' new_board_id ?? 'none'
)(getState()) )(getState())
@ -1276,10 +1423,10 @@ export const imagesApi = api.injectEndpoints({
const isCacheFullyPopulated = const isCacheFullyPopulated =
currentCache.data && currentCache.data &&
currentCache.data.ids.length >= (previousTotal ?? 0); currentCache.data.ids.length >= (data?.total ?? 0);
const isInDateRange = const isInDateRange =
(previousTotal || 0) >= IMAGE_LIMIT (data?.total ?? 0) >= IMAGE_LIMIT
? getIsImageInDateRange(currentCache.data, imageDTO) ? getIsImageInDateRange(currentCache.data, imageDTO)
: true; : true;
@ -1319,10 +1466,7 @@ export const imagesApi = api.injectEndpoints({
}), }),
invalidatesTags: (result, error, { imageDTOs }) => { invalidatesTags: (result, error, { imageDTOs }) => {
const touchedBoardIds: string[] = []; const touchedBoardIds: string[] = [];
const tags: ApiTagDescription[] = [ const tags: ApiTagDescription[] = [];
{ type: 'BoardImagesTotal', id: 'none' },
{ type: 'BoardAssetsTotal', id: 'none' },
];
result?.removed_image_names.forEach((image_name) => { result?.removed_image_names.forEach((image_name) => {
const board_id = imageDTOs.find((i) => i.image_name === image_name) const board_id = imageDTOs.find((i) => i.image_name === image_name)
@ -1333,8 +1477,6 @@ export const imagesApi = api.injectEndpoints({
} }
tags.push({ type: 'Board', id: board_id }); tags.push({ type: 'Board', id: board_id });
tags.push({ type: 'BoardImagesTotal', id: board_id });
tags.push({ type: 'BoardAssetsTotal', id: board_id });
}); });
return tags; return tags;
@ -1352,6 +1494,8 @@ export const imagesApi = api.injectEndpoints({
* - *update* getImageDTO for each image * - *update* getImageDTO for each image
* - *remove* from old_board_id/[images|assets] * - *remove* from old_board_id/[images|assets]
* - *add* to no_board/[images|assets] * - *add* to no_board/[images|assets]
* - decrement old board's totals for each image
* - increment new board's (no board) totals for each image
*/ */
removed_image_names.forEach((image_name) => { removed_image_names.forEach((image_name) => {
@ -1372,6 +1516,7 @@ export const imagesApi = api.injectEndpoints({
} }
const categories = getCategories(imageDTO); const categories = getCategories(imageDTO);
const isAsset = ASSETS_CATEGORIES.includes(imageDTO.image_category);
// remove from the old board // remove from the old board
dispatch( dispatch(
@ -1384,6 +1529,28 @@ export const imagesApi = api.injectEndpoints({
) )
); );
// decrement old board's total
dispatch(
boardsApi.util.updateQueryData(
isAsset ? 'getBoardAssetsTotal' : 'getBoardImagesTotal',
imageDTO.board_id ?? 'none',
(draft) => {
draft.total = Math.max(draft.total - 1, 0);
}
)
);
// increment new board's total (no board)
dispatch(
boardsApi.util.updateQueryData(
isAsset ? 'getBoardAssetsTotal' : 'getBoardImagesTotal',
'none',
(draft) => {
draft.total += 1;
}
)
);
// add to `no_board` // add to `no_board`
const queryArgs = { const queryArgs = {
board_id: 'none', board_id: 'none',
@ -1394,9 +1561,7 @@ export const imagesApi = api.injectEndpoints({
queryArgs queryArgs
)(getState()); )(getState());
const { data: total } = IMAGE_CATEGORIES.includes( const { data } = IMAGE_CATEGORIES.includes(imageDTO.image_category)
imageDTO.image_category
)
? boardsApi.endpoints.getBoardImagesTotal.select( ? boardsApi.endpoints.getBoardImagesTotal.select(
imageDTO.board_id ?? 'none' imageDTO.board_id ?? 'none'
)(getState()) )(getState())
@ -1405,10 +1570,11 @@ export const imagesApi = api.injectEndpoints({
)(getState()); )(getState());
const isCacheFullyPopulated = const isCacheFullyPopulated =
currentCache.data && currentCache.data.ids.length >= (total ?? 0); currentCache.data &&
currentCache.data.ids.length >= (data?.total ?? 0);
const isInDateRange = const isInDateRange =
(total || 0) >= IMAGE_LIMIT (data?.total ?? 0) >= IMAGE_LIMIT
? getIsImageInDateRange(currentCache.data, imageDTO) ? getIsImageInDateRange(currentCache.data, imageDTO)
: true; : true;

View File

@ -13,7 +13,7 @@ export const useBoardTotal = (board_id: BoardId) => {
const { data: totalAssets } = useGetBoardAssetsTotalQuery(board_id); const { data: totalAssets } = useGetBoardAssetsTotalQuery(board_id);
const currentViewTotal = useMemo( const currentViewTotal = useMemo(
() => (galleryView === 'images' ? totalImages : totalAssets), () => (galleryView === 'images' ? totalImages?.total : totalAssets?.total),
[galleryView, totalAssets, totalImages] [galleryView, totalAssets, totalImages]
); );