fix(ui): skip optimistic updates for gallery when using search term

This commit is contained in:
psychedelicious
2025-07-05 19:20:04 +10:00
parent 6bd004d868
commit 61a35f1396
10 changed files with 63 additions and 50 deletions

View File

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

View File

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

View File

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

View File

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