diff --git a/invokeai/frontend/web/src/features/dnd/dnd.ts b/invokeai/frontend/web/src/features/dnd/dnd.ts index 3830a7ff7dc..9b8972e3c4a 100644 --- a/invokeai/frontend/web/src/features/dnd/dnd.ts +++ b/invokeai/frontend/web/src/features/dnd/dnd.ts @@ -221,7 +221,7 @@ const _setNodeImageFieldImage = buildTypeAndKey('set-node-image-field-image'); export type SetNodeImageFieldImageDndTargetData = DndData< typeof _setNodeImageFieldImage.type, typeof _setNodeImageFieldImage.key, - { fieldIdentifer: FieldIdentifier } + { fieldIdentifier: FieldIdentifier } >; export const setNodeImageFieldImageDndTarget: DndTarget = { @@ -236,8 +236,8 @@ export const setNodeImageFieldImageDndTarget: DndTarget { const { imageDTO } = sourceData.payload; - const { fieldIdentifer } = targetData.payload; - setNodeImageFieldImage({ fieldIdentifer, imageDTO, dispatch }); + const { fieldIdentifier } = targetData.payload; + setNodeImageFieldImage({ fieldIdentifier, imageDTO, dispatch }); }, }; //#endregion @@ -247,7 +247,7 @@ const _addImagesToNodeImageFieldCollection = buildTypeAndKey('add-images-to-imag export type AddImagesToNodeImageFieldCollection = DndData< typeof _addImagesToNodeImageFieldCollection.type, typeof _addImagesToNodeImageFieldCollection.key, - { fieldIdentifer: FieldIdentifier } + { fieldIdentifier: FieldIdentifier } >; export const addImagesToNodeImageFieldCollectionDndTarget: DndTarget< AddImagesToNodeImageFieldCollection, @@ -267,7 +267,7 @@ export const addImagesToNodeImageFieldCollectionDndTarget: DndTarget< return; } - const { fieldIdentifer } = targetData.payload; + const { fieldIdentifier } = targetData.payload; const imageDTOs: ImageDTO[] = []; if (singleImageDndSource.typeGuard(sourceData)) { @@ -276,7 +276,7 @@ export const addImagesToNodeImageFieldCollectionDndTarget: DndTarget< imageDTOs.push(...sourceData.payload.imageDTOs); } - addImagesToNodeImageFieldCollectionAction({ fieldIdentifer, imageDTOs, dispatch, getState }); + addImagesToNodeImageFieldCollectionAction({ fieldIdentifier, imageDTOs, dispatch, getState }); }, }; //#endregion diff --git a/invokeai/frontend/web/src/features/imageActions/actions.ts b/invokeai/frontend/web/src/features/imageActions/actions.ts index 237adfce36e..ca0e9f99382 100644 --- a/invokeai/frontend/web/src/features/imageActions/actions.ts +++ b/invokeai/frontend/web/src/features/imageActions/actions.ts @@ -69,35 +69,59 @@ export const setUpscaleInitialImage = (arg: { imageDTO: ImageDTO; dispatch: AppD export const setNodeImageFieldImage = (arg: { imageDTO: ImageDTO; - fieldIdentifer: FieldIdentifier; + fieldIdentifier: FieldIdentifier; dispatch: AppDispatch; }) => { - const { imageDTO, fieldIdentifer, dispatch } = arg; - dispatch(fieldImageValueChanged({ ...fieldIdentifer, value: imageDTO })); + const { imageDTO, fieldIdentifier, dispatch } = arg; + dispatch(fieldImageValueChanged({ ...fieldIdentifier, value: imageDTO })); }; export const addImagesToNodeImageFieldCollectionAction = (arg: { imageDTOs: ImageDTO[]; - fieldIdentifer: FieldIdentifier; + fieldIdentifier: FieldIdentifier; dispatch: AppDispatch; getState: () => RootState; }) => { - const { imageDTOs, fieldIdentifer, dispatch, getState } = arg; + const { imageDTOs, fieldIdentifier, dispatch, getState } = arg; const fieldInputInstance = selectFieldInputInstance( selectNodesSlice(getState()), - fieldIdentifer.nodeId, - fieldIdentifer.fieldName + fieldIdentifier.nodeId, + fieldIdentifier.fieldName ); if (!isImageFieldCollectionInputInstance(fieldInputInstance)) { - log.warn({ fieldIdentifer }, 'Attempted to add images to a non-image field collection'); + log.warn({ fieldIdentifier }, 'Attempted to add images to a non-image field collection'); return; } const images = fieldInputInstance.value ? [...fieldInputInstance.value] : []; images.push(...imageDTOs.map(({ image_name }) => ({ image_name }))); const uniqueImages = uniqBy(images, 'image_name'); - dispatch(fieldImageCollectionValueChanged({ ...fieldIdentifer, value: uniqueImages })); + dispatch(fieldImageCollectionValueChanged({ ...fieldIdentifier, value: uniqueImages })); +}; + +export const removeImageFromNodeImageFieldCollectionAction = (arg: { + imageName: string; + fieldIdentifier: FieldIdentifier; + dispatch: AppDispatch; + getState: () => RootState; +}) => { + const { imageName, fieldIdentifier, dispatch, getState } = arg; + const fieldInputInstance = selectFieldInputInstance( + selectNodesSlice(getState()), + fieldIdentifier.nodeId, + fieldIdentifier.fieldName + ); + + if (!isImageFieldCollectionInputInstance(fieldInputInstance)) { + log.warn({ fieldIdentifier }, 'Attempted to remove image from a non-image field collection'); + return; + } + + const images = fieldInputInstance.value ? [...fieldInputInstance.value] : []; + const imagesWithoutTheImageToRemove = images.filter((image) => image.image_name !== imageName); + const uniqueImages = uniqBy(imagesWithoutTheImageToRemove, 'image_name'); + dispatch(fieldImageCollectionValueChanged({ ...fieldIdentifier, value: uniqueImages })); }; export const setComparisonImage = (arg: { imageDTO: ImageDTO; dispatch: AppDispatch }) => { diff --git a/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/inputs/ImageFieldCollectionInputComponent.tsx b/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/inputs/ImageFieldCollectionInputComponent.tsx index 16aa095f31d..a36f389adc9 100644 --- a/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/inputs/ImageFieldCollectionInputComponent.tsx +++ b/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/inputs/ImageFieldCollectionInputComponent.tsx @@ -1,26 +1,30 @@ import type { SystemStyleObject } from '@invoke-ai/ui-library'; -import { Flex, Grid, GridItem, IconButton } from '@invoke-ai/ui-library'; -import { useAppDispatch } from 'app/store/storeHooks'; +import { Flex, Grid, GridItem } from '@invoke-ai/ui-library'; +import { useAppStore } from 'app/store/nanostores/store'; +import { IAINoContentFallback, IAINoContentFallbackWithSpinner } from 'common/components/IAIImageFallback'; import { UploadMultipleImageButton } from 'common/hooks/useImageUploadButton'; import type { AddImagesToNodeImageFieldCollection } from 'features/dnd/dnd'; import { addImagesToNodeImageFieldCollectionDndTarget } from 'features/dnd/dnd'; import { DndDropTarget } from 'features/dnd/DndDropTarget'; -import { DndImageFromImageName } from 'features/dnd/DndImageFromImageName'; +import { DndImage } from 'features/dnd/DndImage'; +import { DndImageIcon } from 'features/dnd/DndImageIcon'; +import { removeImageFromNodeImageFieldCollectionAction } from 'features/imageActions/actions'; import { useFieldIsInvalid } from 'features/nodes/hooks/useFieldIsInvalid'; import { fieldImageCollectionValueChanged } from 'features/nodes/store/nodesSlice'; import type { ImageFieldCollectionInputInstance, ImageFieldCollectionInputTemplate } from 'features/nodes/types/field'; import { memo, useCallback, useMemo } from 'react'; import { useTranslation } from 'react-i18next'; -import { PiArrowCounterClockwiseBold } from 'react-icons/pi'; +import { PiArrowCounterClockwiseBold, PiExclamationMarkBold } from 'react-icons/pi'; +import { useGetImageDTOQuery } from 'services/api/endpoints/images'; import type { ImageDTO } from 'services/api/types'; import type { FieldComponentProps } from './types'; const sx = { + borderWidth: 1, '&[data-error=true]': { borderColor: 'error.500', borderStyle: 'solid', - borderWidth: 1, }, } satisfies SystemStyleObject; @@ -28,27 +32,19 @@ export const ImageFieldCollectionInputComponent = memo( (props: FieldComponentProps) => { const { t } = useTranslation(); const { nodeId, field } = props; - const dispatch = useAppDispatch(); - const isInvalid = useFieldIsInvalid(nodeId, field.name); + const store = useAppStore(); - const onReset = useCallback(() => { - dispatch( - fieldImageCollectionValueChanged({ - nodeId, - fieldName: field.name, - value: [], - }) - ); - }, [dispatch, field.name, nodeId]); + const isInvalid = useFieldIsInvalid(nodeId, field.name); const dndTargetData = useMemo( - () => addImagesToNodeImageFieldCollectionDndTarget.getData({ fieldIdentifer: { nodeId, fieldName: field.name } }), + () => + addImagesToNodeImageFieldCollectionDndTarget.getData({ fieldIdentifier: { nodeId, fieldName: field.name } }), [field, nodeId] ); const onUpload = useCallback( (imageDTOs: ImageDTO[]) => { - dispatch( + store.dispatch( fieldImageCollectionValueChanged({ nodeId, fieldName: field.name, @@ -56,7 +52,19 @@ export const ImageFieldCollectionInputComponent = memo( }) ); }, - [dispatch, field.name, nodeId] + [store, nodeId, field.name] + ); + + const onRemoveImage = useCallback( + (imageName: string) => { + removeImageFromNodeImageFieldCollectionAction({ + imageName, + fieldIdentifier: { nodeId, fieldName: field.name }, + dispatch: store.dispatch, + getState: store.getState, + }); + }, + [field.name, nodeId, store.dispatch, store.getState] ); return ( @@ -80,33 +88,23 @@ export const ImageFieldCollectionInputComponent = memo( /> )} {field.value && field.value.length > 0 && ( - <> - - {field.value.map(({ image_name }) => ( - - - - ))} - - } - position="absolute" - top={0} - insetInlineEnd={0} - onClick={onReset} - /> - + + {field.value.map(({ image_name }) => ( + + + + ))} + )} void }) => { + const query = useGetImageDTOQuery(imageName); + const onClickRemove = useCallback(() => { + onRemoveImage(imageName); + }, [imageName, onRemoveImage]); + + if (query.isLoading) { + return ; + } + + if (!query.data) { + return } />; + } + + return ( + <> + + } + tooltip="Reset Image" + position="absolute" + flexDir="column" + top={1} + insetInlineEnd={1} + gap={1} + /> + + ); + } +); +ImageGridItemContent.displayName = 'ImageGridItemContent'; diff --git a/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/inputs/ImageFieldInputComponent.tsx b/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/inputs/ImageFieldInputComponent.tsx index 8935d284fa9..cabe4d54045 100644 --- a/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/inputs/ImageFieldInputComponent.tsx +++ b/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/inputs/ImageFieldInputComponent.tsx @@ -38,7 +38,7 @@ const ImageFieldInputComponent = (props: FieldComponentProps( () => setNodeImageFieldImageDndTarget.getData( - { fieldIdentifer: { nodeId, fieldName: field.name } }, + { fieldIdentifier: { nodeId, fieldName: field.name } }, field.value?.image_name ), [field, nodeId] @@ -85,13 +85,16 @@ const ImageFieldInputComponent = (props: FieldComponentProps - - : undefined} - tooltip="Reset Image" - /> - + : undefined} + tooltip="Reset Image" + position="absolute" + flexDir="column" + top={1} + insetInlineEnd={1} + gap={1} + /> )}