Merge branch 'master' into fix/dropdown-items

This commit is contained in:
Marcin Gębala 2019-12-02 17:07:17 +01:00 committed by GitHub
commit 56f6744bb8
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
9 changed files with 255 additions and 46 deletions

View file

@ -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 - Use searches as hooks instead of components - #262 by @dominik-zeglen
- Add navigator - #267 by @dominik-zeglen - Add navigator - #267 by @dominik-zeglen
- Fix voucher limit - #271 by @dominik-zeglen - Fix voucher limit - #271 by @dominik-zeglen
- Allow multiple images to be uploaded - #277 by @dominik-zeglen
- Fix dropdown clickable areas - #281 by @dominik-zeglen - Fix dropdown clickable areas - #281 by @dominik-zeglen
## 2.0.0 ## 2.0.0

View file

@ -77,6 +77,7 @@ const CategoryBackground: React.FC<CategoryBackgroundProps> = props => {
onChange={event => onImageUpload(event.target.files[0])} onChange={event => onImageUpload(event.target.files[0])}
type="file" type="file"
ref={anchor} ref={anchor}
accept="image/*"
/> />
</> </>
} }
@ -90,7 +91,7 @@ const CategoryBackground: React.FC<CategoryBackgroundProps> = props => {
</div> </div>
</CardContent> </CardContent>
) : image === null ? ( ) : image === null ? (
<ImageUpload onImageUpload={onImageUpload} /> <ImageUpload onImageUpload={files => onImageUpload(files[0])} />
) : ( ) : (
<CardContent> <CardContent>
<ImageTile image={image} onImageDelete={onImageDelete} /> <ImageTile image={image} onImageDelete={onImageDelete} />

View file

@ -92,6 +92,7 @@ export const CollectionImage: React.FC<CollectionImageProps> = props => {
onChange={event => onImageUpload(event.target.files[0])} onChange={event => onImageUpload(event.target.files[0])}
type="file" type="file"
ref={anchor} ref={anchor}
accept="image/*"
/> />
</> </>
} }
@ -105,7 +106,7 @@ export const CollectionImage: React.FC<CollectionImageProps> = props => {
</div> </div>
</CardContent> </CardContent>
) : image === null ? ( ) : image === null ? (
<ImageUpload onImageUpload={onImageUpload} /> <ImageUpload onImageUpload={files => onImageUpload(files[0])} />
) : ( ) : (
<CardContent> <CardContent>
<ImageTile image={image} onImageDelete={onImageDelete} /> <ImageTile image={image} onImageDelete={onImageDelete} />

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,30 +56,39 @@ 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
<div className={classes.imageOverlayToolbar}> className={classNames(classes.imageOverlay, {
{onImageEdit && ( [classes.imageOverlayShow]: loading
<IconButton color="primary" onClick={onImageEdit}> })}
<EditIcon /> >
</IconButton> {loading ? (
)} <CircularProgress size={32} />
{onImageDelete && ( ) : (
<IconButton color="primary" onClick={onImageDelete}> <div className={classes.imageOverlayToolbar}>
<DeleteIcon /> {onImageEdit && (
</IconButton> <IconButton color="primary" onClick={onImageEdit}>
)} <EditIcon />
</div> </IconButton>
)}
{onImageDelete && (
<IconButton color="primary" onClick={onImageDelete}>
<DeleteIcon />
</IconButton>
)}
</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

@ -16612,6 +16612,7 @@ Ctrl + K"
</span> </span>
</button> </button>
<input <input
accept="image/*"
class="makeStyles-fileField-id" class="makeStyles-fileField-id"
id="fileUpload" id="fileUpload"
type="file" type="file"
@ -17389,6 +17390,7 @@ Ctrl + K"
</span> </span>
</button> </button>
<input <input
accept="image/*"
class="makeStyles-fileField-id" class="makeStyles-fileField-id"
id="fileUpload" id="fileUpload"
type="file" type="file"
@ -18341,6 +18343,7 @@ Ctrl + K"
</span> </span>
</button> </button>
<input <input
accept="image/*"
class="makeStyles-fileField-id" class="makeStyles-fileField-id"
id="fileUpload" id="fileUpload"
type="file" type="file"
@ -18361,6 +18364,7 @@ Ctrl + K"
class="" class=""
> >
<input <input
accept="image/*"
autocomplete="off" autocomplete="off"
class="makeStyles-fileField-id" class="makeStyles-fileField-id"
multiple="" multiple=""
@ -19270,6 +19274,7 @@ Ctrl + K"
</span> </span>
</button> </button>
<input <input
accept="image/*"
class="makeStyles-fileField-id" class="makeStyles-fileField-id"
id="fileUpload" id="fileUpload"
type="file" type="file"
@ -20274,6 +20279,7 @@ Ctrl + K"
</span> </span>
</button> </button>
<input <input
accept="image/*"
class="makeStyles-fileField-id" class="makeStyles-fileField-id"
id="fileUpload" id="fileUpload"
type="file" type="file"
@ -21249,6 +21255,7 @@ Ctrl + K"
</span> </span>
</button> </button>
<input <input
accept="image/*"
class="makeStyles-fileField-id" class="makeStyles-fileField-id"
id="fileUpload" id="fileUpload"
type="file" type="file"
@ -22851,6 +22858,7 @@ Ctrl + K"
</span> </span>
</button> </button>
<input <input
accept="image/*"
class="CollectionImage-fileField-id" class="CollectionImage-fileField-id"
id="fileUpload" id="fileUpload"
type="file" type="file"
@ -24134,6 +24142,7 @@ Ctrl + K"
</span> </span>
</button> </button>
<input <input
accept="image/*"
class="CollectionImage-fileField-id" class="CollectionImage-fileField-id"
id="fileUpload" id="fileUpload"
type="file" type="file"
@ -25330,6 +25339,7 @@ Ctrl + K"
</span> </span>
</button> </button>
<input <input
accept="image/*"
class="CollectionImage-fileField-id" class="CollectionImage-fileField-id"
id="fileUpload" id="fileUpload"
type="file" type="file"
@ -27454,6 +27464,7 @@ Ctrl + K"
</span> </span>
</button> </button>
<input <input
accept="image/*"
class="CollectionImage-fileField-id" class="CollectionImage-fileField-id"
id="fileUpload" id="fileUpload"
type="file" type="file"
@ -27474,6 +27485,7 @@ Ctrl + K"
class="" class=""
> >
<input <input
accept="image/*"
autocomplete="off" autocomplete="off"
class="makeStyles-fileField-id" class="makeStyles-fileField-id"
multiple="" multiple=""
@ -28195,6 +28207,7 @@ Ctrl + K"
</span> </span>
</button> </button>
<input <input
accept="image/*"
class="CollectionImage-fileField-id" class="CollectionImage-fileField-id"
id="fileUpload" id="fileUpload"
type="file" type="file"
@ -28215,6 +28228,7 @@ Ctrl + K"
class="" class=""
> >
<input <input
accept="image/*"
autocomplete="off" autocomplete="off"
class="makeStyles-fileField-id" class="makeStyles-fileField-id"
multiple="" multiple=""
@ -92988,8 +93002,10 @@ Ctrl + K"
</span> </span>
</button> </button>
<input <input
accept="image/*"
class="makeStyles-fileField-id" class="makeStyles-fileField-id"
id="fileUpload" id="fileUpload"
multiple=""
type="file" type="file"
/> />
</div> </div>
@ -93004,13 +93020,14 @@ Ctrl + K"
class="makeStyles-imageGridContainer-id" class="makeStyles-imageGridContainer-id"
> >
<div <div
class="makeStyles-photosIconContainer-id makeStyles-imageUpload-id" class="makeStyles-imageUpload-id makeStyles-photosIconContainer-id"
tabindex="0" tabindex="0"
> >
<div <div
class="makeStyles-imageUploadIcon-id" class="makeStyles-imageUploadIcon-id"
> >
<input <input
accept="image/*"
autocomplete="off" autocomplete="off"
class="makeStyles-fileField-id" class="makeStyles-fileField-id"
multiple="" multiple=""
@ -94510,8 +94527,10 @@ Ctrl + K"
</span> </span>
</button> </button>
<input <input
accept="image/*"
class="makeStyles-fileField-id" class="makeStyles-fileField-id"
id="fileUpload" id="fileUpload"
multiple=""
type="file" type="file"
/> />
</div> </div>
@ -94526,13 +94545,14 @@ Ctrl + K"
class="makeStyles-imageGridContainer-id" class="makeStyles-imageGridContainer-id"
> >
<div <div
class="makeStyles-photosIconContainer-id makeStyles-imageUpload-id" class="makeStyles-imageUpload-id makeStyles-photosIconContainer-id"
tabindex="0" tabindex="0"
> >
<div <div
class="makeStyles-imageUploadIcon-id" class="makeStyles-imageUploadIcon-id"
> >
<input <input
accept="image/*"
autocomplete="off" autocomplete="off"
class="makeStyles-fileField-id" class="makeStyles-fileField-id"
multiple="" multiple=""
@ -96291,8 +96311,10 @@ Ctrl + K"
</span> </span>
</button> </button>
<input <input
accept="image/*"
class="makeStyles-fileField-id" class="makeStyles-fileField-id"
id="fileUpload" id="fileUpload"
multiple=""
type="file" type="file"
/> />
</div> </div>
@ -96307,13 +96329,14 @@ Ctrl + K"
class="makeStyles-imageGridContainer-id" class="makeStyles-imageGridContainer-id"
> >
<div <div
class="makeStyles-photosIconContainer-id makeStyles-imageUpload-id" class="makeStyles-imageUpload-id makeStyles-photosIconContainer-id"
tabindex="0" tabindex="0"
> >
<div <div
class="makeStyles-imageUploadIcon-id" class="makeStyles-imageUploadIcon-id"
> >
<input <input
accept="image/*"
autocomplete="off" autocomplete="off"
class="makeStyles-fileField-id" class="makeStyles-fileField-id"
multiple="" multiple=""
@ -97874,8 +97897,10 @@ Ctrl + K"
</span> </span>
</button> </button>
<input <input
accept="image/*"
class="makeStyles-fileField-id" class="makeStyles-fileField-id"
id="fileUpload" id="fileUpload"
multiple=""
type="file" type="file"
/> />
</div> </div>
@ -99164,8 +99189,10 @@ Ctrl + K"
</span> </span>
</button> </button>
<input <input
accept="image/*"
class="makeStyles-fileField-id" class="makeStyles-fileField-id"
id="fileUpload" id="fileUpload"
multiple=""
type="file" type="file"
/> />
</div> </div>
@ -99187,6 +99214,7 @@ Ctrl + K"
class="" class=""
> >
<input <input
accept="image/*"
autocomplete="off" autocomplete="off"
class="makeStyles-fileField-id" class="makeStyles-fileField-id"
multiple="" multiple=""
@ -100852,8 +100880,10 @@ Ctrl + K"
</span> </span>
</button> </button>
<input <input
accept="image/*"
class="makeStyles-fileField-id" class="makeStyles-fileField-id"
id="fileUpload" id="fileUpload"
multiple=""
type="file" type="file"
/> />
</div> </div>
@ -100868,13 +100898,14 @@ Ctrl + K"
class="makeStyles-imageGridContainer-id" class="makeStyles-imageGridContainer-id"
> >
<div <div
class="makeStyles-photosIconContainer-id makeStyles-imageUpload-id" class="makeStyles-imageUpload-id makeStyles-photosIconContainer-id"
tabindex="0" tabindex="0"
> >
<div <div
class="makeStyles-imageUploadIcon-id" class="makeStyles-imageUploadIcon-id"
> >
<input <input
accept="image/*"
autocomplete="off" autocomplete="off"
class="makeStyles-fileField-id" class="makeStyles-fileField-id"
multiple="" multiple=""

View file

@ -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();
});
});
});

View file

@ -0,0 +1,58 @@
export type CreateMultiFileUploadHandlerCallbacks = Partial<{
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<T>(
upload: (file: File, fileIndex: number) => Promise<T>,
{
onAfterUpload,
onBeforeUpload,
onCompleted,
onError,
onStart
}: CreateMultiFileUploadHandlerCallbacks
) {
async function uploadImage(files: File[], fileIndex: number): Promise<void> {
if (files.length > fileIndex) {
try {
if (onBeforeUpload) {
onBeforeUpload(fileIndex, files);
}
await upload(files[fileIndex], fileIndex);
if (onAfterUpload) {
onAfterUpload(fileIndex, files);
}
} catch (exception) {
console.error(
`Could not upload file #${fileIndex + 1}. Reason: ${exception}`
);
if (onError) {
onError(fileIndex, files);
}
} finally {
await uploadImage(files, fileIndex + 1);
}
}
}
return async (files: FileList): Promise<void> => {
const fileArray = Array.from(files);
if (onStart) {
onStart(fileArray);
}
await uploadImage(fileArray, 0);
if (onCompleted) {
onCompleted(fileArray);
}
};
}
export default createMultiFileUploadHandler;