Add multiple image upload
This commit is contained in:
parent
1c85800d56
commit
a2f7cdd6b2
4 changed files with 144 additions and 40 deletions
|
@ -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,18 +56,26 @@ 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={classNames(classes.imageOverlay, {
|
||||
[classes.imageOverlayShow]: loading
|
||||
})}
|
||||
>
|
||||
{loading ? (
|
||||
<CircularProgress size={32} />
|
||||
) : (
|
||||
<div className={classes.imageOverlayToolbar}>
|
||||
{onImageEdit && (
|
||||
<IconButton color="primary" onClick={onImageEdit}>
|
||||
|
@ -72,6 +88,7 @@ const ImageTile: React.FC<ImageTileProps> = props => {
|
|||
</IconButton>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<img className={classes.image} src={image.url} alt={image.alt} />
|
||||
</div>
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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>
|
||||
|
|
54
src/utils/handlers/multiFileUploadHandler.ts
Normal file
54
src/utils/handlers/multiFileUploadHandler.ts
Normal 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);
|
||||
}
|
||||
};
|
||||
}
|
Loading…
Reference in a new issue