diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/galleryImageClicked.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/galleryImageClicked.ts index 43f9355125..9cd4790c0c 100644 --- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/galleryImageClicked.ts +++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/galleryImageClicked.ts @@ -32,14 +32,14 @@ export const addGalleryImageClickedListener = (startAppListening: AppStartListen const { imageDTO, shiftKey, ctrlKey, metaKey, altKey } = action.payload; const state = getState(); const queryArgs = selectListImagesQueryArgs(state); - const { data: listImagesData } = imagesApi.endpoints.listImages.select(queryArgs)(state); + const queryResult = imagesApi.endpoints.listImages.select(queryArgs)(state); - if (!listImagesData) { + if (!queryResult.data) { // Should never happen if we have clicked a gallery image return; } - const imageDTOs = imagesSelectors.selectAll(listImagesData); + const imageDTOs = queryResult.data.items const selection = state.gallery.selection; if (altKey) { diff --git a/invokeai/frontend/web/src/common/components/IAIDndImageIcon.tsx b/invokeai/frontend/web/src/common/components/IAIDndImageIcon.tsx index 650a0b6a14..10a0f28a84 100644 --- a/invokeai/frontend/web/src/common/components/IAIDndImageIcon.tsx +++ b/invokeai/frontend/web/src/common/components/IAIDndImageIcon.tsx @@ -1,47 +1,37 @@ -import type { SystemStyleObject } from '@invoke-ai/ui-library'; +import type { IconButtonProps, SystemStyleObject } from '@invoke-ai/ui-library'; import { IconButton } from '@invoke-ai/ui-library'; import type { MouseEvent, ReactElement } from 'react'; -import { memo, useMemo } from 'react'; +import { memo } from 'react'; -type Props = { +const sx: SystemStyleObject = { + minW: 0, + svg: { + transitionProperty: 'common', + transitionDuration: 'normal', + fill: 'base.100', + _hover: { fill: 'base.50' }, + filter: 'drop-shadow(0px 0px 0.1rem var(--invoke-colors-base-800))', + }, +}; + +type Props = Omit & { onClick: (event: MouseEvent) => void; tooltip: string; - icon?: ReactElement; - styleOverrides?: SystemStyleObject; }; const IAIDndImageIcon = (props: Props) => { - const { onClick, tooltip, icon, styleOverrides } = props; - - const sx = useMemo( - () => ({ - position: 'absolute', - top: 1, - insetInlineEnd: 1, - p: 0, - minW: 0, - svg: { - transitionProperty: 'common', - transitionDuration: 'normal', - fill: 'base.100', - _hover: { fill: 'base.50' }, - filter: 'drop-shadow(0px 0px 0.1rem var(--invoke-colors-base-800))', - }, - ...styleOverrides, - }), - [styleOverrides] - ); + const { onClick, tooltip, icon, ...rest } = props; return ( ); }; diff --git a/invokeai/frontend/web/src/features/controlAdapters/components/ControlAdapterImagePreview.tsx b/invokeai/frontend/web/src/features/controlAdapters/components/ControlAdapterImagePreview.tsx index bf1c7dce9f..c46bd90876 100644 --- a/invokeai/frontend/web/src/features/controlAdapters/components/ControlAdapterImagePreview.tsx +++ b/invokeai/frontend/web/src/features/controlAdapters/components/ControlAdapterImagePreview.tsx @@ -185,7 +185,7 @@ const ControlAdapterImagePreview = ({ isSmall, id }: Props) => { /> - <> + : undefined} @@ -195,15 +195,13 @@ const ControlAdapterImagePreview = ({ isSmall, id }: Props) => { onClick={handleSaveControlImage} icon={controlImage ? : undefined} tooltip={t('controlnet.saveControlImage')} - styleOverrides={saveControlImageStyleOverrides} /> : undefined} tooltip={t('controlnet.setControlImageDimensions')} - styleOverrides={setControlImageDimensionsStyleOverrides} /> - + {pendingControlImages.includes(id) && ( { }; export default memo(ControlAdapterImagePreview); - -const saveControlImageStyleOverrides: SystemStyleObject = { mt: 6 }; -const setControlImageDimensionsStyleOverrides: SystemStyleObject = { mt: 12 }; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/ControlAndIPAdapter/ControlAdapterImagePreview.tsx b/invokeai/frontend/web/src/features/controlLayers/components/ControlAndIPAdapter/ControlAdapterImagePreview.tsx index c61cdda4a3..e9507aced1 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/ControlAndIPAdapter/ControlAdapterImagePreview.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/ControlAndIPAdapter/ControlAdapterImagePreview.tsx @@ -203,13 +203,13 @@ export const ControlAdapterImagePreview = memo( onClick={handleSaveControlImage} icon={controlImage ? : undefined} tooltip={t('controlnet.saveControlImage')} - styleOverrides={saveControlImageStyleOverrides} + mt={6} /> : undefined} tooltip={shift ? t('controlnet.setControlImageDimensionsForce') : t('controlnet.setControlImageDimensions')} - styleOverrides={setControlImageDimensionsStyleOverrides} + mt={12} /> @@ -235,6 +235,3 @@ export const ControlAdapterImagePreview = memo( ); ControlAdapterImagePreview.displayName = 'ControlAdapterImagePreview'; - -const saveControlImageStyleOverrides: SystemStyleObject = { mt: 6 }; -const setControlImageDimensionsStyleOverrides: SystemStyleObject = { mt: 12 }; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/ControlAndIPAdapter/IPAdapterImagePreview.tsx b/invokeai/frontend/web/src/features/controlLayers/components/ControlAndIPAdapter/IPAdapterImagePreview.tsx index 93d493bcbc..b89402b80e 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/ControlAndIPAdapter/IPAdapterImagePreview.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/ControlAndIPAdapter/IPAdapterImagePreview.tsx @@ -100,7 +100,7 @@ export const IPAdapterImagePreview = memo( onClick={handleSetControlImageToDimensions} icon={controlImage ? : undefined} tooltip={shift ? t('controlnet.setControlImageDimensionsForce') : t('controlnet.setControlImageDimensions')} - styleOverrides={setControlImageDimensionsStyleOverrides} + mt={6} /> @@ -109,5 +109,3 @@ export const IPAdapterImagePreview = memo( ); IPAdapterImagePreview.displayName = 'IPAdapterImagePreview'; - -const setControlImageDimensionsStyleOverrides: SystemStyleObject = { mt: 6 }; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/IILayer/InitialImagePreview.tsx b/invokeai/frontend/web/src/features/controlLayers/components/IILayer/InitialImagePreview.tsx index 9053c0c123..6c8cb2df8a 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/IILayer/InitialImagePreview.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/IILayer/InitialImagePreview.tsx @@ -97,7 +97,7 @@ export const InitialImagePreview = memo(({ image, onChangeImage, droppableData, onClick={onUseSize} icon={imageDTO ? : undefined} tooltip={shift ? t('controlnet.setControlImageDimensionsForce') : t('controlnet.setControlImageDimensions')} - styleOverrides={useSizeStyleOverrides} + mt={6} /> @@ -105,5 +105,3 @@ export const InitialImagePreview = memo(({ image, onChangeImage, droppableData, }); InitialImagePreview.displayName = 'InitialImagePreview'; - -const useSizeStyleOverrides: SystemStyleObject = { mt: 6 }; diff --git a/invokeai/frontend/web/src/features/gallery/components/ImageGrid/GalleryImage.tsx b/invokeai/frontend/web/src/features/gallery/components/ImageGrid/GalleryImage.tsx index e5e216c97c..b11dd179bf 100644 --- a/invokeai/frontend/web/src/features/gallery/components/ImageGrid/GalleryImage.tsx +++ b/invokeai/frontend/web/src/features/gallery/components/ImageGrid/GalleryImage.tsx @@ -16,13 +16,11 @@ import type { MouseEvent } from 'react'; import { memo, useCallback, useMemo, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { PiStarBold, PiStarFill, PiTrashSimpleFill } from 'react-icons/pi'; -import { useGetImageDTOQuery, useStarImagesMutation, useUnstarImagesMutation } from 'services/api/endpoints/images'; +import { useStarImagesMutation, useUnstarImagesMutation } from 'services/api/endpoints/images'; +import { ImageDTO } from '../../../../services/api/types'; +import { imageItemContainerTestId } from './ImageGridItemContainer'; const imageSx: SystemStyleObject = { w: 'full', h: 'full' }; -const imageIconStyleOverrides: SystemStyleObject = { - bottom: 2, - top: 'auto', -}; const boxSx: SystemStyleObject = { containerType: 'inline-size', }; @@ -34,24 +32,22 @@ const badgeSx: SystemStyleObject = { }; interface HoverableImageProps { - imageName: string; + imageDTO: ImageDTO; index: number; } -const GalleryImage = (props: HoverableImageProps) => { +const GalleryImage = ({ index, imageDTO }: HoverableImageProps) => { const dispatch = useAppDispatch(); - const { imageName } = props; - const { currentData: imageDTO } = useGetImageDTOQuery(imageName); const shift = useShiftModifier(); const { t } = useTranslation(); const selectedBoardId = useAppSelector((s) => s.gallery.selectedBoardId); const alwaysShowImageSizeBadge = useAppSelector((s) => s.gallery.alwaysShowImageSizeBadge); - const isSelectedForCompare = useAppSelector((s) => s.gallery.imageToCompare?.image_name === imageName); + const isSelectedForCompare = useAppSelector((s) => s.gallery.imageToCompare?.image_name === imageDTO.image_name); const { handleClick, isSelected, areMultiplesSelected } = useMultiselect(imageDTO); const customStarUi = useStore($customStarUI); - const imageContainerRef = useScrollIntoView(isSelected, props.index, areMultiplesSelected); + const imageContainerRef = useScrollIntoView(isSelected, index, areMultiplesSelected); const handleDelete = useCallback( (e: MouseEvent) => { @@ -114,32 +110,32 @@ const GalleryImage = (props: HoverableImageProps) => { }, []); const starIcon = useMemo(() => { - if (imageDTO?.starred) { + if (imageDTO.starred) { return customStarUi ? customStarUi.on.icon : ; } - if (!imageDTO?.starred && isHovered) { + if (!imageDTO.starred && isHovered) { return customStarUi ? customStarUi.off.icon : ; } - }, [imageDTO?.starred, isHovered, customStarUi]); + }, [imageDTO.starred, isHovered, customStarUi]); const starTooltip = useMemo(() => { - if (imageDTO?.starred) { + if (imageDTO.starred) { return customStarUi ? customStarUi.off.text : 'Unstar'; } - if (!imageDTO?.starred) { + if (!imageDTO.starred) { return customStarUi ? customStarUi.on.text : 'Star'; } return ''; - }, [imageDTO?.starred, customStarUi]); + }, [imageDTO.starred, customStarUi]); - const dataTestId = useMemo(() => getGalleryImageDataTestId(imageDTO?.image_name), [imageDTO?.image_name]); + const dataTestId = useMemo(() => getGalleryImageDataTestId(imageDTO.image_name), [imageDTO.image_name]); if (!imageDTO) { return ; } return ( - + { pointerEvents="none" >{`${imageDTO.width}x${imageDTO.height}`} )} - + {isHovered && shift && ( } - tooltip={t('gallery.deleteImage', { count: 1 })} - styleOverrides={imageIconStyleOverrides} + tooltip={t('gallery.deleteImage_one')} + position="absolute" + bottom={1} + insetInlineEnd={1} /> )} diff --git a/invokeai/frontend/web/src/features/gallery/components/ImageGrid/GalleryImageGrid.tsx b/invokeai/frontend/web/src/features/gallery/components/ImageGrid/GalleryImageGrid.tsx index 608205c894..a00e425016 100644 --- a/invokeai/frontend/web/src/features/gallery/components/ImageGrid/GalleryImageGrid.tsx +++ b/invokeai/frontend/web/src/features/gallery/components/ImageGrid/GalleryImageGrid.tsx @@ -1,120 +1,41 @@ -import { Box, Button, Flex } from '@invoke-ai/ui-library'; -import type { EntityId } from '@reduxjs/toolkit'; +import { Box, Flex, Grid, IconButton } from '@invoke-ai/ui-library'; +import { EMPTY_ARRAY } from 'app/store/constants'; import { useAppSelector } from 'app/store/storeHooks'; import { IAINoContentFallback } from 'common/components/IAIImageFallback'; -import { overlayScrollbarsParams } from 'common/components/OverlayScrollbars/constants'; -import { virtuosoGridRefs } from 'features/gallery/components/ImageGrid/types'; import { useGalleryHotkeys } from 'features/gallery/hooks/useGalleryHotkeys'; -import { useGalleryImages } from 'features/gallery/hooks/useGalleryImages'; -import { useOverlayScrollbars } from 'overlayscrollbars-react'; -import type { CSSProperties } from 'react'; -import { memo, useCallback, useEffect, useRef, useState } from 'react'; +import { useGalleryPagination } from 'features/gallery/hooks/useGalleryImages'; +import { selectListImagesQueryArgs } from 'features/gallery/store/gallerySelectors'; +import { memo } from 'react'; import { useTranslation } from 'react-i18next'; -import { PiImageBold, PiWarningCircleBold } from 'react-icons/pi'; -import type { GridComponents, ItemContent, ListRange, VirtuosoGridHandle } from 'react-virtuoso'; -import { VirtuosoGrid } from 'react-virtuoso'; -import { useBoardTotal } from 'services/api/hooks/useBoardTotal'; +import { + PiCaretDoubleLeftBold, + PiCaretDoubleRightBold, + PiCaretLeftBold, + PiCaretRightBold, + PiImageBold, + PiWarningCircleBold, +} from 'react-icons/pi'; +import type { ImageDTO } from 'services/api/types'; import GalleryImage from './GalleryImage'; -import ImageGridItemContainer from './ImageGridItemContainer'; -import ImageGridListContainer from './ImageGridListContainer'; +import { useListImagesQuery } from '../../../../services/api/endpoints/images'; -const components: GridComponents = { - Item: ImageGridItemContainer, - List: ImageGridListContainer, -}; - -const virtuosoStyles: CSSProperties = { height: '100%' }; +export const imageListContainerTestId = 'image-list-container'; +export const imageItemContainerTestId = 'image-item-container'; const GalleryImageGrid = () => { - const { t } = useTranslation(); - const rootRef = useRef(null); - const [scroller, setScroller] = useState(null); - const [initialize, osInstance] = useOverlayScrollbars(overlayScrollbarsParams); - const selectedBoardId = useAppSelector((s) => s.gallery.selectedBoardId); - const { currentViewTotal } = useBoardTotal(selectedBoardId); - const virtuosoRangeRef = useRef(null); - const virtuosoRef = useRef(null); - const { - areMoreImagesAvailable, - handleLoadMoreImages, - queryResult: { currentData, isFetching, isSuccess, isError }, - } = useGalleryImages(); useGalleryHotkeys(); - const itemContentFunc: ItemContent = useCallback( - (index, imageName) => , - [] - ); - - useEffect(() => { - // Initialize the gallery's custom scrollbar - const { current: root } = rootRef; - if (scroller && root) { - initialize({ - target: root, - elements: { - viewport: scroller, - }, - }); - } - return () => osInstance()?.destroy(); - }, [scroller, initialize, osInstance]); - - const onRangeChanged = useCallback((range: ListRange) => { - virtuosoRangeRef.current = range; - }, []); - - useEffect(() => { - virtuosoGridRefs.set({ rootRef, virtuosoRangeRef, virtuosoRef }); - return () => { - virtuosoGridRefs.set({}); - }; - }, []); - - if (!currentData) { - return ( - - - - ); - } - - if (isSuccess && currentData?.ids.length === 0) { - return ( - - - - ); - } - - if (isSuccess && currentData) { - return ( - <> - - - - - - ); - } + const { t } = useTranslation(); + const galleryImageMinimumWidth = useAppSelector((s) => s.gallery.galleryImageMinimumWidth); + const queryArgs = useAppSelector(selectListImagesQueryArgs); + const { imageDTOs, isLoading, isSuccess, isError } = useListImagesQuery(queryArgs, { + selectFromResult: ({ data, isLoading, isSuccess, isError }) => ({ + imageDTOs: data?.items ?? EMPTY_ARRAY, + isLoading, + isSuccess, + isError, + }), + }); if (isError) { return ( @@ -124,7 +45,63 @@ const GalleryImageGrid = () => { ); } - return null; + if (isLoading) { + return ( + + + + ); + } + + if (imageDTOs.length === 0) { + return ( + + + + ); + } + + return ( + <> + + + {imageDTOs.map((imageDTO, index) => ( + + ))} + + + + + ); }; export default memo(GalleryImageGrid); + +const GalleryImageContainer = memo(({ imageDTO, index }: { imageDTO: ImageDTO; index: number }) => { + return ( + + + + ); +}); + +GalleryImageContainer.displayName = 'GalleryImageContainer'; + +const GalleryPagination = memo(() => { + const { first, prev, next, last, isFirstEnabled, isPrevEnabled, isNextEnabled, isLastEnabled } = + useGalleryPagination(); + return ( + + } onClick={first} isDisabled={!isFirstEnabled} /> + } onClick={prev} isDisabled={!isPrevEnabled} /> + } onClick={next} isDisabled={!isNextEnabled} /> + } onClick={last} isDisabled={!isLastEnabled} /> + + ); +}); + +GalleryPagination.displayName = 'GalleryPagination'; diff --git a/invokeai/frontend/web/src/features/gallery/components/NextPrevImageButtons.tsx b/invokeai/frontend/web/src/features/gallery/components/NextPrevImageButtons.tsx index 19368455e3..edeaf4e8cb 100644 --- a/invokeai/frontend/web/src/features/gallery/components/NextPrevImageButtons.tsx +++ b/invokeai/frontend/web/src/features/gallery/components/NextPrevImageButtons.tsx @@ -1,6 +1,6 @@ import type { ChakraProps } from '@invoke-ai/ui-library'; import { Box, Flex, IconButton, Spinner } from '@invoke-ai/ui-library'; -import { useGalleryImages } from 'features/gallery/hooks/useGalleryImages'; +import { useGalleryImages, useGalleryPagination } from 'features/gallery/hooks/useGalleryImages'; import { useGalleryNavigation } from 'features/gallery/hooks/useGalleryNavigation'; import { memo } from 'react'; import { useTranslation } from 'react-i18next'; @@ -16,11 +16,8 @@ const NextPrevImageButtons = () => { const { prevImage, nextImage, isOnFirstImage, isOnLastImage } = useGalleryNavigation(); - const { - areMoreImagesAvailable, - handleLoadMoreImages, - queryResult: { isFetching }, - } = useGalleryImages(); + const { isFetching } = useGalleryImages().queryResult; + const { isNextEnabled, next } = useGalleryPagination(); return ( @@ -47,17 +44,17 @@ const NextPrevImageButtons = () => { sx={nextPrevButtonStyles} /> )} - {isOnLastImage && areMoreImagesAvailable && !isFetching && ( + {isOnLastImage && isNextEnabled && !isFetching && ( } variant="unstyled" - onClick={handleLoadMoreImages} + onClick={next} boxSize={16} sx={nextPrevButtonStyles} /> )} - {isOnLastImage && areMoreImagesAvailable && isFetching && ( + {isOnLastImage && isNextEnabled && isFetching && ( diff --git a/invokeai/frontend/web/src/features/gallery/hooks/useGalleryHotkeys.ts b/invokeai/frontend/web/src/features/gallery/hooks/useGalleryHotkeys.ts index 931d93272b..221a85c397 100644 --- a/invokeai/frontend/web/src/features/gallery/hooks/useGalleryHotkeys.ts +++ b/invokeai/frontend/web/src/features/gallery/hooks/useGalleryHotkeys.ts @@ -1,10 +1,12 @@ import { useAppSelector } from 'app/store/storeHooks'; import { isStagingSelector } from 'features/canvas/store/canvasSelectors'; -import { useGalleryImages } from 'features/gallery/hooks/useGalleryImages'; +import { useGalleryPagination } from 'features/gallery/hooks/useGalleryImages'; import { useGalleryNavigation } from 'features/gallery/hooks/useGalleryNavigation'; import { activeTabNameSelector } from 'features/ui/store/uiSelectors'; import { useMemo } from 'react'; import { useHotkeys } from 'react-hotkeys-hook'; +import { useListImagesQuery } from '../../../services/api/endpoints/images'; +import { selectListImagesQueryArgs } from '../store/gallerySelectors'; /** * Registers gallery hotkeys. This hook is a singleton. @@ -17,21 +19,30 @@ export const useGalleryHotkeys = () => { return activeTabName !== 'canvas' || !isStaging; }, [activeTabName, isStaging]); - const { - areMoreImagesAvailable, - handleLoadMoreImages, - queryResult: { isFetching }, - } = useGalleryImages(); + const { next, prev, isNextEnabled, isPrevEnabled } = useGalleryPagination(); + const queryArgs = useAppSelector(selectListImagesQueryArgs); + const queryResult = useListImagesQuery(queryArgs); - const { handleLeftImage, handleRightImage, handleUpImage, handleDownImage, isOnLastImage, areImagesBelowCurrent } = - useGalleryNavigation(); + const { + handleLeftImage, + handleRightImage, + handleUpImage, + handleDownImage, + areImagesBelowCurrent, + isOnFirstImageOfView, + isOnLastImageOfView, + } = useGalleryNavigation(); useHotkeys( ['left', 'alt+left'], (e) => { + if (isOnFirstImageOfView && isPrevEnabled && !queryResult.isFetching) { + prev(); + return; + } canNavigateGallery && handleLeftImage(e.altKey); }, - [handleLeftImage, canNavigateGallery] + [handleLeftImage, canNavigateGallery, isOnFirstImageOfView] ); useHotkeys( @@ -40,15 +51,15 @@ export const useGalleryHotkeys = () => { if (!canNavigateGallery) { return; } - if (isOnLastImage && areMoreImagesAvailable && !isFetching) { - handleLoadMoreImages(); + if (isOnLastImageOfView && isNextEnabled && !queryResult.isFetching) { + next(); return; } - if (!isOnLastImage) { + if (!isOnLastImageOfView) { handleRightImage(e.altKey); } }, - [isOnLastImage, areMoreImagesAvailable, handleLoadMoreImages, isFetching, handleRightImage, canNavigateGallery] + [isOnLastImageOfView, next, isNextEnabled, queryResult.isFetching, handleRightImage, canNavigateGallery] ); useHotkeys( @@ -63,13 +74,13 @@ export const useGalleryHotkeys = () => { useHotkeys( ['down', 'alt+down'], (e) => { - if (!areImagesBelowCurrent && areMoreImagesAvailable && !isFetching) { - handleLoadMoreImages(); + if (!areImagesBelowCurrent && isNextEnabled && !queryResult.isFetching) { + next(); return; } handleDownImage(e.altKey); }, { preventDefault: true }, - [areImagesBelowCurrent, areMoreImagesAvailable, handleLoadMoreImages, isFetching, handleDownImage] + [areImagesBelowCurrent, next, isNextEnabled, queryResult.isFetching, handleDownImage] ); }; diff --git a/invokeai/frontend/web/src/features/gallery/hooks/useGalleryImages.ts b/invokeai/frontend/web/src/features/gallery/hooks/useGalleryImages.ts index b3310d5e34..98b8b601db 100644 --- a/invokeai/frontend/web/src/features/gallery/hooks/useGalleryImages.ts +++ b/invokeai/frontend/web/src/features/gallery/hooks/useGalleryImages.ts @@ -1,38 +1,120 @@ +import { EMPTY_ARRAY } from 'app/store/constants'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import { selectListImagesQueryArgs } from 'features/gallery/store/gallerySelectors'; -import { moreImagesLoaded } from 'features/gallery/store/gallerySlice'; +import { offsetChanged } from 'features/gallery/store/gallerySlice'; import { useCallback, useMemo } from 'react'; import { useGetBoardAssetsTotalQuery, useGetBoardImagesTotalQuery } from 'services/api/endpoints/boards'; import { useListImagesQuery } from 'services/api/endpoints/images'; -/** - * Provides access to the gallery images and a way to imperatively fetch more. - */ +const LIMIT = 20; + export const useGalleryImages = () => { - const dispatch = useAppDispatch(); - const galleryView = useAppSelector((s) => s.gallery.galleryView); const queryArgs = useAppSelector(selectListImagesQueryArgs); const queryResult = useListImagesQuery(queryArgs); - const selectedBoardId = useAppSelector((s) => s.gallery.selectedBoardId); - const { data: assetsTotal } = useGetBoardAssetsTotalQuery(selectedBoardId); - const { data: imagesTotal } = useGetBoardImagesTotalQuery(selectedBoardId); - const currentViewTotal = useMemo( - () => (galleryView === 'images' ? imagesTotal?.total : assetsTotal?.total), - [assetsTotal?.total, galleryView, imagesTotal?.total] - ); - const areMoreImagesAvailable = useMemo(() => { - if (!currentViewTotal || !queryResult.data) { - return false; - } - return queryResult.data.ids.length < currentViewTotal; - }, [queryResult.data, currentViewTotal]); - const handleLoadMoreImages = useCallback(() => { - dispatch(moreImagesLoaded()); - }, [dispatch]); - + const imageDTOs = useMemo(() => queryResult.data?.items ?? EMPTY_ARRAY, [queryResult.data]); return { - areMoreImagesAvailable, - handleLoadMoreImages, + imageDTOs, queryResult, }; }; + +export const useGalleryPagination = () => { + const dispatch = useAppDispatch(); + const offset = useAppSelector((s) => s.gallery.offset); + const galleryView = useAppSelector((s) => s.gallery.galleryView); + const selectedBoardId = useAppSelector((s) => s.gallery.selectedBoardId); + const queryArgs = useAppSelector(selectListImagesQueryArgs); + const { count } = useListImagesQuery(queryArgs, { + selectFromResult: ({ data }) => ({ count: data?.items.length ?? 0 }), + }); + const { data: assetsTotal } = useGetBoardAssetsTotalQuery(selectedBoardId); + const { data: imagesTotal } = useGetBoardImagesTotalQuery(selectedBoardId); + const total = useMemo(() => { + if (galleryView === 'images') { + return imagesTotal?.total ?? 0; + } else { + return assetsTotal?.total ?? 0; + } + }, [assetsTotal?.total, galleryView, imagesTotal?.total]); + const page = useMemo(() => Math.floor(offset / LIMIT), [offset]); + const pages = useMemo(() => Math.floor(total / LIMIT), [total]); + const isNextEnabled = useMemo(() => { + if (!count) { + return false; + } + return page < pages; + }, [count, page, pages]); + const isPrevEnabled = useMemo(() => { + if (!count) { + return false; + } + return offset > 0; + }, [count, offset]); + const next = useCallback(() => { + dispatch(offsetChanged(offset + LIMIT)); + }, [dispatch, offset]); + const prev = useCallback(() => { + dispatch(offsetChanged(Math.max(offset - LIMIT, 0))); + }, [dispatch, offset]); + const goToPage = useCallback( + (page: number) => { + const p = Math.max(0, Math.min(page, pages - 1)); + dispatch(offsetChanged(p)); + }, + [dispatch, pages] + ); + const first = useCallback(() => { + dispatch(offsetChanged(0)); + }, [dispatch]); + const last = useCallback(() => { + dispatch(offsetChanged(pages * LIMIT)); + }, [dispatch, pages]); + // calculate the page buttons to display - current page with 3 around it + const pageButtons = useMemo(() => { + const buttons = []; + const start = Math.max(0, page - 3); + const end = Math.min(pages, start + 6); + for (let i = start; i < end; i++) { + buttons.push(i); + } + return buttons; + }, [page, pages]); + const isFirstEnabled = useMemo(() => page > 0, [page]); + const isLastEnabled = useMemo(() => page < pages - 1, [page, pages]); + + const api = useMemo( + () => ({ + count, + total, + page, + pages, + isNextEnabled, + isPrevEnabled, + next, + prev, + goToPage, + first, + last, + pageButtons, + isFirstEnabled, + isLastEnabled, + }), + [ + count, + total, + page, + pages, + isNextEnabled, + isPrevEnabled, + next, + prev, + goToPage, + first, + last, + pageButtons, + isFirstEnabled, + isLastEnabled, + ] + ); + return api; +}; \ No newline at end of file diff --git a/invokeai/frontend/web/src/features/gallery/hooks/useGalleryNavigation.ts b/invokeai/frontend/web/src/features/gallery/hooks/useGalleryNavigation.ts index ce6b152577..531d5aa1a2 100644 --- a/invokeai/frontend/web/src/features/gallery/hooks/useGalleryNavigation.ts +++ b/invokeai/frontend/web/src/features/gallery/hooks/useGalleryNavigation.ts @@ -29,7 +29,7 @@ import { imagesSelectors } from 'services/api/util'; */ const getImagesPerRow = (): number => { const widthOfGalleryImage = - document.querySelector(`[data-testid="${imageItemContainerTestId}"]`)?.getBoundingClientRect().width ?? 1; + document.querySelector(`.${imageItemContainerTestId}`)?.getBoundingClientRect().width ?? 1; const widthOfGalleryGrid = document.querySelector(`[data-testid="${imageListContainerTestId}"]`)?.getBoundingClientRect().width ?? 0; @@ -115,6 +115,8 @@ type UseGalleryNavigationReturn = { isOnFirstImage: boolean; isOnLastImage: boolean; areImagesBelowCurrent: boolean; + isOnFirstImageOfView: boolean; + isOnLastImageOfView: boolean; }; /** @@ -134,23 +136,19 @@ export const useGalleryNavigation = (): UseGalleryNavigationReturn => { return lastSelected; } }); - const { - queryResult: { data }, - } = useGalleryImages(); - const loadedImagesCount = useMemo(() => data?.ids.length ?? 0, [data?.ids.length]); + const { imageDTOs } = useGalleryImages(); + const loadedImagesCount = useMemo(() => imageDTOs.length, [imageDTOs.length]); + const lastSelectedImageIndex = useMemo(() => { - if (!data || !lastSelectedImage) { + if (imageDTOs.length === 0 || !lastSelectedImage) { return 0; } - return imagesSelectors.selectAll(data).findIndex((i) => i.image_name === lastSelectedImage.image_name); - }, [lastSelectedImage, data]); + return imageDTOs.findIndex((i) => i.image_name === lastSelectedImage.image_name); + }, [imageDTOs, lastSelectedImage]); const handleNavigation = useCallback( (direction: 'left' | 'right' | 'up' | 'down', alt?: boolean) => { - if (!data) { - return; - } - const { index, image } = getImageFuncs[direction](imagesSelectors.selectAll(data), lastSelectedImageIndex); + const { index, image } = getImageFuncs[direction](imageDTOs, lastSelectedImageIndex); if (!image || index === lastSelectedImageIndex) { return; } @@ -161,7 +159,7 @@ export const useGalleryNavigation = (): UseGalleryNavigationReturn => { } scrollToImage(image.image_name, index); }, - [data, lastSelectedImageIndex, dispatch] + [imageDTOs, lastSelectedImageIndex, dispatch] ); const isOnFirstImage = useMemo(() => lastSelectedImageIndex === 0, [lastSelectedImageIndex]); @@ -176,6 +174,14 @@ export const useGalleryNavigation = (): UseGalleryNavigationReturn => { return lastSelectedImageIndex + imagesPerRow < loadedImagesCount; }, [lastSelectedImageIndex, loadedImagesCount]); + const isOnFirstImageOfView = useMemo(() => { + return lastSelectedImageIndex === 0; + }, [lastSelectedImageIndex]); + + const isOnLastImageOfView = useMemo(() => { + return lastSelectedImageIndex === loadedImagesCount - 1; + }, [lastSelectedImageIndex, loadedImagesCount]); + const handleLeftImage = useCallback( (alt?: boolean) => { handleNavigation('left', alt); @@ -222,5 +228,7 @@ export const useGalleryNavigation = (): UseGalleryNavigationReturn => { areImagesBelowCurrent, nextImage, prevImage, + isOnFirstImageOfView, + isOnLastImageOfView, }; }; diff --git a/invokeai/frontend/web/src/features/gallery/store/gallerySlice.ts b/invokeai/frontend/web/src/features/gallery/store/gallerySlice.ts index b1b673d10c..573cc7ab02 100644 --- a/invokeai/frontend/web/src/features/gallery/store/gallerySlice.ts +++ b/invokeai/frontend/web/src/features/gallery/store/gallerySlice.ts @@ -7,7 +7,7 @@ import { imagesApi } from 'services/api/endpoints/images'; import type { ImageDTO } from 'services/api/types'; import type { BoardId, ComparisonMode, GalleryState, GalleryView } from './types'; -import { IMAGE_LIMIT, INITIAL_IMAGE_LIMIT } from './types'; +import { IMAGE_LIMIT } from './types'; const initialGalleryState: GalleryState = { selection: [], @@ -19,7 +19,7 @@ const initialGalleryState: GalleryState = { selectedBoardId: 'none', galleryView: 'images', boardSearchText: '', - limit: INITIAL_IMAGE_LIMIT, + limit: IMAGE_LIMIT, offset: 0, isImageViewerOpen: true, imageToCompare: null, @@ -72,7 +72,7 @@ export const gallerySlice = createSlice({ state.selectedBoardId = action.payload.boardId; state.galleryView = 'images'; state.offset = 0; - state.limit = INITIAL_IMAGE_LIMIT; + state.limit = IMAGE_LIMIT; }, autoAddBoardIdChanged: (state, action: PayloadAction) => { if (!action.payload) { @@ -84,20 +84,11 @@ export const gallerySlice = createSlice({ galleryViewChanged: (state, action: PayloadAction) => { state.galleryView = action.payload; state.offset = 0; - state.limit = INITIAL_IMAGE_LIMIT; + state.limit = IMAGE_LIMIT; }, boardSearchTextChanged: (state, action: PayloadAction) => { state.boardSearchText = action.payload; }, - moreImagesLoaded: (state) => { - if (state.offset === 0 && state.limit === INITIAL_IMAGE_LIMIT) { - state.offset = INITIAL_IMAGE_LIMIT; - state.limit = IMAGE_LIMIT; - } else { - state.offset += IMAGE_LIMIT; - state.limit += IMAGE_LIMIT; - } - }, alwaysShowImageSizeBadgeChanged: (state, action: PayloadAction) => { state.alwaysShowImageSizeBadge = action.payload; }, @@ -114,6 +105,9 @@ export const gallerySlice = createSlice({ comparisonFitChanged: (state, action: PayloadAction<'contain' | 'fill'>) => { state.comparisonFit = action.payload; }, + offsetChanged: (state, action: PayloadAction) => { + state.offset = action.payload; + }, }, extraReducers: (builder) => { builder.addMatcher(isAnyBoardDeleted, (state, action) => { @@ -149,7 +143,6 @@ export const { galleryViewChanged, selectionChanged, boardSearchTextChanged, - moreImagesLoaded, alwaysShowImageSizeBadgeChanged, isImageViewerOpenChanged, imageToCompareChanged, @@ -157,6 +150,7 @@ export const { comparedImagesSwapped, comparisonFitChanged, comparisonModeCycled, + offsetChanged } = gallerySlice.actions; const isAnyBoardDeleted = isAnyOf( diff --git a/invokeai/frontend/web/src/features/gallery/store/types.ts b/invokeai/frontend/web/src/features/gallery/store/types.ts index a88715b0bd..c929ff992e 100644 --- a/invokeai/frontend/web/src/features/gallery/store/types.ts +++ b/invokeai/frontend/web/src/features/gallery/store/types.ts @@ -2,7 +2,6 @@ import type { ImageCategory, ImageDTO } from 'services/api/types'; export const IMAGE_CATEGORIES: ImageCategory[] = ['general']; export const ASSETS_CATEGORIES: ImageCategory[] = ['control', 'mask', 'user', 'other']; -export const INITIAL_IMAGE_LIMIT = 100; export const IMAGE_LIMIT = 20; export type GalleryView = 'images' | 'assets'; diff --git a/invokeai/frontend/web/src/services/api/endpoints/images.ts b/invokeai/frontend/web/src/services/api/endpoints/images.ts index c9052a607d..b88021fea9 100644 --- a/invokeai/frontend/web/src/services/api/endpoints/images.ts +++ b/invokeai/frontend/web/src/services/api/endpoints/images.ts @@ -14,6 +14,7 @@ import type { ImageCategory, ImageDTO, ListImagesArgs, + ListImagesResponse, OffsetPaginatedResults_ImageDTO_, PostUploadAction, } from 'services/api/types'; @@ -50,60 +51,13 @@ export const imagesApi = api.injectEndpoints({ /** * Image Queries */ - listImages: build.query, ListImagesArgs>({ + listImages: build.query({ query: (queryArgs) => ({ // Use the helper to create the URL. url: getListImagesUrl(queryArgs), method: 'GET', }), - providesTags: (result, error, { board_id, categories }) => [ - // Make the tags the same as the cache key - { type: 'ImageList', id: getListImagesUrl({ board_id, categories }) }, - 'FetchOnReconnect', - ], - serializeQueryArgs: ({ queryArgs }) => { - // Create cache & key based on board_id and categories - skip the other args. - // Offset is the size of the cache, and limit is always the same. Both are provided by - // the consumer of the query. - const { board_id, categories } = queryArgs; - - // Just use the same fn used to create the url; it makes an understandable cache key. - // This cache key is the same for any combo of board_id and categories, doesn't change - // when offset & limit change. - const cacheKey = getListImagesUrl({ board_id, categories }); - return cacheKey; - }, - transformResponse(response: OffsetPaginatedResults_ImageDTO_) { - const { items: images } = response; - // Use the adapter to convert the response to the right shape. - // The trick is to just provide an empty state and add the images array to it. This returns - // a properly shaped EntityState. - return imagesAdapter.addMany(imagesAdapter.getInitialState(), images); - }, - merge: (cache, response) => { - // Here we actually update the cache. `response` here is the output of `transformResponse` - // above. In a similar vein to `transformResponse`, we can use the imagesAdapter to get - // things in the right shape. - imagesAdapter.addMany(cache, imagesSelectors.selectAll(response)); - }, - forceRefetch({ currentArg, previousArg }) { - // Refetch when the offset changes (which means we are on a new page). - return currentArg?.offset !== previousArg?.offset; - }, - async onQueryStarted(_, { dispatch, queryFulfilled }) { - try { - const { data } = await queryFulfilled; - - // update the `getImageDTO` cache for each image - imagesSelectors.selectAll(data).forEach((imageDTO) => { - dispatch(imagesApi.util.upsertQueryData('getImageDTO', imageDTO.image_name, imageDTO)); - }); - } catch { - // no-op - } - }, - // 24 hours - reducing this to a few minutes would reduce memory usage. - keepUnusedDataFor: 86400, + providesTags: ['FetchOnReconnect'], }), getIntermediatesCount: build.query({ query: () => ({ url: buildImagesUrl('intermediates') }), @@ -133,53 +87,53 @@ export const imagesApi = api.injectEndpoints({ url: buildImagesUrl(`i/${image_name}`), method: 'DELETE', }), - async onQueryStarted(imageDTO, { dispatch, queryFulfilled }) { - /** - * Cache changes for `deleteImage`: - * - NOT POSSIBLE: *remove* from getImageDTO - * - $cache = [board_id|no_board]/[images|assets] - * - *remove* from $cache - * - decrement the image's board's total - */ + // async onQueryStarted(imageDTO, { dispatch, queryFulfilled }) { + // /** + // * Cache changes for `deleteImage`: + // * - NOT POSSIBLE: *remove* from getImageDTO + // * - $cache = [board_id|no_board]/[images|assets] + // * - *remove* from $cache + // * - decrement the image's board's total + // */ - const { image_name, board_id } = imageDTO; - const isAsset = ASSETS_CATEGORIES.includes(imageDTO.image_category); + // const { image_name, board_id } = imageDTO; + // const isAsset = ASSETS_CATEGORIES.includes(imageDTO.image_category); - const queryArg = { - board_id: board_id ?? 'none', - categories: getCategories(imageDTO), - }; + // const queryArg = { + // board_id: board_id ?? 'none', + // categories: getCategories(imageDTO), + // }; - const patches: PatchCollection[] = []; + // const patches: PatchCollection[] = []; - patches.push( - dispatch( - 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 + // 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 { - await queryFulfilled; - } catch { - patches.forEach((patch) => { - patch.undo(); - }); - } - }, + // try { + // await queryFulfilled; + // } catch { + // patches.forEach((patch) => { + // patch.undo(); + // }); + // } + // }, }), deleteImages: build.mutation({ query: ({ imageDTOs }) => { @@ -192,65 +146,65 @@ export const imagesApi = api.injectEndpoints({ }, }; }, - async onQueryStarted({ imageDTOs }, { dispatch, queryFulfilled }) { - /** - * Cache changes for `deleteImages`: - * - *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 - * arrays. You have to undo *all* patches, else the entity adapter's `ids` array is borked. - * So we have to wait for the query to complete before updating the cache. - */ - try { - const { data } = await queryFulfilled; + // async onQueryStarted({ imageDTOs }, { dispatch, queryFulfilled }) { + // /** + // * Cache changes for `deleteImages`: + // * - *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 + // * arrays. You have to undo *all* patches, else the entity adapter's `ids` array is borked. + // * So we have to wait for the query to complete before updating the cache. + // */ + // try { + // const { data } = await queryFulfilled; - if (data.deleted_images.length < imageDTOs.length) { - toast({ - id: 'problem-deleting-images', - title: t('gallery.problemDeletingImages'), - description: t('gallery.problemDeletingImagesDesc'), - status: 'warning', - }); - } + // if (data.deleted_images.length < imageDTOs.length) { + // toast({ + // id: 'problem-deleting-images', + // title: t('gallery.problemDeletingImages'), + // description: t('gallery.problemDeletingImagesDesc'), + // status: 'warning', + // }); + // } - // convert to an object so we can access the successfully delete image DTOs by name - const groupedImageDTOs = keyBy(imageDTOs, 'image_name'); + // // convert to an object so we can access the successfully delete image DTOs by name + // const groupedImageDTOs = keyBy(imageDTOs, 'image_name'); - data.deleted_images.forEach((image_name) => { - const imageDTO = groupedImageDTOs[image_name]; + // data.deleted_images.forEach((image_name) => { + // const imageDTO = groupedImageDTOs[image_name]; - // should never be undefined - if (imageDTO) { - const queryArg = { - board_id: imageDTO.board_id ?? 'none', - categories: getCategories(imageDTO), - }; - // remove all deleted images from their boards - dispatch( - imagesApi.util.updateQueryData('listImages', queryArg, (draft) => { - imagesAdapter.removeOne(draft, image_name); - }) - ); + // // should never be undefined + // if (imageDTO) { + // const queryArg = { + // board_id: imageDTO.board_id ?? 'none', + // categories: getCategories(imageDTO), + // }; + // // remove all deleted images from their boards + // dispatch( + // imagesApi.util.updateQueryData('listImages', queryArg, (draft) => { + // imagesAdapter.removeOne(draft, image_name); + // }) + // ); - const isAsset = ASSETS_CATEGORIES.includes(imageDTO.image_category); + // 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 { - // - } - }, + // // 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 { + // // + // } + // }, }), /** * Change an image's `is_intermediate` property. @@ -261,118 +215,118 @@ export const imagesApi = api.injectEndpoints({ method: 'PATCH', body: { is_intermediate }, }), - async onQueryStarted({ imageDTO, is_intermediate }, { dispatch, queryFulfilled, getState }) { - /** - * Cache changes for `changeImageIsIntermediate`: - * - *update* getImageDTO - * - $cache = [board_id|no_board]/[images|assets] - * - IF it is being changed to an intermediate: - * - remove from $cache - * - decrement the image's board's total - * - ELSE (it is being changed to a non-intermediate): - * - IF it eligible for insertion into existing $cache: - * - *upsert* to $cache - * - increment the image's board's total - */ + // async onQueryStarted({ imageDTO, is_intermediate }, { dispatch, queryFulfilled, getState }) { + // /** + // * Cache changes for `changeImageIsIntermediate`: + // * - *update* getImageDTO + // * - $cache = [board_id|no_board]/[images|assets] + // * - IF it is being changed to an intermediate: + // * - remove from $cache + // * - decrement the image's board's total + // * - ELSE (it is being changed to a non-intermediate): + // * - IF it eligible for insertion into existing $cache: + // * - *upsert* to $cache + // * - increment the image's board's total + // */ - // Store patches so we can undo if the query fails - const patches: PatchCollection[] = []; + // // 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, { is_intermediate }); - }) - ) - ); + // // *update* getImageDTO + // patches.push( + // dispatch( + // imagesApi.util.updateQueryData('getImageDTO', imageDTO.image_name, (draft) => { + // Object.assign(draft, { is_intermediate }); + // }) + // ) + // ); - // $cache = [board_id|no_board]/[images|assets] - const categories = getCategories(imageDTO); - const isAsset = ASSETS_CATEGORIES.includes(imageDTO.image_category); + // // $cache = [board_id|no_board]/[images|assets] + // const categories = getCategories(imageDTO); + // const isAsset = ASSETS_CATEGORIES.includes(imageDTO.image_category); - 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) => { - imagesAdapter.removeOne(draft, imageDTO.image_name); - } - ) - ) - ); + // 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) => { + // imagesAdapter.removeOne(draft, imageDTO.image_name); + // } + // ) + // ) + // ); - // 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 (it is being changed to a non-intermediate): + // // 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 (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; - } - ) - ) - ); + // // 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 = { - board_id: imageDTO.board_id ?? 'none', - categories, - }; + // const queryArgs = { + // board_id: imageDTO.board_id ?? 'none', + // categories, + // }; - const currentCache = imagesApi.endpoints.listImages.select(queryArgs)(getState()); + // const currentCache = imagesApi.endpoints.listImages.select(queryArgs)(getState()); - const { data } = IMAGE_CATEGORIES.includes(imageDTO.image_category) - ? boardsApi.endpoints.getBoardImagesTotal.select(imageDTO.board_id ?? 'none')(getState()) - : boardsApi.endpoints.getBoardAssetsTotal.select(imageDTO.board_id ?? 'none')(getState()); + // const { data } = IMAGE_CATEGORIES.includes(imageDTO.image_category) + // ? boardsApi.endpoints.getBoardImagesTotal.select(imageDTO.board_id ?? 'none')(getState()) + // : boardsApi.endpoints.getBoardAssetsTotal.select(imageDTO.board_id ?? 'none')(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 + // // 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 >= (data?.total ?? 0); + // const isCacheFullyPopulated = currentCache.data && currentCache.data.ids.length >= (data?.total ?? 0); - const isInDateRange = getIsImageInDateRange(currentCache.data, imageDTO); + // const isInDateRange = getIsImageInDateRange(currentCache.data, imageDTO); - if (isCacheFullyPopulated || isInDateRange) { - // *upsert* to $cache - patches.push( - dispatch( - imagesApi.util.updateQueryData('listImages', queryArgs, (draft) => { - imagesAdapter.upsertOne(draft, imageDTO); - }) - ) - ); - } - } + // if (isCacheFullyPopulated || isInDateRange) { + // // *upsert* to $cache + // patches.push( + // dispatch( + // imagesApi.util.updateQueryData('listImages', queryArgs, (draft) => { + // imagesAdapter.upsertOne(draft, imageDTO); + // }) + // ) + // ); + // } + // } - try { - await queryFulfilled; - } catch { - patches.forEach((patchResult) => patchResult.undo()); - } - }, + // try { + // await queryFulfilled; + // } catch { + // patches.forEach((patchResult) => patchResult.undo()); + // } + // }, }), /** * Star a list of images. @@ -408,65 +362,65 @@ export const imagesApi = api.injectEndpoints({ } return []; }, - async onQueryStarted({ imageDTOs }, { dispatch, queryFulfilled, getState }) { - try { - /** - * Cache changes for pinImages: - * - *update* getImageDTO for each image - * - *upsert* into list for each image - */ + // async onQueryStarted({ imageDTOs }, { dispatch, queryFulfilled, getState }) { + // try { + // /** + // * Cache changes for pinImages: + // * - *update* getImageDTO for each image + // * - *upsert* into list for each image + // */ - const { data } = await queryFulfilled; - const updatedImages = imageDTOs.filter((i) => data.updated_image_names.includes(i.image_name)); + // const { data } = await queryFulfilled; + // const updatedImages = imageDTOs.filter((i) => data.updated_image_names.includes(i.image_name)); - if (!updatedImages[0]) { - return; - } + // if (!updatedImages[0]) { + // return; + // } - // assume all images are on the same board/category - const categories = getCategories(updatedImages[0]); - const boardId = updatedImages[0].board_id; + // // assume all images are on the same board/category + // const categories = getCategories(updatedImages[0]); + // const boardId = updatedImages[0].board_id; - updatedImages.forEach((imageDTO) => { - const { image_name } = imageDTO; - dispatch( - imagesApi.util.updateQueryData('getImageDTO', image_name, (draft) => { - draft.starred = true; - }) - ); + // updatedImages.forEach((imageDTO) => { + // const { image_name } = imageDTO; + // dispatch( + // imagesApi.util.updateQueryData('getImageDTO', image_name, (draft) => { + // draft.starred = true; + // }) + // ); - const queryArgs = { - board_id: boardId ?? 'none', - categories, - }; + // const queryArgs = { + // board_id: boardId ?? 'none', + // categories, + // }; - const currentCache = imagesApi.endpoints.listImages.select(queryArgs)(getState()); + // const currentCache = imagesApi.endpoints.listImages.select(queryArgs)(getState()); - const { data } = IMAGE_CATEGORIES.includes(imageDTO.image_category) - ? boardsApi.endpoints.getBoardImagesTotal.select(boardId ?? 'none')(getState()) - : boardsApi.endpoints.getBoardAssetsTotal.select(boardId ?? 'none')(getState()); + // const { data } = IMAGE_CATEGORIES.includes(imageDTO.image_category) + // ? boardsApi.endpoints.getBoardImagesTotal.select(boardId ?? 'none')(getState()) + // : boardsApi.endpoints.getBoardAssetsTotal.select(boardId ?? 'none')(getState()); - const isCacheFullyPopulated = currentCache.data && currentCache.data.ids.length >= (data?.total ?? 0); + // const isCacheFullyPopulated = currentCache.data && currentCache.data.ids.length >= (data?.total ?? 0); - const isInDateRange = - (data?.total ?? 0) >= IMAGE_LIMIT ? getIsImageInDateRange(currentCache.data, imageDTO) : true; + // const isInDateRange = + // (data?.total ?? 0) >= IMAGE_LIMIT ? getIsImageInDateRange(currentCache.data, imageDTO) : true; - if (isCacheFullyPopulated || isInDateRange) { - // *upsert* to $cache - dispatch( - imagesApi.util.updateQueryData('listImages', queryArgs, (draft) => { - imagesAdapter.upsertOne(draft, { - ...imageDTO, - starred: true, - }); - }) - ); - } - }); - } catch { - // no-op - } - }, + // if (isCacheFullyPopulated || isInDateRange) { + // // *upsert* to $cache + // dispatch( + // imagesApi.util.updateQueryData('listImages', queryArgs, (draft) => { + // imagesAdapter.upsertOne(draft, { + // ...imageDTO, + // starred: true, + // }); + // }) + // ); + // } + // }); + // } catch { + // // no-op + // } + // }, }), /** * Unstar a list of images. @@ -501,64 +455,64 @@ export const imagesApi = api.injectEndpoints({ } return []; }, - async onQueryStarted({ imageDTOs }, { dispatch, queryFulfilled, getState }) { - try { - /** - * Cache changes for unstarImages: - * - *update* getImageDTO for each image - * - *upsert* into list for each image - */ + // async onQueryStarted({ imageDTOs }, { dispatch, queryFulfilled, getState }) { + // try { + // /** + // * Cache changes for unstarImages: + // * - *update* getImageDTO for each image + // * - *upsert* into list for each image + // */ - const { data } = await queryFulfilled; - const updatedImages = imageDTOs.filter((i) => data.updated_image_names.includes(i.image_name)); + // const { data } = await queryFulfilled; + // const updatedImages = imageDTOs.filter((i) => data.updated_image_names.includes(i.image_name)); - if (!updatedImages[0]) { - return; - } - // assume all images are on the same board/category - const categories = getCategories(updatedImages[0]); - const boardId = updatedImages[0].board_id; + // if (!updatedImages[0]) { + // return; + // } + // // assume all images are on the same board/category + // const categories = getCategories(updatedImages[0]); + // const boardId = updatedImages[0].board_id; - updatedImages.forEach((imageDTO) => { - const { image_name } = imageDTO; - dispatch( - imagesApi.util.updateQueryData('getImageDTO', image_name, (draft) => { - draft.starred = false; - }) - ); + // updatedImages.forEach((imageDTO) => { + // const { image_name } = imageDTO; + // dispatch( + // imagesApi.util.updateQueryData('getImageDTO', image_name, (draft) => { + // draft.starred = false; + // }) + // ); - const queryArgs = { - board_id: boardId ?? 'none', - categories, - }; + // const queryArgs = { + // board_id: boardId ?? 'none', + // categories, + // }; - const currentCache = imagesApi.endpoints.listImages.select(queryArgs)(getState()); + // const currentCache = imagesApi.endpoints.listImages.select(queryArgs)(getState()); - const { data } = IMAGE_CATEGORIES.includes(imageDTO.image_category) - ? boardsApi.endpoints.getBoardImagesTotal.select(boardId ?? 'none')(getState()) - : boardsApi.endpoints.getBoardAssetsTotal.select(boardId ?? 'none')(getState()); + // const { data } = IMAGE_CATEGORIES.includes(imageDTO.image_category) + // ? boardsApi.endpoints.getBoardImagesTotal.select(boardId ?? 'none')(getState()) + // : boardsApi.endpoints.getBoardAssetsTotal.select(boardId ?? 'none')(getState()); - const isCacheFullyPopulated = currentCache.data && currentCache.data.ids.length >= (data?.total ?? 0); + // const isCacheFullyPopulated = currentCache.data && currentCache.data.ids.length >= (data?.total ?? 0); - const isInDateRange = - (data?.total ?? 0) >= IMAGE_LIMIT ? getIsImageInDateRange(currentCache.data, imageDTO) : true; + // const isInDateRange = + // (data?.total ?? 0) >= IMAGE_LIMIT ? getIsImageInDateRange(currentCache.data, imageDTO) : true; - if (isCacheFullyPopulated || isInDateRange) { - // *upsert* to $cache - dispatch( - imagesApi.util.updateQueryData('listImages', queryArgs, (draft) => { - imagesAdapter.upsertOne(draft, { - ...imageDTO, - starred: false, - }); - }) - ); - } - }); - } catch { - // no-op - } - }, + // if (isCacheFullyPopulated || isInDateRange) { + // // *upsert* to $cache + // dispatch( + // imagesApi.util.updateQueryData('listImages', queryArgs, (draft) => { + // imagesAdapter.upsertOne(draft, { + // ...imageDTO, + // starred: false, + // }); + // }) + // ); + // } + // }); + // } catch { + // // no-op + // } + // }, }), uploadImage: build.mutation< ImageDTO, @@ -592,54 +546,54 @@ export const imagesApi = api.injectEndpoints({ }, }; }, - async onQueryStarted(_, { 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 - * - update the image's board's assets total - */ + // async onQueryStarted(_, { 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 + // * - update the image's board's assets total + // */ - const { data: imageDTO } = await queryFulfilled; + // const { data: imageDTO } = await queryFulfilled; - if (imageDTO.is_intermediate) { - // Don't add it to anything - return; - } + // if (imageDTO.is_intermediate) { + // // Don't add it to anything + // return; + // } - // *add* to `getImageDTO` - dispatch(imagesApi.util.upsertQueryData('getImageDTO', imageDTO.image_name, imageDTO)); + // // *add* to `getImageDTO` + // dispatch(imagesApi.util.upsertQueryData('getImageDTO', imageDTO.image_name, imageDTO)); - const categories = getCategories(imageDTO); + // const categories = getCategories(imageDTO); - // *add* to no_board/assets - dispatch( - imagesApi.util.updateQueryData( - 'listImages', - { - board_id: imageDTO.board_id ?? 'none', - categories, - }, - (draft) => { - imagesAdapter.addOne(draft, imageDTO); - } - ) - ); + // // *add* to no_board/assets + // dispatch( + // imagesApi.util.updateQueryData( + // 'listImages', + // { + // board_id: imageDTO.board_id ?? 'none', + // categories, + // }, + // (draft) => { + // imagesAdapter.addOne(draft, imageDTO); + // } + // ) + // ); - // increment new board's total - dispatch( - boardsApi.util.updateQueryData('getBoardAssetsTotal', imageDTO.board_id ?? 'none', (draft) => { - draft.total += 1; - }) - ); - } catch { - // query failed, no action needed - } - }, + // // increment new board's total + // dispatch( + // boardsApi.util.updateQueryData('getBoardAssetsTotal', imageDTO.board_id ?? 'none', (draft) => { + // draft.total += 1; + // }) + // ); + // } catch { + // // query failed, no action needed + // } + // }, }), deleteBoard: build.mutation({ @@ -662,73 +616,73 @@ export const imagesApi = api.injectEndpoints({ }), }, ], - async onQueryStarted(board_id, { dispatch, queryFulfilled }) { - /** - * Cache changes for deleteBoard: - * - Update every image in the 'getImageDTO' cache that has the board_id - * - Update every image in the 'All Images' cache that has the board_id - * - Update every image in the 'All Assets' cache that has the board_id - * - Invalidate the 'No Board' cache: - * Ideally we'd be able to insert all deleted images into the cache, but we don't - * 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' - * cache. - * - set the board's totals to zero - */ + // async onQueryStarted(board_id, { dispatch, queryFulfilled }) { + // /** + // * Cache changes for deleteBoard: + // * - Update every image in the 'getImageDTO' cache that has the board_id + // * - Update every image in the 'All Images' cache that has the board_id + // * - Update every image in the 'All Assets' cache that has the board_id + // * - Invalidate the 'No Board' cache: + // * Ideally we'd be able to insert all deleted images into the cache, but we don't + // * 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' + // * cache. + // * - set the board's totals to zero + // */ - try { - const { data } = await queryFulfilled; - const { deleted_board_images } = data; + // try { + // const { data } = await queryFulfilled; + // const { deleted_board_images } = data; - // update getImageDTO caches - deleted_board_images.forEach((image_id) => { - dispatch( - imagesApi.util.updateQueryData('getImageDTO', image_id, (draft) => { - draft.board_id = undefined; - }) - ); - }); + // // update getImageDTO caches + // deleted_board_images.forEach((image_id) => { + // dispatch( + // imagesApi.util.updateQueryData('getImageDTO', image_id, (draft) => { + // draft.board_id = undefined; + // }) + // ); + // }); - // 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 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; - }) - ); + // // 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 - const queryArgsToUpdate = [ - { - categories: IMAGE_CATEGORIES, - }, - { - categories: ASSETS_CATEGORIES, - }, - ]; + // // update 'All Images' & 'All Assets' caches + // const queryArgsToUpdate = [ + // { + // categories: IMAGE_CATEGORIES, + // }, + // { + // categories: ASSETS_CATEGORIES, + // }, + // ]; - const updates: Update[] = deleted_board_images.map((image_name) => ({ - id: image_name, - changes: { board_id: undefined }, - })); + // const updates: Update[] = deleted_board_images.map((image_name) => ({ + // id: image_name, + // changes: { board_id: undefined }, + // })); - queryArgsToUpdate.forEach((queryArgs) => { - dispatch( - imagesApi.util.updateQueryData('listImages', queryArgs, (draft) => { - imagesAdapter.updateMany(draft, updates); - }) - ); - }); - } catch { - //no-op - } - }, + // queryArgsToUpdate.forEach((queryArgs) => { + // dispatch( + // imagesApi.util.updateQueryData('listImages', queryArgs, (draft) => { + // imagesAdapter.updateMany(draft, updates); + // }) + // ); + // }); + // } catch { + // //no-op + // } + // }, }), deleteBoardAndImages: build.mutation({ @@ -754,56 +708,56 @@ export const imagesApi = api.injectEndpoints({ }), }, ], - async onQueryStarted(board_id, { dispatch, queryFulfilled }) { - /** - * Cache changes for deleteBoardAndImages: - * - ~~Remove every image in the 'getImageDTO' cache that has the board_id~~ - * This isn't actually possible, you cannot remove cache entries with RTK Query. - * 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 Assets' cache that has the board_id - * - set the board's totals to zero - */ + // async onQueryStarted(board_id, { dispatch, queryFulfilled }) { + // /** + // * Cache changes for deleteBoardAndImages: + // * - ~~Remove every image in the 'getImageDTO' cache that has the board_id~~ + // * This isn't actually possible, you cannot remove cache entries with RTK Query. + // * 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 Assets' cache that has the board_id + // * - set the board's totals to zero + // */ - try { - const { data } = await queryFulfilled; - const { deleted_images } = data; + // try { + // const { data } = await queryFulfilled; + // const { deleted_images } = data; - // update 'All Images' & 'All Assets' caches - const queryArgsToUpdate = [ - { - categories: IMAGE_CATEGORIES, - }, - { - categories: ASSETS_CATEGORIES, - }, - ]; + // // update 'All Images' & 'All Assets' caches + // const queryArgsToUpdate = [ + // { + // categories: IMAGE_CATEGORIES, + // }, + // { + // categories: ASSETS_CATEGORIES, + // }, + // ]; - queryArgsToUpdate.forEach((queryArgs) => { - dispatch( - imagesApi.util.updateQueryData('listImages', queryArgs, (draft) => { - imagesAdapter.removeMany(draft, deleted_images); - }) - ); - }); + // queryArgsToUpdate.forEach((queryArgs) => { + // dispatch( + // imagesApi.util.updateQueryData('listImages', queryArgs, (draft) => { + // imagesAdapter.removeMany(draft, deleted_images); + // }) + // ); + // }); - // 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 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 { - //no-op - } - }, + // // 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 { + // //no-op + // } + // }, }), addImageToBoard: build.mutation({ query: ({ board_id, imageDTO }) => { @@ -818,113 +772,113 @@ export const imagesApi = api.injectEndpoints({ // refresh the board itself { type: 'Board', id: board_id }, ], - async onQueryStarted({ board_id, imageDTO }, { dispatch, queryFulfilled, getState }) { - /** - * Cache changes for `addImageToBoard`: - * - *update* getImageDTO - * - IF it is intermediate: - * - BAIL OUT ON FURTHER CHANGES - * - 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 - * - decrement both old board's total - * - increment the new board's total - */ + // async onQueryStarted({ board_id, imageDTO }, { dispatch, queryFulfilled, getState }) { + // /** + // * Cache changes for `addImageToBoard`: + // * - *update* getImageDTO + // * - IF it is intermediate: + // * - BAIL OUT ON FURTHER CHANGES + // * - 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 + // * - decrement both old board's total + // * - increment the new board's total + // */ - const patches: PatchCollection[] = []; - const categories = getCategories(imageDTO); - const isAsset = ASSETS_CATEGORIES.includes(imageDTO.image_category); - // *update* getImageDTO - patches.push( - dispatch( - imagesApi.util.updateQueryData('getImageDTO', imageDTO.image_name, (draft) => { - draft.board_id = board_id; - }) - ) - ); + // const patches: PatchCollection[] = []; + // const categories = getCategories(imageDTO); + // const isAsset = ASSETS_CATEGORIES.includes(imageDTO.image_category); + // // *update* getImageDTO + // patches.push( + // dispatch( + // imagesApi.util.updateQueryData('getImageDTO', imageDTO.image_name, (draft) => { + // draft.board_id = board_id; + // }) + // ) + // ); - if (!imageDTO.is_intermediate) { - // *remove* from [no_board|board_id]/[images|assets] - patches.push( - dispatch( - imagesApi.util.updateQueryData( - 'listImages', - { - board_id: imageDTO.board_id ?? 'none', - categories, - }, - (draft) => { - imagesAdapter.removeOne(draft, imageDTO.image_name); - } - ) - ) - ); + // if (!imageDTO.is_intermediate) { + // // *remove* from [no_board|board_id]/[images|assets] + // patches.push( + // dispatch( + // imagesApi.util.updateQueryData( + // 'listImages', + // { + // board_id: imageDTO.board_id ?? 'none', + // categories, + // }, + // (draft) => { + // imagesAdapter.removeOne(draft, imageDTO.image_name); + // } + // ) + // ) + // ); - // 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); - } - ) - ) - ); + // // 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; - } - ) - ) - ); + // // 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] - const queryArgs = { board_id: board_id ?? 'none', categories }; - const currentCache = imagesApi.endpoints.listImages.select(queryArgs)(getState()); + // // $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 + // // 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 { data } = IMAGE_CATEGORIES.includes(imageDTO.image_category) - ? boardsApi.endpoints.getBoardImagesTotal.select(imageDTO.board_id ?? 'none')(getState()) - : boardsApi.endpoints.getBoardAssetsTotal.select(imageDTO.board_id ?? 'none')(getState()); + // const { data } = IMAGE_CATEGORIES.includes(imageDTO.image_category) + // ? boardsApi.endpoints.getBoardImagesTotal.select(imageDTO.board_id ?? 'none')(getState()) + // : boardsApi.endpoints.getBoardAssetsTotal.select(imageDTO.board_id ?? 'none')(getState()); - const isCacheFullyPopulated = currentCache.data && currentCache.data.ids.length >= (data?.total ?? 0); + // const isCacheFullyPopulated = currentCache.data && currentCache.data.ids.length >= (data?.total ?? 0); - const isInDateRange = getIsImageInDateRange(currentCache.data, imageDTO); + // const isInDateRange = getIsImageInDateRange(currentCache.data, imageDTO); - if (isCacheFullyPopulated || isInDateRange) { - // THEN *add* to $cache - patches.push( - dispatch( - imagesApi.util.updateQueryData('listImages', queryArgs, (draft) => { - imagesAdapter.addOne(draft, imageDTO); - }) - ) - ); - } - } + // if (isCacheFullyPopulated || isInDateRange) { + // // THEN *add* to $cache + // patches.push( + // dispatch( + // imagesApi.util.updateQueryData('listImages', queryArgs, (draft) => { + // imagesAdapter.addOne(draft, imageDTO); + // }) + // ) + // ); + // } + // } - try { - await queryFulfilled; - } catch { - patches.forEach((patchResult) => patchResult.undo()); - } - }, + // try { + // await queryFulfilled; + // } catch { + // patches.forEach((patchResult) => patchResult.undo()); + // } + // }, }), removeImageFromBoard: build.mutation({ query: ({ imageDTO }) => { @@ -942,104 +896,104 @@ export const imagesApi = api.injectEndpoints({ { type: 'Board', id: board_id ?? 'none' }, ]; }, - async onQueryStarted({ imageDTO }, { dispatch, queryFulfilled, getState }) { - /** - * 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 - * - decrement old board's total - * - increment the new board's total (no board) - */ + // async onQueryStarted({ imageDTO }, { dispatch, queryFulfilled, getState }) { + // /** + // * 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 + // * - decrement old board's total + // * - increment the new board's total (no board) + // */ - const categories = getCategories(imageDTO); - const patches: PatchCollection[] = []; - const isAsset = ASSETS_CATEGORIES.includes(imageDTO.image_category); + // const categories = getCategories(imageDTO); + // const patches: PatchCollection[] = []; + // const isAsset = ASSETS_CATEGORIES.includes(imageDTO.image_category); - // *update* getImageDTO - patches.push( - dispatch( - imagesApi.util.updateQueryData('getImageDTO', imageDTO.image_name, (draft) => { - draft.board_id = undefined; - }) - ) - ); + // // *update* getImageDTO + // patches.push( + // dispatch( + // imagesApi.util.updateQueryData('getImageDTO', imageDTO.image_name, (draft) => { + // draft.board_id = undefined; + // }) + // ) + // ); - // *remove* from board_id/[images|assets] - patches.push( - dispatch( - imagesApi.util.updateQueryData( - 'listImages', - { - board_id: imageDTO.board_id ?? 'none', - categories, - }, - (draft) => { - imagesAdapter.removeOne(draft, imageDTO.image_name); - } - ) - ) - ); + // // *remove* from board_id/[images|assets] + // patches.push( + // dispatch( + // imagesApi.util.updateQueryData( + // 'listImages', + // { + // board_id: imageDTO.board_id ?? 'none', + // categories, + // }, + // (draft) => { + // imagesAdapter.removeOne(draft, imageDTO.image_name); + // } + // ) + // ) + // ); - // 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); - } - ) - ) - ); + // // 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; - }) - ) - ); + // // 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] - const queryArgs = { board_id: 'none', categories }; - const currentCache = imagesApi.endpoints.listImages.select(queryArgs)(getState()); + // // $cache = no_board/[images|assets] + // const queryArgs = { 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 + // // 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 { data } = IMAGE_CATEGORIES.includes(imageDTO.image_category) - ? boardsApi.endpoints.getBoardImagesTotal.select(imageDTO.board_id ?? 'none')(getState()) - : boardsApi.endpoints.getBoardAssetsTotal.select(imageDTO.board_id ?? 'none')(getState()); + // const { data } = IMAGE_CATEGORIES.includes(imageDTO.image_category) + // ? boardsApi.endpoints.getBoardImagesTotal.select(imageDTO.board_id ?? 'none')(getState()) + // : boardsApi.endpoints.getBoardAssetsTotal.select(imageDTO.board_id ?? 'none')(getState()); - const isCacheFullyPopulated = currentCache.data && currentCache.data.ids.length >= (data?.total ?? 0); + // const isCacheFullyPopulated = currentCache.data && currentCache.data.ids.length >= (data?.total ?? 0); - const isInDateRange = getIsImageInDateRange(currentCache.data, imageDTO); + // const isInDateRange = getIsImageInDateRange(currentCache.data, imageDTO); - if (isCacheFullyPopulated || isInDateRange) { - // THEN *upsert* to $cache - patches.push( - dispatch( - imagesApi.util.updateQueryData('listImages', queryArgs, (draft) => { - imagesAdapter.upsertOne(draft, imageDTO); - }) - ) - ); - } + // if (isCacheFullyPopulated || isInDateRange) { + // // THEN *upsert* to $cache + // patches.push( + // dispatch( + // imagesApi.util.updateQueryData('listImages', queryArgs, (draft) => { + // imagesAdapter.upsertOne(draft, imageDTO); + // }) + // ) + // ); + // } - try { - await queryFulfilled; - } catch { - patches.forEach((patchResult) => patchResult.undo()); - } - }, + // try { + // await queryFulfilled; + // } catch { + // patches.forEach((patchResult) => patchResult.undo()); + // } + // }, }), addImagesToBoard: build.mutation< components['schemas']['AddImagesToBoardResult'], @@ -1062,102 +1016,102 @@ export const imagesApi = api.injectEndpoints({ { type: 'Board', id: board_id ?? 'none' }, ]; }, - async onQueryStarted({ board_id: new_board_id, imageDTOs }, { dispatch, queryFulfilled, getState }) { - try { - const { data } = await queryFulfilled; - const { added_image_names } = data; + // async onQueryStarted({ board_id: new_board_id, imageDTOs }, { dispatch, queryFulfilled, getState }) { + // try { + // const { data } = await queryFulfilled; + // const { added_image_names } = data; - /** - * Cache changes for addImagesToBoard: - * - *update* getImageDTO for each image - * - *add* to board_id/[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 - */ + // /** + // * Cache changes for addImagesToBoard: + // * - *update* getImageDTO for each image + // * - *add* to board_id/[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) => { - dispatch( - imagesApi.util.updateQueryData('getImageDTO', image_name, (draft) => { - draft.board_id = new_board_id === 'none' ? undefined : new_board_id; - }) - ); + // added_image_names.forEach((image_name) => { + // dispatch( + // imagesApi.util.updateQueryData('getImageDTO', image_name, (draft) => { + // draft.board_id = new_board_id === 'none' ? undefined : new_board_id; + // }) + // ); - const imageDTO = imageDTOs.find((i) => i.image_name === image_name); + // const imageDTO = imageDTOs.find((i) => i.image_name === image_name); - if (!imageDTO) { - return; - } + // if (!imageDTO) { + // return; + // } - const categories = getCategories(imageDTO); - const old_board_id = imageDTO.board_id; - const isAsset = ASSETS_CATEGORIES.includes(imageDTO.image_category); + // const categories = getCategories(imageDTO); + // const old_board_id = imageDTO.board_id; + // const isAsset = ASSETS_CATEGORIES.includes(imageDTO.image_category); - // remove from the old board - dispatch( - imagesApi.util.updateQueryData( - 'listImages', - { board_id: old_board_id ?? 'none', categories }, - (draft) => { - imagesAdapter.removeOne(draft, imageDTO.image_name); - } - ) - ); + // // remove from the old board + // dispatch( + // imagesApi.util.updateQueryData( + // 'listImages', + // { board_id: old_board_id ?? 'none', categories }, + // (draft) => { + // imagesAdapter.removeOne(draft, imageDTO.image_name); + // } + // ) + // ); - // 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); - } - ) - ); + // // 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; - } - ) - ); + // // increment new board's total + // dispatch( + // boardsApi.util.updateQueryData( + // isAsset ? 'getBoardAssetsTotal' : 'getBoardImagesTotal', + // new_board_id ?? 'none', + // (draft) => { + // draft.total += 1; + // } + // ) + // ); - const queryArgs = { - board_id: new_board_id, - categories, - }; + // const queryArgs = { + // board_id: new_board_id, + // categories, + // }; - const currentCache = imagesApi.endpoints.listImages.select(queryArgs)(getState()); + // const currentCache = imagesApi.endpoints.listImages.select(queryArgs)(getState()); - const { data } = IMAGE_CATEGORIES.includes(imageDTO.image_category) - ? boardsApi.endpoints.getBoardImagesTotal.select(new_board_id ?? 'none')(getState()) - : boardsApi.endpoints.getBoardAssetsTotal.select(new_board_id ?? 'none')(getState()); + // const { data } = IMAGE_CATEGORIES.includes(imageDTO.image_category) + // ? boardsApi.endpoints.getBoardImagesTotal.select(new_board_id ?? 'none')(getState()) + // : boardsApi.endpoints.getBoardAssetsTotal.select(new_board_id ?? 'none')(getState()); - const isCacheFullyPopulated = currentCache.data && currentCache.data.ids.length >= (data?.total ?? 0); + // const isCacheFullyPopulated = currentCache.data && currentCache.data.ids.length >= (data?.total ?? 0); - const isInDateRange = - (data?.total ?? 0) >= IMAGE_LIMIT ? getIsImageInDateRange(currentCache.data, imageDTO) : true; + // const isInDateRange = + // (data?.total ?? 0) >= IMAGE_LIMIT ? getIsImageInDateRange(currentCache.data, imageDTO) : true; - if (isCacheFullyPopulated || isInDateRange) { - // *upsert* to $cache - dispatch( - imagesApi.util.updateQueryData('listImages', queryArgs, (draft) => { - imagesAdapter.upsertOne(draft, { - ...imageDTO, - board_id: new_board_id, - }); - }) - ); - } - }); - } catch { - // no-op - } - }, + // if (isCacheFullyPopulated || isInDateRange) { + // // *upsert* to $cache + // dispatch( + // imagesApi.util.updateQueryData('listImages', queryArgs, (draft) => { + // imagesAdapter.upsertOne(draft, { + // ...imageDTO, + // board_id: new_board_id, + // }); + // }) + // ); + // } + // }); + // } catch { + // // no-op + // } + // }, }), removeImagesFromBoard: build.mutation< components['schemas']['RemoveImagesFromBoardResult'], @@ -1188,102 +1142,102 @@ export const imagesApi = api.injectEndpoints({ return tags; }, - async onQueryStarted({ imageDTOs }, { dispatch, queryFulfilled, getState }) { - try { - const { data } = await queryFulfilled; - const { removed_image_names } = data; + // async onQueryStarted({ imageDTOs }, { dispatch, queryFulfilled, getState }) { + // try { + // const { data } = await queryFulfilled; + // const { removed_image_names } = data; - /** - * Cache changes for removeImagesFromBoard: - * - *update* getImageDTO for each image - * - *remove* from old_board_id/[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 - */ + // /** + // * Cache changes for removeImagesFromBoard: + // * - *update* getImageDTO for each image + // * - *remove* from old_board_id/[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) => { - dispatch( - imagesApi.util.updateQueryData('getImageDTO', image_name, (draft) => { - draft.board_id = undefined; - }) - ); + // removed_image_names.forEach((image_name) => { + // dispatch( + // imagesApi.util.updateQueryData('getImageDTO', image_name, (draft) => { + // draft.board_id = undefined; + // }) + // ); - const imageDTO = imageDTOs.find((i) => i.image_name === image_name); + // const imageDTO = imageDTOs.find((i) => i.image_name === image_name); - if (!imageDTO) { - return; - } + // if (!imageDTO) { + // return; + // } - const categories = getCategories(imageDTO); - const isAsset = ASSETS_CATEGORIES.includes(imageDTO.image_category); + // const categories = getCategories(imageDTO); + // const isAsset = ASSETS_CATEGORIES.includes(imageDTO.image_category); - // remove from the old board - dispatch( - imagesApi.util.updateQueryData( - 'listImages', - { board_id: imageDTO.board_id ?? 'none', categories }, - (draft) => { - imagesAdapter.removeOne(draft, imageDTO.image_name); - } - ) - ); + // // remove from the old board + // dispatch( + // imagesApi.util.updateQueryData( + // 'listImages', + // { board_id: imageDTO.board_id ?? 'none', categories }, + // (draft) => { + // imagesAdapter.removeOne(draft, imageDTO.image_name); + // } + // ) + // ); - // 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); - } - ) - ); + // // 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; - } - ) - ); + // // increment new board's total (no board) + // dispatch( + // boardsApi.util.updateQueryData( + // isAsset ? 'getBoardAssetsTotal' : 'getBoardImagesTotal', + // 'none', + // (draft) => { + // draft.total += 1; + // } + // ) + // ); - // add to `no_board` - const queryArgs = { - board_id: 'none', - categories, - }; + // // add to `no_board` + // const queryArgs = { + // board_id: 'none', + // categories, + // }; - const currentCache = imagesApi.endpoints.listImages.select(queryArgs)(getState()); + // const currentCache = imagesApi.endpoints.listImages.select(queryArgs)(getState()); - const { data } = IMAGE_CATEGORIES.includes(imageDTO.image_category) - ? boardsApi.endpoints.getBoardImagesTotal.select(imageDTO.board_id ?? 'none')(getState()) - : boardsApi.endpoints.getBoardAssetsTotal.select(imageDTO.board_id ?? 'none')(getState()); + // const { data } = IMAGE_CATEGORIES.includes(imageDTO.image_category) + // ? boardsApi.endpoints.getBoardImagesTotal.select(imageDTO.board_id ?? 'none')(getState()) + // : boardsApi.endpoints.getBoardAssetsTotal.select(imageDTO.board_id ?? 'none')(getState()); - const isCacheFullyPopulated = currentCache.data && currentCache.data.ids.length >= (data?.total ?? 0); + // const isCacheFullyPopulated = currentCache.data && currentCache.data.ids.length >= (data?.total ?? 0); - const isInDateRange = - (data?.total ?? 0) >= IMAGE_LIMIT ? getIsImageInDateRange(currentCache.data, imageDTO) : true; + // const isInDateRange = + // (data?.total ?? 0) >= IMAGE_LIMIT ? getIsImageInDateRange(currentCache.data, imageDTO) : true; - if (isCacheFullyPopulated || isInDateRange) { - // *upsert* to $cache - dispatch( - imagesApi.util.updateQueryData('listImages', queryArgs, (draft) => { - imagesAdapter.upsertOne(draft, { - ...imageDTO, - board_id: 'none', - }); - }) - ); - } - }); - } catch { - // no-op - } - }, + // if (isCacheFullyPopulated || isInDateRange) { + // // *upsert* to $cache + // dispatch( + // imagesApi.util.updateQueryData('listImages', queryArgs, (draft) => { + // imagesAdapter.upsertOne(draft, { + // ...imageDTO, + // board_id: 'none', + // }); + // }) + // ); + // } + // }); + // } catch { + // // no-op + // } + // }, }), bulkDownloadImages: build.mutation< components['schemas']['ImagesDownloaded'], diff --git a/invokeai/frontend/web/src/services/api/types.ts b/invokeai/frontend/web/src/services/api/types.ts index 90ddf3cca1..50c63fd806 100644 --- a/invokeai/frontend/web/src/services/api/types.ts +++ b/invokeai/frontend/web/src/services/api/types.ts @@ -7,6 +7,7 @@ export type S = components['schemas']; export type ImageCache = EntityState; export type ListImagesArgs = NonNullable; +export type ListImagesResponse = paths['/api/v1/images/']['get']['responses']['200']['content']['application/json']; export type DeleteBoardResult = paths['/api/v1/boards/{board_id}']['delete']['responses']['200']['content']['application/json'];