mirror of
https://github.com/invoke-ai/InvokeAI
synced 2024-08-30 20:32:17 +00:00
feat: add multi-select to gallery
multi-select actions include: - drag to board to move all to that board - right click to add all to board or delete all backend changes: - add routes for changing board for list of image names, deleting list of images - change image-specific routes to `images/i/{image_name}` to not clobber other routes (like `images/upload`, `images/delete`) - subclass pydantic `BaseModel` as `BaseModelExcludeNull`, which excludes null values when calling `dict()` on the model. this fixes inconsistent types related to JSON parsing null values into `null` instead of `undefined` - remove `board_id` from `remove_image_from_board` frontend changes: - multi-selection stuff uses `ImageDTO[]` as payloads, for dnd and other mutations. this gives us access to image `board_id`s when hitting routes, and enables efficient cache updates. - consolidate change board and delete image modals to handle single and multiples - board totals are now re-fetched on mutation and not kept in sync manually - was way too tedious to do this - fixed warning about nested `<p>` elements - closes #4088 , need to handle case when `autoAddBoardId` is `"none"` - add option to show gallery image delete button on every gallery image frontend refactors/organisation: - make typegen script js instead of ts - enable `noUncheckedIndexedAccess` to help avoid bugs when indexing into arrays, many small changes needed to satisfy TS after this - move all image-related endpoints into `endpoints/images.ts`, its a big file now, but this fixes a number of circular dependency issues that were otherwise felt impossible to resolve
This commit is contained in:
@ -1,36 +0,0 @@
|
||||
import { api } from '..';
|
||||
|
||||
export const boardImagesApi = api.injectEndpoints({
|
||||
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[] = [
|
||||
// { type: 'BoardImage', id: `${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;
|
||||
// },
|
||||
// }),
|
||||
}),
|
||||
});
|
||||
|
||||
// export const { useListBoardImagesQuery } = boardImagesApi;
|
@ -1,28 +1,16 @@
|
||||
import { Update } from '@reduxjs/toolkit';
|
||||
import {
|
||||
ASSETS_CATEGORIES,
|
||||
IMAGE_CATEGORIES,
|
||||
} from 'features/gallery/store/types';
|
||||
import {
|
||||
BoardDTO,
|
||||
ImageDTO,
|
||||
ListBoardsArg,
|
||||
OffsetPaginatedResults_BoardDTO_,
|
||||
OffsetPaginatedResults_ImageDTO_,
|
||||
UpdateBoardArg,
|
||||
} from 'services/api/types';
|
||||
import { ApiFullTagDescription, LIST_TAG, api } from '..';
|
||||
import { paths } from '../schema';
|
||||
import { getListImagesUrl, imagesAdapter, imagesApi } from './images';
|
||||
|
||||
type ListBoardsArg = NonNullable<
|
||||
paths['/api/v1/boards/']['get']['parameters']['query']
|
||||
>;
|
||||
|
||||
type UpdateBoardArg =
|
||||
paths['/api/v1/boards/{board_id}']['patch']['parameters']['path'] & {
|
||||
changes: paths['/api/v1/boards/{board_id}']['patch']['requestBody']['content']['application/json'];
|
||||
};
|
||||
|
||||
type DeleteBoardResult =
|
||||
paths['/api/v1/boards/{board_id}']['delete']['responses']['200']['content']['application/json'];
|
||||
import { getListImagesUrl } from '../util';
|
||||
|
||||
export const boardsApi = api.injectEndpoints({
|
||||
endpoints: (build) => ({
|
||||
@ -82,6 +70,44 @@ export const boardsApi = api.injectEndpoints({
|
||||
keepUnusedDataFor: 0,
|
||||
}),
|
||||
|
||||
getBoardImagesTotal: build.query<number, string | undefined>({
|
||||
query: (board_id) => ({
|
||||
url: getListImagesUrl({
|
||||
board_id: board_id ?? 'none',
|
||||
categories: IMAGE_CATEGORIES,
|
||||
is_intermediate: false,
|
||||
limit: 0,
|
||||
offset: 0,
|
||||
}),
|
||||
method: 'GET',
|
||||
}),
|
||||
providesTags: (result, error, arg) => [
|
||||
{ type: 'BoardImagesTotal', id: arg ?? 'none' },
|
||||
],
|
||||
transformResponse: (response: OffsetPaginatedResults_ImageDTO_) => {
|
||||
return response.total;
|
||||
},
|
||||
}),
|
||||
|
||||
getBoardAssetsTotal: build.query<number, string | undefined>({
|
||||
query: (board_id) => ({
|
||||
url: getListImagesUrl({
|
||||
board_id: board_id ?? 'none',
|
||||
categories: ASSETS_CATEGORIES,
|
||||
is_intermediate: false,
|
||||
limit: 0,
|
||||
offset: 0,
|
||||
}),
|
||||
method: 'GET',
|
||||
}),
|
||||
providesTags: (result, error, arg) => [
|
||||
{ type: 'BoardAssetsTotal', id: arg ?? 'none' },
|
||||
],
|
||||
transformResponse: (response: OffsetPaginatedResults_ImageDTO_) => {
|
||||
return response.total;
|
||||
},
|
||||
}),
|
||||
|
||||
/**
|
||||
* Boards Mutations
|
||||
*/
|
||||
@ -105,176 +131,15 @@ export const boardsApi = api.injectEndpoints({
|
||||
{ type: 'Board', id: arg.board_id },
|
||||
],
|
||||
}),
|
||||
|
||||
deleteBoard: build.mutation<DeleteBoardResult, string>({
|
||||
query: (board_id) => ({ url: `boards/${board_id}`, method: 'DELETE' }),
|
||||
invalidatesTags: (result, error, board_id) => [
|
||||
{ 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,
|
||||
}),
|
||||
},
|
||||
{ type: 'BoardImagesTotal', id: 'none' },
|
||||
{ type: 'BoardAssetsTotal', id: 'none' },
|
||||
],
|
||||
async onQueryStarted(board_id, { dispatch, queryFulfilled, getState }) {
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
|
||||
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 'All Images' & 'All Assets' caches
|
||||
const queryArgsToUpdate = [
|
||||
{
|
||||
categories: IMAGE_CATEGORIES,
|
||||
},
|
||||
{
|
||||
categories: ASSETS_CATEGORIES,
|
||||
},
|
||||
];
|
||||
|
||||
const updates: Update<ImageDTO>[] = deleted_board_images.map(
|
||||
(image_name) => ({
|
||||
id: image_name,
|
||||
changes: { board_id: undefined },
|
||||
})
|
||||
);
|
||||
|
||||
queryArgsToUpdate.forEach((queryArgs) => {
|
||||
dispatch(
|
||||
imagesApi.util.updateQueryData(
|
||||
'listImages',
|
||||
queryArgs,
|
||||
(draft) => {
|
||||
const oldTotal = draft.total;
|
||||
const newState = imagesAdapter.updateMany(draft, updates);
|
||||
const delta = newState.total - oldTotal;
|
||||
draft.total = draft.total + delta;
|
||||
}
|
||||
)
|
||||
);
|
||||
});
|
||||
} catch {
|
||||
//no-op
|
||||
}
|
||||
},
|
||||
}),
|
||||
|
||||
deleteBoardAndImages: build.mutation<DeleteBoardResult, string>({
|
||||
query: (board_id) => ({
|
||||
url: `boards/${board_id}`,
|
||||
method: 'DELETE',
|
||||
params: { include_images: true },
|
||||
}),
|
||||
invalidatesTags: (result, error, board_id) => [
|
||||
{ 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,
|
||||
}),
|
||||
},
|
||||
{ type: 'BoardImagesTotal', id: 'none' },
|
||||
{ type: 'BoardAssetsTotal', id: 'none' },
|
||||
],
|
||||
async onQueryStarted(board_id, { dispatch, queryFulfilled, getState }) {
|
||||
/**
|
||||
* 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
|
||||
*/
|
||||
|
||||
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) => {
|
||||
const oldTotal = draft.total;
|
||||
const newState = imagesAdapter.removeMany(
|
||||
draft,
|
||||
deleted_images
|
||||
);
|
||||
const delta = newState.total - oldTotal;
|
||||
draft.total = draft.total + delta;
|
||||
}
|
||||
)
|
||||
);
|
||||
});
|
||||
} catch {
|
||||
//no-op
|
||||
}
|
||||
},
|
||||
}),
|
||||
}),
|
||||
});
|
||||
|
||||
export const {
|
||||
useListBoardsQuery,
|
||||
useListAllBoardsQuery,
|
||||
useGetBoardImagesTotalQuery,
|
||||
useGetBoardAssetsTotalQuery,
|
||||
useCreateBoardMutation,
|
||||
useUpdateBoardMutation,
|
||||
useDeleteBoardMutation,
|
||||
useDeleteBoardAndImagesMutation,
|
||||
useListAllImageNamesForBoardQuery,
|
||||
} = boardsApi;
|
||||
|
@ -1,93 +1,37 @@
|
||||
import { EntityState, createEntityAdapter } from '@reduxjs/toolkit';
|
||||
import { EntityState, Update } from '@reduxjs/toolkit';
|
||||
import { PatchCollection } from '@reduxjs/toolkit/dist/query/core/buildThunks';
|
||||
import { dateComparator } from 'common/util/dateComparator';
|
||||
import {
|
||||
ASSETS_CATEGORIES,
|
||||
BoardId,
|
||||
IMAGE_CATEGORIES,
|
||||
} from 'features/gallery/store/types';
|
||||
import queryString from 'query-string';
|
||||
import { ApiFullTagDescription, api } from '..';
|
||||
import { components, paths } from '../schema';
|
||||
import { keyBy } from 'lodash';
|
||||
import { ApiFullTagDescription, LIST_TAG, api } from '..';
|
||||
import { components } from '../schema';
|
||||
import {
|
||||
DeleteBoardResult,
|
||||
ImageCategory,
|
||||
ImageDTO,
|
||||
ListImagesArgs,
|
||||
OffsetPaginatedResults_ImageDTO_,
|
||||
PostUploadAction,
|
||||
UnsafeImageMetadata,
|
||||
} from '../types';
|
||||
|
||||
const getIsImageInDateRange = (
|
||||
data: ImageCache | undefined,
|
||||
imageDTO: ImageDTO
|
||||
) => {
|
||||
if (!data) {
|
||||
return false;
|
||||
}
|
||||
const cacheImageDTOS = imagesSelectors.selectAll(data);
|
||||
|
||||
if (cacheImageDTOS.length > 1) {
|
||||
// Images are sorted by `created_at` DESC
|
||||
// check if the image is newer than the oldest image in the cache
|
||||
const createdDate = new Date(imageDTO.created_at);
|
||||
const oldestDate = new Date(
|
||||
cacheImageDTOS[cacheImageDTOS.length - 1].created_at
|
||||
);
|
||||
return createdDate >= oldestDate;
|
||||
} else if ([0, 1].includes(cacheImageDTOS.length)) {
|
||||
// if there are only 1 or 0 images in the cache, we consider the image to be in the date range
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
const getCategories = (imageDTO: ImageDTO) => {
|
||||
if (IMAGE_CATEGORIES.includes(imageDTO.image_category)) {
|
||||
return IMAGE_CATEGORIES;
|
||||
}
|
||||
return ASSETS_CATEGORIES;
|
||||
};
|
||||
|
||||
export type ListImagesArgs = NonNullable<
|
||||
paths['/api/v1/images/']['get']['parameters']['query']
|
||||
>;
|
||||
|
||||
/**
|
||||
* This is an unsafe type; the object inside is not guaranteed to be valid.
|
||||
*/
|
||||
export type UnsafeImageMetadata = {
|
||||
metadata: components['schemas']['CoreMetadata'];
|
||||
graph: NonNullable<components['schemas']['Graph']>;
|
||||
};
|
||||
|
||||
export type ImageCache = EntityState<ImageDTO> & { total: number };
|
||||
|
||||
// The adapter is not actually the data store - it just provides helper functions to interact
|
||||
// with some other store of data. We will use the RTK Query cache as that store.
|
||||
export const imagesAdapter = createEntityAdapter<ImageDTO>({
|
||||
selectId: (image) => image.image_name,
|
||||
sortComparer: (a, b) => dateComparator(b.updated_at, a.updated_at),
|
||||
});
|
||||
|
||||
// We want to also store the images total in the cache. When we initialize the cache state,
|
||||
// we will provide this type arg so the adapter knows we want the total.
|
||||
export type AdditionalImagesAdapterState = { total: number };
|
||||
|
||||
// Create selectors for the adapter.
|
||||
export const imagesSelectors = imagesAdapter.getSelectors();
|
||||
|
||||
// Helper to create the url for the listImages endpoint. Also we use it to create the cache key.
|
||||
export const getListImagesUrl = (queryArgs: ListImagesArgs) =>
|
||||
`images/?${queryString.stringify(queryArgs, { arrayFormat: 'none' })}`;
|
||||
import {
|
||||
getCategories,
|
||||
getIsImageInDateRange,
|
||||
getListImagesUrl,
|
||||
imagesAdapter,
|
||||
imagesSelectors,
|
||||
} from '../util';
|
||||
import { boardsApi } from './boards';
|
||||
|
||||
export const imagesApi = api.injectEndpoints({
|
||||
endpoints: (build) => ({
|
||||
/**
|
||||
* Image Queries
|
||||
*/
|
||||
listImages: build.query<
|
||||
EntityState<ImageDTO> & { total: number },
|
||||
ListImagesArgs
|
||||
>({
|
||||
listImages: build.query<EntityState<ImageDTO>, ListImagesArgs>({
|
||||
query: (queryArgs) => ({
|
||||
// Use the helper to create the URL.
|
||||
url: getListImagesUrl(queryArgs),
|
||||
@ -110,23 +54,17 @@ export const imagesApi = api.injectEndpoints({
|
||||
return cacheKey;
|
||||
},
|
||||
transformResponse(response: OffsetPaginatedResults_ImageDTO_) {
|
||||
const { total, items: images } = response;
|
||||
// Use the adapter to convert the response to the right shape, and adding the new total.
|
||||
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<AdditionalImagesAdapterState>({
|
||||
total,
|
||||
}),
|
||||
images
|
||||
);
|
||||
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. Also update the total image count.
|
||||
// things in the right shape.
|
||||
imagesAdapter.addMany(cache, imagesSelectors.selectAll(response));
|
||||
cache.total = response.total;
|
||||
},
|
||||
forceRefetch({ currentArg, previousArg }) {
|
||||
// Refetch when the offset changes (which means we are on a new page).
|
||||
@ -161,69 +99,26 @@ export const imagesApi = api.injectEndpoints({
|
||||
},
|
||||
}),
|
||||
getImageDTO: build.query<ImageDTO, string>({
|
||||
query: (image_name) => ({ url: `images/${image_name}` }),
|
||||
providesTags: (result, error, arg) => {
|
||||
const tags: ApiFullTagDescription[] = [{ type: 'Image', id: arg }];
|
||||
if (result?.board_id) {
|
||||
tags.push({ type: 'Board', id: result.board_id });
|
||||
}
|
||||
return tags;
|
||||
},
|
||||
query: (image_name) => ({ url: `images/i/${image_name}` }),
|
||||
providesTags: (result, error, image_name) => [
|
||||
{ type: 'Image', id: image_name },
|
||||
],
|
||||
keepUnusedDataFor: 86400, // 24 hours
|
||||
}),
|
||||
getImageMetadata: build.query<UnsafeImageMetadata, string>({
|
||||
query: (image_name) => ({ url: `images/${image_name}/metadata` }),
|
||||
providesTags: (result, error, arg) => {
|
||||
const tags: ApiFullTagDescription[] = [
|
||||
{ type: 'ImageMetadata', id: arg },
|
||||
];
|
||||
return tags;
|
||||
},
|
||||
query: (image_name) => ({ url: `images/i/${image_name}/metadata` }),
|
||||
providesTags: (result, error, image_name) => [
|
||||
{ type: 'ImageMetadata', id: image_name },
|
||||
],
|
||||
keepUnusedDataFor: 86400, // 24 hours
|
||||
}),
|
||||
getBoardImagesTotal: build.query<number, string | undefined>({
|
||||
query: (board_id) => ({
|
||||
url: getListImagesUrl({
|
||||
board_id: board_id ?? 'none',
|
||||
categories: IMAGE_CATEGORIES,
|
||||
is_intermediate: false,
|
||||
limit: 0,
|
||||
offset: 0,
|
||||
}),
|
||||
method: 'GET',
|
||||
}),
|
||||
providesTags: (result, error, arg) => [
|
||||
{ type: 'BoardImagesTotal', id: arg ?? 'none' },
|
||||
],
|
||||
transformResponse: (response: OffsetPaginatedResults_ImageDTO_) => {
|
||||
return response.total;
|
||||
},
|
||||
}),
|
||||
getBoardAssetsTotal: build.query<number, string | undefined>({
|
||||
query: (board_id) => ({
|
||||
url: getListImagesUrl({
|
||||
board_id: board_id ?? 'none',
|
||||
categories: ASSETS_CATEGORIES,
|
||||
is_intermediate: false,
|
||||
limit: 0,
|
||||
offset: 0,
|
||||
}),
|
||||
method: 'GET',
|
||||
}),
|
||||
providesTags: (result, error, arg) => [
|
||||
{ type: 'BoardAssetsTotal', id: arg ?? 'none' },
|
||||
],
|
||||
transformResponse: (response: OffsetPaginatedResults_ImageDTO_) => {
|
||||
return response.total;
|
||||
},
|
||||
}),
|
||||
clearIntermediates: build.mutation<number, void>({
|
||||
query: () => ({ url: `images/clear-intermediates`, method: 'POST' }),
|
||||
invalidatesTags: ['IntermediatesCount'],
|
||||
}),
|
||||
deleteImage: build.mutation<void, ImageDTO>({
|
||||
query: ({ image_name }) => ({
|
||||
url: `images/${image_name}`,
|
||||
url: `images/i/${image_name}`,
|
||||
method: 'DELETE',
|
||||
}),
|
||||
invalidatesTags: (result, error, { board_id }) => [
|
||||
@ -240,33 +135,77 @@ export const imagesApi = api.injectEndpoints({
|
||||
|
||||
const { image_name, board_id } = imageDTO;
|
||||
|
||||
// Store patches so we can undo if the query fails
|
||||
const patches: PatchCollection[] = [];
|
||||
const queryArg = {
|
||||
board_id: board_id ?? 'none',
|
||||
categories: getCategories(imageDTO),
|
||||
};
|
||||
|
||||
// determine `categories`, i.e. do we update "All Images" or "All Assets"
|
||||
// $cache = [board_id|no_board]/[images|assets]
|
||||
const categories = getCategories(imageDTO);
|
||||
|
||||
// *remove* from $cache
|
||||
patches.push(
|
||||
dispatch(
|
||||
imagesApi.util.updateQueryData(
|
||||
'listImages',
|
||||
{ board_id: board_id ?? 'none', categories },
|
||||
(draft) => {
|
||||
const oldTotal = draft.total;
|
||||
const newState = imagesAdapter.removeOne(draft, image_name);
|
||||
const delta = newState.total - oldTotal;
|
||||
draft.total = draft.total + delta;
|
||||
}
|
||||
)
|
||||
)
|
||||
const patch = dispatch(
|
||||
imagesApi.util.updateQueryData('listImages', queryArg, (draft) => {
|
||||
imagesAdapter.removeOne(draft, image_name);
|
||||
})
|
||||
);
|
||||
|
||||
try {
|
||||
await queryFulfilled;
|
||||
} catch {
|
||||
patches.forEach((patchResult) => patchResult.undo());
|
||||
patch.undo();
|
||||
}
|
||||
},
|
||||
}),
|
||||
deleteImages: build.mutation<
|
||||
components['schemas']['DeleteImagesFromListResult'],
|
||||
{ imageDTOs: ImageDTO[] }
|
||||
>({
|
||||
query: ({ imageDTOs }) => {
|
||||
const image_names = imageDTOs.map((imageDTO) => imageDTO.image_name);
|
||||
return {
|
||||
url: `images/delete`,
|
||||
method: 'POST',
|
||||
body: {
|
||||
image_names,
|
||||
},
|
||||
};
|
||||
},
|
||||
invalidatesTags: (result, error, imageDTOs) => [],
|
||||
async onQueryStarted({ imageDTOs }, { dispatch, queryFulfilled }) {
|
||||
/**
|
||||
* Cache changes for `deleteImages`:
|
||||
* - *remove* the deleted images from their boards
|
||||
*
|
||||
* 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;
|
||||
|
||||
// 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);
|
||||
}
|
||||
)
|
||||
);
|
||||
}
|
||||
});
|
||||
} catch {
|
||||
//
|
||||
}
|
||||
},
|
||||
}),
|
||||
@ -278,7 +217,7 @@ export const imagesApi = api.injectEndpoints({
|
||||
{ imageDTO: ImageDTO; is_intermediate: boolean }
|
||||
>({
|
||||
query: ({ imageDTO, is_intermediate }) => ({
|
||||
url: `images/${imageDTO.image_name}`,
|
||||
url: `images/i/${imageDTO.image_name}`,
|
||||
method: 'PATCH',
|
||||
body: { is_intermediate },
|
||||
}),
|
||||
@ -329,20 +268,13 @@ export const imagesApi = api.injectEndpoints({
|
||||
'listImages',
|
||||
{ board_id: imageDTO.board_id ?? 'none', categories },
|
||||
(draft) => {
|
||||
const oldTotal = draft.total;
|
||||
const newState = imagesAdapter.removeOne(
|
||||
draft,
|
||||
imageDTO.image_name
|
||||
);
|
||||
const delta = newState.total - oldTotal;
|
||||
draft.total = draft.total + delta;
|
||||
imagesAdapter.removeOne(draft, imageDTO.image_name);
|
||||
}
|
||||
)
|
||||
)
|
||||
);
|
||||
} else {
|
||||
// ELSE (it is being changed to a non-intermediate):
|
||||
console.log(imageDTO);
|
||||
const queryArgs = {
|
||||
board_id: imageDTO.board_id ?? 'none',
|
||||
categories,
|
||||
@ -352,6 +284,16 @@ export const imagesApi = api.injectEndpoints({
|
||||
getState()
|
||||
);
|
||||
|
||||
const { data: total } = 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
|
||||
@ -359,8 +301,7 @@ export const imagesApi = api.injectEndpoints({
|
||||
// - The image's `created_at` is within the range of the cached images
|
||||
|
||||
const isCacheFullyPopulated =
|
||||
currentCache.data &&
|
||||
currentCache.data.ids.length >= currentCache.data.total;
|
||||
currentCache.data && currentCache.data.ids.length >= (total ?? 0);
|
||||
|
||||
const isInDateRange = getIsImageInDateRange(
|
||||
currentCache.data,
|
||||
@ -375,10 +316,7 @@ export const imagesApi = api.injectEndpoints({
|
||||
'listImages',
|
||||
queryArgs,
|
||||
(draft) => {
|
||||
const oldTotal = draft.total;
|
||||
const newState = imagesAdapter.upsertOne(draft, imageDTO);
|
||||
const delta = newState.total - oldTotal;
|
||||
draft.total = draft.total + delta;
|
||||
imagesAdapter.upsertOne(draft, imageDTO);
|
||||
}
|
||||
)
|
||||
)
|
||||
@ -401,7 +339,7 @@ export const imagesApi = api.injectEndpoints({
|
||||
{ imageDTO: ImageDTO; session_id: string }
|
||||
>({
|
||||
query: ({ imageDTO, session_id }) => ({
|
||||
url: `images/${imageDTO.image_name}`,
|
||||
url: `images/i/${imageDTO.image_name}`,
|
||||
method: 'PATCH',
|
||||
body: { session_id },
|
||||
}),
|
||||
@ -464,14 +402,14 @@ export const imagesApi = api.injectEndpoints({
|
||||
const formData = new FormData();
|
||||
formData.append('file', file);
|
||||
return {
|
||||
url: `images/`,
|
||||
url: `images/upload`,
|
||||
method: 'POST',
|
||||
body: formData,
|
||||
params: {
|
||||
image_category,
|
||||
is_intermediate,
|
||||
session_id,
|
||||
board_id,
|
||||
board_id: board_id === 'none' ? undefined : board_id,
|
||||
crop_visible,
|
||||
},
|
||||
};
|
||||
@ -524,10 +462,7 @@ export const imagesApi = api.injectEndpoints({
|
||||
categories,
|
||||
},
|
||||
(draft) => {
|
||||
const oldTotal = draft.total;
|
||||
const newState = imagesAdapter.addOne(draft, imageDTO);
|
||||
const delta = newState.total - oldTotal;
|
||||
draft.total = draft.total + delta;
|
||||
imagesAdapter.addOne(draft, imageDTO);
|
||||
}
|
||||
)
|
||||
);
|
||||
@ -543,6 +478,158 @@ export const imagesApi = api.injectEndpoints({
|
||||
}
|
||||
},
|
||||
}),
|
||||
|
||||
deleteBoard: build.mutation<DeleteBoardResult, string>({
|
||||
query: (board_id) => ({ url: `boards/${board_id}`, method: 'DELETE' }),
|
||||
invalidatesTags: (result, error, board_id) => [
|
||||
{ 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,
|
||||
}),
|
||||
},
|
||||
{ type: 'BoardImagesTotal', id: 'none' },
|
||||
{ type: 'BoardAssetsTotal', id: 'none' },
|
||||
],
|
||||
async onQueryStarted(board_id, { dispatch, queryFulfilled, getState }) {
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
|
||||
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 'All Images' & 'All Assets' caches
|
||||
const queryArgsToUpdate = [
|
||||
{
|
||||
categories: IMAGE_CATEGORIES,
|
||||
},
|
||||
{
|
||||
categories: ASSETS_CATEGORIES,
|
||||
},
|
||||
];
|
||||
|
||||
const updates: Update<ImageDTO>[] = 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: `boards/${board_id}`,
|
||||
method: 'DELETE',
|
||||
params: { include_images: true },
|
||||
}),
|
||||
invalidatesTags: (result, error, board_id) => [
|
||||
{ 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,
|
||||
}),
|
||||
},
|
||||
{ type: 'BoardImagesTotal', id: 'none' },
|
||||
{ type: 'BoardAssetsTotal', id: 'none' },
|
||||
],
|
||||
async onQueryStarted(board_id, { dispatch, queryFulfilled, getState }) {
|
||||
/**
|
||||
* 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
|
||||
*/
|
||||
|
||||
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);
|
||||
}
|
||||
)
|
||||
);
|
||||
});
|
||||
} catch {
|
||||
//no-op
|
||||
}
|
||||
},
|
||||
}),
|
||||
addImageToBoard: build.mutation<
|
||||
void,
|
||||
{ board_id: BoardId; imageDTO: ImageDTO }
|
||||
@ -556,10 +643,13 @@ export const imagesApi = api.injectEndpoints({
|
||||
};
|
||||
},
|
||||
invalidatesTags: (result, error, { board_id, imageDTO }) => [
|
||||
// refresh the board itself
|
||||
{ type: 'Board', id: board_id },
|
||||
// update old board totals
|
||||
{ type: 'BoardImagesTotal', id: board_id },
|
||||
{ type: 'BoardImagesTotal', id: imageDTO.board_id ?? 'none' },
|
||||
{ type: 'BoardAssetsTotal', id: board_id },
|
||||
// update new board totals
|
||||
{ type: 'BoardImagesTotal', id: imageDTO.board_id ?? 'none' },
|
||||
{ type: 'BoardAssetsTotal', id: imageDTO.board_id ?? 'none' },
|
||||
],
|
||||
async onQueryStarted(
|
||||
@ -589,7 +679,7 @@ export const imagesApi = api.injectEndpoints({
|
||||
'getImageDTO',
|
||||
imageDTO.image_name,
|
||||
(draft) => {
|
||||
Object.assign(draft, { board_id });
|
||||
draft.board_id = board_id;
|
||||
}
|
||||
)
|
||||
)
|
||||
@ -606,13 +696,7 @@ export const imagesApi = api.injectEndpoints({
|
||||
categories,
|
||||
},
|
||||
(draft) => {
|
||||
const oldTotal = draft.total;
|
||||
const newState = imagesAdapter.removeOne(
|
||||
draft,
|
||||
imageDTO.image_name
|
||||
);
|
||||
const delta = newState.total - oldTotal;
|
||||
draft.total = draft.total + delta;
|
||||
imagesAdapter.removeOne(draft, imageDTO.image_name);
|
||||
}
|
||||
)
|
||||
)
|
||||
@ -630,9 +714,18 @@ export const imagesApi = api.injectEndpoints({
|
||||
// OR
|
||||
// - The image's `created_at` is within the range of the cached images
|
||||
|
||||
const { data: total } = 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 >= currentCache.data.total;
|
||||
currentCache.data && currentCache.data.ids.length >= (total ?? 0);
|
||||
|
||||
const isInDateRange = getIsImageInDateRange(
|
||||
currentCache.data,
|
||||
@ -647,10 +740,7 @@ export const imagesApi = api.injectEndpoints({
|
||||
'listImages',
|
||||
queryArgs,
|
||||
(draft) => {
|
||||
const oldTotal = draft.total;
|
||||
const newState = imagesAdapter.addOne(draft, imageDTO);
|
||||
const delta = newState.total - oldTotal;
|
||||
draft.total = draft.total + delta;
|
||||
imagesAdapter.addOne(draft, imageDTO);
|
||||
}
|
||||
)
|
||||
)
|
||||
@ -667,20 +757,26 @@ export const imagesApi = api.injectEndpoints({
|
||||
}),
|
||||
removeImageFromBoard: build.mutation<void, { imageDTO: ImageDTO }>({
|
||||
query: ({ imageDTO }) => {
|
||||
const { board_id, image_name } = imageDTO;
|
||||
const { image_name } = imageDTO;
|
||||
return {
|
||||
url: `board_images/`,
|
||||
method: 'DELETE',
|
||||
body: { board_id, image_name },
|
||||
body: { image_name },
|
||||
};
|
||||
},
|
||||
invalidatesTags: (result, error, { imageDTO }) => [
|
||||
{ type: 'Board', id: imageDTO.board_id },
|
||||
{ type: 'BoardImagesTotal', id: imageDTO.board_id },
|
||||
{ type: 'BoardImagesTotal', id: 'none' },
|
||||
{ type: 'BoardAssetsTotal', id: imageDTO.board_id },
|
||||
{ type: 'BoardAssetsTotal', id: 'none' },
|
||||
],
|
||||
invalidatesTags: (result, error, { imageDTO }) => {
|
||||
const { board_id } = imageDTO;
|
||||
return [
|
||||
// invalidate the image's old board
|
||||
{ type: 'Board', id: board_id ?? 'none' },
|
||||
// update old board totals
|
||||
{ type: 'BoardImagesTotal', id: board_id ?? 'none' },
|
||||
{ type: 'BoardAssetsTotal', id: board_id ?? 'none' },
|
||||
// update the no_board totals
|
||||
{ type: 'BoardImagesTotal', id: 'none' },
|
||||
{ type: 'BoardAssetsTotal', id: 'none' },
|
||||
];
|
||||
},
|
||||
async onQueryStarted(
|
||||
{ imageDTO },
|
||||
{ dispatch, queryFulfilled, getState }
|
||||
@ -704,7 +800,7 @@ export const imagesApi = api.injectEndpoints({
|
||||
'getImageDTO',
|
||||
imageDTO.image_name,
|
||||
(draft) => {
|
||||
Object.assign(draft, { board_id: undefined });
|
||||
draft.board_id = undefined;
|
||||
}
|
||||
)
|
||||
)
|
||||
@ -720,13 +816,7 @@ export const imagesApi = api.injectEndpoints({
|
||||
categories,
|
||||
},
|
||||
(draft) => {
|
||||
const oldTotal = draft.total;
|
||||
const newState = imagesAdapter.removeOne(
|
||||
draft,
|
||||
imageDTO.image_name
|
||||
);
|
||||
const delta = newState.total - oldTotal;
|
||||
draft.total = draft.total + delta;
|
||||
imagesAdapter.removeOne(draft, imageDTO.image_name);
|
||||
}
|
||||
)
|
||||
)
|
||||
@ -744,9 +834,18 @@ export const imagesApi = api.injectEndpoints({
|
||||
// OR
|
||||
// - The image's `created_at` is within the range of the cached images
|
||||
|
||||
const { data: total } = 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 >= currentCache.data.total;
|
||||
currentCache.data && currentCache.data.ids.length >= (total ?? 0);
|
||||
|
||||
const isInDateRange = getIsImageInDateRange(
|
||||
currentCache.data,
|
||||
@ -761,10 +860,7 @@ export const imagesApi = api.injectEndpoints({
|
||||
'listImages',
|
||||
queryArgs,
|
||||
(draft) => {
|
||||
const oldTotal = draft.total;
|
||||
const newState = imagesAdapter.upsertOne(draft, imageDTO);
|
||||
const delta = newState.total - oldTotal;
|
||||
draft.total = draft.total + delta;
|
||||
imagesAdapter.upsertOne(draft, imageDTO);
|
||||
}
|
||||
)
|
||||
)
|
||||
@ -778,6 +874,255 @@ export const imagesApi = api.injectEndpoints({
|
||||
}
|
||||
},
|
||||
}),
|
||||
addImagesToBoard: build.mutation<
|
||||
components['schemas']['AddImagesToBoardResult'],
|
||||
{
|
||||
board_id: string;
|
||||
imageDTOs: ImageDTO[];
|
||||
}
|
||||
>({
|
||||
query: ({ board_id, imageDTOs }) => ({
|
||||
url: `board_images/batch`,
|
||||
method: 'POST',
|
||||
body: {
|
||||
image_names: imageDTOs.map((i) => i.image_name),
|
||||
board_id,
|
||||
},
|
||||
}),
|
||||
invalidatesTags: (result, error, { board_id }) => [
|
||||
// update the destination board
|
||||
{ type: 'Board', id: board_id ?? 'none' },
|
||||
// update old board totals
|
||||
{ type: 'BoardImagesTotal', id: board_id ?? 'none' },
|
||||
{ type: 'BoardAssetsTotal', id: board_id ?? 'none' },
|
||||
// update the no_board totals
|
||||
{ type: 'BoardImagesTotal', id: 'none' },
|
||||
{ type: 'BoardAssetsTotal', id: 'none' },
|
||||
],
|
||||
async onQueryStarted(
|
||||
{ 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]
|
||||
*/
|
||||
|
||||
added_image_names.forEach((image_name) => {
|
||||
dispatch(
|
||||
imagesApi.util.updateQueryData(
|
||||
'getImageDTO',
|
||||
image_name,
|
||||
(draft) => {
|
||||
draft.board_id = 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;
|
||||
|
||||
// remove from the old board
|
||||
dispatch(
|
||||
imagesApi.util.updateQueryData(
|
||||
'listImages',
|
||||
{ board_id: old_board_id ?? 'none', categories },
|
||||
(draft) => {
|
||||
imagesAdapter.removeOne(draft, imageDTO.image_name);
|
||||
}
|
||||
)
|
||||
);
|
||||
|
||||
const queryArgs = {
|
||||
board_id,
|
||||
categories,
|
||||
};
|
||||
|
||||
const currentCache = imagesApi.endpoints.listImages.select(
|
||||
queryArgs
|
||||
)(getState());
|
||||
|
||||
const { data: total } = 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 >= (total ?? 0);
|
||||
|
||||
const isInDateRange = getIsImageInDateRange(
|
||||
currentCache.data,
|
||||
imageDTO
|
||||
);
|
||||
|
||||
if (isCacheFullyPopulated || isInDateRange) {
|
||||
// *upsert* to $cache
|
||||
dispatch(
|
||||
imagesApi.util.updateQueryData(
|
||||
'listImages',
|
||||
queryArgs,
|
||||
(draft) => {
|
||||
imagesAdapter.upsertOne(draft, {
|
||||
...imageDTO,
|
||||
board_id,
|
||||
});
|
||||
}
|
||||
)
|
||||
);
|
||||
}
|
||||
});
|
||||
} catch {
|
||||
// no-op
|
||||
}
|
||||
},
|
||||
}),
|
||||
removeImagesFromBoard: build.mutation<
|
||||
components['schemas']['RemoveImagesFromBoardResult'],
|
||||
{
|
||||
imageDTOs: ImageDTO[];
|
||||
}
|
||||
>({
|
||||
query: ({ imageDTOs }) => ({
|
||||
url: `board_images/batch/delete`,
|
||||
method: 'POST',
|
||||
body: {
|
||||
image_names: imageDTOs.map((i) => i.image_name),
|
||||
},
|
||||
}),
|
||||
invalidatesTags: (result, error, { imageDTOs }) => {
|
||||
const touchedBoardIds: string[] = [];
|
||||
const tags: ApiFullTagDescription[] = [
|
||||
{ type: 'BoardImagesTotal', id: 'none' },
|
||||
{ type: 'BoardAssetsTotal', id: 'none' },
|
||||
];
|
||||
|
||||
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 });
|
||||
tags.push({ type: 'BoardImagesTotal', id: board_id });
|
||||
tags.push({ type: 'BoardAssetsTotal', 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]
|
||||
*/
|
||||
|
||||
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);
|
||||
|
||||
// remove from the old board
|
||||
dispatch(
|
||||
imagesApi.util.updateQueryData(
|
||||
'listImages',
|
||||
{ board_id: imageDTO.board_id ?? 'none', categories },
|
||||
(draft) => {
|
||||
imagesAdapter.removeOne(draft, imageDTO.image_name);
|
||||
}
|
||||
)
|
||||
);
|
||||
|
||||
// add to `no_board`
|
||||
const queryArgs = {
|
||||
board_id: 'none',
|
||||
categories,
|
||||
};
|
||||
|
||||
const currentCache = imagesApi.endpoints.listImages.select(
|
||||
queryArgs
|
||||
)(getState());
|
||||
|
||||
const { data: total } = 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 >= (total ?? 0);
|
||||
|
||||
const isInDateRange = getIsImageInDateRange(
|
||||
currentCache.data,
|
||||
imageDTO
|
||||
);
|
||||
|
||||
if (isCacheFullyPopulated || isInDateRange) {
|
||||
// *upsert* to $cache
|
||||
dispatch(
|
||||
imagesApi.util.updateQueryData(
|
||||
'listImages',
|
||||
queryArgs,
|
||||
(draft) => {
|
||||
imagesAdapter.upsertOne(draft, {
|
||||
...imageDTO,
|
||||
board_id: undefined,
|
||||
});
|
||||
}
|
||||
)
|
||||
);
|
||||
}
|
||||
});
|
||||
} catch {
|
||||
// no-op
|
||||
}
|
||||
},
|
||||
}),
|
||||
}),
|
||||
});
|
||||
|
||||
@ -788,10 +1133,15 @@ export const {
|
||||
useGetImageDTOQuery,
|
||||
useGetImageMetadataQuery,
|
||||
useDeleteImageMutation,
|
||||
useGetBoardImagesTotalQuery,
|
||||
useGetBoardAssetsTotalQuery,
|
||||
useDeleteImagesMutation,
|
||||
useUploadImageMutation,
|
||||
useClearIntermediatesMutation,
|
||||
useAddImagesToBoardMutation,
|
||||
useRemoveImagesFromBoardMutation,
|
||||
useAddImageToBoardMutation,
|
||||
useRemoveImageFromBoardMutation,
|
||||
useClearIntermediatesMutation,
|
||||
useChangeImageIsIntermediateMutation,
|
||||
useChangeImageSessionIdMutation,
|
||||
useDeleteBoardAndImagesMutation,
|
||||
useDeleteBoardMutation,
|
||||
} = imagesApi;
|
||||
|
@ -5,7 +5,6 @@ import {
|
||||
BaseModelType,
|
||||
CheckpointModelConfig,
|
||||
ControlNetModelConfig,
|
||||
ConvertModelConfig,
|
||||
DiffusersModelConfig,
|
||||
ImportModelConfig,
|
||||
LoRAModelConfig,
|
||||
@ -83,7 +82,7 @@ type DeleteLoRAModelResponse = void;
|
||||
type ConvertMainModelArg = {
|
||||
base_model: BaseModelType;
|
||||
model_name: string;
|
||||
params: ConvertModelConfig;
|
||||
convert_dest_directory?: string;
|
||||
};
|
||||
|
||||
type ConvertMainModelResponse =
|
||||
@ -122,7 +121,7 @@ type CheckpointConfigsResponse =
|
||||
|
||||
type SearchFolderArg = operations['search_for_models']['parameters']['query'];
|
||||
|
||||
const mainModelsAdapter = createEntityAdapter<MainModelConfigEntity>({
|
||||
export const mainModelsAdapter = createEntityAdapter<MainModelConfigEntity>({
|
||||
sortComparer: (a, b) => a.model_name.localeCompare(b.model_name),
|
||||
});
|
||||
|
||||
@ -132,15 +131,15 @@ const onnxModelsAdapter = createEntityAdapter<OnnxModelConfigEntity>({
|
||||
const loraModelsAdapter = createEntityAdapter<LoRAModelConfigEntity>({
|
||||
sortComparer: (a, b) => a.model_name.localeCompare(b.model_name),
|
||||
});
|
||||
const controlNetModelsAdapter =
|
||||
export const controlNetModelsAdapter =
|
||||
createEntityAdapter<ControlNetModelConfigEntity>({
|
||||
sortComparer: (a, b) => a.model_name.localeCompare(b.model_name),
|
||||
});
|
||||
const textualInversionModelsAdapter =
|
||||
export const textualInversionModelsAdapter =
|
||||
createEntityAdapter<TextualInversionModelConfigEntity>({
|
||||
sortComparer: (a, b) => a.model_name.localeCompare(b.model_name),
|
||||
});
|
||||
const vaeModelsAdapter = createEntityAdapter<VaeModelConfigEntity>({
|
||||
export const vaeModelsAdapter = createEntityAdapter<VaeModelConfigEntity>({
|
||||
sortComparer: (a, b) => a.model_name.localeCompare(b.model_name),
|
||||
});
|
||||
|
||||
@ -320,11 +319,11 @@ export const modelsApi = api.injectEndpoints({
|
||||
ConvertMainModelResponse,
|
||||
ConvertMainModelArg
|
||||
>({
|
||||
query: ({ base_model, model_name, params }) => {
|
||||
query: ({ base_model, model_name, convert_dest_directory }) => {
|
||||
return {
|
||||
url: `models/convert/${base_model}/main/${model_name}`,
|
||||
method: 'PUT',
|
||||
params: params,
|
||||
params: { convert_dest_directory },
|
||||
};
|
||||
},
|
||||
invalidatesTags: [
|
||||
|
@ -1,7 +1,7 @@
|
||||
import { BoardId } from 'features/gallery/store/types';
|
||||
import { useListAllBoardsQuery } from '../endpoints/boards';
|
||||
|
||||
export const useBoardName = (board_id: BoardId | null | undefined) => {
|
||||
export const useBoardName = (board_id: BoardId) => {
|
||||
const { boardName } = useListAllBoardsQuery(undefined, {
|
||||
selectFromResult: ({ data }) => {
|
||||
const selectedBoard = data?.find((b) => b.board_id === board_id);
|
||||
|
@ -4,7 +4,7 @@ import { useMemo } from 'react';
|
||||
import {
|
||||
useGetBoardAssetsTotalQuery,
|
||||
useGetBoardImagesTotalQuery,
|
||||
} from '../endpoints/images';
|
||||
} from '../endpoints/boards';
|
||||
|
||||
export const useBoardTotal = (board_id: BoardId) => {
|
||||
const galleryView = useAppSelector((state) => state.gallery.galleryView);
|
||||
|
302
invokeai/frontend/web/src/services/api/schema.d.ts
vendored
302
invokeai/frontend/web/src/services/api/schema.d.ts
vendored
@ -135,19 +135,14 @@ export type paths = {
|
||||
*/
|
||||
put: operations["merge_models"];
|
||||
};
|
||||
"/api/v1/images/": {
|
||||
/**
|
||||
* List Image Dtos
|
||||
* @description Gets a list of image DTOs
|
||||
*/
|
||||
get: operations["list_image_dtos"];
|
||||
"/api/v1/images/upload": {
|
||||
/**
|
||||
* Upload Image
|
||||
* @description Uploads an image
|
||||
*/
|
||||
post: operations["upload_image"];
|
||||
};
|
||||
"/api/v1/images/{image_name}": {
|
||||
"/api/v1/images/i/{image_name}": {
|
||||
/**
|
||||
* Get Image Dto
|
||||
* @description Gets an image's DTO
|
||||
@ -171,34 +166,45 @@ export type paths = {
|
||||
*/
|
||||
post: operations["clear_intermediates"];
|
||||
};
|
||||
"/api/v1/images/{image_name}/metadata": {
|
||||
"/api/v1/images/i/{image_name}/metadata": {
|
||||
/**
|
||||
* Get Image Metadata
|
||||
* @description Gets an image's metadata
|
||||
*/
|
||||
get: operations["get_image_metadata"];
|
||||
};
|
||||
"/api/v1/images/{image_name}/full": {
|
||||
"/api/v1/images/i/{image_name}/full": {
|
||||
/**
|
||||
* Get Image Full
|
||||
* @description Gets a full-resolution image file
|
||||
*/
|
||||
get: operations["get_image_full"];
|
||||
};
|
||||
"/api/v1/images/{image_name}/thumbnail": {
|
||||
"/api/v1/images/i/{image_name}/thumbnail": {
|
||||
/**
|
||||
* Get Image Thumbnail
|
||||
* @description Gets a thumbnail image file
|
||||
*/
|
||||
get: operations["get_image_thumbnail"];
|
||||
};
|
||||
"/api/v1/images/{image_name}/urls": {
|
||||
"/api/v1/images/i/{image_name}/urls": {
|
||||
/**
|
||||
* Get Image Urls
|
||||
* @description Gets an image and thumbnail URL
|
||||
*/
|
||||
get: operations["get_image_urls"];
|
||||
};
|
||||
"/api/v1/images/": {
|
||||
/**
|
||||
* List Image Dtos
|
||||
* @description Gets a list of image DTOs
|
||||
*/
|
||||
get: operations["list_image_dtos"];
|
||||
};
|
||||
"/api/v1/images/delete": {
|
||||
/** Delete Images From List */
|
||||
post: operations["delete_images_from_list"];
|
||||
};
|
||||
"/api/v1/boards/": {
|
||||
/**
|
||||
* List Boards
|
||||
@ -237,15 +243,29 @@ export type paths = {
|
||||
};
|
||||
"/api/v1/board_images/": {
|
||||
/**
|
||||
* Create Board Image
|
||||
* Add Image To Board
|
||||
* @description Creates a board_image
|
||||
*/
|
||||
post: operations["create_board_image"];
|
||||
post: operations["add_image_to_board"];
|
||||
/**
|
||||
* Remove Board Image
|
||||
* @description Deletes a board_image
|
||||
* Remove Image From Board
|
||||
* @description Removes an image from its board, if it had one
|
||||
*/
|
||||
delete: operations["remove_board_image"];
|
||||
delete: operations["remove_image_from_board"];
|
||||
};
|
||||
"/api/v1/board_images/batch": {
|
||||
/**
|
||||
* Add Images To Board
|
||||
* @description Adds a list of images to a board
|
||||
*/
|
||||
post: operations["add_images_to_board"];
|
||||
};
|
||||
"/api/v1/board_images/batch/delete": {
|
||||
/**
|
||||
* Remove Images From Board
|
||||
* @description Removes a list of images from their board, if they had one
|
||||
*/
|
||||
post: operations["remove_images_from_board"];
|
||||
};
|
||||
"/api/v1/app/version": {
|
||||
/** Get Version */
|
||||
@ -273,6 +293,19 @@ export type webhooks = Record<string, never>;
|
||||
|
||||
export type components = {
|
||||
schemas: {
|
||||
/** AddImagesToBoardResult */
|
||||
AddImagesToBoardResult: {
|
||||
/**
|
||||
* Board Id
|
||||
* @description The id of the board the images were added to
|
||||
*/
|
||||
board_id: string;
|
||||
/**
|
||||
* Added Image Names
|
||||
* @description The image names that were added to the board
|
||||
*/
|
||||
added_image_names: (string)[];
|
||||
};
|
||||
/**
|
||||
* AddInvocation
|
||||
* @description Adds two numbers
|
||||
@ -405,8 +438,8 @@ export type components = {
|
||||
*/
|
||||
image_count: number;
|
||||
};
|
||||
/** Body_create_board_image */
|
||||
Body_create_board_image: {
|
||||
/** Body_add_image_to_board */
|
||||
Body_add_image_to_board: {
|
||||
/**
|
||||
* Board Id
|
||||
* @description The id of the board to add to
|
||||
@ -418,6 +451,27 @@ export type components = {
|
||||
*/
|
||||
image_name: string;
|
||||
};
|
||||
/** Body_add_images_to_board */
|
||||
Body_add_images_to_board: {
|
||||
/**
|
||||
* Board Id
|
||||
* @description The id of the board to add to
|
||||
*/
|
||||
board_id: string;
|
||||
/**
|
||||
* Image Names
|
||||
* @description The names of the images to add
|
||||
*/
|
||||
image_names: (string)[];
|
||||
};
|
||||
/** Body_delete_images_from_list */
|
||||
Body_delete_images_from_list: {
|
||||
/**
|
||||
* Image Names
|
||||
* @description The list of names of images to delete
|
||||
*/
|
||||
image_names: (string)[];
|
||||
};
|
||||
/** Body_import_model */
|
||||
Body_import_model: {
|
||||
/**
|
||||
@ -465,19 +519,22 @@ export type components = {
|
||||
*/
|
||||
merge_dest_directory?: string;
|
||||
};
|
||||
/** Body_remove_board_image */
|
||||
Body_remove_board_image: {
|
||||
/**
|
||||
* Board Id
|
||||
* @description The id of the board
|
||||
*/
|
||||
board_id: string;
|
||||
/** Body_remove_image_from_board */
|
||||
Body_remove_image_from_board: {
|
||||
/**
|
||||
* Image Name
|
||||
* @description The name of the image to remove
|
||||
*/
|
||||
image_name: string;
|
||||
};
|
||||
/** Body_remove_images_from_board */
|
||||
Body_remove_images_from_board: {
|
||||
/**
|
||||
* Image Names
|
||||
* @description The names of the images to remove
|
||||
*/
|
||||
image_names: (string)[];
|
||||
};
|
||||
/** Body_upload_image */
|
||||
Body_upload_image: {
|
||||
/**
|
||||
@ -1157,6 +1214,11 @@ export type components = {
|
||||
*/
|
||||
deleted_images: (string)[];
|
||||
};
|
||||
/** DeleteImagesFromListResult */
|
||||
DeleteImagesFromListResult: {
|
||||
/** Deleted Images */
|
||||
deleted_images: (string)[];
|
||||
};
|
||||
/**
|
||||
* DivideInvocation
|
||||
* @description Divides two numbers
|
||||
@ -4627,6 +4689,14 @@ export type components = {
|
||||
*/
|
||||
step?: number;
|
||||
};
|
||||
/** RemoveImagesFromBoardResult */
|
||||
RemoveImagesFromBoardResult: {
|
||||
/**
|
||||
* Removed Image Names
|
||||
* @description The image names that were removed from their board
|
||||
*/
|
||||
removed_image_names: (string)[];
|
||||
};
|
||||
/**
|
||||
* ResizeLatentsInvocation
|
||||
* @description Resizes latents to explicit width/height (in pixels). Provided dimensions are floor-divided by 8.
|
||||
@ -5891,18 +5961,6 @@ export type components = {
|
||||
*/
|
||||
image?: components["schemas"]["ImageField"];
|
||||
};
|
||||
/**
|
||||
* ControlNetModelFormat
|
||||
* @description An enumeration.
|
||||
* @enum {string}
|
||||
*/
|
||||
ControlNetModelFormat: "checkpoint" | "diffusers";
|
||||
/**
|
||||
* StableDiffusionXLModelFormat
|
||||
* @description An enumeration.
|
||||
* @enum {string}
|
||||
*/
|
||||
StableDiffusionXLModelFormat: "checkpoint" | "diffusers";
|
||||
/**
|
||||
* StableDiffusionOnnxModelFormat
|
||||
* @description An enumeration.
|
||||
@ -5921,6 +5979,18 @@ export type components = {
|
||||
* @enum {string}
|
||||
*/
|
||||
StableDiffusion1ModelFormat: "checkpoint" | "diffusers";
|
||||
/**
|
||||
* StableDiffusionXLModelFormat
|
||||
* @description An enumeration.
|
||||
* @enum {string}
|
||||
*/
|
||||
StableDiffusionXLModelFormat: "checkpoint" | "diffusers";
|
||||
/**
|
||||
* ControlNetModelFormat
|
||||
* @description An enumeration.
|
||||
* @enum {string}
|
||||
*/
|
||||
ControlNetModelFormat: "checkpoint" | "diffusers";
|
||||
};
|
||||
responses: never;
|
||||
parameters: never;
|
||||
@ -6547,42 +6617,6 @@ export type operations = {
|
||||
};
|
||||
};
|
||||
};
|
||||
/**
|
||||
* List Image Dtos
|
||||
* @description Gets a list of image DTOs
|
||||
*/
|
||||
list_image_dtos: {
|
||||
parameters: {
|
||||
query?: {
|
||||
/** @description The origin of images to list. */
|
||||
image_origin?: components["schemas"]["ResourceOrigin"];
|
||||
/** @description The categories of image to include. */
|
||||
categories?: (components["schemas"]["ImageCategory"])[];
|
||||
/** @description Whether to list intermediate images. */
|
||||
is_intermediate?: boolean;
|
||||
/** @description The board id to filter by. Use 'none' to find images without a board. */
|
||||
board_id?: string;
|
||||
/** @description The page offset */
|
||||
offset?: number;
|
||||
/** @description The number of images per page */
|
||||
limit?: number;
|
||||
};
|
||||
};
|
||||
responses: {
|
||||
/** @description Successful Response */
|
||||
200: {
|
||||
content: {
|
||||
"application/json": components["schemas"]["OffsetPaginatedResults_ImageDTO_"];
|
||||
};
|
||||
};
|
||||
/** @description Validation Error */
|
||||
422: {
|
||||
content: {
|
||||
"application/json": components["schemas"]["HTTPValidationError"];
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
/**
|
||||
* Upload Image
|
||||
* @description Uploads an image
|
||||
@ -6829,6 +6863,64 @@ export type operations = {
|
||||
};
|
||||
};
|
||||
};
|
||||
/**
|
||||
* List Image Dtos
|
||||
* @description Gets a list of image DTOs
|
||||
*/
|
||||
list_image_dtos: {
|
||||
parameters: {
|
||||
query?: {
|
||||
/** @description The origin of images to list. */
|
||||
image_origin?: components["schemas"]["ResourceOrigin"];
|
||||
/** @description The categories of image to include. */
|
||||
categories?: (components["schemas"]["ImageCategory"])[];
|
||||
/** @description Whether to list intermediate images. */
|
||||
is_intermediate?: boolean;
|
||||
/** @description The board id to filter by. Use 'none' to find images without a board. */
|
||||
board_id?: string;
|
||||
/** @description The page offset */
|
||||
offset?: number;
|
||||
/** @description The number of images per page */
|
||||
limit?: number;
|
||||
};
|
||||
};
|
||||
responses: {
|
||||
/** @description Successful Response */
|
||||
200: {
|
||||
content: {
|
||||
"application/json": components["schemas"]["OffsetPaginatedResults_ImageDTO_"];
|
||||
};
|
||||
};
|
||||
/** @description Validation Error */
|
||||
422: {
|
||||
content: {
|
||||
"application/json": components["schemas"]["HTTPValidationError"];
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
/** Delete Images From List */
|
||||
delete_images_from_list: {
|
||||
requestBody: {
|
||||
content: {
|
||||
"application/json": components["schemas"]["Body_delete_images_from_list"];
|
||||
};
|
||||
};
|
||||
responses: {
|
||||
/** @description Successful Response */
|
||||
200: {
|
||||
content: {
|
||||
"application/json": components["schemas"]["DeleteImagesFromListResult"];
|
||||
};
|
||||
};
|
||||
/** @description Validation Error */
|
||||
422: {
|
||||
content: {
|
||||
"application/json": components["schemas"]["HTTPValidationError"];
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
/**
|
||||
* List Boards
|
||||
* @description Gets a list of boards
|
||||
@ -6999,13 +7091,13 @@ export type operations = {
|
||||
};
|
||||
};
|
||||
/**
|
||||
* Create Board Image
|
||||
* Add Image To Board
|
||||
* @description Creates a board_image
|
||||
*/
|
||||
create_board_image: {
|
||||
add_image_to_board: {
|
||||
requestBody: {
|
||||
content: {
|
||||
"application/json": components["schemas"]["Body_create_board_image"];
|
||||
"application/json": components["schemas"]["Body_add_image_to_board"];
|
||||
};
|
||||
};
|
||||
responses: {
|
||||
@ -7024,13 +7116,13 @@ export type operations = {
|
||||
};
|
||||
};
|
||||
/**
|
||||
* Remove Board Image
|
||||
* @description Deletes a board_image
|
||||
* Remove Image From Board
|
||||
* @description Removes an image from its board, if it had one
|
||||
*/
|
||||
remove_board_image: {
|
||||
remove_image_from_board: {
|
||||
requestBody: {
|
||||
content: {
|
||||
"application/json": components["schemas"]["Body_remove_board_image"];
|
||||
"application/json": components["schemas"]["Body_remove_image_from_board"];
|
||||
};
|
||||
};
|
||||
responses: {
|
||||
@ -7048,6 +7140,56 @@ export type operations = {
|
||||
};
|
||||
};
|
||||
};
|
||||
/**
|
||||
* Add Images To Board
|
||||
* @description Adds a list of images to a board
|
||||
*/
|
||||
add_images_to_board: {
|
||||
requestBody: {
|
||||
content: {
|
||||
"application/json": components["schemas"]["Body_add_images_to_board"];
|
||||
};
|
||||
};
|
||||
responses: {
|
||||
/** @description Images were added to board successfully */
|
||||
201: {
|
||||
content: {
|
||||
"application/json": components["schemas"]["AddImagesToBoardResult"];
|
||||
};
|
||||
};
|
||||
/** @description Validation Error */
|
||||
422: {
|
||||
content: {
|
||||
"application/json": components["schemas"]["HTTPValidationError"];
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
/**
|
||||
* Remove Images From Board
|
||||
* @description Removes a list of images from their board, if they had one
|
||||
*/
|
||||
remove_images_from_board: {
|
||||
requestBody: {
|
||||
content: {
|
||||
"application/json": components["schemas"]["Body_remove_images_from_board"];
|
||||
};
|
||||
};
|
||||
responses: {
|
||||
/** @description Images were removed from board successfully */
|
||||
201: {
|
||||
content: {
|
||||
"application/json": components["schemas"]["RemoveImagesFromBoardResult"];
|
||||
};
|
||||
};
|
||||
/** @description Validation Error */
|
||||
422: {
|
||||
content: {
|
||||
"application/json": components["schemas"]["HTTPValidationError"];
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
/** Get Version */
|
||||
app_version: {
|
||||
responses: {
|
||||
|
@ -1,13 +1,40 @@
|
||||
import { UseToastOptions } from '@chakra-ui/react';
|
||||
import { EntityState } from '@reduxjs/toolkit';
|
||||
import { O } from 'ts-toolbelt';
|
||||
import { components } from './schema';
|
||||
import { components, paths } from './schema';
|
||||
|
||||
type schemas = components['schemas'];
|
||||
export type ImageCache = EntityState<ImageDTO>;
|
||||
|
||||
export type ListImagesArgs = NonNullable<
|
||||
paths['/api/v1/images/']['get']['parameters']['query']
|
||||
>;
|
||||
|
||||
export type DeleteBoardResult =
|
||||
paths['/api/v1/boards/{board_id}']['delete']['responses']['200']['content']['application/json'];
|
||||
|
||||
export type ListBoardsArg = NonNullable<
|
||||
paths['/api/v1/boards/']['get']['parameters']['query']
|
||||
>;
|
||||
|
||||
export type UpdateBoardArg =
|
||||
paths['/api/v1/boards/{board_id}']['patch']['parameters']['path'] & {
|
||||
changes: paths['/api/v1/boards/{board_id}']['patch']['requestBody']['content']['application/json'];
|
||||
};
|
||||
|
||||
/**
|
||||
* This is an unsafe type; the object inside is not guaranteed to be valid.
|
||||
*/
|
||||
export type UnsafeImageMetadata = {
|
||||
metadata: components['schemas']['CoreMetadata'];
|
||||
graph: NonNullable<components['schemas']['Graph']>;
|
||||
};
|
||||
|
||||
/**
|
||||
* Marks the `type` property as required. Use for nodes.
|
||||
*/
|
||||
type TypeReq<T> = O.Required<T, 'type'>;
|
||||
type TypeReq<T extends object> = O.Required<T, 'type'>;
|
||||
|
||||
// Extracted types from API schema
|
||||
|
||||
// App Info
|
||||
export type AppVersion = components['schemas']['AppVersion'];
|
||||
@ -72,7 +99,6 @@ export type AnyModelConfig =
|
||||
| OnnxModelConfig;
|
||||
|
||||
export type MergeModelConfig = components['schemas']['Body_merge_models'];
|
||||
export type ConvertModelConfig = components['schemas']['Body_convert_model'];
|
||||
export type ImportModelConfig = components['schemas']['Body_import_model'];
|
||||
|
||||
// Graphs
|
56
invokeai/frontend/web/src/services/api/util.ts
Normal file
56
invokeai/frontend/web/src/services/api/util.ts
Normal file
@ -0,0 +1,56 @@
|
||||
import {
|
||||
ASSETS_CATEGORIES,
|
||||
IMAGE_CATEGORIES,
|
||||
} from 'features/gallery/store/types';
|
||||
import { ImageCache, ImageDTO, ListImagesArgs } from './types';
|
||||
import { createEntityAdapter } from '@reduxjs/toolkit';
|
||||
import { dateComparator } from 'common/util/dateComparator';
|
||||
import queryString from 'query-string';
|
||||
|
||||
export const getIsImageInDateRange = (
|
||||
data: ImageCache | undefined,
|
||||
imageDTO: ImageDTO
|
||||
) => {
|
||||
if (!data) {
|
||||
return false;
|
||||
}
|
||||
const cacheImageDTOS = imagesSelectors.selectAll(data);
|
||||
|
||||
if (cacheImageDTOS.length > 1) {
|
||||
// Images are sorted by `created_at` DESC
|
||||
// check if the image is newer than the oldest image in the cache
|
||||
const createdDate = new Date(imageDTO.created_at);
|
||||
const oldestImage = cacheImageDTOS[cacheImageDTOS.length - 1];
|
||||
if (!oldestImage) {
|
||||
// satisfy TS gods, we already confirmed the array has more than one image
|
||||
return false;
|
||||
}
|
||||
const oldestDate = new Date(oldestImage.created_at);
|
||||
return createdDate >= oldestDate;
|
||||
} else if ([0, 1].includes(cacheImageDTOS.length)) {
|
||||
// if there are only 1 or 0 images in the cache, we consider the image to be in the date range
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
export const getCategories = (imageDTO: ImageDTO) => {
|
||||
if (IMAGE_CATEGORIES.includes(imageDTO.image_category)) {
|
||||
return IMAGE_CATEGORIES;
|
||||
}
|
||||
return ASSETS_CATEGORIES;
|
||||
};
|
||||
|
||||
// The adapter is not actually the data store - it just provides helper functions to interact
|
||||
// with some other store of data. We will use the RTK Query cache as that store.
|
||||
export const imagesAdapter = createEntityAdapter<ImageDTO>({
|
||||
selectId: (image) => image.image_name,
|
||||
sortComparer: (a, b) => dateComparator(b.updated_at, a.updated_at),
|
||||
});
|
||||
|
||||
// Create selectors for the adapter.
|
||||
export const imagesSelectors = imagesAdapter.getSelectors();
|
||||
|
||||
// Helper to create the url for the listImages endpoint. Also we use it to create the cache key.
|
||||
export const getListImagesUrl = (queryArgs: ListImagesArgs) =>
|
||||
`images/?${queryString.stringify(queryArgs, { arrayFormat: 'none' })}`;
|
Reference in New Issue
Block a user