Skip to content

Commit 1afc2cb

Browse files
feat(ui): support different labels for external drop targets (e.g. uploads)
1 parent ee83592 commit 1afc2cb

File tree

12 files changed

+68
-162
lines changed

12 files changed

+68
-162
lines changed

invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/imageUploaded.ts

Lines changed: 27 additions & 89 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,8 @@
11
import { logger } from 'app/logging/logger';
22
import type { AppStartListening } from 'app/store/middleware/listenerMiddleware';
33
import type { RootState } from 'app/store/store';
4-
import {
5-
entityRasterized,
6-
entitySelected,
7-
referenceImageIPAdapterImageChanged,
8-
rgIPAdapterImageChanged,
9-
} from 'features/controlLayers/store/canvasSlice';
10-
import { selectCanvasSlice } from 'features/controlLayers/store/selectors';
11-
import { imageDTOToImageObject } from 'features/controlLayers/store/util';
124
import { selectListBoardsQueryArgs } from 'features/gallery/store/gallerySelectors';
135
import { boardIdSelected, galleryViewChanged } from 'features/gallery/store/gallerySlice';
14-
import { fieldImageValueChanged } from 'features/nodes/store/nodesSlice';
15-
import { upscaleInitialImageChanged } from 'features/parameters/store/upscaleSlice';
166
import { toast } from 'features/toast/toast';
177
import { t } from 'i18next';
188
import { omit } from 'lodash-es';
@@ -51,93 +41,41 @@ export const addImageUploadedFulfilledListener = (startAppListening: AppStartLis
5141

5242
log.debug({ imageDTO }, 'Image uploaded');
5343

54-
const { postUploadAction } = action.meta.arg.originalArgs;
55-
56-
if (!postUploadAction) {
57-
return;
58-
}
59-
6044
const DEFAULT_UPLOADED_TOAST = {
6145
id: 'IMAGE_UPLOADED',
6246
title: t('toast.imageUploaded'),
6347
status: 'success',
6448
} as const;
6549

6650
// default action - just upload and alert user
67-
if (postUploadAction.type === 'TOAST') {
68-
const boardId = imageDTO.board_id ?? 'none';
69-
if (lastUploadedToastTimeout !== null) {
70-
window.clearTimeout(lastUploadedToastTimeout);
71-
}
72-
const toastApi = toast({
73-
...DEFAULT_UPLOADED_TOAST,
74-
title: postUploadAction.title || DEFAULT_UPLOADED_TOAST.title,
75-
description: getUploadedToastDescription(boardId, state),
76-
duration: null, // we will close the toast manually
77-
});
78-
lastUploadedToastTimeout = window.setTimeout(() => {
79-
toastApi.close();
80-
}, 3000);
81-
/**
82-
* We only want to change the board and view if this is the first upload of a batch, else we end up hijacking
83-
* the user's gallery board and view selection:
84-
* - User uploads multiple images
85-
* - A couple uploads finish, but others are pending still
86-
* - User changes the board selection
87-
* - Pending uploads finish and change the board back to the original board
88-
* - User is confused as to why the board changed
89-
*
90-
* Default to true to not require _all_ image upload handlers to set this value
91-
*/
92-
const isFirstUploadOfBatch = action.meta.arg.originalArgs.isFirstUploadOfBatch ?? true;
93-
if (isFirstUploadOfBatch) {
94-
dispatch(boardIdSelected({ boardId }));
95-
dispatch(galleryViewChanged('assets'));
96-
}
97-
return;
98-
}
99-
100-
if (postUploadAction.type === 'SET_UPSCALE_INITIAL_IMAGE') {
101-
dispatch(upscaleInitialImageChanged(imageDTO));
102-
toast({
103-
...DEFAULT_UPLOADED_TOAST,
104-
description: 'set as upscale initial image',
105-
});
106-
return;
107-
}
108-
109-
if (postUploadAction.type === 'SET_IPA_IMAGE') {
110-
const { id } = postUploadAction;
111-
dispatch(referenceImageIPAdapterImageChanged({ entityIdentifier: { id, type: 'reference_image' }, imageDTO }));
112-
toast({ ...DEFAULT_UPLOADED_TOAST, description: t('toast.setControlImage') });
113-
return;
51+
const boardId = imageDTO.board_id ?? 'none';
52+
if (lastUploadedToastTimeout !== null) {
53+
window.clearTimeout(lastUploadedToastTimeout);
11454
}
115-
116-
if (postUploadAction.type === 'SET_RG_IP_ADAPTER_IMAGE') {
117-
const { id, referenceImageId } = postUploadAction;
118-
dispatch(
119-
rgIPAdapterImageChanged({ entityIdentifier: { id, type: 'regional_guidance' }, referenceImageId, imageDTO })
120-
);
121-
toast({ ...DEFAULT_UPLOADED_TOAST, description: t('toast.setControlImage') });
122-
return;
123-
}
124-
125-
if (postUploadAction.type === 'SET_NODES_IMAGE') {
126-
const { nodeId, fieldName } = postUploadAction;
127-
dispatch(fieldImageValueChanged({ nodeId, fieldName, value: imageDTO }));
128-
toast({ ...DEFAULT_UPLOADED_TOAST, description: `${t('toast.setNodeField')} ${fieldName}` });
129-
return;
130-
}
131-
132-
if (postUploadAction.type === 'REPLACE_LAYER_WITH_IMAGE') {
133-
const { entityIdentifier } = postUploadAction;
134-
135-
const state = getState();
136-
const imageObject = imageDTOToImageObject(imageDTO);
137-
const { x, y } = selectCanvasSlice(state).bbox.rect;
138-
dispatch(entityRasterized({ entityIdentifier, imageObject, position: { x, y }, replaceObjects: true }));
139-
dispatch(entitySelected({ entityIdentifier }));
140-
return;
55+
const toastApi = toast({
56+
...DEFAULT_UPLOADED_TOAST,
57+
title: DEFAULT_UPLOADED_TOAST.title,
58+
description: getUploadedToastDescription(boardId, state),
59+
duration: null, // we will close the toast manually
60+
});
61+
lastUploadedToastTimeout = window.setTimeout(() => {
62+
toastApi.close();
63+
}, 3000);
64+
/**
65+
* We only want to change the board and view if this is the first upload of a batch, else we end up hijacking
66+
* the user's gallery board and view selection:
67+
* - User uploads multiple images
68+
* - A couple uploads finish, but others are pending still
69+
* - User changes the board selection
70+
* - Pending uploads finish and change the board back to the original board
71+
* - User is confused as to why the board changed
72+
*
73+
* Default to true to not require _all_ image upload handlers to set this value
74+
*/
75+
const isFirstUploadOfBatch = action.meta.arg.originalArgs.isFirstUploadOfBatch ?? true;
76+
if (isFirstUploadOfBatch) {
77+
dispatch(boardIdSelected({ boardId }));
78+
dispatch(galleryViewChanged('assets'));
14179
}
14280
},
14381
});

