Add multiple image upload

This commit is contained in:
dominik-zeglen 2019-11-28 16:17:39 +01:00
parent 1c85800d56
commit a2f7cdd6b2
4 changed files with 144 additions and 40 deletions

View file

@ -1,9 +1,10 @@
import { makeStyles } from "@material-ui/core/styles"; import CircularProgress from "@material-ui/core/CircularProgress";
import React from "react";
import IconButton from "@material-ui/core/IconButton"; import IconButton from "@material-ui/core/IconButton";
import { makeStyles } from "@material-ui/core/styles";
import DeleteIcon from "@material-ui/icons/Delete"; import DeleteIcon from "@material-ui/icons/Delete";
import EditIcon from "@material-ui/icons/Edit"; import EditIcon from "@material-ui/icons/Edit";
import classNames from "classnames";
import React from "react";
const useStyles = makeStyles(theme => ({ const useStyles = makeStyles(theme => ({
image: { image: {
@ -37,6 +38,13 @@ const useStyles = makeStyles(theme => ({
top: 0, top: 0,
width: 148 width: 148
}, },
imageOverlayShow: {
"&$imageOverlay": {
alignItems: "center",
display: "flex",
justifyContent: "center"
}
},
imageOverlayToolbar: { imageOverlayToolbar: {
display: "flex", display: "flex",
justifyContent: "flex-end" justifyContent: "flex-end"
@ -48,18 +56,26 @@ interface ImageTileProps {
alt?: string; alt?: string;
url: string; url: string;
}; };
loading?: boolean;
onImageDelete?: () => void; onImageDelete?: () => void;
onImageEdit?: (event: React.ChangeEvent<any>) => void; onImageEdit?: (event: React.ChangeEvent<any>) => void;
} }
const ImageTile: React.FC<ImageTileProps> = props => { const ImageTile: React.FC<ImageTileProps> = props => {
const { onImageDelete, onImageEdit, image } = props; const { loading, onImageDelete, onImageEdit, image } = props;
const classes = useStyles(props); const classes = useStyles(props);
return ( return (
<div className={classes.imageContainer} data-tc="product-image"> <div className={classes.imageContainer} data-tc="product-image">
<div className={classes.imageOverlay}> <div
className={classNames(classes.imageOverlay, {
[classes.imageOverlayShow]: loading
})}
>
{loading ? (
<CircularProgress size={32} />
) : (
<div className={classes.imageOverlayToolbar}> <div className={classes.imageOverlayToolbar}>
{onImageEdit && ( {onImageEdit && (
<IconButton color="primary" onClick={onImageEdit}> <IconButton color="primary" onClick={onImageEdit}>
@ -72,6 +88,7 @@ const ImageTile: React.FC<ImageTileProps> = props => {
</IconButton> </IconButton>
)} )}
</div> </div>
)}
</div> </div>
<img className={classes.image} src={image.url} alt={image.alt} /> <img className={classes.image} src={image.url} alt={image.alt} />
</div> </div>

View file

@ -14,11 +14,11 @@ interface ImageUploadProps {
isActiveClassName?: string; isActiveClassName?: string;
iconContainerClassName?: string; iconContainerClassName?: string;
iconContainerActiveClassName?: string; iconContainerActiveClassName?: string;
onImageUpload: (file: File) => void; onImageUpload: (file: FileList) => void;
} }
const useStyles = makeStyles(theme => ({ const useStyles = makeStyles(theme => ({
containerDragActive: { backdrop: {
background: fade(theme.palette.primary.main, 0.1), background: fade(theme.palette.primary.main, 0.1),
color: theme.palette.primary.main color: theme.palette.primary.main
}, },
@ -57,41 +57,37 @@ const useStyles = makeStyles(theme => ({
export const ImageUpload: React.FC<ImageUploadProps> = props => { export const ImageUpload: React.FC<ImageUploadProps> = props => {
const { const {
children, children,
className, className,
disableClick, disableClick,
isActiveClassName,
iconContainerActiveClassName, iconContainerActiveClassName,
iconContainerClassName, iconContainerClassName,
isActiveClassName,
onImageUpload onImageUpload
} = props; } = props;
const classes = useStyles(props); const classes = useStyles(props);
return ( return (
<Dropzone <Dropzone disableClick={disableClick} onDrop={onImageUpload}>
disableClick={disableClick}
onDrop={files => onImageUpload(files[0])}
>
{({ isDragActive, getInputProps, getRootProps }) => ( {({ isDragActive, getInputProps, getRootProps }) => (
<> <>
<div <div
{...getRootProps()} {...getRootProps()}
className={classNames({ className={classNames(className, classes.photosIconContainer, {
[classes.photosIconContainer]: true, [classes.backdrop]: isDragActive,
[classes.containerDragActive]: isDragActive, [isActiveClassName]: isDragActive
[className]: !!className,
[isActiveClassName]: !!isActiveClassName && isDragActive
})} })}
> >
<div <div
className={classNames({ className={classNames(iconContainerClassName, {
[iconContainerClassName]: !!iconContainerClassName, [iconContainerActiveClassName]: isDragActive
[iconContainerActiveClassName]:
!!iconContainerActiveClassName && isDragActive
})} })}
> >
<input {...getInputProps()} className={classes.fileField} /> <input
{...getInputProps()}
className={classes.fileField}
accept="image/*"
/>
<ImageIcon className={classes.photosIcon} /> <ImageIcon className={classes.photosIcon} />
<Typography className={classes.uploadText}> <Typography className={classes.uploadText}>
<FormattedMessage <FormattedMessage

View file

@ -7,6 +7,7 @@ import ImageTile from "@saleor/components/ImageTile";
import ImageUpload from "@saleor/components/ImageUpload"; import ImageUpload from "@saleor/components/ImageUpload";
import { commonMessages } from "@saleor/intl"; import { commonMessages } from "@saleor/intl";
import { ReorderAction } from "@saleor/types"; import { ReorderAction } from "@saleor/types";
import { createMultiFileUploadHandler } from "@saleor/utils/handlers/multiFileUploadHandler";
import classNames from "classnames"; import classNames from "classnames";
import React from "react"; import React from "react";
import { useIntl } from "react-intl"; import { useIntl } from "react-intl";
@ -72,6 +73,7 @@ const useStyles = makeStyles(theme => ({
imageUpload: { imageUpload: {
height: "100%", height: "100%",
left: 0, left: 0,
outline: 0,
position: "absolute", position: "absolute",
top: 0, top: 0,
width: "100%" width: "100%"
@ -134,13 +136,14 @@ const SortableImage = SortableElement<SortableImageProps>(
interface ImageListContainerProps { interface ImageListContainerProps {
className: string; className: string;
items: any; items: ProductDetails_product_images[];
preview: ProductDetails_product_images[];
onImageDelete: (id: string) => () => void; onImageDelete: (id: string) => () => void;
onImageEdit: (id: string) => () => void; onImageEdit: (id: string) => () => void;
} }
const ImageListContainer = SortableContainer<ImageListContainerProps>( const ImageListContainer = SortableContainer<ImageListContainerProps>(
({ items, onImageDelete, onImageEdit, ...props }) => ( ({ items, preview, onImageDelete, onImageEdit, ...props }) => (
<div {...props}> <div {...props}>
{items.map((image, index) => ( {items.map((image, index) => (
<SortableImage <SortableImage
@ -151,6 +154,11 @@ const ImageListContainer = SortableContainer<ImageListContainerProps>(
onImageDelete={onImageDelete(image.id)} onImageDelete={onImageDelete(image.id)}
/> />
))} ))}
{preview
.sort((a, b) => (a.sortOrder > b.sortOrder ? 1 : -1))
.map(image => (
<ImageTile loading={true} image={image} />
))}
</div> </div>
) )
); );
@ -169,6 +177,32 @@ const ProductImages: React.FC<ProductImagesProps> = props => {
const classes = useStyles(props); const classes = useStyles(props);
const intl = useIntl(); const intl = useIntl();
const upload = React.useRef(null); 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 ( return (
<Card className={classes.card}> <Card className={classes.card}>
@ -191,9 +225,11 @@ const ProductImages: React.FC<ProductImagesProps> = props => {
<input <input
className={classes.fileField} className={classes.fileField}
id="fileUpload" id="fileUpload"
onChange={event => onImageUpload(event.target.files[0])} onChange={event => handleImageUpload(event.target.files)}
multiple
type="file" type="file"
ref={upload} ref={upload}
accept="image/*"
/> />
</> </>
} }
@ -215,7 +251,7 @@ const ProductImages: React.FC<ProductImagesProps> = props => {
disableClick={true} disableClick={true}
iconContainerClassName={classes.imageUploadIcon} iconContainerClassName={classes.imageUploadIcon}
iconContainerActiveClassName={classes.imageUploadIconActive} iconContainerActiveClassName={classes.imageUploadIconActive}
onImageUpload={onImageUpload} onImageUpload={handleImageUpload}
> >
{({ isDragActive }) => ( {({ isDragActive }) => (
<CardContent> <CardContent>
@ -224,6 +260,7 @@ const ProductImages: React.FC<ProductImagesProps> = props => {
helperClass="dragged" helperClass="dragged"
axis="xy" axis="xy"
items={images} items={images}
preview={imagesToUpload}
onSortEnd={onImageReorder} onSortEnd={onImageReorder}
className={classNames({ className={classNames({
[classes.root]: true, [classes.root]: true,
@ -237,7 +274,7 @@ const ProductImages: React.FC<ProductImagesProps> = props => {
</ImageUpload> </ImageUpload>
</> </>
) : ( ) : (
<ImageUpload onImageUpload={onImageUpload} /> <ImageUpload onImageUpload={handleImageUpload} />
)} )}
</div> </div>
</Card> </Card>

View file

@ -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<T>(
upload: (file: File) => Promise<T>,
{
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);
}
};
}