mirror of
https://github.com/invoke-ai/InvokeAI.git
synced 2026-02-13 21:25:04 -05:00
fix(ui): skip optimistic updates for gallery when using search term
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
import { isAnyOf } from '@reduxjs/toolkit';
|
||||
import type { AppStartListening } from 'app/store/middleware/listenerMiddleware';
|
||||
import { selectListImageNamesQueryArgs, selectSelectedBoardId } from 'features/gallery/store/gallerySelectors';
|
||||
import { selectGetImageNamesQueryArgs, selectSelectedBoardId } from 'features/gallery/store/gallerySelectors';
|
||||
import { boardIdSelected, galleryViewChanged, imageSelected } from 'features/gallery/store/gallerySlice';
|
||||
import { imagesApi } from 'services/api/endpoints/images';
|
||||
|
||||
@@ -20,7 +20,7 @@ export const addBoardIdSelectedListener = (startAppListening: AppStartListening)
|
||||
|
||||
const board_id = selectSelectedBoardId(state);
|
||||
|
||||
const queryArgs = { ...selectListImageNamesQueryArgs(state), board_id };
|
||||
const queryArgs = { ...selectGetImageNamesQueryArgs(state), board_id };
|
||||
|
||||
// wait until the board has some images - maybe it already has some from a previous fetch
|
||||
// must use getState() to ensure we do not have stale state
|
||||
|
||||
@@ -11,7 +11,7 @@ import {
|
||||
import { selectCanvasSlice } from 'features/controlLayers/store/selectors';
|
||||
import type { CanvasState, RefImagesState } from 'features/controlLayers/store/types';
|
||||
import type { ImageUsage } from 'features/deleteImageModal/store/types';
|
||||
import { selectListImageNamesQueryArgs } from 'features/gallery/store/gallerySelectors';
|
||||
import { selectGetImageNamesQueryArgs } from 'features/gallery/store/gallerySelectors';
|
||||
import { imageSelected } from 'features/gallery/store/gallerySlice';
|
||||
import { fieldImageCollectionValueChanged, fieldImageValueChanged } from 'features/nodes/store/nodesSlice';
|
||||
import { selectNodesSlice } from 'features/nodes/store/selectors';
|
||||
@@ -80,7 +80,7 @@ const handleDeletions = async (image_names: string[], store: AppStore) => {
|
||||
try {
|
||||
const { dispatch, getState } = store;
|
||||
const state = getState();
|
||||
const { data } = imagesApi.endpoints.getImageNames.select(selectListImageNamesQueryArgs(state))(state);
|
||||
const { data } = imagesApi.endpoints.getImageNames.select(selectGetImageNamesQueryArgs(state))(state);
|
||||
const index = data?.image_names.findIndex((name) => name === image_names[0]);
|
||||
const { deleted_images } = await dispatch(
|
||||
imagesApi.endpoints.deleteImages.initiate({ image_names }, { track: false })
|
||||
|
||||
@@ -16,7 +16,7 @@ import { useImageContextMenu } from 'features/gallery/components/ImageContextMen
|
||||
import { GalleryImageHoverIcons } from 'features/gallery/components/ImageGrid/GalleryImageHoverIcons';
|
||||
import { getGalleryImageDataTestId } from 'features/gallery/components/ImageGrid/getGalleryImageDataTestId';
|
||||
import {
|
||||
selectListImageNamesQueryArgs,
|
||||
selectGetImageNamesQueryArgs,
|
||||
selectSelectedBoardId,
|
||||
selectSelection,
|
||||
} from 'features/gallery/store/gallerySelectors';
|
||||
@@ -93,7 +93,7 @@ const buildOnClick =
|
||||
(imageName: string, dispatch: AppDispatch, getState: AppGetState) => (e: MouseEvent<HTMLDivElement>) => {
|
||||
const { shiftKey, ctrlKey, metaKey, altKey } = e;
|
||||
const state = getState();
|
||||
const queryArgs = selectListImageNamesQueryArgs(state);
|
||||
const queryArgs = selectGetImageNamesQueryArgs(state);
|
||||
const imageNames = imagesApi.endpoints.getImageNames.select(queryArgs)(state).data?.image_names ?? [];
|
||||
|
||||
// If we don't have the image names cached, we can't perform selection operations
|
||||
|
||||
@@ -3,7 +3,7 @@ import { createSelector } from '@reduxjs/toolkit';
|
||||
import { logger } from 'app/logging/logger';
|
||||
import { useAppSelector, useAppStore } from 'app/store/storeHooks';
|
||||
import { useRangeBasedImageFetching } from 'features/gallery/hooks/useRangeBasedImageFetching';
|
||||
import type { selectListImageNamesQueryArgs } from 'features/gallery/store/gallerySelectors';
|
||||
import type { selectGetImageNamesQueryArgs } from 'features/gallery/store/gallerySelectors';
|
||||
import {
|
||||
selectGalleryImageMinimumWidth,
|
||||
selectImageToCompare,
|
||||
@@ -32,7 +32,7 @@ import { useGalleryImageNames } from './use-gallery-image-names';
|
||||
|
||||
const log = logger('gallery');
|
||||
|
||||
type ListImageNamesQueryArgs = ReturnType<typeof selectListImageNamesQueryArgs>;
|
||||
type ListImageNamesQueryArgs = ReturnType<typeof selectGetImageNamesQueryArgs>;
|
||||
|
||||
type GridContext = {
|
||||
queryArgs: ListImageNamesQueryArgs;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { EMPTY_ARRAY } from 'app/store/constants';
|
||||
import { useAppSelector } from 'app/store/storeHooks';
|
||||
import { selectListImageNamesQueryArgs } from 'features/gallery/store/gallerySelectors';
|
||||
import { selectGetImageNamesQueryArgs } from 'features/gallery/store/gallerySelectors';
|
||||
import { useGetImageNamesQuery } from 'services/api/endpoints/images';
|
||||
import { useDebounce } from 'use-debounce';
|
||||
|
||||
@@ -14,7 +14,7 @@ const getImageNamesQueryOptions = {
|
||||
} satisfies Parameters<typeof useGetImageNamesQuery>[1];
|
||||
|
||||
export const useGalleryImageNames = () => {
|
||||
const _queryArgs = useAppSelector(selectListImageNamesQueryArgs);
|
||||
const _queryArgs = useAppSelector(selectGetImageNamesQueryArgs);
|
||||
const [queryArgs] = useDebounce(_queryArgs, 300);
|
||||
const { imageNames, isLoading, isFetching } = useGetImageNamesQuery(queryArgs, getImageNamesQueryOptions);
|
||||
return { imageNames, isLoading, isFetching, queryArgs };
|
||||
|
||||
@@ -2,7 +2,7 @@ import { createSelector } from '@reduxjs/toolkit';
|
||||
import { createMemoizedSelector } from 'app/store/createMemoizedSelector';
|
||||
import { selectGallerySlice } from 'features/gallery/store/gallerySlice';
|
||||
import { ASSETS_CATEGORIES, IMAGE_CATEGORIES } from 'features/gallery/store/types';
|
||||
import type { ListBoardsArgs } from 'services/api/types';
|
||||
import type { GetImageNamesArgs, ListBoardsArgs } from 'services/api/types';
|
||||
|
||||
export const selectFirstSelectedImage = createSelector(selectGallerySlice, (gallery) => gallery.selection.at(0));
|
||||
export const selectLastSelectedImage = createSelector(selectGallerySlice, (gallery) => gallery.selection.at(-1));
|
||||
@@ -27,7 +27,7 @@ const selectGallerySearchTerm = createSelector(selectGallerySlice, (gallery) =>
|
||||
const selectGalleryOrderDir = createSelector(selectGallerySlice, (gallery) => gallery.orderDir);
|
||||
const selectGalleryStarredFirst = createSelector(selectGallerySlice, (gallery) => gallery.starredFirst);
|
||||
|
||||
export const selectListImageNamesQueryArgs = createMemoizedSelector(
|
||||
export const selectGetImageNamesQueryArgs = createMemoizedSelector(
|
||||
[
|
||||
selectSelectedBoardId,
|
||||
selectGalleryQueryCategories,
|
||||
@@ -35,7 +35,7 @@ export const selectListImageNamesQueryArgs = createMemoizedSelector(
|
||||
selectGalleryOrderDir,
|
||||
selectGalleryStarredFirst,
|
||||
],
|
||||
(board_id, categories, search_term, order_dir, starred_first) => ({
|
||||
(board_id, categories, search_term, order_dir, starred_first): GetImageNamesArgs => ({
|
||||
board_id,
|
||||
categories,
|
||||
search_term,
|
||||
|
||||
@@ -4,15 +4,14 @@ import { getStore } from 'app/store/nanostores/store';
|
||||
import { ASSETS_CATEGORIES, IMAGE_CATEGORIES } from 'features/gallery/store/types';
|
||||
import type { components, paths } from 'services/api/schema';
|
||||
import type {
|
||||
GetImageNamesArgs,
|
||||
GetImageNamesResult,
|
||||
GraphAndWorkflowResponse,
|
||||
ImageCategory,
|
||||
ImageDTO,
|
||||
ImageNamesResult,
|
||||
ImageUploadEntryRequest,
|
||||
ImageUploadEntryResponse,
|
||||
ListImagesArgs,
|
||||
ListImagesResponse,
|
||||
SQLiteDirection,
|
||||
UploadImageArg,
|
||||
} from 'services/api/types';
|
||||
import { getListImagesUrl } from 'services/api/util';
|
||||
@@ -419,16 +418,7 @@ export const imagesApi = api.injectEndpoints({
|
||||
/**
|
||||
* Get ordered list of image names for selection operations
|
||||
*/
|
||||
getImageNames: build.query<
|
||||
ImageNamesResult,
|
||||
{
|
||||
categories?: ImageCategory[] | null;
|
||||
is_intermediate?: boolean | null;
|
||||
board_id?: string | null;
|
||||
search_term?: string | null;
|
||||
order_dir?: SQLiteDirection;
|
||||
}
|
||||
>({
|
||||
getImageNames: build.query<GetImageNamesResult, GetImageNamesArgs>({
|
||||
query: (queryArgs) => ({
|
||||
url: buildImagesUrl('names', queryArgs),
|
||||
method: 'GET',
|
||||
|
||||
@@ -7,7 +7,9 @@ export type S = components['schemas'];
|
||||
export type ListImagesArgs = NonNullable<paths['/api/v1/images/']['get']['parameters']['query']>;
|
||||
export type ListImagesResponse = paths['/api/v1/images/']['get']['responses']['200']['content']['application/json'];
|
||||
|
||||
export type ImageNamesResult = S['ImageNamesResult'];
|
||||
export type GetImageNamesResult =
|
||||
paths['/api/v1/images/names']['get']['responses']['200']['content']['application/json'];
|
||||
export type GetImageNamesArgs = NonNullable<paths['/api/v1/images/names']['get']['parameters']['query']>;
|
||||
|
||||
export type ListBoardsArgs = NonNullable<paths['/api/v1/boards/']['get']['parameters']['query']>;
|
||||
|
||||
@@ -36,6 +38,7 @@ export type ImageDTO = S['ImageDTO'];
|
||||
export type BoardDTO = S['BoardDTO'];
|
||||
export type ImageCategory = S['ImageCategory'];
|
||||
export type OffsetPaginatedResults_ImageDTO_ = S['OffsetPaginatedResults_ImageDTO_'];
|
||||
export type GetImageNamesArg = NonNullable<paths['/api/v1/images/names']['get']['parameters']['query']>;
|
||||
|
||||
// Models
|
||||
export type ModelType = S['ModelType'];
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { OrderDir } from 'features/gallery/store/types';
|
||||
import type { ImageDTO, ImageNamesResult } from 'services/api/types';
|
||||
import type { GetImageNamesResult, ImageDTO } from 'services/api/types';
|
||||
|
||||
/**
|
||||
* Calculates the optimal insertion position for a new image in the names list.
|
||||
@@ -31,11 +31,11 @@ function calculateImageInsertionPosition(
|
||||
* Optimistically inserts a new image into the ImageNamesResult at the correct position
|
||||
*/
|
||||
export function insertImageIntoNamesResult(
|
||||
currentResult: ImageNamesResult,
|
||||
currentResult: GetImageNamesResult,
|
||||
imageDTO: ImageDTO,
|
||||
starredFirst: boolean,
|
||||
orderDir: OrderDir = 'DESC'
|
||||
): ImageNamesResult {
|
||||
): GetImageNamesResult {
|
||||
// Don't insert if the image is already in the list
|
||||
if (currentResult.image_names.includes(imageDTO.image_name)) {
|
||||
return currentResult;
|
||||
|
||||
@@ -4,7 +4,7 @@ import { deepClone } from 'common/util/deepClone';
|
||||
import {
|
||||
selectAutoSwitch,
|
||||
selectGalleryView,
|
||||
selectListImageNamesQueryArgs,
|
||||
selectGetImageNamesQueryArgs,
|
||||
selectSelectedBoardId,
|
||||
} from 'features/gallery/store/gallerySelectors';
|
||||
import { boardIdSelected, galleryViewChanged, imageSelected } from 'features/gallery/store/gallerySlice';
|
||||
@@ -17,6 +17,7 @@ import type { ImageDTO, S } from 'services/api/types';
|
||||
import { getCategories } from 'services/api/util';
|
||||
import { insertImageIntoNamesResult } from 'services/api/util/optimisticUpdates';
|
||||
import { $lastProgressEvent } from 'services/events/stores';
|
||||
import stableHash from 'stable-hash';
|
||||
import type { Param0 } from 'tsafe';
|
||||
import { objectEntries } from 'tsafe';
|
||||
import type { JsonObject } from 'type-fest';
|
||||
@@ -40,7 +41,7 @@ export const buildOnInvocationComplete = (getState: AppGetState, dispatch: AppDi
|
||||
// For efficiency's sake, we want to minimize the number of dispatches and invalidations we do.
|
||||
// We'll keep track of each change we need to make and do them all at once.
|
||||
const boardTotalAdditions: Record<string, number> = {};
|
||||
const listImageNamesArg = selectListImageNamesQueryArgs(getState());
|
||||
const getImageNamesArg = selectGetImageNamesQueryArgs(getState());
|
||||
|
||||
for (const imageDTO of imageDTOs) {
|
||||
if (imageDTO.is_intermediate) {
|
||||
@@ -70,26 +71,38 @@ export const buildOnInvocationComplete = (getState: AppGetState, dispatch: AppDi
|
||||
}
|
||||
dispatch(boardsApi.util.upsertQueryEntries(entries));
|
||||
|
||||
// Optimistically update image names lists - DTOs are already cached by getResultImageDTOs
|
||||
const state = getState();
|
||||
|
||||
/**
|
||||
* Optimistic update and cache invalidation for image names queries that match this image's board and categories.
|
||||
* - Optimistic update for the cache that does not have a search term (we cannot derive the correct insertion
|
||||
* position when a search term is present).
|
||||
* - Cache invalidation for the query that has a search term, so it will be refetched.
|
||||
*
|
||||
* Note: The image DTO objects are already implicitly cached by the getResultImageDTOs function. We do not need
|
||||
* to explicitly cache them again here.
|
||||
*/
|
||||
for (const imageDTO of imageDTOs) {
|
||||
// Construct the expected query args for this image's getImageNames query
|
||||
// Use the current gallery query args as base, but override board_id and categories for this specific image
|
||||
const expectedQueryArgs = {
|
||||
...listImageNamesArg,
|
||||
// Override board_id and categories for this specific image to build the "expected" args for the query.
|
||||
const imageSpecificArgs = {
|
||||
categories: getCategories(imageDTO),
|
||||
board_id: imageDTO.board_id ?? 'none',
|
||||
};
|
||||
|
||||
// Check if we have cached image names for this query
|
||||
const cachedNamesResult = imagesApi.endpoints.getImageNames.select(expectedQueryArgs)(state);
|
||||
const expectedQueryArgs = {
|
||||
...getImageNamesArg,
|
||||
...imageSpecificArgs,
|
||||
search_term: '',
|
||||
};
|
||||
|
||||
if (cachedNamesResult.data) {
|
||||
// We have cached names - optimistically insert the new image
|
||||
dispatch(
|
||||
imagesApi.util.updateQueryData('getImageNames', expectedQueryArgs, (draft) => {
|
||||
// Use the utility function to insert at the correct position
|
||||
// If the cache for the query args provided here does not exist, RTK Query will ignore the update.
|
||||
dispatch(
|
||||
imagesApi.util.updateQueryData(
|
||||
'getImageNames',
|
||||
{
|
||||
...getImageNamesArg,
|
||||
...imageSpecificArgs,
|
||||
search_term: '',
|
||||
},
|
||||
(draft) => {
|
||||
const updatedResult = insertImageIntoNamesResult(
|
||||
draft,
|
||||
imageDTO,
|
||||
@@ -97,14 +110,21 @@ export const buildOnInvocationComplete = (getState: AppGetState, dispatch: AppDi
|
||||
expectedQueryArgs.order_dir
|
||||
);
|
||||
|
||||
// Replace the draft contents
|
||||
draft.image_names = updatedResult.image_names;
|
||||
draft.starred_count = updatedResult.starred_count;
|
||||
draft.total_count = updatedResult.total_count;
|
||||
})
|
||||
);
|
||||
}
|
||||
)
|
||||
);
|
||||
|
||||
// If there is a search term present, we need to invalidate that query to ensure the search results are updated.
|
||||
if (getImageNamesArg.search_term) {
|
||||
const expectedQueryArgs = {
|
||||
...getImageNamesArg,
|
||||
...imageSpecificArgs,
|
||||
};
|
||||
dispatch(imagesApi.util.invalidateTags([{ type: 'ImageNameList', id: stableHash(expectedQueryArgs) }]));
|
||||
}
|
||||
// If no cached data, we don't need to do anything - there's no list to update
|
||||
}
|
||||
|
||||
// No need to invalidate tags since we're doing optimistic updates
|
||||
|
||||
Reference in New Issue
Block a user