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:
psychedelicious
2023-07-31 18:16:52 +10:00
parent e080fd1e08
commit bf94412d14
116 changed files with 2470 additions and 2181 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View 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' })}`;