wip attempt to rewrite to use no adapter

This commit is contained in:
psychedelicious 2023-07-11 16:13:22 +10:00
parent 2990fa23fe
commit 704cfd8ff5
31 changed files with 870 additions and 562 deletions

View File

@ -1,11 +1,9 @@
from fastapi import Body, HTTPException, Path, Query
from fastapi import Body, HTTPException, Path
from fastapi.routing import APIRouter
from invokeai.app.models.image import (AddManyImagesToBoardResult,
GetAllBoardImagesForBoardResult,
RemoveManyImagesFromBoardResult)
from invokeai.app.services.image_record_storage import OffsetPaginatedResults
from invokeai.app.services.models.image_record import ImageDTO
from ..dependencies import ApiDependencies
@ -13,7 +11,7 @@ board_images_router = APIRouter(prefix="/v1/board_images", tags=["boards"])
@board_images_router.post(
"/",
"/{board_id}",
operation_id="create_board_image",
responses={
201: {"description": "The image was added to a board successfully"},
@ -21,7 +19,7 @@ board_images_router = APIRouter(prefix="/v1/board_images", tags=["boards"])
status_code=201,
)
async def create_board_image(
board_id: str = Body(description="The id of the board to add to"),
board_id: str = Path(description="The id of the board to add to"),
image_name: str = Body(description="The name of the image to add"),
):
"""Creates a board_image"""

View File

@ -1,11 +1,13 @@
from typing import Optional, Union
from fastapi import Body, HTTPException, Path, Query
from fastapi.routing import APIRouter
from invokeai.app.models.image import DeleteManyImagesResult
from invokeai.app.services.board_record_storage import BoardChanges
from invokeai.app.services.image_record_storage import OffsetPaginatedResults
from invokeai.app.services.models.board_record import BoardDTO
from ..dependencies import ApiDependencies
boards_router = APIRouter(prefix="/v1/boards", tags=["boards"])
@ -69,25 +71,26 @@ async def update_board(
raise HTTPException(status_code=500, detail="Failed to update board")
@boards_router.delete("/{board_id}", operation_id="delete_board")
@boards_router.delete("/{board_id}", operation_id="delete_board", response_model=DeleteManyImagesResult)
async def delete_board(
board_id: str = Path(description="The id of board to delete"),
include_images: Optional[bool] = Query(
description="Permanently delete all images on the board", default=False
),
) -> None:
) -> DeleteManyImagesResult:
"""Deletes a board"""
try:
if include_images is True:
ApiDependencies.invoker.services.images.delete_images_on_board(
result = ApiDependencies.invoker.services.images.delete_images_on_board(
board_id=board_id
)
ApiDependencies.invoker.services.boards.delete(board_id=board_id)
else:
ApiDependencies.invoker.services.boards.delete(board_id=board_id)
result = DeleteManyImagesResult(deleted_images=[])
return result
except Exception as e:
# TODO: Does this need any exception handling at all?
pass
raise HTTPException(status_code=500, detail="Failed to delete images on board")
@boards_router.get(

View File

@ -113,7 +113,7 @@ class ImageServiceABC(ABC):
pass
@abstractmethod
def delete_images_on_board(self, board_id: str):
def delete_images_on_board(self, board_id: str) -> DeleteManyImagesResult:
"""Deletes all images on a board."""
pass
@ -386,7 +386,7 @@ class ImageService(ImageServiceABC):
deleted_images.append(image_name)
return DeleteManyImagesResult(deleted_images=deleted_images)
def delete_images_on_board(self, board_id: str):
def delete_images_on_board(self, board_id: str) -> DeleteManyImagesResult:
try:
board_images = (
self._services.board_image_records.get_all_board_images_for_board(
@ -397,6 +397,7 @@ class ImageService(ImageServiceABC):
for image_name in image_name_list:
self._services.image_files.delete(image_name)
self._services.image_records.delete_many(image_name_list)
return DeleteManyImagesResult(deleted_images=board_images.image_names)
except ImageRecordDeleteException:
self._services.logger.error(f"Failed to delete image records")
raise

View File

@ -128,13 +128,13 @@
"@types/react-redux": "^7.1.25",
"@types/react-transition-group": "^4.4.6",
"@types/uuid": "^9.0.2",
"@typescript-eslint/eslint-plugin": "^5.60.0",
"@typescript-eslint/parser": "^5.60.0",
"@typescript-eslint/eslint-plugin": "^6.0.0",
"@typescript-eslint/parser": "^6.0.0",
"@vitejs/plugin-react-swc": "^3.3.2",
"axios": "^1.4.0",
"babel-plugin-transform-imports": "^2.0.0",
"concurrently": "^8.2.0",
"eslint": "^8.43.0",
"eslint": "^8.44.0",
"eslint-config-prettier": "^8.8.0",
"eslint-plugin-prettier": "^4.2.1",
"eslint-plugin-react": "^7.32.2",
@ -151,6 +151,8 @@
"rollup-plugin-visualizer": "^5.9.2",
"terser": "^5.18.1",
"ts-toolbelt": "^9.6.0",
"typescript": "^5.1.6",
"typescript-eslint": "^0.0.1-alpha.0",
"vite": "^4.3.9",
"vite-plugin-css-injected-by-js": "^3.1.1",
"vite-plugin-dts": "^2.3.0",

View File

@ -39,10 +39,7 @@ import {
addImageUploadedFulfilledListener,
addImageUploadedRejectedListener,
} from './listeners/imageUploaded';
import {
addImageUrlsReceivedFulfilledListener,
addImageUrlsReceivedRejectedListener,
} from './listeners/imageUrlsReceived';
import { addImagesLoadedListener } from './listeners/imagesLoaded';
import { addInitialImageSelectedListener } from './listeners/initialImageSelected';
import { addModelSelectedListener } from './listeners/modelSelected';
import { addReceivedOpenAPISchemaListener } from './listeners/receivedOpenAPISchema';
@ -125,10 +122,6 @@ addImageToDeleteSelectedListener();
addImageDTOReceivedFulfilledListener();
addImageDTOReceivedRejectedListener();
// Image URLs
addImageUrlsReceivedFulfilledListener();
addImageUrlsReceivedRejectedListener();
// User Invoked
addUserInvokedCanvasListener();
addUserInvokedNodesListener();
@ -184,6 +177,7 @@ addSessionCanceledRejectedListener();
// Fetching images
addReceivedPageOfImagesListener();
addImagesLoadedListener();
// ControlNet
addControlNetImageProcessedListener();

View File

@ -1,8 +1,4 @@
import { log } from 'app/logging/useLogger';
import {
imageUpdatedMany,
imageUpdatedOne,
} from 'features/gallery/store/gallerySlice';
import { boardImagesApi } from 'services/api/endpoints/boardImages';
import { startAppListening } from '..';
@ -19,13 +15,6 @@ export const addBoardApiListeners = () => {
{ data: { board_id, image_name } },
'Image added to board'
);
dispatch(
imageUpdatedOne({
id: image_name,
changes: { board_id },
})
);
},
});
@ -49,13 +38,6 @@ export const addBoardApiListeners = () => {
const { image_name } = action.meta.arg.originalArgs;
moduleLog.debug({ data: { image_name } }, 'Image removed from board');
dispatch(
imageUpdatedOne({
id: image_name,
changes: { board_id: undefined },
})
);
},
});
@ -82,13 +64,6 @@ export const addBoardApiListeners = () => {
{ data: { board_id, image_names } },
'Images added to board'
);
const updates = image_names.map((image_name) => ({
id: image_name,
changes: { board_id },
}));
dispatch(imageUpdatedMany(updates));
},
});
@ -112,13 +87,6 @@ export const addBoardApiListeners = () => {
const { image_names } = action.meta.arg.originalArgs;
moduleLog.debug({ data: { image_names } }, 'Images removed from board');
const updates = image_names.map((image_name) => ({
id: image_name,
changes: { board_id: undefined },
}));
dispatch(imageUpdatedMany(updates));
},
});

View File

@ -1,9 +1,4 @@
import { createAction } from '@reduxjs/toolkit';
import {
INITIAL_IMAGE_LIMIT,
isLoadingChanged,
} from 'features/gallery/store/gallerySlice';
import { receivedPageOfImages } from 'services/api/thunks/image';
import { startAppListening } from '..';
export const appStarted = createAction('app/appStarted');
@ -15,29 +10,27 @@ export const addAppStartedListener = () => {
action,
{ getState, dispatch, unsubscribe, cancelActiveListeners }
) => {
cancelActiveListeners();
unsubscribe();
// fill up the gallery tab with images
await dispatch(
receivedPageOfImages({
categories: ['general'],
is_intermediate: false,
offset: 0,
limit: INITIAL_IMAGE_LIMIT,
})
);
// fill up the assets tab with images
await dispatch(
receivedPageOfImages({
categories: ['control', 'mask', 'user', 'other'],
is_intermediate: false,
offset: 0,
limit: INITIAL_IMAGE_LIMIT,
})
);
dispatch(isLoadingChanged(false));
// cancelActiveListeners();
// unsubscribe();
// // fill up the gallery tab with images
// await dispatch(
// receivedPageOfImages({
// categories: ['general'],
// is_intermediate: false,
// offset: 0,
// // limit: INITIAL_IMAGE_LIMIT,
// })
// );
// // fill up the assets tab with images
// await dispatch(
// receivedPageOfImages({
// categories: ['control', 'mask', 'user', 'other'],
// is_intermediate: false,
// offset: 0,
// // limit: INITIAL_IMAGE_LIMIT,
// })
// );
// dispatch(isLoadingChanged(false));
},
});
};

View File

