mirror of
https://github.com/invoke-ai/InvokeAI.git
synced 2026-02-14 17:35:11 -05:00
Add concepts for metadata handlers. Handlers include parsers, recallers and validators for different metadata types: - Parsers parse a raw metadata object of any shape to a structured object. - Recallers load the parsed metadata into state. Recallers are optional, as some metadata types don't need to be loaded into state. - Validators provide an additional layer of validation before recalling the metadata. This is needed because a metadata object may be valid, but not able to be recalled due to some other requirement, like base model compatibility. Validators are optional. Sometimes metadata is not a single object but a list of items - like LoRAs. Metadata handlers may implement an optional set of "item" handlers which operate on individual items in the list. Parsers and validators are async to allow fetching additional data, like a model config. Recallers are synchronous. The these handlers are composed into a public API, exported as a `handlers` object. Besides the handlers functions, a metadata handler set includes: - A function to get the label of the metadata type. - An optional function to render the value of the metadata type. - An optional function to render the _item_ value of the metadata type.
1360 lines
46 KiB
TypeScript
1360 lines
46 KiB
TypeScript
import type { EntityState, Update } from '@reduxjs/toolkit';
|
|
import type { PatchCollection } from '@reduxjs/toolkit/dist/query/core/buildThunks';
|
|
import type { JSONObject } from 'common/types';
|
|
import type { BoardId } from 'features/gallery/store/types';
|
|
import { ASSETS_CATEGORIES, IMAGE_CATEGORIES, IMAGE_LIMIT } from 'features/gallery/store/types';
|
|
import { addToast } from 'features/system/store/systemSlice';
|
|
import { t } from 'i18next';
|
|
import { keyBy } from 'lodash-es';
|
|
import type { components, paths } from 'services/api/schema';
|
|
import type {
|
|
DeleteBoardResult,
|
|
ImageCategory,
|
|
ImageDTO,
|
|
ListImagesArgs,
|
|
OffsetPaginatedResults_ImageDTO_,
|
|
PostUploadAction,
|
|
} from 'services/api/types';
|
|
import {
|
|
getCategories,
|
|
getIsImageInDateRange,
|
|
getListImagesUrl,
|
|
imagesAdapter,
|
|
imagesSelectors,
|
|
} from 'services/api/util';
|
|
|
|
import type { ApiTagDescription } from '..';
|
|
import { api, buildV1Url, LIST_TAG } from '..';
|
|
import { boardsApi, buildBoardsUrl } from './boards';
|
|
|
|
/**
|
|
* Builds an endpoint URL for the images router
|
|
* @example
|
|
* buildImagesUrl('some-path')
|
|
* // '/api/v1/images/some-path'
|
|
*/
|
|
const buildImagesUrl = (path: string = '') => buildV1Url(`images/${path}`);
|
|
|
|
/**
|
|
* Builds an endpoint URL for the board_images router
|
|
* @example
|
|
* buildBoardImagesUrl('some-path')
|
|
* // '/api/v1/board_images/some-path'
|
|
*/
|
|
const buildBoardImagesUrl = (path: string = '') => buildV1Url(`board_images/${path}`);
|
|
|
|
export const imagesApi = api.injectEndpoints({
|
|
endpoints: (build) => ({
|
|
/**
|
|
* Image Queries
|
|
*/
|
|
listImages: build.query<EntityState<ImageDTO, string>, ListImagesArgs>({
|
|
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,
|
|
}),
|
|
getIntermediatesCount: build.query<number, void>({
|
|
query: () => ({ url: buildImagesUrl('intermediates') }),
|
|
providesTags: ['IntermediatesCount', 'FetchOnReconnect'],
|
|
}),
|
|
clearIntermediates: build.mutation<number, void>({
|
|
query: () => ({ url: buildImagesUrl('intermediates'), method: 'DELETE' }),
|
|
invalidatesTags: ['IntermediatesCount'],
|
|
}),
|
|
getImageDTO: build.query<ImageDTO, string>({
|
|
query: (image_name) => ({ url: buildImagesUrl(`i/${image_name}`) }),
|
|
providesTags: (result, error, image_name) => [{ type: 'Image', id: image_name }],
|
|
keepUnusedDataFor: 86400, // 24 hours
|
|
}),
|
|
getImageMetadata: build.query<JSONObject | undefined, string>({
|
|
query: (image_name) => ({ url: buildImagesUrl(`i/${image_name}/metadata`) }),
|
|
providesTags: (result, error, image_name) => [{ type: 'ImageMetadata', id: image_name }],
|
|
keepUnusedDataFor: 86400, // 24 hours
|
|
}),
|
|
getImageWorkflow: build.query<
|
|
paths['/api/v1/images/i/{image_name}/workflow']['get']['responses']['200']['content']['application/json'],
|
|
string
|
|
>({
|
|
query: (image_name) => ({ url: `images/i/${image_name}/workflow` }),
|
|
providesTags: (result, error, image_name) => [{ type: 'ImageWorkflow', id: image_name }],
|
|
keepUnusedDataFor: 86400, // 24 hours
|
|
}),
|
|
deleteImage: build.mutation<void, ImageDTO>({
|
|
query: ({ image_name }) => ({
|
|
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
|
|
*/
|
|
|
|
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 patches: PatchCollection[] = [];
|
|
|
|
patches.push(
|
|
dispatch(
|
|
imagesApi.util.updateQueryData('listImages', queryArg, (draft) => {
|
|
imagesAdapter.removeOne(draft, image_name);
|
|
})
|
|
)
|
|
);
|
|
|
|
patches.push(
|
|
dispatch(
|
|
boardsApi.util.updateQueryData(
|
|
isAsset ? 'getBoardAssetsTotal' : 'getBoardImagesTotal',
|
|
imageDTO.board_id ?? 'none',
|
|
(draft) => {
|
|
draft.total = Math.max(draft.total - 1, 0);
|
|
}
|
|
)
|
|
)
|
|
); // decrement the image board's total
|
|
|
|
try {
|
|
await queryFulfilled;
|
|
} catch {
|
|
patches.forEach((patch) => {
|
|
patch.undo();
|
|
});
|
|
}
|
|
},
|
|
}),
|
|
deleteImages: build.mutation<components['schemas']['DeleteImagesFromListResult'], { imageDTOs: ImageDTO[] }>({
|
|
query: ({ imageDTOs }) => {
|
|
const image_names = imageDTOs.map((imageDTO) => imageDTO.image_name);
|
|
return {
|
|
url: buildImagesUrl('delete'),
|
|
method: 'POST',
|
|
body: {
|
|
image_names,
|
|
},
|
|
};
|
|
},
|
|
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) {
|
|
dispatch(
|
|
addToast({
|
|
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');
|
|
|
|
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);
|
|
})
|
|
);
|
|
|
|
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 {
|
|
//
|
|
}
|
|
},
|
|
}),
|
|
/**
|
|
* Change an image's `is_intermediate` property.
|
|
*/
|
|
changeImageIsIntermediate: build.mutation<ImageDTO, { imageDTO: ImageDTO; is_intermediate: boolean }>({
|
|
query: ({ imageDTO, is_intermediate }) => ({
|
|
url: buildImagesUrl(`i/${imageDTO.image_name}`),
|
|
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
|
|
*/
|
|
|
|
// 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 });
|
|
})
|
|
)
|
|
);
|
|
|
|
// $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);
|
|
}
|
|
)
|
|
)
|
|
);
|
|
|
|
// 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;
|
|
}
|
|
)
|
|
)
|
|
);
|
|
|
|
const queryArgs = {
|
|
board_id: imageDTO.board_id ?? 'none',
|
|
categories,
|
|
};
|
|
|
|
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());
|
|
|
|
// 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 isInDateRange = getIsImageInDateRange(currentCache.data, 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());
|
|
}
|
|
},
|
|
}),
|
|
/**
|
|
* Change an image's `session_id` association.
|
|
*/
|
|
changeImageSessionId: build.mutation<ImageDTO, { imageDTO: ImageDTO; session_id: string }>({
|
|
query: ({ imageDTO, session_id }) => ({
|
|
url: buildImagesUrl(`i/${imageDTO.image_name}`),
|
|
method: 'PATCH',
|
|
body: { session_id },
|
|
}),
|
|
async onQueryStarted({ imageDTO, session_id }, { dispatch, queryFulfilled }) {
|
|
/**
|
|
* Cache changes for `changeImageSessionId`:
|
|
* - *update* getImageDTO
|
|
*/
|
|
|
|
// 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, { session_id });
|
|
})
|
|
)
|
|
);
|
|
|
|
try {
|
|
await queryFulfilled;
|
|
} catch {
|
|
patches.forEach((patchResult) => patchResult.undo());
|
|
}
|
|
},
|
|
}),
|
|
/**
|
|
* Star a list of images.
|
|
*/
|
|
starImages: build.mutation<
|
|
paths['/api/v1/images/unstar']['post']['responses']['200']['content']['application/json'],
|
|
{ imageDTOs: ImageDTO[] }
|
|
>({
|
|
query: ({ imageDTOs: images }) => ({
|
|
url: buildImagesUrl('star'),
|
|
method: 'POST',
|
|
body: { image_names: images.map((img) => img.image_name) },
|
|
}),
|
|
invalidatesTags: (result, error, { imageDTOs: images }) => {
|
|
// assume all images are on the same board/category
|
|
if (images[0]) {
|
|
const categories = getCategories(images[0]);
|
|
const boardId = images[0].board_id ?? undefined;
|
|
|
|
return [
|
|
{
|
|
type: 'ImageList',
|
|
id: getListImagesUrl({
|
|
board_id: boardId,
|
|
categories,
|
|
}),
|
|
},
|
|
{
|
|
type: 'Board',
|
|
id: boardId,
|
|
},
|
|
];
|
|
}
|
|
return [];
|
|
},
|
|
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));
|
|
|
|
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 = true;
|
|
})
|
|
);
|
|
|
|
const queryArgs = {
|
|
board_id: boardId ?? 'none',
|
|
categories,
|
|
};
|
|
|
|
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 isCacheFullyPopulated = currentCache.data && currentCache.data.ids.length >= (data?.total ?? 0);
|
|
|
|
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
|
|
}
|
|
},
|
|
}),
|
|
/**
|
|
* Unstar a list of images.
|
|
*/
|
|
unstarImages: build.mutation<
|
|
paths['/api/v1/images/unstar']['post']['responses']['200']['content']['application/json'],
|
|
{ imageDTOs: ImageDTO[] }
|
|
>({
|
|
query: ({ imageDTOs: images }) => ({
|
|
url: buildImagesUrl('unstar'),
|
|
method: 'POST',
|
|
body: { image_names: images.map((img) => img.image_name) },
|
|
}),
|
|
invalidatesTags: (result, error, { imageDTOs: images }) => {
|
|
// assume all images are on the same board/category
|
|
if (images[0]) {
|
|
const categories = getCategories(images[0]);
|
|
const boardId = images[0].board_id ?? undefined;
|
|
return [
|
|
{
|
|
type: 'ImageList',
|
|
id: getListImagesUrl({
|
|
board_id: boardId,
|
|
categories,
|
|
}),
|
|
},
|
|
{
|
|
type: 'Board',
|
|
id: boardId,
|
|
},
|
|
];
|
|
}
|
|
return [];
|
|
},
|
|
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));
|
|
|
|
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;
|
|
})
|
|
);
|
|
|
|
const queryArgs = {
|
|
board_id: boardId ?? 'none',
|
|
categories,
|
|
};
|
|
|
|
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 isCacheFullyPopulated = currentCache.data && currentCache.data.ids.length >= (data?.total ?? 0);
|
|
|
|
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
|
|
}
|
|
},
|
|
}),
|
|
uploadImage: build.mutation<
|
|
ImageDTO,
|
|
{
|
|
file: File;
|
|
image_category: ImageCategory;
|
|
is_intermediate: boolean;
|
|
postUploadAction?: PostUploadAction;
|
|
session_id?: string;
|
|
board_id?: string;
|
|
crop_visible?: boolean;
|
|
}
|
|
>({
|
|
query: ({ file, image_category, is_intermediate, session_id, board_id, crop_visible }) => {
|
|
const formData = new FormData();
|
|
formData.append('file', file);
|
|
return {
|
|
url: buildImagesUrl('upload'),
|
|
method: 'POST',
|
|
body: formData,
|
|
params: {
|
|
image_category,
|
|
is_intermediate,
|
|
session_id,
|
|
board_id: board_id === 'none' ? undefined : board_id,
|
|
crop_visible,
|
|
},
|
|
};
|
|
},
|
|
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;
|
|
|
|
if (imageDTO.is_intermediate) {
|
|
// Don't add it to anything
|
|
return;
|
|
}
|
|
|
|
// *add* to `getImageDTO`
|
|
dispatch(imagesApi.util.upsertQueryData('getImageDTO', imageDTO.image_name, 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);
|
|
}
|
|
)
|
|
);
|
|
|
|
// 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<DeleteBoardResult, string>({
|
|
query: (board_id) => ({ url: buildBoardsUrl(board_id), method: 'DELETE' }),
|
|
invalidatesTags: () => [
|
|
{ type: 'Board', id: LIST_TAG },
|
|
// invalidate the 'No Board' cache
|
|
{
|
|
type: 'ImageList',
|
|
id: getListImagesUrl({
|
|
board_id: 'none',
|
|
categories: IMAGE_CATEGORIES,
|
|
}),
|
|
},
|
|
{
|
|
type: 'ImageList',
|
|
id: getListImagesUrl({
|
|
board_id: 'none',
|
|
categories: ASSETS_CATEGORIES,
|
|
}),
|
|
},
|
|
],
|
|
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;
|
|
|
|
// 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 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,
|
|
},
|
|
];
|
|
|
|
const updates: Update<ImageDTO, string>[] = 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
|
|
}
|
|
},
|
|
}),
|
|
|
|
deleteBoardAndImages: build.mutation<DeleteBoardResult, string>({
|
|
query: (board_id) => ({
|
|
url: buildBoardsUrl(board_id),
|
|
method: 'DELETE',
|
|
params: { include_images: true },
|
|
}),
|
|
invalidatesTags: () => [
|
|
{ type: 'Board', id: LIST_TAG },
|
|
{
|
|
type: 'ImageList',
|
|
id: getListImagesUrl({
|
|
board_id: 'none',
|
|
categories: IMAGE_CATEGORIES,
|
|
}),
|
|
},
|
|
{
|
|
type: 'ImageList',
|
|
id: getListImagesUrl({
|
|
board_id: 'none',
|
|
categories: ASSETS_CATEGORIES,
|
|
}),
|
|
},
|
|
],
|
|
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;
|
|
|
|
// 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);
|
|
})
|
|
);
|
|
});
|
|
|
|
// 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
|
|
}
|
|
},
|
|
}),
|
|
addImageToBoard: build.mutation<void, { board_id: BoardId; imageDTO: ImageDTO }>({
|
|
query: ({ board_id, imageDTO }) => {
|
|
const { image_name } = imageDTO;
|
|
return {
|
|
url: buildBoardImagesUrl(),
|
|
method: 'POST',
|
|
body: { board_id, image_name },
|
|
};
|
|
},
|
|
invalidatesTags: (result, error, { board_id }) => [
|
|
// 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
|
|
*/
|
|
|
|
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);
|
|
}
|
|
)
|
|
)
|
|
);
|
|
|
|
// decrement old board's total
|
|
patches.push(
|
|
dispatch(
|
|
boardsApi.util.updateQueryData(
|
|
isAsset ? 'getBoardAssetsTotal' : 'getBoardImagesTotal',
|
|
imageDTO.board_id ?? 'none',
|
|
(draft) => {
|
|
draft.total = Math.max(draft.total - 1, 0);
|
|
}
|
|
)
|
|
)
|
|
);
|
|
|
|
// increment new board's total
|
|
patches.push(
|
|
dispatch(
|
|
boardsApi.util.updateQueryData(
|
|
isAsset ? 'getBoardAssetsTotal' : 'getBoardImagesTotal',
|
|
board_id ?? 'none',
|
|
(draft) => {
|
|
draft.total += 1;
|
|
}
|
|
)
|
|
)
|
|
);
|
|
|
|
// $cache = board_id/[images|assets]
|
|
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
|
|
|
|
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 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);
|
|
})
|
|
)
|
|
);
|
|
}
|
|
}
|
|
|
|
try {
|
|
await queryFulfilled;
|
|
} catch {
|
|
patches.forEach((patchResult) => patchResult.undo());
|
|
}
|
|
},
|
|
}),
|
|
removeImageFromBoard: build.mutation<void, { imageDTO: ImageDTO }>({
|
|
query: ({ imageDTO }) => {
|
|
const { image_name } = imageDTO;
|
|
return {
|
|
url: buildBoardImagesUrl(),
|
|
method: 'DELETE',
|
|
body: { image_name },
|
|
};
|
|
},
|
|
invalidatesTags: (result, error, { imageDTO }) => {
|
|
const { board_id } = imageDTO;
|
|
return [
|
|
// invalidate the image's old board
|
|
{ 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)
|
|
*/
|
|
|
|
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;
|
|
})
|
|
)
|
|
);
|
|
|
|
// *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);
|
|
}
|
|
)
|
|
)
|
|
);
|
|
|
|
// 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());
|
|
|
|
// 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 isCacheFullyPopulated = currentCache.data && currentCache.data.ids.length >= (data?.total ?? 0);
|
|
|
|
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);
|
|
})
|
|
)
|
|
);
|
|
}
|
|
|
|
try {
|
|
await queryFulfilled;
|
|
} catch {
|
|
patches.forEach((patchResult) => patchResult.undo());
|
|
}
|
|
},
|
|
}),
|
|
addImagesToBoard: build.mutation<
|
|
components['schemas']['AddImagesToBoardResult'],
|
|
{
|
|
board_id: string;
|
|
imageDTOs: ImageDTO[];
|
|
}
|
|
>({
|
|
query: ({ board_id, imageDTOs }) => ({
|
|
url: buildBoardImagesUrl('batch'),
|
|
method: 'POST',
|
|
body: {
|
|
image_names: imageDTOs.map((i) => i.image_name),
|
|
board_id,
|
|
},
|
|
}),
|
|
invalidatesTags: (result, error, { board_id }) => {
|
|
return [
|
|
// update the destination board
|
|
{ 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;
|
|
|
|
/**
|
|
* 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;
|
|
})
|
|
);
|
|
|
|
const imageDTO = imageDTOs.find((i) => i.image_name === image_name);
|
|
|
|
if (!imageDTO) {
|
|
return;
|
|
}
|
|
|
|
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);
|
|
}
|
|
)
|
|
);
|
|
|
|
// decrement old board's total
|
|
dispatch(
|
|
boardsApi.util.updateQueryData(
|
|
isAsset ? 'getBoardAssetsTotal' : 'getBoardImagesTotal',
|
|
old_board_id ?? 'none',
|
|
(draft) => {
|
|
draft.total = Math.max(draft.total - 1, 0);
|
|
}
|
|
)
|
|
);
|
|
|
|
// increment new board's total
|
|
dispatch(
|
|
boardsApi.util.updateQueryData(
|
|
isAsset ? 'getBoardAssetsTotal' : 'getBoardImagesTotal',
|
|
new_board_id ?? 'none',
|
|
(draft) => {
|
|
draft.total += 1;
|
|
}
|
|
)
|
|
);
|
|
|
|
const queryArgs = {
|
|
board_id: new_board_id,
|
|
categories,
|
|
};
|
|
|
|
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 isCacheFullyPopulated = currentCache.data && currentCache.data.ids.length >= (data?.total ?? 0);
|
|
|
|
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
|
|
}
|
|
},
|
|
}),
|
|
removeImagesFromBoard: build.mutation<
|
|
components['schemas']['RemoveImagesFromBoardResult'],
|
|
{
|
|
imageDTOs: ImageDTO[];
|
|
}
|
|
>({
|
|
query: ({ imageDTOs }) => ({
|
|
url: buildBoardImagesUrl('batch/delete'),
|
|
method: 'POST',
|
|
body: {
|
|
image_names: imageDTOs.map((i) => i.image_name),
|
|
},
|
|
}),
|
|
invalidatesTags: (result, error, { imageDTOs }) => {
|
|
const touchedBoardIds: string[] = [];
|
|
const tags: ApiTagDescription[] = [];
|
|
|
|
result?.removed_image_names.forEach((image_name) => {
|
|
const board_id = imageDTOs.find((i) => i.image_name === image_name)?.board_id;
|
|
|
|
if (!board_id || touchedBoardIds.includes(board_id)) {
|
|
return;
|
|
}
|
|
|
|
tags.push({ type: 'Board', id: board_id });
|
|
});
|
|
|
|
return tags;
|
|
},
|
|
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
|
|
*/
|
|
|
|
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);
|
|
|
|
if (!imageDTO) {
|
|
return;
|
|
}
|
|
|
|
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);
|
|
}
|
|
)
|
|
);
|
|
|
|
// decrement old board's total
|
|
dispatch(
|
|
boardsApi.util.updateQueryData(
|
|
isAsset ? 'getBoardAssetsTotal' : 'getBoardImagesTotal',
|
|
imageDTO.board_id ?? 'none',
|
|
(draft) => {
|
|
draft.total = Math.max(draft.total - 1, 0);
|
|
}
|
|
)
|
|
);
|
|
|
|
// increment new board's total (no board)
|
|
dispatch(
|
|
boardsApi.util.updateQueryData(
|
|
isAsset ? 'getBoardAssetsTotal' : 'getBoardImagesTotal',
|
|
'none',
|
|
(draft) => {
|
|
draft.total += 1;
|
|
}
|
|
)
|
|
);
|
|
|
|
// add to `no_board`
|
|
const queryArgs = {
|
|
board_id: 'none',
|
|
categories,
|
|
};
|
|
|
|
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 isCacheFullyPopulated = currentCache.data && currentCache.data.ids.length >= (data?.total ?? 0);
|
|
|
|
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
|
|
}
|
|
},
|
|
}),
|
|
bulkDownloadImages: build.mutation<
|
|
components['schemas']['ImagesDownloaded'],
|
|
components['schemas']['Body_download_images_from_list']
|
|
>({
|
|
query: ({ image_names, board_id }) => ({
|
|
url: buildImagesUrl('download'),
|
|
method: 'POST',
|
|
body: {
|
|
image_names,
|
|
board_id,
|
|
},
|
|
}),
|
|
}),
|
|
}),
|
|
});
|
|
|
|
export const {
|
|
useGetIntermediatesCountQuery,
|
|
useListImagesQuery,
|
|
useLazyListImagesQuery,
|
|
useGetImageDTOQuery,
|
|
useGetImageMetadataQuery,
|
|
useGetImageWorkflowQuery,
|
|
useLazyGetImageWorkflowQuery,
|
|
useDeleteImageMutation,
|
|
useDeleteImagesMutation,
|
|
useUploadImageMutation,
|
|
useClearIntermediatesMutation,
|
|
useAddImagesToBoardMutation,
|
|
useRemoveImagesFromBoardMutation,
|
|
useAddImageToBoardMutation,
|
|
useRemoveImageFromBoardMutation,
|
|
useChangeImageIsIntermediateMutation,
|
|
useChangeImageSessionIdMutation,
|
|
useDeleteBoardAndImagesMutation,
|
|
useDeleteBoardMutation,
|
|
useStarImagesMutation,
|
|
useUnstarImagesMutation,
|
|
useBulkDownloadImagesMutation,
|
|
} = imagesApi;
|