Compare commits

...

4 Commits

5 changed files with 134 additions and 168 deletions

View File

@ -1,11 +1,5 @@
import { createAction } from '@reduxjs/toolkit'; import { createAction } from '@reduxjs/toolkit';
import { import { isLoadingChanged } from 'features/gallery/store/gallerySlice';
ASSETS_CATEGORIES,
IMAGE_CATEGORIES,
INITIAL_IMAGE_LIMIT,
isLoadingChanged,
} from 'features/gallery/store/gallerySlice';
import { receivedPageOfImages } from 'services/api/thunks/image';
import { startAppListening } from '..'; import { startAppListening } from '..';
export const appStarted = createAction('app/appStarted'); export const appStarted = createAction('app/appStarted');
@ -19,25 +13,6 @@ export const addAppStartedListener = () => {
) => { ) => {
cancelActiveListeners(); cancelActiveListeners();
unsubscribe(); unsubscribe();
// fill up the gallery tab with images
await dispatch(
receivedPageOfImages({
categories: IMAGE_CATEGORIES,
is_intermediate: false,
offset: 0,
limit: INITIAL_IMAGE_LIMIT,
})
);
// fill up the assets tab with images
await dispatch(
receivedPageOfImages({
categories: ASSETS_CATEGORIES,
is_intermediate: false,
offset: 0,
limit: INITIAL_IMAGE_LIMIT,
})
);
dispatch(isLoadingChanged(false)); dispatch(isLoadingChanged(false));
}, },

View File

