mirror of
https://github.com/invoke-ai/InvokeAI.git
synced 2026-04-23 03:00:31 -04:00
feat: add multi-select to gallery
multi-select actions include:
- drag to board to move all to that board
- right click to add all to board or delete all
backend changes:
- add routes for changing board for list of image names, deleting list of images
- change image-specific routes to `images/i/{image_name}` to not clobber other routes (like `images/upload`, `images/delete`)
- subclass pydantic `BaseModel` as `BaseModelExcludeNull`, which excludes null values when calling `dict()` on the model. this fixes inconsistent types related to JSON parsing null values into `null` instead of `undefined`
- remove `board_id` from `remove_image_from_board`
frontend changes:
- multi-selection stuff uses `ImageDTO[]` as payloads, for dnd and other mutations. this gives us access to image `board_id`s when hitting routes, and enables efficient cache updates.
- consolidate change board and delete image modals to handle single and multiples
- board totals are now re-fetched on mutation and not kept in sync manually - was way too tedious to do this
- fixed warning about nested `<p>` elements
- closes #4088 , need to handle case when `autoAddBoardId` is `"none"`
- add option to show gallery image delete button on every gallery image
frontend refactors/organisation:
- make typegen script js instead of ts
- enable `noUncheckedIndexedAccess` to help avoid bugs when indexing into arrays, many small changes needed to satisfy TS after this
- move all image-related endpoints into `endpoints/images.ts`, its a big file now, but this fixes a number of circular dependency issues that were otherwise felt impossible to resolve
This commit is contained in:
@@ -0,0 +1,8 @@
|
||||
import { createAction } from '@reduxjs/toolkit';
|
||||
import { ImageDTO } from 'services/api/types';
|
||||
import { ImageUsage } from './types';
|
||||
|
||||
export const imageDeletionConfirmed = createAction<{
|
||||
imageDTOs: ImageDTO[];
|
||||
imagesUsage: ImageUsage[];
|
||||
}>('deleteImageModal/imageDeletionConfirmed');
|
||||
@@ -0,0 +1,6 @@
|
||||
import { DeleteImageState } from './types';
|
||||
|
||||
export const initialDeleteImageState: DeleteImageState = {
|
||||
imagesToDelete: [],
|
||||
isModalOpen: false,
|
||||
};
|
||||
@@ -0,0 +1,55 @@
|
||||
import { createSelector } from '@reduxjs/toolkit';
|
||||
import { RootState } from 'app/store/store';
|
||||
import { defaultSelectorOptions } from 'app/store/util/defaultMemoizeOptions';
|
||||
import { some } from 'lodash-es';
|
||||
import { ImageUsage } from './types';
|
||||
|
||||
export const getImageUsage = (state: RootState, image_name: string) => {
|
||||
const { generation, canvas, nodes, controlNet } = state;
|
||||
const isInitialImage = generation.initialImage?.imageName === image_name;
|
||||
|
||||
const isCanvasImage = canvas.layerState.objects.some(
|
||||
(obj) => obj.kind === 'image' && obj.imageName === image_name
|
||||
);
|
||||
|
||||
const isNodesImage = nodes.nodes.some((node) => {
|
||||
return some(
|
||||
node.data.inputs,
|
||||
(input) =>
|
||||
input.type === 'image' && input.value?.image_name === image_name
|
||||
);
|
||||
});
|
||||
|
||||
const isControlNetImage = some(
|
||||
controlNet.controlNets,
|
||||
(c) =>
|
||||
c.controlImage === image_name || c.processedControlImage === image_name
|
||||
);
|
||||
|
||||
const imageUsage: ImageUsage = {
|
||||
isInitialImage,
|
||||
isCanvasImage,
|
||||
isNodesImage,
|
||||
isControlNetImage,
|
||||
};
|
||||
|
||||
return imageUsage;
|
||||
};
|
||||
|
||||
export const selectImageUsage = createSelector(
|
||||
[(state: RootState) => state],
|
||||
(state) => {
|
||||
const { imagesToDelete } = state.deleteImageModal;
|
||||
|
||||
if (!imagesToDelete.length) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const imagesUsage = imagesToDelete.map((i) =>
|
||||
getImageUsage(state, i.image_name)
|
||||
);
|
||||
|
||||
return imagesUsage;
|
||||
},
|
||||
defaultSelectorOptions
|
||||
);
|
||||
@@ -0,0 +1,28 @@
|
||||
import { PayloadAction, createSlice } from '@reduxjs/toolkit';
|
||||
import { ImageDTO } from 'services/api/types';
|
||||
import { initialDeleteImageState } from './initialState';
|
||||
|
||||
const deleteImageModal = createSlice({
|
||||
name: 'deleteImageModal',
|
||||
initialState: initialDeleteImageState,
|
||||
reducers: {
|
||||
isModalOpenChanged: (state, action: PayloadAction<boolean>) => {
|
||||
state.isModalOpen = action.payload;
|
||||
},
|
||||
imagesToDeleteSelected: (state, action: PayloadAction<ImageDTO[]>) => {
|
||||
state.imagesToDelete = action.payload;
|
||||
},
|
||||
imageDeletionCanceled: (state) => {
|
||||
state.imagesToDelete = [];
|
||||
state.isModalOpen = false;
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
export const {
|
||||
isModalOpenChanged,
|
||||
imagesToDeleteSelected,
|
||||
imageDeletionCanceled,
|
||||
} = deleteImageModal.actions;
|
||||
|
||||
export default deleteImageModal.reducer;
|
||||
@@ -0,0 +1,13 @@
|
||||
import { ImageDTO } from 'services/api/types';
|
||||
|
||||
export type DeleteImageState = {
|
||||
imagesToDelete: ImageDTO[];
|
||||
isModalOpen: boolean;
|
||||
};
|
||||
|
||||
export type ImageUsage = {
|
||||
isInitialImage: boolean;
|
||||
isCanvasImage: boolean;
|
||||
isNodesImage: boolean;
|
||||
isControlNetImage: boolean;
|
||||
};
|
||||
Reference in New Issue
Block a user