@ -1,15 +1,6 @@
import { log } from 'app/logging/useLogger';
import { boardIdSelected } from 'features/gallery/store/gallerySlice';
import { startAppListening } from '..';
import {
imageSelected,
selectImagesAll,
boardIdSelected,
} from 'features/gallery/store/gallerySlice';
import {
IMAGES_PER_PAGE,
receivedPageOfImages,
} from 'services/api/thunks/image';
import { boardsApi } from 'services/api/endpoints/boards';
const moduleLog = log.child({ namespace: 'boards' });
@ -17,49 +8,40 @@ export const addBoardIdSelectedListener = () => {
startAppListening({
actionCreator: boardIdSelected,
effect: (action, { getState, dispatch }) => {
const board_id = action.payload;
// we need to check if we need to fetch more images
const state = getState();
const allImages = selectImagesAll(state);
if (!board_id) {
// a board was unselected
dispatch(imageSelected(allImages[0]?.image_name));
return;
}
const { categories } = state.gallery;
const filteredImages = allImages.filter((i) => {
const isInCategory = categories.includes(i.image_category);
const isInSelectedBoard = board_id ? i.board_id === board_id : true;
return isInCategory && isInSelectedBoard;
});
// get the board from the cache
const { data: boards } =
boardsApi.endpoints.listAllBoards.select()(state);
const board = boards?.find((b) => b.board_id === board_id);
if (!board) {
// can't find the board in cache...
dispatch(imageSelected(allImages[0]?.image_name));
return;
}
dispatch(imageSelected(board.cover_image_name ?? null));
// if we haven't loaded one full page of images from this board, load more
if (
filteredImages.length < board.image_count &&
filteredImages.length < IMAGES_PER_PAGE
) {
dispatch(
receivedPageOfImages({ categories, board_id, is_intermediate: false })
);
}
// const board_id = action.payload;
// // we need to check if we need to fetch more images
// const state = getState();
// const allImages = selectImagesAll(state);
// if (!board_id) {
// // a board was unselected
// dispatch(imageSelected(allImages[0]?.image_name));
// return;
// }
// const { categories } = state.gallery;
// const filteredImages = allImages.filter((i) => {
// const isInCategory = categories.includes(i.image_category);
// const isInSelectedBoard = board_id ? i.board_id === board_id : true;
// return isInCategory && isInSelectedBoard;
// });
// // get the board from the cache
// const { data: boards } =
// boardsApi.endpoints.listAllBoards.select()(state);
// const board = boards?.find((b) => b.board_id === board_id);
// if (!board) {
// // can't find the board in cache...
// dispatch(imageSelected(allImages[0]?.image_name));
// return;
// }
// dispatch(imageSelected(board.cover_image_name ?? null));
// // if we haven't loaded one full page of images from this board, load more
// if (
// filteredImages.length < board.image_count &&
// filteredImages.length < IMAGES_PER_PAGE
// ) {
// dispatch(
// receivedPageOfImages({ categories, board_id, is_intermediate: false })
// );
// }
},
});
};
@ -68,43 +50,36 @@ export const addBoardIdSelected_changeSelectedImage_listener = () => {
startAppListening({
actionCreator: boardIdSelected,
effect: (action, { getState, dispatch }) => {
const board_id = action.payload;
const state = getState();
// we need to check if we need to fetch more images
if (!board_id) {
// a board was unselected - we don't need to do anything
return;
}
const { categories } = state.gallery;
const filteredImages = selectImagesAll(state).filter((i) => {
const isInCategory = categories.includes(i.image_category);
const isInSelectedBoard = board_id ? i.board_id === board_id : true;
return isInCategory && isInSelectedBoard;
});
// get the board from the cache
const { data: boards } =
boardsApi.endpoints.listAllBoards.select()(state);
const board = boards?.find((b) => b.board_id === board_id);
if (!board) {
// can't find the board in cache...
return;
}
// if we haven't loaded one full page of images from this board, load more
if (
filteredImages.length < board.image_count &&
filteredImages.length < IMAGES_PER_PAGE
) {
dispatch(
receivedPageOfImages({ categories, board_id, is_intermediate: false })
);
}
// const board_id = action.payload;
// const state = getState();
// // we need to check if we need to fetch more images
// if (!board_id) {
// // a board was unselected - we don't need to do anything
// return;
// }
// const { categories } = state.gallery;
// const filteredImages = selectImagesAll(state).filter((i) => {
// const isInCategory = categories.includes(i.image_category);
// const isInSelectedBoard = board_id ? i.board_id === board_id : true;
// return isInCategory && isInSelectedBoard;
// });
// // get the board from the cache
// const { data: boards } =
// boardsApi.endpoints.listAllBoards.select()(state);
// const board = boards?.find((b) => b.board_id === board_id);
// if (!board) {
// // can't find the board in cache...
// return;
// }
// // if we haven't loaded one full page of images from this board, load more
// if (
// filteredImages.length < board.image_count &&
// filteredImages.length < IMAGES_PER_PAGE
// ) {
// dispatch(
// receivedPageOfImages({ categories, board_id, is_intermediate: false })
// );
// }
},
});
};

View File

@ -1,10 +1,8 @@
import { resetCanvas } from 'features/canvas/store/canvasSlice';
import { controlNetReset } from 'features/controlNet/store/controlNetSlice';
import { requestedBoardImagesDeletion } from 'features/gallery/store/actions';
import { requestedBoardImagesDeletion as requestedBoardAndImagesDeletion } from 'features/gallery/store/actions';
import {
imageSelected,
imagesRemoved,
selectImagesAll,
selectImagesById,
} from 'features/gallery/store/gallerySlice';
import { nodeEditorReset } from 'features/nodes/store/nodesSlice';
@ -15,7 +13,7 @@ import { boardsApi } from '../../../../../services/api/endpoints/boards';
export const addRequestedBoardImageDeletionListener = () => {
startAppListening({
actionCreator: requestedBoardImagesDeletion,
actionCreator: requestedBoardAndImagesDeletion,
effect: async (action, { dispatch, getState, condition }) => {
const { board, imagesUsage } = action.payload;
@ -51,20 +49,12 @@ export const addRequestedBoardImageDeletionListener = () => {
dispatch(nodeEditorReset());
}
// Preemptively remove from gallery
const images = selectImagesAll(state).reduce((acc: string[], img) => {
if (img.board_id === board_id) {
acc.push(img.image_name);
}
return acc;
}, []);
dispatch(imagesRemoved(images));
// Delete from server
dispatch(boardsApi.endpoints.deleteBoardAndImages.initiate(board_id));
const result =
boardsApi.endpoints.deleteBoardAndImages.select(board_id)(state);
const { isSuccess } = result;
const { isSuccess, data } = result;
// Wait for successful deletion, then trigger boards to re-fetch
const wasBoardDeleted = await condition(() => !!isSuccess, 30000);

View File

@ -1,10 +1,10 @@
import { canvasSavedToGallery } from 'features/canvas/store/actions';
import { startAppListening } from '..';
import { log } from 'app/logging/useLogger';
import { imageUploaded } from 'services/api/thunks/image';
import { canvasSavedToGallery } from 'features/canvas/store/actions';
import { getBaseLayerBlob } from 'features/canvas/util/getBaseLayerBlob';
import { addToast } from 'features/system/store/systemSlice';
import { imageUpserted } from 'features/gallery/store/gallerySlice';
import { imagesApi } from 'services/api/endpoints/images';
import { imageUploaded } from 'services/api/thunks/image';
import { startAppListening } from '..';
const moduleLog = log.child({ namespace: 'canvasSavedToGalleryListener' });
@ -49,7 +49,11 @@ export const addCanvasSavedToGalleryListener = () => {
uploadedImageAction.meta.requestId === imageUploadedRequest.requestId
);
dispatch(imageUpserted(uploadedImageDTO));
imagesApi.util.upsertQueryData(
'getImageDTO',
uploadedImageDTO.image_name,
uploadedImageDTO
);
},
});
};

View File

@ -1,5 +1,5 @@
import { log } from 'app/logging/useLogger';
import { imageUpserted } from 'features/gallery/store/gallerySlice';
import { imagesApi } from 'services/api/endpoints/images';
import { imageDTOReceived, imageUpdated } from 'services/api/thunks/image';
import { startAppListening } from '..';
@ -33,7 +33,7 @@ export const addImageDTOReceivedFulfilledListener = () => {
}
moduleLog.debug({ data: { image } }, 'Image metadata received');
dispatch(imageUpserted(image));
imagesApi.util.upsertQueryData('getImageDTO', image.image_name, image);
},
});
};

View File

