Skip to content

Commit 2d974f6

Browse files
feat(ui): restore missing upload buttons
1 parent 75f0da9 commit 2d974f6

File tree

9 files changed

+100
-195
lines changed

9 files changed

+100
-195
lines changed

invokeai/frontend/web/src/common/hooks/useImageUploadButton.tsx

Lines changed: 33 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
import { Flex, Icon, type SystemStyleObject } from '@invoke-ai/ui-library';
1+
import type { IconButtonProps, SystemStyleObject } from '@invoke-ai/ui-library';
2+
import { IconButton } from '@invoke-ai/ui-library';
23
import { logger } from 'app/logging/logger';
34
import { useAppSelector } from 'app/store/storeHooks';
45
import { selectAutoAddBoardId } from 'features/gallery/store/gallerySelectors';
@@ -8,10 +9,11 @@ import { useCallback } from 'react';
89
import type { FileRejection } from 'react-dropzone';
910
import { useDropzone } from 'react-dropzone';
1011
import { useTranslation } from 'react-i18next';
11-
import { PiUploadSimpleBold } from 'react-icons/pi';
12+
import { PiUploadBold } from 'react-icons/pi';
1213
import { uploadImages, useUploadImageMutation } from 'services/api/endpoints/images';
1314
import type { ImageDTO } from 'services/api/types';
1415
import { assert } from 'tsafe';
16+
import type { SetOptional } from 'type-fest';
1517

1618
type UseImageUploadButtonArgs =
1719
| {
@@ -48,7 +50,7 @@ const log = logger('gallery');
4850
*/
4951
export const useImageUploadButton = ({ onUpload, isDisabled, allowMultiple }: UseImageUploadButtonArgs) => {
5052
const autoAddBoardId = useAppSelector(selectAutoAddBoardId);
51-
const [uploadImage] = useUploadImageMutation();
53+
const [uploadImage, request] = useUploadImageMutation();
5254
const maxImageUploadCount = useAppSelector(selectMaxImageUploadCount);
5355
const { t } = useTranslation();
5456

@@ -128,36 +130,42 @@ export const useImageUploadButton = ({ onUpload, isDisabled, allowMultiple }: Us
128130
maxFiles: maxImageUploadCount,
129131
});
130132

131-
return { getUploadButtonProps, getUploadInputProps, openUploader };
133+
return { getUploadButtonProps, getUploadInputProps, openUploader, request };
132134
};
133135

