InvokeAI/invokeai/frontend/web/src/common/hooks/useImageUploadButton.tsx
psychedelicious bf94412d14 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
2023-08-03 11:46:59 +10:00

72 lines
2.0 KiB
TypeScript

import { useAppSelector } from 'app/store/storeHooks';
import { useCallback } from 'react';
import { useDropzone } from 'react-dropzone';
import { useUploadImageMutation } from 'services/api/endpoints/images';
import { PostUploadAction } from 'services/api/types';
type UseImageUploadButtonArgs = {
postUploadAction?: PostUploadAction;
isDisabled?: boolean;
};
/**
* Provides image uploader functionality to any component.
*
* @example
* const { getUploadButtonProps, getUploadInputProps, openUploader } = useImageUploadButton({
* postUploadAction: {
* type: 'SET_CONTROLNET_IMAGE',
* controlNetId: '12345',
* },
* isDisabled: getIsUploadDisabled(),
* });
*
* // open the uploaded directly
* const handleSomething = () => { openUploader() }
*
* // in the render function
* <Button {...getUploadButtonProps()} /> // will open the file dialog on click
* <input {...getUploadInputProps()} /> // hidden, handles native upload functionality
*/
export const useImageUploadButton = ({
postUploadAction,
isDisabled,
}: UseImageUploadButtonArgs) => {
const autoAddBoardId = useAppSelector(
(state) => state.gallery.autoAddBoardId
);
const [uploadImage] = useUploadImageMutation();
const onDropAccepted = useCallback(
(files: File[]) => {
const file = files[0];
if (!file) {
return;
}
uploadImage({
file,
image_category: 'user',
is_intermediate: false,
postUploadAction: postUploadAction ?? { type: 'TOAST' },
board_id: autoAddBoardId === 'none' ? undefined : autoAddBoardId,
});
},
[autoAddBoardId, postUploadAction, uploadImage]
);
const {
getRootProps: getUploadButtonProps,
getInputProps: getUploadInputProps,
open: openUploader,
} = useDropzone({
accept: { 'image/png': ['.png'], 'image/jpeg': ['.jpg', '.jpeg', '.png'] },
onDropAccepted,
disabled: isDisabled,
noDrag: true,
multiple: false,
});
return { getUploadButtonProps, getUploadInputProps, openUploader };
};