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

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

View File

@@ -0,0 +1,6 @@
import { DeleteImageState } from './types';
export const initialDeleteImageState: DeleteImageState = {
imagesToDelete: [],
isModalOpen: false,
};

View File

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

View File

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

View File

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