From a2f7cdd6b26e21f01cc1cfd1d595772ff1a9dc69 Mon Sep 17 00:00:00 2001 From: dominik-zeglen Date: Thu, 28 Nov 2019 16:17:39 +0100 Subject: [PATCH 1/6] Add multiple image upload --- src/components/ImageTile/ImageTile.tsx | 51 ++++++++++++------ src/components/ImageUpload/ImageUpload.tsx | 32 +++++------ .../ProductImages/ProductImages.tsx | 47 ++++++++++++++-- src/utils/handlers/multiFileUploadHandler.ts | 54 +++++++++++++++++++ 4 files changed, 144 insertions(+), 40 deletions(-) create mode 100644 src/utils/handlers/multiFileUploadHandler.ts diff --git a/src/components/ImageTile/ImageTile.tsx b/src/components/ImageTile/ImageTile.tsx index 9e71294a5..5d9312f22 100644 --- a/src/components/ImageTile/ImageTile.tsx +++ b/src/components/ImageTile/ImageTile.tsx @@ -1,9 +1,10 @@ -import { makeStyles } from "@material-ui/core/styles"; -import React from "react"; - +import CircularProgress from "@material-ui/core/CircularProgress"; import IconButton from "@material-ui/core/IconButton"; +import { makeStyles } from "@material-ui/core/styles"; import DeleteIcon from "@material-ui/icons/Delete"; import EditIcon from "@material-ui/icons/Edit"; +import classNames from "classnames"; +import React from "react"; const useStyles = makeStyles(theme => ({ image: { @@ -37,6 +38,13 @@ const useStyles = makeStyles(theme => ({ top: 0, width: 148 }, + imageOverlayShow: { + "&$imageOverlay": { + alignItems: "center", + display: "flex", + justifyContent: "center" + } + }, imageOverlayToolbar: { display: "flex", justifyContent: "flex-end" @@ -48,30 +56,39 @@ interface ImageTileProps { alt?: string; url: string; }; + loading?: boolean; onImageDelete?: () => void; onImageEdit?: (event: React.ChangeEvent) => void; } const ImageTile: React.FC = props => { - const { onImageDelete, onImageEdit, image } = props; + const { loading, onImageDelete, onImageEdit, image } = props; const classes = useStyles(props); return (
-
-
- {onImageEdit && ( - - - - )} - {onImageDelete && ( - - - - )} -
+
+ {loading ? ( + + ) : ( +
+ {onImageEdit && ( + + + + )} + {onImageDelete && ( + + + + )} +
+ )}
{image.alt}
diff --git a/src/components/ImageUpload/ImageUpload.tsx b/src/components/ImageUpload/ImageUpload.tsx index 5216a0013..070ecc974 100644 --- a/src/components/ImageUpload/ImageUpload.tsx +++ b/src/components/ImageUpload/ImageUpload.tsx @@ -14,11 +14,11 @@ interface ImageUploadProps { isActiveClassName?: string; iconContainerClassName?: string; iconContainerActiveClassName?: string; - onImageUpload: (file: File) => void; + onImageUpload: (file: FileList) => void; } const useStyles = makeStyles(theme => ({ - containerDragActive: { + backdrop: { background: fade(theme.palette.primary.main, 0.1), color: theme.palette.primary.main }, @@ -57,41 +57,37 @@ const useStyles = makeStyles(theme => ({ export const ImageUpload: React.FC = props => { const { children, - className, disableClick, - isActiveClassName, iconContainerActiveClassName, iconContainerClassName, + isActiveClassName, onImageUpload } = props; const classes = useStyles(props); return ( - onImageUpload(files[0])} - > + {({ isDragActive, getInputProps, getRootProps }) => ( <>
- + ({ imageUpload: { height: "100%", left: 0, + outline: 0, position: "absolute", top: 0, width: "100%" @@ -134,13 +136,14 @@ const SortableImage = SortableElement( interface ImageListContainerProps { className: string; - items: any; + items: ProductDetails_product_images[]; + preview: ProductDetails_product_images[]; onImageDelete: (id: string) => () => void; onImageEdit: (id: string) => () => void; } const ImageListContainer = SortableContainer( - ({ items, onImageDelete, onImageEdit, ...props }) => ( + ({ items, preview, onImageDelete, onImageEdit, ...props }) => (
{items.map((image, index) => ( ( onImageDelete={onImageDelete(image.id)} /> ))} + {preview + .sort((a, b) => (a.sortOrder > b.sortOrder ? 1 : -1)) + .map(image => ( + + ))}
) ); @@ -169,6 +177,32 @@ const ProductImages: React.FC = props => { const classes = useStyles(props); const intl = useIntl(); const upload = React.useRef(null); + const [imagesToUpload, setImagesToUpload] = React.useState< + ProductDetails_product_images[] + >([]); + + const handleImageUpload = createMultiFileUploadHandler(onImageUpload, { + onAfterUpload: () => + setImagesToUpload(prevImagesToUpload => prevImagesToUpload.slice(1)), + onStart: files => { + Array.from(files).forEach((file, fileIndex) => { + const reader = new FileReader(); + reader.onload = event => { + setImagesToUpload(prevImagesToUpload => [ + ...prevImagesToUpload, + { + __typename: "ProductImage", + alt: "", + id: "", + sortOrder: fileIndex, + url: event.target.result as string + } + ]); + }; + reader.readAsDataURL(file); + }); + } + }); return ( @@ -191,9 +225,11 @@ const ProductImages: React.FC = props => { onImageUpload(event.target.files[0])} + onChange={event => handleImageUpload(event.target.files)} + multiple type="file" ref={upload} + accept="image/*" /> } @@ -215,7 +251,7 @@ const ProductImages: React.FC = props => { disableClick={true} iconContainerClassName={classes.imageUploadIcon} iconContainerActiveClassName={classes.imageUploadIconActive} - onImageUpload={onImageUpload} + onImageUpload={handleImageUpload} > {({ isDragActive }) => ( @@ -224,6 +260,7 @@ const ProductImages: React.FC = props => { helperClass="dragged" axis="xy" items={images} + preview={imagesToUpload} onSortEnd={onImageReorder} className={classNames({ [classes.root]: true, @@ -237,7 +274,7 @@ const ProductImages: React.FC = props => { ) : ( - + )}
diff --git a/src/utils/handlers/multiFileUploadHandler.ts b/src/utils/handlers/multiFileUploadHandler.ts new file mode 100644 index 000000000..b52591243 --- /dev/null +++ b/src/utils/handlers/multiFileUploadHandler.ts @@ -0,0 +1,54 @@ +type CreateMultiFileUploadHandlerCallbacks = Partial<{ + onAfterUpload: (index: number, all: number) => void; + onBeforeUpload: (index: number, all: number) => void; + onCompleted: (files: FileList) => void; + onError: (index: number, all: number) => void; + onStart: (files: FileList) => void; +}>; + +export function createMultiFileUploadHandler( + upload: (file: File) => Promise, + { + onAfterUpload, + onBeforeUpload, + onCompleted, + onError, + onStart + }: CreateMultiFileUploadHandlerCallbacks +) { + async function uploadImage(files: FileList, fileIndex: number) { + if (files.length > fileIndex) { + try { + if (onBeforeUpload) { + onBeforeUpload(fileIndex, files.length); + } + + await upload(files[fileIndex]); + + if (onAfterUpload) { + onAfterUpload(fileIndex, files.length); + } + } catch (exception) { + console.error( + `Could not upload file #${fileIndex + 1}. Reason: ${exception}` + ); + if (onError) { + onError(fileIndex, files.length); + } + } finally { + await uploadImage(files, fileIndex + 1); + } + } + } + return async (files: FileList) => { + if (onStart) { + onStart(files); + } + + await uploadImage(files, 0); + + if (onCompleted) { + onCompleted(files); + } + }; +} From af95cb3be2640c55400998ed7b07dafe8543a3f9 Mon Sep 17 00:00:00 2001 From: dominik-zeglen Date: Thu, 28 Nov 2019 16:19:25 +0100 Subject: [PATCH 2/6] Fix handlers --- .../components/CategoryBackground/CategoryBackground.tsx | 3 ++- src/collections/components/CollectionImage/CollectionImage.tsx | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/src/categories/components/CategoryBackground/CategoryBackground.tsx b/src/categories/components/CategoryBackground/CategoryBackground.tsx index ca042996e..8e340e304 100644 --- a/src/categories/components/CategoryBackground/CategoryBackground.tsx +++ b/src/categories/components/CategoryBackground/CategoryBackground.tsx @@ -77,6 +77,7 @@ const CategoryBackground: React.FC = props => { onChange={event => onImageUpload(event.target.files[0])} type="file" ref={anchor} + accept="image/*" /> } @@ -90,7 +91,7 @@ const CategoryBackground: React.FC = props => {
) : image === null ? ( - + onImageUpload(files[0])} /> ) : ( diff --git a/src/collections/components/CollectionImage/CollectionImage.tsx b/src/collections/components/CollectionImage/CollectionImage.tsx index a4cb01d32..6e96904c6 100644 --- a/src/collections/components/CollectionImage/CollectionImage.tsx +++ b/src/collections/components/CollectionImage/CollectionImage.tsx @@ -92,6 +92,7 @@ export const CollectionImage: React.FC = props => { onChange={event => onImageUpload(event.target.files[0])} type="file" ref={anchor} + accept="image/*" /> } @@ -105,7 +106,7 @@ export const CollectionImage: React.FC = props => {
) : image === null ? ( - + onImageUpload(files[0])} /> ) : ( From 4cf4b3b45b09ec201322480a5720f9c72ec7a558 Mon Sep 17 00:00:00 2001 From: dominik-zeglen Date: Thu, 28 Nov 2019 16:23:26 +0100 Subject: [PATCH 3/6] Update snapshots --- .../__snapshots__/Stories.test.ts.snap | 39 +++++++++++++++++-- 1 file changed, 35 insertions(+), 4 deletions(-) diff --git a/src/storybook/__snapshots__/Stories.test.ts.snap b/src/storybook/__snapshots__/Stories.test.ts.snap index ec9718967..6487a2acc 100644 --- a/src/storybook/__snapshots__/Stories.test.ts.snap +++ b/src/storybook/__snapshots__/Stories.test.ts.snap @@ -16634,6 +16634,7 @@ Ctrl + K" @@ -93026,13 +93042,14 @@ Ctrl + K" class="makeStyles-imageGridContainer-id" >
@@ -94548,13 +94567,14 @@ Ctrl + K" class="makeStyles-imageGridContainer-id" >
@@ -96329,13 +96351,14 @@ Ctrl + K" class="makeStyles-imageGridContainer-id" >
@@ -99186,8 +99211,10 @@ Ctrl + K"
@@ -99209,6 +99236,7 @@ Ctrl + K" class="" >
@@ -100890,13 +100920,14 @@ Ctrl + K" class="makeStyles-imageGridContainer-id" >
Date: Thu, 28 Nov 2019 16:25:42 +0100 Subject: [PATCH 4/6] Update changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5bb57de0f..075d61cb5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,7 @@ All notable, unreleased changes to this project will be documented in this file. - Use searches as hooks instead of components - #262 by @dominik-zeglen - Add navigator - #267 by @dominik-zeglen - Fix voucher limit - #271 by @dominik-zeglen +- Allow multiple images to be uploaded - #277 by @dominik-zeglen ## 2.0.0 From b77962f53ae4ac554dedd79afbbfb768b70e57e1 Mon Sep 17 00:00:00 2001 From: dominik-zeglen Date: Fri, 29 Nov 2019 11:21:36 +0100 Subject: [PATCH 5/6] Use default export --- src/products/components/ProductImages/ProductImages.tsx | 2 +- src/utils/handlers/multiFileUploadHandler.ts | 6 ++++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/src/products/components/ProductImages/ProductImages.tsx b/src/products/components/ProductImages/ProductImages.tsx index 58443c2ac..9a34d63e2 100644 --- a/src/products/components/ProductImages/ProductImages.tsx +++ b/src/products/components/ProductImages/ProductImages.tsx @@ -7,7 +7,7 @@ import ImageTile from "@saleor/components/ImageTile"; import ImageUpload from "@saleor/components/ImageUpload"; import { commonMessages } from "@saleor/intl"; import { ReorderAction } from "@saleor/types"; -import { createMultiFileUploadHandler } from "@saleor/utils/handlers/multiFileUploadHandler"; +import createMultiFileUploadHandler from "@saleor/utils/handlers/multiFileUploadHandler"; import classNames from "classnames"; import React from "react"; import { useIntl } from "react-intl"; diff --git a/src/utils/handlers/multiFileUploadHandler.ts b/src/utils/handlers/multiFileUploadHandler.ts index b52591243..99fa2460f 100644 --- a/src/utils/handlers/multiFileUploadHandler.ts +++ b/src/utils/handlers/multiFileUploadHandler.ts @@ -1,4 +1,4 @@ -type CreateMultiFileUploadHandlerCallbacks = Partial<{ +export type CreateMultiFileUploadHandlerCallbacks = Partial<{ onAfterUpload: (index: number, all: number) => void; onBeforeUpload: (index: number, all: number) => void; onCompleted: (files: FileList) => void; @@ -6,7 +6,7 @@ type CreateMultiFileUploadHandlerCallbacks = Partial<{ onStart: (files: FileList) => void; }>; -export function createMultiFileUploadHandler( +function createMultiFileUploadHandler( upload: (file: File) => Promise, { onAfterUpload, @@ -52,3 +52,5 @@ export function createMultiFileUploadHandler( } }; } + +export default createMultiFileUploadHandler; From 81c5d37afa0ca7a18bd4084901b9ef44033a3398 Mon Sep 17 00:00:00 2001 From: dominik-zeglen Date: Fri, 29 Nov 2019 13:06:16 +0100 Subject: [PATCH 6/6] Add tests --- .../handlers/multiFileUploadHandler.test.ts | 67 +++++++++++++++++++ src/utils/handlers/multiFileUploadHandler.ts | 32 ++++----- 2 files changed, 84 insertions(+), 15 deletions(-) create mode 100644 src/utils/handlers/multiFileUploadHandler.test.ts diff --git a/src/utils/handlers/multiFileUploadHandler.test.ts b/src/utils/handlers/multiFileUploadHandler.test.ts new file mode 100644 index 000000000..cc1294c40 --- /dev/null +++ b/src/utils/handlers/multiFileUploadHandler.test.ts @@ -0,0 +1,67 @@ +import createMultiFileUploadHandler from "./multiFileUploadHandler"; + +const testFiles = Array(5) + .fill(0) + .map(() => new File([""], "mockFile")); + +describe("Multiple file upload handler", () => { + it("properly handles success", done => { + const cbs = { + onAfterUpload: jest.fn(), + onBeforeUpload: jest.fn(), + onCompleted: jest.fn(files => + expect(files.length).toBe(testFiles.length) + ), + onError: jest.fn(), + onStart: jest.fn() + }; + const handle = createMultiFileUploadHandler(() => { + const promise = new Promise(resolve => { + expect(cbs.onBeforeUpload).toBeCalledTimes( + cbs.onAfterUpload.mock.calls.length + 1 + ); + resolve(); + }); + return promise; + }, cbs); + + handle((testFiles as unknown) as FileList).then(() => { + expect(cbs.onAfterUpload).toBeCalledTimes(testFiles.length); + expect(cbs.onBeforeUpload).toBeCalledTimes(testFiles.length); + expect(cbs.onCompleted).toBeCalledTimes(1); + expect(cbs.onError).toBeCalledTimes(0); + expect(cbs.onStart).toBeCalledTimes(1); + done(); + }); + }); + + it("properly handles error", done => { + const cbs = { + onAfterUpload: jest.fn(), + onBeforeUpload: jest.fn(), + onCompleted: jest.fn(files => + expect(files.length).toBe(testFiles.length) + ), + onError: jest.fn(), + onStart: jest.fn() + }; + const handle = createMultiFileUploadHandler((_, fileIndex) => { + const promise = new Promise((resolve, reject) => { + if (fileIndex === 2) { + reject(); + } else { + resolve(); + } + }); + return promise; + }, cbs); + + handle((testFiles as unknown) as FileList).then(() => { + expect(cbs.onAfterUpload).toBeCalledTimes(testFiles.length - 1); + expect(cbs.onBeforeUpload).toBeCalledTimes(testFiles.length); + expect(cbs.onCompleted).toBeCalledTimes(1); + expect(cbs.onError).toBeCalledTimes(1); + done(); + }); + }); +}); diff --git a/src/utils/handlers/multiFileUploadHandler.ts b/src/utils/handlers/multiFileUploadHandler.ts index 99fa2460f..c97b17418 100644 --- a/src/utils/handlers/multiFileUploadHandler.ts +++ b/src/utils/handlers/multiFileUploadHandler.ts @@ -1,13 +1,13 @@ export type CreateMultiFileUploadHandlerCallbacks = Partial<{ - onAfterUpload: (index: number, all: number) => void; - onBeforeUpload: (index: number, all: number) => void; - onCompleted: (files: FileList) => void; - onError: (index: number, all: number) => void; - onStart: (files: FileList) => void; + onAfterUpload: (index: number, files: File[]) => void; + onBeforeUpload: (index: number, files: File[]) => void; + onCompleted: (files: File[]) => void; + onError: (index: number, files: File[]) => void; + onStart: (files: File[]) => void; }>; function createMultiFileUploadHandler( - upload: (file: File) => Promise, + upload: (file: File, fileIndex: number) => Promise, { onAfterUpload, onBeforeUpload, @@ -16,39 +16,41 @@ function createMultiFileUploadHandler( onStart }: CreateMultiFileUploadHandlerCallbacks ) { - async function uploadImage(files: FileList, fileIndex: number) { + async function uploadImage(files: File[], fileIndex: number): Promise { if (files.length > fileIndex) { try { if (onBeforeUpload) { - onBeforeUpload(fileIndex, files.length); + onBeforeUpload(fileIndex, files); } - await upload(files[fileIndex]); + await upload(files[fileIndex], fileIndex); if (onAfterUpload) { - onAfterUpload(fileIndex, files.length); + onAfterUpload(fileIndex, files); } } catch (exception) { console.error( `Could not upload file #${fileIndex + 1}. Reason: ${exception}` ); if (onError) { - onError(fileIndex, files.length); + onError(fileIndex, files); } } finally { await uploadImage(files, fileIndex + 1); } } } - return async (files: FileList) => { + return async (files: FileList): Promise => { + const fileArray = Array.from(files); + if (onStart) { - onStart(files); + onStart(fileArray); } - await uploadImage(files, 0); + await uploadImage(fileArray, 0); if (onCompleted) { - onCompleted(files); + onCompleted(fileArray); } }; }