@ -16,33 +16,26 @@ import {
ASSETS_CATEGORIES, ASSETS_CATEGORIES,
IMAGE_CATEGORIES, IMAGE_CATEGORIES,
IMAGE_LIMIT, IMAGE_LIMIT,
selectImagesAll, INITIAL_IMAGE_LIMIT,
} from 'features/gallery//store/gallerySlice'; } from 'features/gallery//store/gallerySlice';
import { selectFilteredImages } from 'features/gallery/store/gallerySelectors';
import { VirtuosoGrid } from 'react-virtuoso'; import { VirtuosoGrid } from 'react-virtuoso';
import { receivedPageOfImages } from 'services/api/thunks/image'; import { receivedPageOfImages } from 'services/api/thunks/image';
import ImageGridItemContainer from './ImageGridItemContainer'; import ImageGridItemContainer from './ImageGridItemContainer';
import ImageGridListContainer from './ImageGridListContainer'; import ImageGridListContainer from './ImageGridListContainer';
import { useListBoardImagesQuery } from '../../../../services/api/endpoints/boardImages'; import {
imagesApi,
useListImagesQuery,
} from '../../../../services/api/endpoints/images';
import { ImageDTO } from '../../../../services/api/types';
const selector = createSelector( const selector = createSelector(
[stateSelector, selectFilteredImages], [stateSelector],
(state, filteredImages) => { (state) => {
const { const { selectedBoardId, galleryView } = state.gallery;
galleryImageMinimumWidth,
selectedBoardId,
galleryView,
total,
isLoading,
} = state.gallery;
return { return {
imageNames: filteredImages.map((i) => i.image_name),
total,
selectedBoardId, selectedBoardId,
galleryView, galleryView,
galleryImageMinimumWidth,
isLoading,
}; };
}, },
defaultSelectorOptions defaultSelectorOptions
@ -53,6 +46,10 @@ const GalleryImageGrid = () => {
const rootRef = useRef<HTMLDivElement>(null); const rootRef = useRef<HTMLDivElement>(null);
const emptyGalleryRef = useRef<HTMLDivElement>(null); const emptyGalleryRef = useRef<HTMLDivElement>(null);
const [scroller, setScroller] = useState<HTMLElement | null>(null); const [scroller, setScroller] = useState<HTMLElement | null>(null);
const [offset, setOffset] = useState(0);
const [limit, setLimit] = useState(INITIAL_IMAGE_LIMIT);
const [imageList, setImageList] = useState<ImageDTO[]>([]);
const [isLoadingMore, setIsLoadingMore] = useState(true);
const [initialize, osInstance] = useOverlayScrollbars({ const [initialize, osInstance] = useOverlayScrollbars({
defer: true, defer: true,
options: { options: {
@ -66,97 +63,90 @@ const GalleryImageGrid = () => {
}, },
}); });
const [didInitialFetch, setDidInitialFetch] = useState(false);
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
const { const { selectedBoardId, galleryView } = useAppSelector(selector);
galleryImageMinimumWidth,
imageNames: imageNamesAll, //all images names loaded on main tab,
total: totalAll,
selectedBoardId,
galleryView,
isLoading: isLoadingAll,
} = useAppSelector(selector);
const { data: imagesForBoard, isLoading: isLoadingImagesForBoard } = useEffect(() => {
useListBoardImagesQuery( setImageList([]);
{ board_id: selectedBoardId }, setOffset(0);
{ skip: selectedBoardId === 'all' } setLimit(INITIAL_IMAGE_LIMIT);
); }, [selectedBoardId]);
const imageNames = useMemo(() => { const { data: imageListResponse, isLoading: isLoading } = useListImagesQuery({
if (selectedBoardId === 'all') { categories: galleryView === 'images' ? IMAGE_CATEGORIES : ASSETS_CATEGORIES,
return imageNamesAll; // already sorted by images/uploads in gallery selector is_intermediate: false,
} else { offset: 0,
const categories = limit: INITIAL_IMAGE_LIMIT,
galleryView === 'images' ? IMAGE_CATEGORIES : ASSETS_CATEGORIES; ...(selectedBoardId === 'all' ? {} : { board_id: selectedBoardId }),
const imageList = (imagesForBoard?.items || []).filter((img) => });
categories.includes(img.image_category)
const { data: paginatedData } = useListImagesQuery(
{
categories:
galleryView === 'images' ? IMAGE_CATEGORIES : ASSETS_CATEGORIES,
is_intermediate: false,
offset,
limit,
...(selectedBoardId === 'all' ? {} : { board_id: selectedBoardId }),
},
{ skip: offset === 0 }
);
useEffect(() => {
if (imageListResponse) setImageList(imageListResponse.items);
}, [imageListResponse]);
useEffect(() => {
if (paginatedData) {
dispatch(
imagesApi.util.updateQueryData(
'listImages',
{
categories:
galleryView === 'images' ? IMAGE_CATEGORIES : ASSETS_CATEGORIES,
is_intermediate: false,
offset: 0,
limit: INITIAL_IMAGE_LIMIT,
...(selectedBoardId === 'all' ? {} : { board_id: selectedBoardId }),
},
(draftPosts) => {
paginatedData.items.forEach((item) => {
draftPosts.items.push(item);
});
}
)
); );
return imageList.map((img) => img.image_name);
} }
}, [selectedBoardId, galleryView, imagesForBoard, imageNamesAll]); //eslint-disable-next-line
}, [paginatedData, dispatch]);
const areMoreAvailable = useMemo(() => { const areMoreAvailable = useMemo(() => {
return selectedBoardId === 'all' ? totalAll > imageNamesAll.length : false; if (imageListResponse?.total) {
}, [selectedBoardId, imageNamesAll.length, totalAll]); return imageListResponse?.total > imageList.length;
}
const isLoading = useMemo(() => { }, [imageListResponse?.total, imageList.length]);
return selectedBoardId === 'all' ? isLoadingAll : isLoadingImagesForBoard;
}, [selectedBoardId, isLoadingAll, isLoadingImagesForBoard]);
const handleLoadMoreImages = useCallback(() => { const handleLoadMoreImages = useCallback(() => {
dispatch( setOffset(imageList.length);
receivedPageOfImages({ setLimit(IMAGE_LIMIT);
categories: }, [imageList.length]);
galleryView === 'images' ? IMAGE_CATEGORIES : ASSETS_CATEGORIES,
is_intermediate: false,
offset: imageNames.length,
limit: IMAGE_LIMIT,
})
);
}, [dispatch, imageNames.length, galleryView]);
const handleEndReached = useMemo(() => { useEffect(() => {
if (areMoreAvailable) { // Set up gallery scroler
return handleLoadMoreImages; const { current: root } = rootRef;
if (scroller && root) {
initialize({
target: root,
elements: {
viewport: scroller,
},
});
} }
return undefined; return () => osInstance()?.destroy();
}, [areMoreAvailable, handleLoadMoreImages]); }, [scroller, initialize, osInstance]);
// useEffect(() => { if (!isLoading && imageList.length === 0) {
// if (!didInitialFetch) {
// return;
// }
// // rough, conservative calculation of how many images fit in the gallery
// // TODO: this gets an incorrect value on first load...
// const galleryHeight = rootRef.current?.clientHeight ?? 0;
// const galleryWidth = rootRef.current?.clientHeight ?? 0;
// const rows = galleryHeight / galleryImageMinimumWidth;
// const columns = galleryWidth / galleryImageMinimumWidth;
// const imagesToLoad = Math.ceil(rows * columns);
// setDidInitialFetch(true);
// // load up that many images
// dispatch(
// receivedPageOfImages({
// offset: 0,
// limit: 10,
// })
// );
// }, [
// didInitialFetch,
// dispatch,
// galleryImageMinimumWidth,
// galleryView,
// selectedBoardId,
// ]);
if (!isLoading && imageNames.length === 0) {
return ( return (
<Box ref={emptyGalleryRef} sx={{ w: 'full', h: 'full' }}> <Box ref={emptyGalleryRef} sx={{ w: 'full', h: 'full' }}>
<IAINoContentFallback <IAINoContentFallback
@ -166,22 +156,24 @@ const GalleryImageGrid = () => {
</Box> </Box>
); );
} }
console.log({ selectedBoardId });
if (status !== 'rejected') { if (!isLoading) {
return ( return (
<> <>
<Box ref={rootRef} data-overlayscrollbars="" h="100%"> <Box ref={rootRef} data-overlayscrollbars="" h="100%">
<VirtuosoGrid <VirtuosoGrid
style={{ height: '100%' }} style={{ height: '100%' }}
data={imageNames} data={imageList}
components={{ components={{
Item: ImageGridItemContainer, Item: ImageGridItemContainer,
List: ImageGridListContainer, List: ImageGridListContainer,
}} }}
scrollerRef={setScroller} scrollerRef={setScroller}
itemContent={(index, imageName) => ( itemContent={(index, image) => (
<GalleryImage key={imageName} imageName={imageName} /> <GalleryImage
key={image.image_name}
imageName={image.image_name}
/>
)} )}
/> />
</Box> </Box>

View File

@ -1,11 +1,6 @@
import { OffsetPaginatedResults_ImageDTO_ } from 'services/api/types'; import { api } from '..';
import { ApiFullTagDescription, LIST_TAG, api } from '..';
import { paths } from '../schema'; import { paths } from '../schema';
type ListBoardImagesArg =
paths['/api/v1/board_images/{board_id}']['get']['parameters']['path'] &
paths['/api/v1/board_images/{board_id}']['get']['parameters']['query'];
type AddImageToBoardArg = type AddImageToBoardArg =
paths['/api/v1/board_images/']['post']['requestBody']['content']['application/json']; paths['/api/v1/board_images/']['post']['requestBody']['content']['application/json'];
@ -14,37 +9,6 @@ type RemoveImageFromBoardArg =
export const boardImagesApi = api.injectEndpoints({ export const boardImagesApi = api.injectEndpoints({
endpoints: (build) => ({ endpoints: (build) => ({
/**
* Board Images Queries
*/
listBoardImages: build.query<
OffsetPaginatedResults_ImageDTO_,
ListBoardImagesArg
>({
query: ({ board_id, offset, limit }) => ({
url: `board_images/${board_id}`,
method: 'GET',
}),
providesTags: (result, error, arg) => {
// any list of boardimages
const tags: ApiFullTagDescription[] = [{ id: 'BoardImage', type: `${arg.board_id}_${LIST_TAG}` }];
if (result) {
// and individual tags for each boardimage
tags.push(
...result.items.map(({ board_id, image_name }) => ({
type: 'BoardImage' as const,
id: `${board_id}_${image_name}`,
}))
);
}
return tags;
},
}),
/** /**
* Board Images Mutations * Board Images Mutations
*/ */
@ -77,6 +41,5 @@ export const boardImagesApi = api.injectEndpoints({
export const { export const {
useAddImageToBoardMutation, useAddImageToBoardMutation,
useRemoveImageFromBoardMutation, useRemoveImageFromBoardMutation
useListBoardImagesQuery,
} = boardImagesApi; } = boardImagesApi;

View File

@ -1,6 +1,10 @@
import { ApiFullTagDescription, api } from '..'; import { ApiFullTagDescription, LIST_TAG, api } from '..';
import { components } from '../schema'; import { components, paths } from '../schema';
import { ImageDTO } from '../types'; import { ImageDTO, OffsetPaginatedResults_ImageDTO_ } from '../types';
type ListImagesArg = NonNullable<
paths['/api/v1/images/']['get']['parameters']['query']
>;
/** /**
* This is an unsafe type; the object inside is not guaranteed to be valid. * This is an unsafe type; the object inside is not guaranteed to be valid.
@ -15,6 +19,34 @@ export const imagesApi = api.injectEndpoints({
/** /**
* Image Queries * Image Queries
*/ */
listImages: build.query<OffsetPaginatedResults_ImageDTO_, ListImagesArg>({
query: (arg) => ({ url: 'images/', params: arg }),
providesTags: (result, error, arg) => {
// any list of images
const tags: ApiFullTagDescription[] = [{ id: 'Image', type: LIST_TAG }];
if (result) {
// and individual tags for each image
tags.push(
...result.items.map(({ image_name, board_id }) => ({
type: 'Image' as const,
id: image_name,
}))
);
}
if (result && arg.board_id) {
tags.push(
...result.items.map(({ image_name, board_id }) => ({
type: 'BoardImage' as const,
id: `${image_name}_${board_id}`,
}))
);
}
return tags;
},
}),
getImageDTO: build.query<ImageDTO, string>({ getImageDTO: build.query<ImageDTO, string>({
query: (image_name) => ({ url: `images/${image_name}` }), query: (image_name) => ({ url: `images/${image_name}` }),
providesTags: (result, error, arg) => { providesTags: (result, error, arg) => {
@ -39,4 +71,4 @@ export const imagesApi = api.injectEndpoints({
}), }),
}); });
export const { useGetImageDTOQuery, useGetImageMetadataQuery } = imagesApi; export const { useGetImageDTOQuery, useGetImageMetadataQuery, useListImagesQuery } = imagesApi;

View File

@ -6,6 +6,7 @@ import {
createApi, createApi,
fetchBaseQuery, fetchBaseQuery,
} from '@reduxjs/toolkit/query/react'; } from '@reduxjs/toolkit/query/react';
import queryString from 'query-string';
import { $authToken, $baseUrl } from 'services/api/client'; import { $authToken, $baseUrl } from 'services/api/client';
export const tagTypes = ['Board', 'Image', 'ImageMetadata', 'Model']; export const tagTypes = ['Board', 'Image', 'ImageMetadata', 'Model'];
@ -31,6 +32,9 @@ const dynamicBaseQuery: BaseQueryFn<
return headers; return headers;
}, },
paramsSerializer: (params: Record<string, any>) => {
return queryString.stringify(params, { arrayFormat: 'none' })
}
}); });
return rawBaseQuery(args, api, extraOptions); return rawBaseQuery(args, api, extraOptions);