@ -2,10 +2,7 @@ import { log } from 'app/logging/useLogger';
import { resetCanvas } from 'features/canvas/store/canvasSlice';
import { controlNetReset } from 'features/controlNet/store/controlNetSlice';
import { selectFilteredImages } from 'features/gallery/store/gallerySelectors';
import {
imageRemoved,
imageSelected,
} from 'features/gallery/store/gallerySlice';
import { imageSelected } from 'features/gallery/store/gallerySlice';
import {
imageDeletionConfirmed,
isModalOpenChanged,
@ -80,9 +77,6 @@ export const addRequestedImageDeletionListener = () => {
dispatch(nodeEditorReset());
}
// Preemptively remove from gallery
dispatch(imageRemoved(image_name));
// Delete from server
const { requestId } = dispatch(imageDeleted({ image_name }));
@ -91,7 +85,7 @@ export const addRequestedImageDeletionListener = () => {
(action): action is ReturnType<typeof imageDeleted.fulfilled> =>
imageDeleted.fulfilled.match(action) &&
action.meta.requestId === requestId,
30000
30_000
);
if (wasImageDeleted) {

View File

@ -2,10 +2,10 @@ import { log } from 'app/logging/useLogger';
import { imageAddedToBatch } from 'features/batch/store/batchSlice';
import { setInitialCanvasImage } from 'features/canvas/store/canvasSlice';
import { controlNetImageChanged } from 'features/controlNet/store/controlNetSlice';
import { imageUpserted } from 'features/gallery/store/gallerySlice';
import { fieldValueChanged } from 'features/nodes/store/nodesSlice';
import { initialImageChanged } from 'features/parameters/store/generationSlice';
import { addToast } from 'features/system/store/systemSlice';
import { imagesApi } from 'services/api/endpoints/images';
import { imageUploaded } from 'services/api/thunks/image';
import { startAppListening } from '..';
@ -24,7 +24,8 @@ export const addImageUploadedFulfilledListener = () => {
return;
}
dispatch(imageUpserted(image));
// update RTK query cache
imagesApi.util.upsertQueryData('getImageDTO', image.image_name, image);
const { postUploadAction } = action.meta.arg;
@ -73,7 +74,7 @@ export const addImageUploadedFulfilledListener = () => {
}
if (postUploadAction?.type === 'ADD_TO_BATCH') {
dispatch(imageAddedToBatch(image));
dispatch(imageAddedToBatch(image.image_name));
return;
}
},

View File

@ -1,37 +0,0 @@
import { log } from 'app/logging/useLogger';
import { startAppListening } from '..';
import { imageUrlsReceived } from 'services/api/thunks/image';
import { imageUpdatedOne } from 'features/gallery/store/gallerySlice';
const moduleLog = log.child({ namespace: 'image' });
export const addImageUrlsReceivedFulfilledListener = () => {
startAppListening({
actionCreator: imageUrlsReceived.fulfilled,
effect: (action, { getState, dispatch }) => {
const image = action.payload;
moduleLog.debug({ data: { image } }, 'Image URLs received');
const { image_name, image_url, thumbnail_url } = image;
dispatch(
imageUpdatedOne({
id: image_name,
changes: { image_url, thumbnail_url },
})
);
},
});
};
export const addImageUrlsReceivedRejectedListener = () => {
startAppListening({
actionCreator: imageUrlsReceived.rejected,
effect: (action, { getState, dispatch }) => {
moduleLog.debug(
{ data: { image: action.meta.arg } },
'Problem getting image URLs'
);
},
});
};

View File

@ -0,0 +1,38 @@
import { log } from 'app/logging/useLogger';
import { serializeError } from 'serialize-error';
import { imagesApi } from 'services/api/endpoints/images';
import { imagesLoaded } from 'services/api/thunks/image';
import { startAppListening } from '..';
const moduleLog = log.child({ namespace: 'gallery' });
export const addImagesLoadedListener = () => {
startAppListening({
actionCreator: imagesLoaded.fulfilled,
effect: (action, { getState, dispatch }) => {
const { items } = action.payload;
moduleLog.debug(
{ data: { payload: action.payload } },
`Loaded ${items.length} images`
);
items.forEach((image) => {
dispatch(
imagesApi.util.upsertQueryData('getImageDTO', image.image_name, image)
);
});
},
});
startAppListening({
actionCreator: imagesLoaded.rejected,
effect: (action, { getState, dispatch }) => {
if (action.payload) {
moduleLog.debug(
{ data: { error: serializeError(action.payload) } },
'Problem loading images'
);
}
},
});
};

View File

@ -16,6 +16,8 @@ export const addReceivedPageOfImagesListener = () => {
`Received ${items.length} images`
);
// inject the received images into the RTK Query cache so consumers of the useGetImageDTOQuery
// hook can get their data from the cache instead of fetching the data again
items.forEach((image) => {
dispatch(
imagesApi.util.upsertQueryData('getImageDTO', image.image_name, image)

View File

@ -2,6 +2,7 @@ import { log } from 'app/logging/useLogger';
import { addImageToStagingArea } from 'features/canvas/store/canvasSlice';
import { progressImageSet } from 'features/system/store/systemSlice';
import { boardImagesApi } from 'services/api/endpoints/boardImages';
import { imagesApi } from 'services/api/endpoints/images';
import { isImageOutput } from 'services/api/guards';
import { imageDTOReceived } from 'services/api/thunks/image';
import { sessionCanceled } from 'services/api/thunks/session';
@ -41,14 +42,16 @@ export const addInvocationCompleteEventListener = () => {
const { image_name } = result.image;
// Get its metadata
dispatch(
const { requestId } = dispatch(
imageDTOReceived({
image_name,
})
);
const [{ payload: imageDTO }] = await take(
imageDTOReceived.fulfilled.match
(action): action is ReturnType<typeof imageDTOReceived.fulfilled> =>
imageDTOReceived.fulfilled.match(action) &&
action.meta.requestId === requestId
);
// Handle canvas image
@ -59,6 +62,15 @@ export const addInvocationCompleteEventListener = () => {
dispatch(addImageToStagingArea(imageDTO));
}
// Update the RTK Query cache
dispatch(
imagesApi.util.upsertQueryData(
'getImageDTO',
imageDTO.image_name,
imageDTO
)
);
if (boardIdToAddTo && !imageDTO.is_intermediate) {
dispatch(
boardImagesApi.endpoints.addBoardImage.initiate({
@ -66,6 +78,17 @@ export const addInvocationCompleteEventListener = () => {
image_name,
})
);
// Set the board_id on the image in the RTK Query cache
dispatch(
imagesApi.util.updateQueryData(
'getImageDTO',
imageDTO.image_name,
(draft) => {
Object.assign(draft, { board_id: boardIdToAddTo });
}
)
);
}
dispatch(progressImageSet(null));

View File

@ -1,9 +1,9 @@
import { stagingAreaImageSaved } from 'features/canvas/store/actions';
import { startAppListening } from '..';
import { log } from 'app/logging/useLogger';
import { imageUpdated } from 'services/api/thunks/image';
import { imageUpserted } from 'features/gallery/store/gallerySlice';
import { stagingAreaImageSaved } from 'features/canvas/store/actions';
import { addToast } from 'features/system/store/systemSlice';
import { imagesApi } from 'services/api/endpoints/images';
import { imageUpdated } from 'services/api/thunks/image';
import { startAppListening } from '..';
const moduleLog = log.child({ namespace: 'canvas' });
@ -43,7 +43,10 @@ export const addStagingAreaImageSavedListener = () => {
}
if (imageUpdated.fulfilled.match(imageUpdatedAction)) {
dispatch(imageUpserted(imageUpdatedAction.payload));
// update cache
imagesApi.util.updateQueryData('getImageDTO', imageName, (draft) => {
Object.assign(draft, { is_intermediate: false });
});
dispatch(addToast({ title: 'Image Saved', status: 'success' }));
}
},

View File

@ -104,10 +104,10 @@ export const store = configureStore({
// manually type state, cannot type the arg
// const typedState = state as ReturnType<typeof rootReducer>;
if (action.type.startsWith('api/')) {
// don't log api actions, with manual cache updates they are extremely noisy
return false;
}
// if (action.type.startsWith('api/')) {
// // don't log api actions, with manual cache updates they are extremely noisy
// return false;
// }
if (actionsDenylist.includes(action.type)) {
// don't log other noisy actions

View File

@ -8,7 +8,7 @@ const AllImagesBoard = ({ isSelected }: { isSelected: boolean }) => {
const dispatch = useDispatch();
const handleAllImagesBoardClick = () => {
dispatch(boardIdSelected());
dispatch(boardIdSelected('all'));
};
const droppableData: MoveBoardDropData = {

View File

@ -118,7 +118,7 @@ const BoardsList = (props: Props) => {
{!searchMode && (
<>
<GridItem sx={{ p: 1.5 }}>
<AllImagesBoard isSelected={!selectedBoardId} />
<AllImagesBoard isSelected={selectedBoardId === 'all'} />
</GridItem>
<GridItem sx={{ p: 1.5 }}>
<BatchBoard isSelected={selectedBoardId === 'batch'} />

View File

@ -29,16 +29,10 @@ import { createSelector } from '@reduxjs/toolkit';
import { stateSelector } from 'app/store/store';
import { defaultSelectorOptions } from 'app/store/util/defaultMemoizeOptions';
import { requestCanvasRescale } from 'features/canvas/store/thunks/requestCanvasScale';
import {
ASSETS_CATEGORIES,
IMAGE_CATEGORIES,
imageCategoriesChanged,
shouldAutoSwitchChanged,
} from 'features/gallery/store/gallerySlice';
import { shouldAutoSwitchChanged } from 'features/gallery/store/gallerySlice';
import { useListAllBoardsQuery } from 'services/api/endpoints/boards';
import { mode } from 'theme/util/mode';
import BatchGrid from './BatchGrid';
import BoardGrid from './BoardGrid';
import BoardsList from './Boards/BoardsList';
import ImageGalleryGrid from './ImageGalleryGrid';
@ -68,6 +62,7 @@ const ImageGalleryContent = () => {
const dispatch = useAppDispatch();
const { t } = useTranslation();
const resizeObserverRef = useRef<HTMLDivElement>(null);
const galleryGridRef = useRef<HTMLDivElement>(null);
const { colorMode } = useColorMode();
@ -107,12 +102,10 @@ const ImageGalleryContent = () => {
};
const handleClickImagesCategory = useCallback(() => {
dispatch(imageCategoriesChanged(IMAGE_CATEGORIES));
dispatch(setGalleryView('images'));
}, [dispatch]);
const handleClickAssetsCategory = useCallback(() => {
dispatch(imageCategoriesChanged(ASSETS_CATEGORIES));
dispatch(setGalleryView('assets'));
}, [dispatch]);
@ -228,14 +221,8 @@ const ImageGalleryContent = () => {
<BoardsList isOpen={isBoardListOpen} />
</Box>
</Box>
<Flex direction="column" gap={2} h="full" w="full">
{selectedBoardId === 'batch' ? (
<BatchGrid />
) : selectedBoardId ? (
<BoardGrid board_id={selectedBoardId} />
) : (
<ImageGalleryGrid />
)}
<Flex ref={galleryGridRef} direction="column" gap={2} h="full" w="full">
{selectedBoardId === 'batch' ? <BatchGrid /> : <ImageGalleryGrid />}
</Flex>
</VStack>
);

View File

@ -1,7 +1,6 @@
import { Box, Flex, Skeleton, Spinner } from '@chakra-ui/react';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { Box } from '@chakra-ui/react';
import { useAppSelector } from 'app/store/storeHooks';
import IAIButton from 'common/components/IAIButton';
import { IMAGE_LIMIT } from 'features/gallery/store/gallerySlice';
import { useOverlayScrollbars } from 'overlayscrollbars-react';
import { memo, useCallback, useEffect, useMemo, useRef, useState } from 'react';
@ -13,48 +12,27 @@ import { createSelector } from '@reduxjs/toolkit';
import { stateSelector } from 'app/store/store';
import { defaultSelectorOptions } from 'app/store/util/defaultMemoizeOptions';
import { IAINoContentFallback } from 'common/components/IAIImageFallback';
import { selectFilteredImages } from 'features/gallery/store/gallerySelectors';
import { VirtuosoGrid } from 'react-virtuoso';
import { useListAllBoardsQuery } from 'services/api/endpoints/boards';
import { receivedPageOfImages } from 'services/api/thunks/image';
import { ImageDTO } from 'services/api/types';
import { useLoadMoreImages } from '../hooks/useLoadMoreImages';
import ItemContainer from './ItemContainer';
import ListContainer from './ListContainer';
const selector = createSelector(
[stateSelector, selectFilteredImages],
(state, filteredImages) => {
const {
categories,
total: allImagesTotal,
isLoading,
isFetching,
selectedBoardId,
} = state.gallery;
let images = filteredImages as (ImageDTO | 'loading')[];
if (!isLoading && isFetching) {
// loading, not not the initial load
images = images.concat(Array(IMAGE_LIMIT).fill('loading'));
}
[stateSelector],
(state) => {
const { galleryImageMinimumWidth } = state.gallery;
return {
images,
allImagesTotal,
isLoading,
isFetching,
categories,
selectedBoardId,
galleryImageMinimumWidth,
};
},
defaultSelectorOptions
);
const ImageGalleryGrid = () => {
const dispatch = useAppDispatch();
const { t } = useTranslation();
const rootRef = useRef(null);
const rootRef = useRef<HTMLDivElement>(null);
const emptyGalleryRef = useRef<HTMLDivElement>(null);
const [scroller, setScroller] = useState<HTMLElement | null>(null);
const [initialize, osInstance] = useOverlayScrollbars({
defer: true,
@ -69,46 +47,27 @@ const ImageGalleryGrid = () => {
},
});
const { galleryImageMinimumWidth } = useAppSelector(selector);
const {
images,
isLoading,
isFetching,
allImagesTotal,
categories,
imageNames,
galleryView,
loadMoreImages,
selectedBoardId,
} = useAppSelector(selector);
const { selectedBoard } = useListAllBoardsQuery(undefined, {
selectFromResult: ({ data }) => ({
selectedBoard: data?.find((b) => b.board_id === selectedBoardId),
}),
});
const filteredImagesTotal = useMemo(
() => selectedBoard?.image_count ?? allImagesTotal,
[allImagesTotal, selectedBoard?.image_count]
);
const areMoreAvailable = useMemo(() => {
return images.length < filteredImagesTotal;
}, [images.length, filteredImagesTotal]);
status,
areMoreAvailable,
} = useLoadMoreImages();
const handleLoadMoreImages = useCallback(() => {
dispatch(
receivedPageOfImages({
categories,
board_id: selectedBoardId,
is_intermediate: false,
})
);
}, [categories, dispatch, selectedBoardId]);
loadMoreImages({});
}, [loadMoreImages]);
const handleEndReached = useMemo(() => {
if (areMoreAvailable && !isLoading) {
if (areMoreAvailable && status !== 'pending') {
return handleLoadMoreImages;
}
return undefined;
}, [areMoreAvailable, handleLoadMoreImages, isLoading]);
}, [areMoreAvailable, handleLoadMoreImages, status]);
useEffect(() => {
const { current: root } = rootRef;
@ -123,53 +82,68 @@ const ImageGalleryGrid = () => {
return () => osInstance()?.destroy();
}, [scroller, initialize, osInstance]);
if (isLoading) {
useEffect(() => {
// TODO: this doesn't actually prevent 2 intial image loads...
if (status !== undefined) {
return;
}
// rough, conservative calculation of how many images fit in the gallery
// TODO: this gets an incorrect value on first load...
const galleryHeight = rootRef.current?.clientHeight ?? 0;
const galleryWidth = rootRef.current?.clientHeight ?? 0;
const rows = galleryHeight / galleryImageMinimumWidth;
const columns = galleryWidth / galleryImageMinimumWidth;
const imagesToLoad = Math.ceil(rows * columns);
// load up that many images
loadMoreImages({
offset: 0,
limit: imagesToLoad,
});
}, [
galleryImageMinimumWidth,
galleryView,
loadMoreImages,
selectedBoardId,
status,
]);
if (status === 'fulfilled' && imageNames.length === 0) {
return (
<Flex
sx={{
w: 'full',
h: 'full',
alignItems: 'center',
justifyContent: 'center',
}}
>
<Spinner
size="xl"
sx={{ color: 'base.300', _dark: { color: 'base.700' } }}
<Box ref={emptyGalleryRef} sx={{ w: 'full', h: 'full' }}>
<IAINoContentFallback
label={t('gallery.noImagesInGallery')}
icon={FaImage}
/>
</Flex>
</Box>
);
}
if (images.length) {
if (status !== 'rejected') {
return (
<>
<Box ref={rootRef} data-overlayscrollbars="" h="100%">
<VirtuosoGrid
style={{ height: '100%' }}
data={images}
data={imageNames}
endReached={handleEndReached}
components={{
Item: ItemContainer,
List: ListContainer,
}}
scrollerRef={setScroller}
itemContent={(index, item) =>
typeof item === 'string' ? (
<Skeleton sx={{ w: 'full', h: 'full', aspectRatio: '1/1' }} />
) : (
<GalleryImage
key={`${item.image_name}-${item.thumbnail_url}`}
imageName={item.image_name}
/>
)
}
itemContent={(index, imageName) => (
<GalleryImage key={imageName} imageName={imageName} />
)}
/>
</Box>
<IAIButton
onClick={handleLoadMoreImages}
isDisabled={!areMoreAvailable}
isLoading={isFetching}
isLoading={status === 'pending'}
loadingText="Loading"
flexShrink={0}
>
@ -180,13 +154,6 @@ const ImageGalleryGrid = () => {
</>
);
}
return (
<IAINoContentFallback
label={t('gallery.noImagesInGallery')}
icon={FaImage}
/>
);
};
export default memo(ImageGalleryGrid);

View File

@ -0,0 +1,64 @@
import { createSelector } from '@reduxjs/toolkit';
import { stateSelector } from 'app/store/store';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { defaultSelectorOptions } from 'app/store/util/defaultMemoizeOptions';
import { useCallback } from 'react';
import { ImagesLoadedArg, imagesLoaded } from 'services/api/thunks/image';
const selector = createSelector(
[stateSelector],
(state) => {
const { selectedBoardId, galleryView } = state.gallery;
const imageNames =
state.gallery.imageNamesByIdAndView[selectedBoardId]?.[galleryView]
.imageNames ?? [];
const total =
state.gallery.imageNamesByIdAndView[selectedBoardId]?.[galleryView]
.total ?? 0;
const status =
state.gallery.statusByIdAndView[selectedBoardId]?.[galleryView] ??
undefined;
return {
imageNames,
status,
total,
selectedBoardId,
galleryView,
};
},
defaultSelectorOptions
);
export const useLoadMoreImages = () => {
const dispatch = useAppDispatch();
const { selectedBoardId, imageNames, galleryView, total, status } =
useAppSelector(selector);
const loadMoreImages = useCallback(
(arg: Partial<ImagesLoadedArg>) => {
dispatch(
imagesLoaded({
board_id: selectedBoardId,
offset: imageNames.length,
view: galleryView,
...arg,
})
);
},
[dispatch, galleryView, imageNames.length, selectedBoardId]
);
return {
loadMoreImages,
selectedBoardId,
imageNames,
galleryView,
areMoreAvailable: imageNames.length < total,
total,
status,
};
};

View File

@ -15,4 +15,6 @@ export const galleryPersistDenylist: (keyof typeof initialGalleryState)[] = [
'galleryView',
'total',
'isInitialized',
'imageNamesByIdAndView',
'statusByIdAndView',
];

View File

@ -1,15 +1,12 @@
import type { PayloadAction, Update } from '@reduxjs/toolkit';
import type { PayloadAction } from '@reduxjs/toolkit';
import { createEntityAdapter, createSlice } from '@reduxjs/toolkit';
import { RootState } from 'app/store/store';
import { dateComparator } from 'common/util/dateComparator';
import { uniq } from 'lodash-es';
import { filter, forEach, uniq } from 'lodash-es';
import { boardImagesApi } from 'services/api/endpoints/boardImages';
import { boardsApi } from 'services/api/endpoints/boards';
import {
imageUrlsReceived,
receivedPageOfImages,
} from 'services/api/thunks/image';
import { imageDeleted, imagesLoaded } from 'services/api/thunks/image';
import { ImageCategory, ImageDTO } from 'services/api/types';
import { selectFilteredImagesLocal } from './gallerySelectors';
export const galleryImagesAdapter = createEntityAdapter<ImageDTO>({
selectId: (image) => image.image_name,
@ -27,6 +24,40 @@ export const ASSETS_CATEGORIES: ImageCategory[] = [
export const INITIAL_IMAGE_LIMIT = 100;
export const IMAGE_LIMIT = 20;
type RequestState = 'pending' | 'fulfilled' | 'rejected';
type GalleryView = 'images' | 'assets';
// dirty hack to get autocompletion while still accepting any string
type BoardPath =
| 'all.images'
| 'all.assets'
| 'none.images'
| 'none.assets'
| 'batch.images'
| 'batch.assets'
| `${string}.${GalleryView}`;
const systemBoards = [
'all.images',
'all.assets',
'none.images',
'none.assets',
'batch.images',
'batch.assets',
];
type Boards = Record<
BoardPath,
{
path: BoardPath;
id: 'all' | 'none' | 'batch' | (string & Record<never, never>);
view: GalleryView;
imageNames: string[];
total: number;
status: RequestState | undefined;
}
>;
type AdditionalGalleryState = {
offset: number;
limit: number;
@ -34,12 +65,54 @@ type AdditionalGalleryState = {
isLoading: boolean;
isFetching: boolean;
categories: ImageCategory[];
selectedBoardId?: 'batch' | string;
selection: string[];
shouldAutoSwitch: boolean;
galleryImageMinimumWidth: number;
galleryView: 'images' | 'assets';
isInitialized: boolean;
galleryView: GalleryView;
selectedBoardId: 'all' | 'none' | 'batch' | (string & Record<never, never>);
boards: Boards;
};
const initialBoardState = { imageNames: [], total: 0, status: undefined };
const initialBoards: Boards = {
'all.images': {
path: 'all.images',
id: 'all',
view: 'images',
...initialBoardState,
},
'all.assets': {
path: 'all.assets',
id: 'all',
view: 'assets',
...initialBoardState,
},
'none.images': {
path: 'none.images',
id: 'none',
view: 'images',
...initialBoardState,
},
'none.assets': {
path: 'none.assets',
id: 'none',
view: 'assets',
...initialBoardState,
},
'batch.images': {
path: 'batch.images',
id: 'batch',
view: 'images',
...initialBoardState,
},
'batch.assets': {
path: 'batch.assets',
id: 'batch',
view: 'assets',
...initialBoardState,
},
};
export const initialGalleryState =
@ -55,60 +128,45 @@ export const initialGalleryState =
galleryImageMinimumWidth: 96,
galleryView: 'images',
isInitialized: false,
selectedBoardId: 'all',
boards: initialBoards,
});
export const gallerySlice = createSlice({
name: 'gallery',
initialState: initialGalleryState,
reducers: {
imageUpserted: (state, action: PayloadAction<ImageDTO>) => {
galleryImagesAdapter.upsertOne(state, action.payload);
if (
state.shouldAutoSwitch &&
action.payload.image_category === 'general'
) {
state.selection = [action.payload.image_name];
state.galleryView = 'images';
state.categories = IMAGE_CATEGORIES;
}
},
imageUpdatedOne: (state, action: PayloadAction<Update<ImageDTO>>) => {
galleryImagesAdapter.updateOne(state, action.payload);
},
imageUpdatedMany: (state, action: PayloadAction<Update<ImageDTO>[]>) => {
galleryImagesAdapter.updateMany(state, action.payload);
},
imageRemoved: (state, action: PayloadAction<string>) => {
galleryImagesAdapter.removeOne(state, action.payload);
},
imagesRemoved: (state, action: PayloadAction<string[]>) => {
galleryImagesAdapter.removeMany(state, action.payload);
},
imageCategoriesChanged: (state, action: PayloadAction<ImageCategory[]>) => {
state.categories = action.payload;
},
imageRangeEndSelected: (state, action: PayloadAction<string>) => {
const rangeEndImageName = action.payload;
const lastSelectedImage = state.selection[state.selection.length - 1];
const filteredImages = selectFilteredImagesLocal(state);
// get image names for the current board and view
const imageNames =
state.boards[`${state.selectedBoardId}.${state.galleryView}`]
.imageNames;
const lastClickedIndex = filteredImages.findIndex(
(n) => n.image_name === lastSelectedImage
// get the index of the last selected image
const lastClickedIndex = imageNames.findIndex(
(n) => n === lastSelectedImage
);
const currentClickedIndex = filteredImages.findIndex(
(n) => n.image_name === rangeEndImageName
// get the index of the just-clicked image
const currentClickedIndex = imageNames.findIndex(
(n) => n === rangeEndImageName
);
if (lastClickedIndex > -1 && currentClickedIndex > -1) {
// We have a valid range!
// We have a valid range, selected it!
const start = Math.min(lastClickedIndex, currentClickedIndex);
const end = Math.max(lastClickedIndex, currentClickedIndex);
const imagesToSelect = filteredImages
.slice(start, end + 1)
.map((i) => i.image_name);
const imagesToSelect = imageNames.slice(start, end + 1);
state.selection = uniq(state.selection.concat(imagesToSelect));
}
@ -121,9 +179,10 @@ export const gallerySlice = createSlice({
state.selection = state.selection.filter(
(imageName) => imageName !== action.payload
);
} else {
state.selection = uniq(state.selection.concat(action.payload));
return;
}
state.selection = uniq(state.selection.concat(action.payload));
},
imageSelected: (state, action: PayloadAction<string | null>) => {
state.selection = action.payload
@ -136,59 +195,210 @@ export const gallerySlice = createSlice({
setGalleryImageMinimumWidth: (state, action: PayloadAction<number>) => {
state.galleryImageMinimumWidth = action.payload;
},
setGalleryView: (state, action: PayloadAction<'images' | 'assets'>) => {
setGalleryView: (state, action: PayloadAction<GalleryView>) => {
state.galleryView = action.payload;
},
boardIdSelected: (state, action: PayloadAction<string | undefined>) => {
state.selectedBoardId = action.payload;
boardIdSelected: (state, action: PayloadAction<BoardPath>) => {
const boardId = action.payload;
if (state.selectedBoardId === boardId) {
// selected same board, no-op
return;
}
state.selectedBoardId = boardId;
// handle selecting an unitialized board
const boardImagesId: BoardPath = `${boardId}.images`;
const boardAssetsId: BoardPath = `${boardId}.assets`;
if (!state.boards[boardImagesId]) {
state.boards[boardImagesId] = {
path: boardImagesId,
id: boardId,
view: 'images',
...initialBoardState,
};
}
if (!state.boards[boardAssetsId]) {
state.boards[boardAssetsId] = {
path: boardAssetsId,
id: boardId,
view: 'assets',
...initialBoardState,
};
}
// set the first image as selected
const firstImageName =
state.boards[`${boardId}.${state.galleryView}`].imageNames[0];
state.selection = firstImageName ? [firstImageName] : [];
},
isLoadingChanged: (state, action: PayloadAction<boolean>) => {
state.isLoading = action.payload;
},
},
extraReducers: (builder) => {
builder.addCase(receivedPageOfImages.pending, (state) => {
state.isFetching = true;
/**
* Image deleted
*/
builder.addCase(imageDeleted.pending, (state, action) => {
// optimistic update, but no undo :/
const { image_name } = action.meta.arg;
// remove image from all boards
forEach(state.boards, (board) => {
board.imageNames = board.imageNames.filter((n) => n !== image_name);
});
// and selection
state.selection = state.selection.filter((n) => n !== image_name);
});
builder.addCase(receivedPageOfImages.rejected, (state) => {
state.isFetching = false;
/**
* Images loaded into gallery - PENDING
*/
builder.addCase(imagesLoaded.pending, (state, action) => {
const { board_id, view } = action.meta.arg;
state.boards[`${board_id}.${view}`].status = 'pending';
});
builder.addCase(receivedPageOfImages.fulfilled, (state, action) => {
state.isFetching = false;
const { board_id, categories, image_origin, is_intermediate } =
action.meta.arg;
/**
* Images loaded into gallery - FULFILLED
*/
builder.addCase(imagesLoaded.fulfilled, (state, action) => {
const { items, total } = action.payload;
const { board_id, view } = action.meta.arg;
const board = state.boards[`${board_id}.${view}`];
const { items, offset, limit, total } = action.payload;
board.status = 'fulfilled';
galleryImagesAdapter.upsertMany(state, items);
board.imageNames = uniq(
board.imageNames.concat(items.map((i) => i.image_name))
);
board.total = total;
if (state.selection.length === 0 && items.length) {
state.selection = [items[0].image_name];
}
});
/**
* Images loaded into gallery - REJECTED
*/
builder.addCase(imagesLoaded.rejected, (state, action) => {
const { board_id, view } = action.meta.arg;
state.boards[`${board_id}.${view}`].status = 'rejected';
});
/**
* Image added to board
*/
builder.addMatcher(
boardImagesApi.endpoints.addBoardImage.matchFulfilled,
(state, action) => {
const { board_id, image_name } = action.meta.arg.originalArgs;
// update user board stores
const userBoards = selectUserBoards(state);
userBoards.forEach((board) => {
// only update the current view
if (board.view !== state.galleryView) {
return;
}
if (!categories?.includes('general') || board_id) {
// need to skip updating the total images count if the images recieved were for a specific board
// TODO: this doesn't work when on the Asset tab/category...
return;
if (board_id === board.id) {
// add image to the board
board.imageNames = uniq(board.imageNames.concat(image_name));
} else {
// remove image from other boards
board.imageNames = board.imageNames.filter((n) => n !== image_name);
}
});
}
state.offset = offset;
state.total = total;
});
builder.addCase(imageUrlsReceived.fulfilled, (state, action) => {
const { image_name, image_url, thumbnail_url } = action.payload;
galleryImagesAdapter.updateOne(state, {
id: image_name,
changes: { image_url, thumbnail_url },
});
});
);
/**
* Many images added to board
*/
builder.addMatcher(
boardImagesApi.endpoints.addManyBoardImages.matchFulfilled,
(state, action) => {
const { board_id, image_names } = action.meta.arg.originalArgs;
// update local board stores
forEach(state.boards, (board, board_id) => {
// only update the current view
if (board_id === board.id) {
// add images to the board
board.imageNames = uniq(board.imageNames.concat(image_names));
} else {
// remove images from other boards
board.imageNames = board.imageNames.filter((n) =>
image_names.includes(n)
);
}
});
}
);
/**
* Board deleted (not images)
*/
builder.addMatcher(
boardsApi.endpoints.deleteBoard.matchFulfilled,
(state, action) => {
if (action.meta.arg.originalArgs === state.selectedBoardId) {
state.selectedBoardId = undefined;
const deletedBoardId = action.meta.arg.originalArgs;
if (deletedBoardId === state.selectedBoardId) {
state.selectedBoardId = 'all';
}
// remove board from local store
delete state.boards[`${deletedBoardId}.images`];
delete state.boards[`${deletedBoardId}.assets`];
}
);
/**
* Board deleted (with images)
*/
builder.addMatcher(
boardsApi.endpoints.deleteBoardAndImages.matchFulfilled,
(state, action) => {
const { deleted_images } = action.payload;
const deletedBoardId = action.meta.arg.originalArgs;
// remove images from all boards
forEach(state.boards, (board) => {
// remove images from all boards
board.imageNames = board.imageNames.filter((n) =>
deleted_images.includes(n)
);
});
delete state.boards[`${deletedBoardId}.images`];
delete state.boards[`${deletedBoardId}.assets`];
}
);
/**
* Image removed from board; i.e. Board reset for image
*/
builder.addMatcher(
boardImagesApi.endpoints.deleteBoardImage.matchFulfilled,
(state, action) => {
const { image_name } = action.meta.arg.originalArgs;
// remove from all user boards (skip all, none, batch)
const userBoards = selectUserBoards(state);
userBoards.forEach((board) => {
board.imageNames = board.imageNames.filter((n) => n !== image_name);
});
}
);
/**
* Many images removed from board; i.e. Board reset for many images
*/
builder.addMatcher(
boardImagesApi.endpoints.deleteManyBoardImages.matchFulfilled,
(state, action) => {
const { image_names } = action.meta.arg.originalArgs;
// remove images from all boards
forEach(state.imageNamesByIdAndView, (board) => {
// only update the current view
const view = board[state.galleryView];
view.imageNames = view.imageNames.filter((n) =>
image_names.includes(n)
);
});
}
);
},
@ -203,12 +413,7 @@ export const {
} = galleryImagesAdapter.getSelectors<RootState>((state) => state.gallery);
export const {
imageUpserted,
imageUpdatedOne,
imageUpdatedMany,
imageRemoved,
imagesRemoved,
imageCategoriesChanged,
imageRangeEndSelected,
imageSelectionToggled,
imageSelected,
@ -220,3 +425,13 @@ export const {
} = gallerySlice.actions;
export default gallerySlice.reducer;
const selectUserBoards = (state: typeof initialGalleryState) =>
filter(state.boards, (board, path) => !systemBoards.includes(path));
const selectCurrentBoard = (state: typeof initialGalleryState) =>
state.boards[`${state.selectedBoardId}.${state.galleryView}`];
const isImagesView = (board: BoardPath) => board.split('.')[1] === 'images';
const isAssetsView = (board: BoardPath) => board.split('.')[1] === 'assets';

View File

@ -4,7 +4,7 @@ import { components, paths } from '../schema';
import { imagesApi } from './images';
type AddImageToBoardArg =
paths['/api/v1/board_images/']['post']['requestBody']['content']['application/json'];
paths['/api/v1/board_images/{board_id}']['post']['requestBody']['content']['application/json'];
type AddManyImagesToBoardArg =
paths['/api/v1/board_images/{board_id}/images']['patch']['requestBody']['content']['application/json'];
@ -44,11 +44,14 @@ export const boardImagesApi = api.injectEndpoints({
* Board Images Mutations
*/
addBoardImage: build.mutation<void, AddImageToBoardArg>({
addBoardImage: build.mutation<
void,
{ board_id: string; image_name: string }
>({
query: ({ board_id, image_name }) => ({
url: `board_images/`,
url: `board_images/${board_id}`,
method: 'POST',
body: { board_id, image_name },
body: image_name,
}),
invalidatesTags: (result, error, arg) => [
{ type: 'Board', id: arg.board_id },

View File

@ -1,6 +1,6 @@
import { BoardDTO, OffsetPaginatedResults_BoardDTO_ } from 'services/api/types';
import { ApiFullTagDescription, LIST_TAG, api } from '..';
import { paths } from '../schema';
import { components, paths } from '../schema';
type ListBoardsArg = NonNullable<
paths['/api/v1/boards/']['get']['parameters']['query']
@ -86,7 +86,10 @@ export const boardsApi = api.injectEndpoints({
query: (board_id) => ({ url: `boards/${board_id}`, method: 'DELETE' }),
invalidatesTags: (result, error, arg) => [{ type: 'Board', id: arg }],
}),
deleteBoardAndImages: build.mutation<void, string>({
deleteBoardAndImages: build.mutation<
components['schemas']['DeleteManyImagesResult'],
string
>({
query: (board_id) => ({
url: `boards/${board_id}`,
method: 'DELETE',

View File

@ -200,24 +200,24 @@ export type paths = {
*/
patch: operations["update_board"];
};
"/api/v1/board_images/": {
/**
* Create Board Image
* @description Creates a board_image
*/
post: operations["create_board_image"];
/**
* Remove Board Image
* @description Deletes a board_image
*/
delete: operations["remove_board_image"];
};
"/api/v1/board_images/{board_id}": {
/**
* Get All Board Images For Board
* @description Gets all image names for a board
*/
get: operations["get_all_board_images_for_board"];
/**
* Create Board Image
* @description Creates a board_image
*/
post: operations["create_board_image"];
};
"/api/v1/board_images/": {
/**
* Remove Board Image
* @description Deletes a board_image
*/
delete: operations["remove_board_image"];
};
"/api/v1/board_images/{board_id}/images": {
/**
@ -346,19 +346,6 @@ export type components = {
*/
image_count: number;
};
/** Body_create_board_image */
Body_create_board_image: {
/**
* Board Id
* @description The id of the board to add to
*/
board_id: string;
/**
* Image Name
* @description The name of the image to add
*/
image_name: string;
};
/** Body_import_model */
Body_import_model: {
/**
@ -4478,18 +4465,18 @@ export type components = {
*/
image?: components["schemas"]["ImageField"];
};
/**
* StableDiffusion2ModelFormat
* @description An enumeration.
* @enum {string}
*/
StableDiffusion2ModelFormat: "checkpoint" | "diffusers";
/**
* StableDiffusion1ModelFormat
* @description An enumeration.
* @enum {string}
*/
StableDiffusion1ModelFormat: "checkpoint" | "diffusers";
/**
* StableDiffusion2ModelFormat
* @description An enumeration.
* @enum {string}
*/
StableDiffusion2ModelFormat: "checkpoint" | "diffusers";
};
responses: never;
parameters: never;
@ -5418,7 +5405,7 @@ export type operations = {
/** @description Successful Response */
200: {
content: {
"application/json": unknown;
"application/json": components["schemas"]["DeleteManyImagesResult"];
};
};
/** @description Validation Error */
@ -5460,14 +5447,46 @@ export type operations = {
};
};
};
/**
* Get All Board Images For Board
* @description Gets all image names for a board
*/
get_all_board_images_for_board: {
parameters: {
path: {
/** @description The id of the board */
board_id: string;
};
};
responses: {
/** @description Successful Response */
200: {
content: {
"application/json": components["schemas"]["GetAllBoardImagesForBoardResult"];
};
};
/** @description Validation Error */
422: {
content: {
"application/json": components["schemas"]["HTTPValidationError"];
};
};
};
};
/**
* Create Board Image
* @description Creates a board_image
*/
create_board_image: {
parameters: {
path: {
/** @description The id of the board to add to */
board_id: string;
};
};
requestBody: {
content: {
"application/json": components["schemas"]["Body_create_board_image"];
"application/json": string;
};
};
responses: {
@ -5510,32 +5529,6 @@ export type operations = {
};
};
};
/**
* Get All Board Images For Board
* @description Gets all image names for a board
*/
get_all_board_images_for_board: {
parameters: {
path: {
/** @description The id of the board */
board_id: string;
};
};
responses: {
/** @description Successful Response */
200: {
content: {
"application/json": components["schemas"]["GetAllBoardImagesForBoardResult"];
};
};
/** @description Validation Error */
422: {
content: {
"application/json": components["schemas"]["HTTPValidationError"];
};
};
};
};
/**
* Create Multiple Board Images
* @description Add many images to a board

View File

@ -4,6 +4,7 @@ import { size } from 'lodash-es';
import queryString from 'query-string';
import { $client } from 'services/api/client';
import { paths } from 'services/api/schema';
import { ImageCategory, OffsetPaginatedResults_ImageDTO_ } from '../types';
type GetImageUrlsArg =
paths['/api/v1/images/{image_name}/urls']['get']['parameters']['path'];
@ -329,6 +330,75 @@ export const receivedPageOfImages = createAppAsyncThunk<
}
);
export type ImagesLoadedArg = {
board_id: 'all' | 'none' | (string & Record<never, never>);
view: 'images' | 'assets';
offset: number;
limit?: number;
};
type ImagesLoadedThunkConfig = {
rejectValue: {
arg: ImagesLoadedArg;
error: unknown;
};
};
const getCategories = (view: 'images' | 'assets'): ImageCategory[] => {
if (view === 'images') {
return ['general'];
}
return ['control', 'mask', 'user', 'other'];
};
const getBoardId = (
board_id: 'all' | 'none' | (string & Record<never, never>)
) => {
if (board_id === 'all') {
return undefined;
}
if (board_id === 'none') {
return 'none';
}
return board_id;
};
/**
* `ImagesService.listImagesWithMetadata()` thunk
*/
export const imagesLoaded = createAppAsyncThunk<
OffsetPaginatedResults_ImageDTO_,
ImagesLoadedArg,
ImagesLoadedThunkConfig
>(
'thunkApi/imagesLoaded',
async (arg, { getState, rejectWithValue, requestId }) => {
const { get } = $client.get();
// TODO: do not make request if request in progress
const query = {
categories: getCategories(arg.view),
board_id: getBoardId(arg.board_id),
offset: arg.offset,
limit: arg.limit ?? IMAGES_PER_PAGE,
};
const { data, error, response } = await get('/api/v1/images/', {
params: {
query,
},
querySerializer: (q) => queryString.stringify(q, { arrayFormat: 'none' }),
});
if (error) {
return rejectWithValue({ arg, error });
}
return data;
}
);
type GetImagesByNamesArg = NonNullable<
paths['/api/v1/images/']['post']['requestBody']['content']['application/json']
>;

View File

@ -1175,14 +1175,14 @@
resolved "https://registry.yarnpkg.com/@esbuild/win32-x64/-/win32-x64-0.17.19.tgz#8cfaf2ff603e9aabb910e9c0558c26cf32744061"
integrity sha512-lAhycmKnVOuRYNtRtatQR1LPQf2oYCkRGkSFnseDAKPl8lu5SOsK/e1sXe5a0Pc5kHIHe6P2I/ilntNv2xf3cA==
"@eslint-community/eslint-utils@^4.2.0":
"@eslint-community/eslint-utils@^4.2.0", "@eslint-community/eslint-utils@^4.3.0":
version "4.4.0"
resolved "https://registry.yarnpkg.com/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz#a23514e8fb9af1269d5f7788aa556798d61c6b59"
integrity sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA==
dependencies:
eslint-visitor-keys "^3.3.0"
"@eslint-community/regexpp@^4.4.0":
"@eslint-community/regexpp@^4.4.0", "@eslint-community/regexpp@^4.5.0":
version "4.5.1"
resolved "https://registry.yarnpkg.com/@eslint-community/regexpp/-/regexpp-4.5.1.tgz#cdd35dce4fa1a89a4fd42b1599eb35b3af408884"
integrity sha512-Z5ba73P98O1KUYCCJTUeVpja9RcGoMdncZ6T49FCUl2lN38JtCJ+3WgIDBv0AuY4WChU5PmtJmOCTlN6FZTFKQ==
@ -1982,7 +1982,7 @@
resolved "https://registry.yarnpkg.com/@types/js-cookie/-/js-cookie-2.2.7.tgz#226a9e31680835a6188e887f3988e60c04d3f6a3"
integrity sha512-aLkWa0C0vO5b4Sr798E26QgOkss68Un0bLjs7u9qxzPT5CG+8DuNTffWES58YzJs3hrVAOs1wonycqEBqNJubA==
"@types/json-schema@*", "@types/json-schema@^7.0.6", "@types/json-schema@^7.0.9":
"@types/json-schema@*", "@types/json-schema@^7.0.11", "@types/json-schema@^7.0.6":
version "7.0.12"
resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.12.tgz#d70faba7039d5fca54c83c7dbab41051d2b6f6cb"
integrity sha512-Hr5Jfhc9eYOQNPYO5WLDq/n4jqijdHNlDXjuAQkkt+mWdQR+XJToOHrsD4cPaMXpn6KO7y2+wM8AZEs8VpBLVA==
@ -2086,49 +2086,53 @@
resolved "https://registry.yarnpkg.com/@types/uuid/-/uuid-9.0.2.tgz#ede1d1b1e451548d44919dc226253e32a6952c4b"
integrity sha512-kNnC1GFBLuhImSnV7w4njQkUiJi0ZXUycu1rUaouPqiKlXkh77JKgdRnTAp1x5eBwcIwbtI+3otwzuIDEuDoxQ==
"@typescript-eslint/eslint-plugin@^5.60.0":
version "5.60.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.60.0.tgz#2f4bea6a3718bed2ba52905358d0f45cd3620d31"
integrity sha512-78B+anHLF1TI8Jn/cD0Q00TBYdMgjdOn980JfAVa9yw5sop8nyTfVOQAv6LWywkOGLclDBtv5z3oxN4w7jxyNg==
"@typescript-eslint/eslint-plugin@^6.0.0":
version "6.0.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-6.0.0.tgz#19ff4f1cab8d6f8c2c1825150f7a840bc5d9bdc4"
integrity sha512-xuv6ghKGoiq856Bww/yVYnXGsKa588kY3M0XK7uUW/3fJNNULKRfZfSBkMTSpqGG/8ZCXCadfh8G/z/B4aqS/A==
dependencies:
"@eslint-community/regexpp" "^4.4.0"
"@typescript-eslint/scope-manager" "5.60.0"
"@typescript-eslint/type-utils" "5.60.0"
"@typescript-eslint/utils" "5.60.0"
"@eslint-community/regexpp" "^4.5.0"
"@typescript-eslint/scope-manager" "6.0.0"
"@typescript-eslint/type-utils" "6.0.0"
"@typescript-eslint/utils" "6.0.0"
"@typescript-eslint/visitor-keys" "6.0.0"
debug "^4.3.4"
grapheme-splitter "^1.0.4"
ignore "^5.2.0"
graphemer "^1.4.0"
ignore "^5.2.4"
natural-compare "^1.4.0"
natural-compare-lite "^1.4.0"
semver "^7.3.7"
tsutils "^3.21.0"
semver "^7.5.0"
ts-api-utils "^1.0.1"
"@typescript-eslint/parser@^5.60.0":
version "5.60.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-5.60.0.tgz#08f4daf5fc6548784513524f4f2f359cebb4068a"
integrity sha512-jBONcBsDJ9UoTWrARkRRCgDz6wUggmH5RpQVlt7BimSwaTkTjwypGzKORXbR4/2Hqjk9hgwlon2rVQAjWNpkyQ==
"@typescript-eslint/parser@^6.0.0":
version "6.0.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-6.0.0.tgz#46b2600fd1f67e62fc00a28093a75f41bf7effc4"
integrity sha512-TNaufYSPrr1U8n+3xN+Yp9g31vQDJqhXzzPSHfQDLcaO4tU+mCfODPxCwf4H530zo7aUBE3QIdxCXamEnG04Tg==
dependencies:
"@typescript-eslint/scope-manager" "5.60.0"
"@typescript-eslint/types" "5.60.0"
"@typescript-eslint/typescript-estree" "5.60.0"
"@typescript-eslint/scope-manager" "6.0.0"
"@typescript-eslint/types" "6.0.0"
"@typescript-eslint/typescript-estree" "6.0.0"
"@typescript-eslint/visitor-keys" "6.0.0"
debug "^4.3.4"
"@typescript-eslint/scope-manager@5.60.0":
version "5.60.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-5.60.0.tgz#ae511967b4bd84f1d5e179bb2c82857334941c1c"
integrity sha512-hakuzcxPwXi2ihf9WQu1BbRj1e/Pd8ZZwVTG9kfbxAMZstKz8/9OoexIwnmLzShtsdap5U/CoQGRCWlSuPbYxQ==
"@typescript-eslint/scope-manager@6.0.0":
version "6.0.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-6.0.0.tgz#8ede47a37cb2b7ed82d329000437abd1113b5e11"
integrity sha512-o4q0KHlgCZTqjuaZ25nw5W57NeykZT9LiMEG4do/ovwvOcPnDO1BI5BQdCsUkjxFyrCL0cSzLjvIMfR9uo7cWg==
dependencies:
"@typescript-eslint/types" "5.60.0"
"@typescript-eslint/visitor-keys" "5.60.0"
"@typescript-eslint/types" "6.0.0"
"@typescript-eslint/visitor-keys" "6.0.0"
"@typescript-eslint/type-utils@5.60.0":
version "5.60.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/type-utils/-/type-utils-5.60.0.tgz#69b09087eb12d7513d5b07747e7d47f5533aa228"
integrity sha512-X7NsRQddORMYRFH7FWo6sA9Y/zbJ8s1x1RIAtnlj6YprbToTiQnM6vxcMu7iYhdunmoC0rUWlca13D5DVHkK2g==
"@typescript-eslint/type-utils@6.0.0":
version "6.0.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/type-utils/-/type-utils-6.0.0.tgz#0478d8a94f05e51da2877cc0500f1b3c27ac7e18"
integrity sha512-ah6LJvLgkoZ/pyJ9GAdFkzeuMZ8goV6BH7eC9FPmojrnX9yNCIsfjB+zYcnex28YO3RFvBkV6rMV6WpIqkPvoQ==
dependencies:
"@typescript-eslint/typescript-estree" "5.60.0"
"@typescript-eslint/utils" "5.60.0"
"@typescript-eslint/typescript-estree" "6.0.0"
"@typescript-eslint/utils" "6.0.0"
debug "^4.3.4"
tsutils "^3.21.0"
ts-api-utils "^1.0.1"
"@typescript-eslint/types@4.33.0":
version "4.33.0"
@ -2140,18 +2144,23 @@
resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-5.60.0.tgz#3179962b28b4790de70e2344465ec97582ce2558"
integrity sha512-ascOuoCpNZBccFVNJRSC6rPq4EmJ2NkuoKnd6LDNyAQmdDnziAtxbCGWCbefG1CNzmDvd05zO36AmB7H8RzKPA==
"@typescript-eslint/typescript-estree@5.60.0", "@typescript-eslint/typescript-estree@^5.55.0":
version "5.60.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-5.60.0.tgz#4ddf1a81d32a850de66642d9b3ad1e3254fb1600"
integrity sha512-R43thAuwarC99SnvrBmh26tc7F6sPa2B3evkXp/8q954kYL6Ro56AwASYWtEEi+4j09GbiNAHqYwNNZuNlARGQ==
"@typescript-eslint/types@6.0.0":
version "6.0.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-6.0.0.tgz#19795f515f8decbec749c448b0b5fc76d82445a1"
integrity sha512-Zk9KDggyZM6tj0AJWYYKgF0yQyrcnievdhG0g5FqyU3Y2DRxJn4yWY21sJC0QKBckbsdKKjYDV2yVrrEvuTgxg==
"@typescript-eslint/typescript-estree@6.0.0":
version "6.0.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-6.0.0.tgz#1e09aab7320e404fb9f83027ea568ac24e372f81"
integrity sha512-2zq4O7P6YCQADfmJ5OTDQTP3ktajnXIRrYAtHM9ofto/CJZV3QfJ89GEaM2BNGeSr1KgmBuLhEkz5FBkS2RQhQ==
dependencies:
"@typescript-eslint/types" "5.60.0"
"@typescript-eslint/visitor-keys" "5.60.0"
"@typescript-eslint/types" "6.0.0"
"@typescript-eslint/visitor-keys" "6.0.0"
debug "^4.3.4"
globby "^11.1.0"
is-glob "^4.0.3"
semver "^7.3.7"
tsutils "^3.21.0"
semver "^7.5.0"
ts-api-utils "^1.0.1"
"@typescript-eslint/typescript-estree@^4.33.0":
version "4.33.0"
@ -2166,19 +2175,32 @@
semver "^7.3.5"
tsutils "^3.21.0"
"@typescript-eslint/utils@5.60.0":
"@typescript-eslint/typescript-estree@^5.55.0":
version "5.60.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-5.60.0.tgz#4667c5aece82f9d4f24a667602f0f300864b554c"
integrity sha512-ba51uMqDtfLQ5+xHtwlO84vkdjrqNzOnqrnwbMHMRY8Tqeme8C2Q8Fc7LajfGR+e3/4LoYiWXUM6BpIIbHJ4hQ==
resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-5.60.0.tgz#4ddf1a81d32a850de66642d9b3ad1e3254fb1600"
integrity sha512-R43thAuwarC99SnvrBmh26tc7F6sPa2B3evkXp/8q954kYL6Ro56AwASYWtEEi+4j09GbiNAHqYwNNZuNlARGQ==
dependencies:
"@eslint-community/eslint-utils" "^4.2.0"
"@types/json-schema" "^7.0.9"
"@types/semver" "^7.3.12"
"@typescript-eslint/scope-manager" "5.60.0"
"@typescript-eslint/types" "5.60.0"
"@typescript-eslint/typescript-estree" "5.60.0"
eslint-scope "^5.1.1"
"@typescript-eslint/visitor-keys" "5.60.0"
debug "^4.3.4"
globby "^11.1.0"
is-glob "^4.0.3"
semver "^7.3.7"
tsutils "^3.21.0"
"@typescript-eslint/utils@6.0.0":
version "6.0.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-6.0.0.tgz#27a16d0d8f2719274a39417b9782f7daa3802db0"
integrity sha512-SOr6l4NB6HE4H/ktz0JVVWNXqCJTOo/mHnvIte1ZhBQ0Cvd04x5uKZa3zT6tiodL06zf5xxdK8COiDvPnQ27JQ==
dependencies:
"@eslint-community/eslint-utils" "^4.3.0"
"@types/json-schema" "^7.0.11"
"@types/semver" "^7.3.12"
"@typescript-eslint/scope-manager" "6.0.0"
"@typescript-eslint/types" "6.0.0"
"@typescript-eslint/typescript-estree" "6.0.0"
eslint-scope "^5.1.1"
semver "^7.5.0"
"@typescript-eslint/visitor-keys@4.33.0":
version "4.33.0"
@ -2196,6 +2218,14 @@
"@typescript-eslint/types" "5.60.0"
eslint-visitor-keys "^3.3.0"
"@typescript-eslint/visitor-keys@6.0.0":
version "6.0.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-6.0.0.tgz#0b49026049fbd096d2c00c5e784866bc69532a31"
integrity sha512-cvJ63l8c0yXdeT5POHpL0Q1cZoRcmRKFCtSjNGJxPkcP571EfZMcNbzWAc7oK3D1dRzm/V5EwtkANTZxqvuuUA==
dependencies:
"@typescript-eslint/types" "6.0.0"
eslint-visitor-keys "^3.4.1"
"@vitejs/plugin-react-swc@^3.3.2":
version "3.3.2"
resolved "https://registry.yarnpkg.com/@vitejs/plugin-react-swc/-/plugin-react-swc-3.3.2.tgz#34a82c1728066f48a86dfecb2f15df60f89207fb"
@ -3426,7 +3456,7 @@ eslint-visitor-keys@^3.3.0, eslint-visitor-keys@^3.4.1:
resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-3.4.1.tgz#c22c48f48942d08ca824cc526211ae400478a994"
integrity sha512-pZnmmLwYzf+kWaM/Qgrvpen51upAktaaiI01nsJD/Yr3lMOdNtq0cxkrrg16w64VtisN6okbs7Q8AfGqj4c9fA==
eslint@^8.43.0:
eslint@^8.44.0:
version "8.44.0"
resolved "https://registry.yarnpkg.com/eslint/-/eslint-8.44.0.tgz#51246e3889b259bbcd1d7d736a0c10add4f0e500"
integrity sha512-0wpHoUbDUHgNCyvFB5aXLiQVfK9B0at6gUvzy83k4kAsQ/u769TQDX6iKC+aO4upIHO9WSaA3QoXYQDHbNwf1A==
@ -4069,7 +4099,7 @@ ieee754@^1.1.13:
resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.2.1.tgz#8eb7a10a63fff25d15a57b001586d177d1b0d352"
integrity sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==
ignore@^5.2.0:
ignore@^5.2.0, ignore@^5.2.4:
version "5.2.4"
resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.2.4.tgz#a291c0c6178ff1b960befe47fcdec301674a6324"
integrity sha512-MAb38BcSbH0eHNBxn7ql2NH/kX33OkB3lZ1BNdh7ENeRChHTYsTvWrMubiIAMNS2llXEEgZ1MUOBtXChP3kaFQ==
@ -5795,6 +5825,13 @@ semver@^7.3.5, semver@^7.3.7:
dependencies:
lru-cache "^6.0.0"
semver@^7.5.0:
version "7.5.4"
resolved "https://registry.yarnpkg.com/semver/-/semver-7.5.4.tgz#483986ec4ed38e1c6c48c34894a9182dbff68a6e"
integrity sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==
dependencies:
lru-cache "^6.0.0"
semver@~7.3.0:
version "7.3.8"
resolved "https://registry.yarnpkg.com/semver/-/semver-7.3.8.tgz#07a78feafb3f7b32347d725e33de7e2a2df67798"
@ -6235,6 +6272,11 @@ tree-kill@^1.2.2:
resolved "https://registry.yarnpkg.com/tree-kill/-/tree-kill-1.2.2.tgz#4ca09a9092c88b73a7cdc5e8a01b507b0790a0cc"
integrity sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==
ts-api-utils@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/ts-api-utils/-/ts-api-utils-1.0.1.tgz#8144e811d44c749cd65b2da305a032510774452d"
integrity sha512-lC/RGlPmwdrIBFTX59wwNzqh7aR2otPNPR/5brHZm/XKFYKsfqxihXUe9pU3JI+3vGkl+vyCoNNnPhJn3aLK1A==
ts-easing@^0.2.0:
version "0.2.0"
resolved "https://registry.yarnpkg.com/ts-easing/-/ts-easing-0.2.0.tgz#c8a8a35025105566588d87dbda05dd7fbfa5a4ec"
@ -6338,6 +6380,11 @@ typed-array-length@^1.0.4:
for-each "^0.3.3"
is-typed-array "^1.1.9"
typescript-eslint@^0.0.1-alpha.0:
version "0.0.1-alpha.0"
resolved "https://registry.yarnpkg.com/typescript-eslint/-/typescript-eslint-0.0.1-alpha.0.tgz#285d68a4e96588295cd436278801bcb6a6b916c1"
integrity sha512-1hNKM37dAWML/2ltRXupOq2uqcdRQyDFphl+341NTPXFLLLiDhErXx8VtaSLh3xP7SyHZdcCgpt9boYYVb3fQg==
typescript@^3.9.10, typescript@^3.9.7:
version "3.9.10"
resolved "https://registry.yarnpkg.com/typescript/-/typescript-3.9.10.tgz#70f3910ac7a51ed6bef79da7800690b19bf778b8"
@ -6348,6 +6395,11 @@ typescript@^4.0.0, typescript@^4.9.5:
resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.9.5.tgz#095979f9bcc0d09da324d58d03ce8f8374cbe65a"
integrity sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==
typescript@^5.1.6:
version "5.1.6"
resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.1.6.tgz#02f8ac202b6dad2c0dd5e0913745b47a37998274"
integrity sha512-zaWCozRZ6DLEWAWFrVDz1H6FVXzUSfTy5FUMWsQlU8Ym5JP9eO4xkTIROFCQvhQf61z6O/G6ugw3SgAnvvm+HA==
typescript@~5.0.4:
version "5.0.4"
resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.0.4.tgz#b217fd20119bd61a94d4011274e0ab369058da3b"