invokeai/frontend/web/src/features/controlLayers/components/CanvasDropArea.tsx

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { Grid, GridItem } from '@invoke-ai/ui-library';
2+
import { useCanvasIsBusy } from 'features/controlLayers/hooks/useCanvasIsBusy';
23
import { Dnd } from 'features/dnd/dnd';
34
import { DndDropTarget } from 'features/dnd/DndDropTarget';
45
import { useImageViewer } from 'features/gallery/components/ImageViewer/useImageViewer';
@@ -14,6 +15,7 @@ const addGlobalReferenceImageFromImageDndTargetData = Dnd.Target.newGlobalRefere
1415
export const CanvasDropArea = memo(() => {
1516
const { t } = useTranslation();
1617
const imageViewer = useImageViewer();
18+
const isBusy = useCanvasIsBusy();
1719

1820
if (imageViewer.isOpen) {
1921
return null;
@@ -33,27 +35,31 @@ export const CanvasDropArea = memo(() => {
3335
>
3436
<GridItem position="relative">
3537
<DndDropTarget
36-
label={t('controlLayers.canvasContextMenu.newRasterLayer')}
3738
targetData={addRasterLayerFromImageDndTargetData}
39+
label={t('controlLayers.canvasContextMenu.newRasterLayer')}
40+
isDisabled={isBusy}
3841
/>
3942
</GridItem>
4043
<GridItem position="relative">
4144
<DndDropTarget
42-
label={t('controlLayers.canvasContextMenu.newControlLayer')}
4345
targetData={addControlLayerFromImageDndTargetData}
46+
label={t('controlLayers.canvasContextMenu.newControlLayer')}
47+
isDisabled={isBusy}
4448
/>
4549
</GridItem>
4650

4751
<GridItem position="relative">
4852
<DndDropTarget
49-
label={t('controlLayers.canvasContextMenu.newRegionalReferenceImage')}
5053
targetData={addRegionalGuidanceReferenceImageFromImageDndTargetData}
54+
label={t('controlLayers.canvasContextMenu.newRegionalReferenceImage')}
55+
isDisabled={isBusy}
5156
/>
5257
</GridItem>
5358
<GridItem position="relative">
5459
<DndDropTarget
55-
label={t('controlLayers.canvasContextMenu.newGlobalReferenceImage')}
5660
targetData={addGlobalReferenceImageFromImageDndTargetData}
61+
label={t('controlLayers.canvasContextMenu.newGlobalReferenceImage')}
62+
isDisabled={isBusy}
5763
/>
5864
</GridItem>
5965
</Grid>

invokeai/frontend/web/src/features/controlLayers/components/ControlLayer/ControlLayer.tsx

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import { ControlLayerBadges } from 'features/controlLayers/components/ControlLay
99
import { ControlLayerSettings } from 'features/controlLayers/components/ControlLayer/ControlLayerSettings';
1010
import { ControlLayerAdapterGate } from 'features/controlLayers/contexts/EntityAdapterContext';
1111
import { EntityIdentifierContext } from 'features/controlLayers/contexts/EntityIdentifierContext';
12+
import { useCanvasIsBusy } from 'features/controlLayers/hooks/useCanvasIsBusy';
1213
import type { CanvasEntityIdentifier } from 'features/controlLayers/store/types';
1314
import { Dnd } from 'features/dnd/dnd';
1415
import { DndDropTarget } from 'features/dnd/DndDropTarget';
@@ -21,6 +22,7 @@ type Props = {
2122

2223
export const ControlLayer = memo(({ id }: Props) => {
2324
const { t } = useTranslation();
25+
const isBusy = useCanvasIsBusy();
2426
const entityIdentifier = useMemo<CanvasEntityIdentifier<'control_layer'>>(
2527
() => ({ id, type: 'control_layer' }),
2628
[id]
@@ -44,7 +46,7 @@ export const ControlLayer = memo(({ id }: Props) => {
4446
<CanvasEntitySettingsWrapper>
4547
<ControlLayerSettings />
4648
</CanvasEntitySettingsWrapper>
47-
<DndDropTarget targetData={targetData} label={t('controlLayers.replaceLayer')} />
49+
<DndDropTarget targetData={targetData} label={t('controlLayers.replaceLayer')} isDisabled={isBusy} />
4850
</CanvasEntityContainer>
4951
</ControlLayerAdapterGate>
5052
</EntityIdentifierContext.Provider>

invokeai/frontend/web/src/features/controlLayers/components/IPAdapter/IPAdapterImagePreview.tsx

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ import { memo, useCallback, useEffect } from 'react';
1111
import { useTranslation } from 'react-i18next';
1212
import { PiArrowCounterClockwiseBold } from 'react-icons/pi';
1313
import { useGetImageDTOQuery } from 'services/api/endpoints/images';
14-
import type { ImageDTO, PostUploadAction } from 'services/api/types';
14+
import type { ImageDTO } from 'services/api/types';
1515
import { $isConnected } from 'services/events/stores';
1616

1717
const sx = {
@@ -32,10 +32,9 @@ type Props = {
3232
image: ImageWithDims | null;
3333
onChangeImage: (imageDTO: ImageDTO | null) => void;
3434
targetData: Dnd.types['TargetDataUnion'];
35-
postUploadAction: PostUploadAction;
3635
};
3736

38-
export const IPAdapterImagePreview = memo(({ image, onChangeImage, targetData, postUploadAction }: Props) => {
37+
export const IPAdapterImagePreview = memo(({ image, onChangeImage, targetData }: Props) => {
3938
const { t } = useTranslation();
4039
const isConnected = useStore($isConnected);
4140
const { currentData: imageDTO, isError } = useGetImageDTOQuery(image?.image_name ?? skipToken);

invokeai/frontend/web/src/features/controlLayers/components/IPAdapter/IPAdapterSettings.tsx

Lines changed: 2 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ import { Dnd } from 'features/dnd/dnd';
2323
import { memo, useCallback, useMemo } from 'react';
2424
import { useTranslation } from 'react-i18next';
2525
import { PiBoundingBoxBold } from 'react-icons/pi';
26-
import type { ImageDTO, IPAdapterModelConfig, IPALayerImagePostUploadAction } from 'services/api/types';
26+
import type { ImageDTO, IPAdapterModelConfig } from 'services/api/types';
2727

2828
import { IPAdapterImagePreview } from './IPAdapterImagePreview';
2929
import { IPAdapterModel } from './IPAdapterModel';
@@ -80,10 +80,6 @@ export const IPAdapterSettings = memo(() => {
8080
[dispatch, entityIdentifier]
8181
);
8282

83-
const postUploadAction = useMemo<IPALayerImagePostUploadAction>(
84-
() => ({ type: 'SET_IPA_IMAGE', id: entityIdentifier.id }),
85-
[entityIdentifier.id]
86-
);
8783
const targetData = useMemo<Dnd.types['TargetDataTypeMap']['setGlobalReferenceImage']>(
8884
() => Dnd.Target.setGlobalReferenceImage.getData({ entityIdentifier }, ipAdapter.image?.image_name),
8985
[entityIdentifier, ipAdapter.image?.image_name]
@@ -121,12 +117,7 @@ export const IPAdapterSettings = memo(() => {
121117
<BeginEndStepPct beginEndStepPct={ipAdapter.beginEndStepPct} onChange={onChangeBeginEndStepPct} />
122118
</Flex>
123119
<Flex alignItems="center" justifyContent="center" h={32} w={32} aspectRatio="1/1">
124-
<IPAdapterImagePreview
125-
image={ipAdapter.image}
126-
onChangeImage={onChangeImage}
127-
targetData={targetData}
128-
postUploadAction={postUploadAction}
129-
/>
120+
<IPAdapterImagePreview image={ipAdapter.image} onChangeImage={onChangeImage} targetData={targetData} />
130121
</Flex>
131122
</Flex>
132123
</Flex>

invokeai/frontend/web/src/features/controlLayers/components/RasterLayer/RasterLayer.tsx

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import { CanvasEntityPreviewImage } from 'features/controlLayers/components/comm
66
import { CanvasEntityEditableTitle } from 'features/controlLayers/components/common/CanvasEntityTitleEdit';
77
import { RasterLayerAdapterGate } from 'features/controlLayers/contexts/EntityAdapterContext';
88
import { EntityIdentifierContext } from 'features/controlLayers/contexts/EntityIdentifierContext';
9+
import { useCanvasIsBusy } from 'features/controlLayers/hooks/useCanvasIsBusy';
910
import type { CanvasEntityIdentifier } from 'features/controlLayers/store/types';
1011
import { Dnd } from 'features/dnd/dnd';
1112
import { DndDropTarget } from 'features/dnd/DndDropTarget';
@@ -18,6 +19,7 @@ type Props = {
1819

1920
export const RasterLayer = memo(({ id }: Props) => {
2021
const { t } = useTranslation();
22+
const isBusy = useCanvasIsBusy();
2123
const entityIdentifier = useMemo<CanvasEntityIdentifier<'raster_layer'>>(() => ({ id, type: 'raster_layer' }), [id]);
2224
const targetData = useMemo<Dnd.types['TargetDataTypeMap']['replaceLayerWithImage']>(
2325
() => Dnd.Target.replaceLayerWithImage.getData({ entityIdentifier }, entityIdentifier.id),
@@ -34,7 +36,7 @@ export const RasterLayer = memo(({ id }: Props) => {
3436
<Spacer />
3537
<CanvasEntityHeaderCommonActions />
3638
</CanvasEntityHeader>
37-
<DndDropTarget targetData={targetData} label={t('controlLayers.replaceLayer')} />
39+
<DndDropTarget targetData={targetData} label={t('controlLayers.replaceLayer')} isDisabled={isBusy} />
3840
</CanvasEntityContainer>
3941
</RasterLayerAdapterGate>
4042
</EntityIdentifierContext.Provider>

invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RegionalGuidanceIPAdapterSettings.tsx

Lines changed: 2 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ import { Dnd } from 'features/dnd/dnd';
2424
import { memo, useCallback, useMemo } from 'react';
2525
import { useTranslation } from 'react-i18next';
2626
import { PiBoundingBoxBold, PiTrashSimpleFill } from 'react-icons/pi';
27-
import type { ImageDTO, IPAdapterModelConfig, RGIPAdapterImagePostUploadAction } from 'services/api/types';
27+
import type { ImageDTO, IPAdapterModelConfig } from 'services/api/types';
2828
import { assert } from 'tsafe';
2929

3030
type Props = {
@@ -100,10 +100,6 @@ export const RegionalGuidanceIPAdapterSettings = memo(({ referenceImageId }: Pro
100100
[entityIdentifier, ipAdapter.image?.image_name, referenceImageId]
101101
);
102102

103-
const postUploadAction = useMemo<RGIPAdapterImagePostUploadAction>(
104-
() => ({ type: 'SET_RG_IP_ADAPTER_IMAGE', id: entityIdentifier.id, referenceImageId: referenceImageId }),
105-
[entityIdentifier.id, referenceImageId]
106-
);
107103
const pullBboxIntoIPAdapter = usePullBboxIntoRegionalGuidanceReferenceImage(entityIdentifier, referenceImageId);
108104
const isBusy = useCanvasIsBusy();
109105

@@ -151,12 +147,7 @@ export const RegionalGuidanceIPAdapterSettings = memo(({ referenceImageId }: Pro
151147
<BeginEndStepPct beginEndStepPct={ipAdapter.beginEndStepPct} onChange={onChangeBeginEndStepPct} />
152148
</Flex>
153149
<Flex alignItems="center" justifyContent="center" h={32} w={32} aspectRatio="1/1">
154-
<IPAdapterImagePreview
155-
image={ipAdapter.image}
156-
onChangeImage={onChangeImage}
157-
targetData={targetData}
158-
postUploadAction={postUploadAction}
159-
/>
150+
<IPAdapterImagePreview image={ipAdapter.image} onChangeImage={onChangeImage} targetData={targetData} />
160151
</Flex>
161152
</Flex>
162153
</Flex>

0 commit comments

Comments
 (0)