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 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<any>) => void;
}
const ImageTile: React.FC<ImageTileProps> = props => {
const { onImageDelete, onImageEdit, image } = props;
const { loading, onImageDelete, onImageEdit, image } = props;
const classes = useStyles(props);
return (
<div className={classes.imageContainer} data-tc="product-image">
<div className={classes.imageOverlay}>
<div className={classes.imageOverlayToolbar}>
{onImageEdit && (
<IconButton color="primary" onClick={onImageEdit}>
<EditIcon />
</IconButton>
)}
{onImageDelete && (
<IconButton color="primary" onClick={onImageDelete}>
<DeleteIcon />
</IconButton>
)}
</div>
<div
className={classNames(classes.imageOverlay, {
[classes.imageOverlayShow]: loading
})}
>
{loading ? (
<CircularProgress size={32} />
) : (
<div className={classes.imageOverlayToolbar}>
{onImageEdit && (
<IconButton color="primary" onClick={onImageEdit}>
<EditIcon />
</IconButton>
)}
{onImageDelete && (
<IconButton color="primary" onClick={onImageDelete}>
<DeleteIcon />
</IconButton>
)}
</div>
)}
</div>
<img className={classes.image} src={image.url} alt={image.alt} />
</div>

View file

@ -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<ImageUploadProps> = props => {
const {
children,
className,
disableClick,
isActiveClassName,
iconContainerActiveClassName,
iconContainerClassName,
isActiveClassName,
onImageUpload
} = props;
const classes = useStyles(props);
return (
<Dropzone
disableClick={disableClick}
onDrop={files => onImageUpload(files[0])}
>
<Dropzone disableClick={disableClick} onDrop={onImageUpload}>
{({ isDragActive, getInputProps, getRootProps }) => (
<>
<div
{...getRootProps()}
className={classNames({
[classes.photosIconContainer]: true,
[classes.containerDragActive]: isDragActive,
[className]: !!className,
[isActiveClassName]: !!isActiveClassName && isDragActive
className={classNames(className, classes.photosIconContainer, {
[classes.backdrop]: isDragActive,
[isActiveClassName]: isDragActive
})}
>
<div
className={classNames({
[iconContainerClassName]: !!iconContainerClassName,
[iconContainerActiveClassName]:
!!iconContainerActiveClassName && isDragActive
className={classNames(iconContainerClassName, {
[iconContainerActiveClassName]: isDragActive
})}
>
<input {...getInputProps()} className={classes.fileField} />
<input
{...getInputProps()}
className={classes.fileField}
accept="image/*"
/>
<ImageIcon className={classes.photosIcon} />
<Typography className={classes.uploadText}>
<FormattedMessage

View file

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