134136
const sx = {
135-
w: 'full',
136-
h: 'full',
137-
alignItems: 'center',
138-
justifyContent: 'center',
137+
borderColor: 'error.500',
138+
borderStyle: 'solid',
139+
borderWidth: 0,
139140
borderRadius: 'base',
140-
transitionProperty: 'common',
141-
transitionDuration: '0.1s',
142-
'&[data-disabled=false]': {
143-
color: 'base.500',
144-
},
145-
'&[data-disabled=true]': {
146-
cursor: 'pointer',
147-
bg: 'base.700',
148-
_hover: {
149-
bg: 'base.650',
150-
color: 'base.300',
151-
},
141+
'&[data-error=true]': {
142+
borderWidth: 1,
152143
},
153144
} satisfies SystemStyleObject;
154145

155-
export const UploadImageButton = (props: UseImageUploadButtonArgs) => {
156-
const uploadApi = useImageUploadButton(props);
146+
export const UploadImageButton = ({
147+
isDisabled = false,
148+
onUpload,
149+
isError = false,
150+
...rest
151+
}: {
152+
onUpload?: (imageDTO: ImageDTO) => void;
153+
isError?: boolean;
154+
} & SetOptional<IconButtonProps, 'aria-label'>) => {
155+
const uploadApi = useImageUploadButton({ isDisabled, allowMultiple: false, onUpload });
157156
return (
158-
<Flex sx={sx} {...uploadApi.getUploadButtonProps()}>
159-
<Icon as={PiUploadSimpleBold} boxSize={16} />
157+
<>
158+
<IconButton
159+
aria-label="Upload image"
160+
variant="ghost"
161+
sx={sx}
162+
data-error={isError}
163+
icon={<PiUploadBold />}
164+
isLoading={uploadApi.request.isLoading}
165+
{...rest}
166+
{...uploadApi.getUploadButtonProps()}
167+
/>
160168
<input {...uploadApi.getUploadInputProps()} />
161-
</Flex>
169+
</>
162170
);
163171
};

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

Lines changed: 10 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
import type { SystemStyleObject } from '@invoke-ai/ui-library';
21
import { Flex } from '@invoke-ai/ui-library';
32
import { useStore } from '@nanostores/react';
43
import { skipToken } from '@reduxjs/toolkit/query';
@@ -15,20 +14,6 @@ import { useGetImageDTOQuery } from 'services/api/endpoints/images';
1514
import type { ImageDTO } from 'services/api/types';
1615
import { $isConnected } from 'services/events/stores';
1716

18-
const sx = {
19-
position: 'relative',
20-
w: 'full',
21-
h: 'full',
22-
alignItems: 'center',
23-
borderColor: 'error.500',
24-
borderStyle: 'solid',
25-
borderWidth: 0,
26-
borderRadius: 'base',
27-
'&[data-error=true]': {
28-
borderWidth: 1,
29-
},
30-
} satisfies SystemStyleObject;
31-
3217
type Props<T extends typeof setGlobalReferenceImageDndTarget | typeof setRegionalGuidanceReferenceImageDndTarget> = {
3318
image: ImageWithDims | null;
3419
onChangeImage: (imageDTO: ImageDTO | null) => void;
@@ -64,8 +49,16 @@ export const IPAdapterImagePreview = memo(
6449
);
6550

6651
return (
67-
<Flex sx={sx} data-error={!imageDTO && !image?.image_name}>
68-
{!imageDTO && <UploadImageButton allowMultiple={false} onUpload={onUpload} />}
52+
<Flex position="relative" w="full" h="full" alignItems="center" data-error={!imageDTO && !image?.image_name}>
53+
{!imageDTO && (
54+
<UploadImageButton
55+
w="full"
56+
h="full"
57+
isError={!imageDTO && !image?.image_name}
58+
onUpload={onUpload}
59+
fontSize={36}
60+
/>
61+
)}
6962
{imageDTO && (
7063
<>
7164
<DndImage imageDTO={imageDTO} />

invokeai/frontend/web/src/features/controlLayers/hooks/useSaveLayerToAssets.ts

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,9 @@ import type { CanvasEntityAdapterRegionalGuidance } from 'features/controlLayers
66
import { canvasToBlob } from 'features/controlLayers/konva/util';
77
import { selectAutoAddBoardId } from 'features/gallery/store/gallerySelectors';
88
import { useCallback } from 'react';
9-
import { useUploadImageMutation } from 'services/api/endpoints/images';
9+
import { uploadImage } from 'services/api/endpoints/images';
1010

1111
export const useSaveLayerToAssets = () => {
12-
const [uploadImage] = useUploadImageMutation();
1312
const autoAddBoardId = useAppSelector(selectAutoAddBoardId);
1413

1514
const saveLayerToAssets = useCallback(
@@ -34,7 +33,7 @@ export const useSaveLayerToAssets = () => {
3433
board_id: autoAddBoardId === 'none' ? undefined : autoAddBoardId,
3534
});
3635
},
37-
[autoAddBoardId, uploadImage]
36+
[autoAddBoardId]
3837
);
3938

4039
return saveLayerToAssets;

invokeai/frontend/web/src/features/dnd/DndDropTarget.tsx

Lines changed: 2 additions & 107 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,5 @@
11
import { combine } from '@atlaskit/pragmatic-drag-and-drop/combine';
22
import { dropTargetForElements, monitorForElements } from '@atlaskit/pragmatic-drag-and-drop/element/adapter';
3-
import { dropTargetForExternal, monitorForExternal } from '@atlaskit/pragmatic-drag-and-drop/external/adapter';
4-
import { containsFiles, getFiles } from '@atlaskit/pragmatic-drag-and-drop/external/file';
5-
import { preventUnhandled } from '@atlaskit/pragmatic-drag-and-drop/prevent-unhandled';
63
import type { SystemStyleObject } from '@invoke-ai/ui-library';
74
import { Box } from '@invoke-ai/ui-library';
85
import { getStore } from 'app/store/nanostores/store';
@@ -11,17 +8,6 @@ import type { AnyDndTarget } from 'features/dnd/dnd';
118
import { DndDropOverlay } from 'features/dnd/DndDropOverlay';
129
import type { DndTargetState } from 'features/dnd/types';
1310
import { memo, useEffect, useRef, useState } from 'react';
14-
import { uploadImage } from 'services/api/endpoints/images';
15-
import { z } from 'zod';
16-
17-
const ACCEPTED_IMAGE_TYPES = ['image/png', 'image/jpg', 'image/jpeg'];
18-
const ACCEPTED_FILE_EXTENSIONS = ['.png', '.jpg', '.jpeg'];
19-
const MAX_IMAGE_SIZE = 4; //In MegaBytes
20-
21-
const sizeInMB = (sizeInBytes: number, decimalsNum = 2) => {
22-
const result = sizeInBytes / (1024 * 1024);
23-
return +result.toFixed(decimalsNum);
24-
};
2511

2612
const sx = {
2713
position: 'absolute',
@@ -38,39 +24,16 @@ const sx = {
3824
},
3925
} satisfies SystemStyleObject;
4026

41-
const zUploadFile = z
42-
.custom<File>()
43-
.refine(
44-
(file) => {
45-
return sizeInMB(file.size) <= MAX_IMAGE_SIZE;
46-
},
47-
() => ({ message: `The maximum image size is ${MAX_IMAGE_SIZE}MB` })
48-
)
49-
.refine(
50-
(file) => {
51-
return ACCEPTED_IMAGE_TYPES.includes(file.type);
52-
},
53-
(file) => ({ message: `File type ${file.type} is not supported` })
54-
)
55-
.refine(
56-
(file) => {
57-
return ACCEPTED_FILE_EXTENSIONS.some((ext) => file.name.endsWith(ext));
58-
},
59-
(file) => ({ message: `File extension .${file.name.split('.').at(-1)} is not supported` })
60-
);
61-
6227
type Props<T extends AnyDndTarget> = {
6328
dndTarget: T;
6429
dndTargetData: ReturnType<T['getData']>;
6530
label: string;
66-
externalLabel?: string;
6731
isDisabled?: boolean;
6832
};
6933

7034
export const DndDropTarget = memo(<T extends AnyDndTarget>(props: Props<T>) => {
71-
const { dndTarget, dndTargetData, label, externalLabel = label, isDisabled } = props;
35+
const { dndTarget, dndTargetData, label, isDisabled } = props;
7236
const [dndState, setDndState] = useState<DndTargetState>('idle');
73-
const [dndOrigin, setDndOrigin] = useState<'element' | 'external' | null>(null);
7437
const ref = useRef<HTMLDivElement>(null);
7538
const dispatch = useAppDispatch();
7639

@@ -112,86 +75,18 @@ export const DndDropTarget = memo(<T extends AnyDndTarget>(props: Props<T>) => {
11275
return dndTarget.isValid(arg);
11376
},
11477
onDragStart: () => {
115-
setDndOrigin('element');
11678
setDndState('potential');
11779
},
11880
onDrop: () => {
119-
setDndOrigin(null);
12081
setDndState('idle');
12182
},
12283
})
12384
);
12485
}, [dispatch, isDisabled, dndTarget, dndTargetData]);
12586

126-
useEffect(() => {
127-
const element = ref.current;
128-
if (!element) {
129-
return;
130-
}
131-
if (isDisabled) {
132-
return;
133-
}
134-
135-
return combine(
136-
dropTargetForExternal({
137-
element,
138-
canDrop: (args) => {
139-
if (!containsFiles(args)) {
140-
return false;
141-
}
142-
return true;
143-
},
144-
onDragEnter: () => {
145-
setDndState('over');
146-
},
147-
onDragLeave: () => {
148-
setDndState('potential');
149-
},
150-
onDrop: async ({ source }) => {
151-
const files = await getFiles({ source });
152-
for (const file of files) {
153-
if (file === null) {
154-
continue;
155-
}
156-
if (!zUploadFile.safeParse(file).success) {
157-
continue;
158-
}
159-
const imageDTO = await uploadImage({
160-
type: 'file',
161-
file: file,
162-
image_category: 'user',
163-
is_intermediate: false,
164-
});
165-
}
166-
},
167-
}),
168-
monitorForExternal({
169-
canMonitor: (args) => {
170-
if (!containsFiles(args)) {
171-
return false;
172-
}
173-
return true;
174-
},
175-
onDragStart: () => {
176-
setDndOrigin('external');
177-
setDndState('potential');
178-
preventUnhandled.start();
179-
},
180-
onDrop: () => {
181-
setDndOrigin(null);
182-
setDndState('idle');
183-
preventUnhandled.stop();
184-
},
185-
})
186-
);
187-
}, [dispatch, isDisabled]);
188-
18987
return (
19088
<Box ref={ref} sx={sx} data-dnd-state={dndState}>
191-
<DndDropOverlay
192-
dndState={dndState}
193-
label={dndOrigin === 'element' ? label : dndOrigin === 'external' ? externalLabel : undefined}
194-
/>
89+
<DndDropOverlay dndState={dndState} label={label} />
19590
</Box>
19691
);
19792
});

invokeai/frontend/web/src/features/dnd/DndImage.tsx

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -22,9 +22,10 @@ const sx = {
2222

2323
type Props = ImageProps & {
2424
imageDTO: ImageDTO;
25+
asThumbnail?: boolean;
2526
};
2627

27-
export const DndImage = memo(({ imageDTO, ...rest }: Props) => {
28+
export const DndImage = memo(({ imageDTO, asThumbnail, ...rest }: Props) => {
2829
const store = useAppStore();
2930
const [isDragging, setIsDragging] = useState(false);
3031
const [element, ref] = useState<HTMLImageElement | null>(null);
@@ -62,8 +63,8 @@ export const DndImage = memo(({ imageDTO, ...rest }: Props) => {
6263
<Image
6364
role="button"
6465
ref={ref}
65-
src={imageDTO.image_url}
66-
fallbackSrc={imageDTO.thumbnail_url}
66+
src={asThumbnail ? imageDTO.thumbnail_url : imageDTO.image_url}
67+
fallbackSrc={asThumbnail ? undefined : imageDTO.thumbnail_url}
6768
w={imageDTO.width}
6869
sx={sx}
6970
data-is-dragging={isDragging}

invokeai/frontend/web/src/features/dnd/useDndMonitor.ts

Lines changed: 0 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,5 @@
11
import { combine } from '@atlaskit/pragmatic-drag-and-drop/combine';
22
import { monitorForElements } from '@atlaskit/pragmatic-drag-and-drop/element/adapter';
3-
import { monitorForExternal } from '@atlaskit/pragmatic-drag-and-drop/external/adapter';
4-
import { containsFiles } from '@atlaskit/pragmatic-drag-and-drop/external/file';
5-
import { preventUnhandled } from '@atlaskit/pragmatic-drag-and-drop/prevent-unhandled';
63
import { logger } from 'app/logging/logger';
74
import { getStore } from 'app/store/nanostores/store';
85
import { useAssertSingleton } from 'common/hooks/useAssertSingleton';
@@ -63,20 +60,6 @@ export const useDndMonitor = () => {
6360

6461
log.warn(parseify({ sourceData, targetData }), 'Invalid drop');
6562
},
66-
}),
67-
monitorForExternal({
68-
canMonitor: (args) => {
69-
if (!containsFiles(args)) {
70-
return false;
71-
}
72-
return true;
73-
},
74-
onDragStart: () => {
75-
preventUnhandled.start();
76-
},
77-
onDrop: () => {
78-
preventUnhandled.stop();
79-
},
8063
})
8164
);
8265
}, []);

0 commit comments

Comments
 (0)