Improve upload error handling; add clearFieldErrors;

This commit is contained in:
SachaG 2018-04-14 17:21:10 +09:00
parent e37704a94c
commit bef525eea8
5 changed files with 102 additions and 28 deletions

View file

@ -1,11 +1,15 @@
/* /*
This component supports either uploading and storing a single image, or This component supports uploading and storing an array of images.
an array of images.
Note also that an image can be stored as a simple string, or as an array of formats Note also that an image can be stored as a simple string, or as an array of formats
(each format being itself an object). (each format being itself an object).
### Deleting Images
When clearing an image, it is addeds to `deletedValues` and set to `null` in the array,
but the array item itself is not deleted. The entire array is then cleaned when submitting the form.
*/ */
import { Components, getSetting, registerSetting, registerComponent } from 'meteor/vulcan:lib'; import { Components, getSetting, registerSetting, registerComponent } from 'meteor/vulcan:lib';
import React, { PureComponent } from 'react'; import React, { PureComponent } from 'react';
@ -13,6 +17,7 @@ import PropTypes from 'prop-types';
import Dropzone from 'react-dropzone'; import Dropzone from 'react-dropzone';
import 'cross-fetch/polyfill'; // patch for browser which don't have fetch implemented import 'cross-fetch/polyfill'; // patch for browser which don't have fetch implemented
import { FormattedMessage } from 'meteor/vulcan:i18n'; import { FormattedMessage } from 'meteor/vulcan:i18n';
import set from 'lodash/set';
registerSetting('cloudinary.cloudName', null, 'Cloudinary cloud name (for image uploads)'); registerSetting('cloudinary.cloudName', null, 'Cloudinary cloud name (for image uploads)');
@ -47,7 +52,7 @@ class Image extends PureComponent {
render() { render() {
return ( return (
<div className={`upload-image ${this.props.loading ? 'upload-image-loading' : ''}`}> <div className={`upload-image ${this.props.loading ? 'upload-image-loading' : ''} ${this.props.error ? 'upload-image-error' : ''}`}>
<div className="upload-image-contents"> <div className="upload-image-contents">
<img style={{ width: 150 }} src={getImageUrl(this.props.image)} /> <img style={{ width: 150 }} src={getImageUrl(this.props.image)} />
{this.props.loading && ( {this.props.loading && (
@ -71,21 +76,38 @@ Cloudinary Image Upload component
*/ */
class Upload extends PureComponent { class Upload extends PureComponent {
state = { uploading: false } constructor(props, context) {
super(props);
count = this.props.value.length; // add callback to clean any preview or error values
context.addToSubmitForm(data => {
// keep only "real" images
const images = this.getImages({ includePreviews: false, includeDeleted: false});
// replace images in `data` object with real images
set(data, this.props.path, images);
return data;
});
}
state = { uploading: false };
/* /*
Check the field's type to decide if the component should handle Check the field's type to decide if the component should handle
multiple image uploads or not. multiple image uploads or not. Default to yes.
For multiple images, the component expects an array of images;
for single images it expects a single image object.
*/ */
enableMultiple = () => { enableMultiple = () => {
return this.props.datatype && this.props.datatype[0].type === Array; return this.props.maxCount !== 1;
};
/*
Whether to disable the dropzone.
*/
isDisabled = () => {
return this.state.uploading || this.props.maxCount <= this.getImages({ includeDeleted: false }).length;
}; };
/* /*
@ -95,6 +117,9 @@ class Upload extends PureComponent {
*/ */
onDrop = files => { onDrop = files => {
const promises = []; const promises = [];
const imagesCount = this.getImages().length;
this.props.clearFieldErrors(this.props.path);
// set the component in upload mode // set the component in upload mode
this.setState({ this.setState({
@ -106,10 +131,9 @@ class Upload extends PureComponent {
// trigger a request for each file // trigger a request for each file
files.forEach((file, index) => { files.forEach((file, index) => {
// figure out update path for current image // figure out update path for current image
const updateIndex = this.count + index; const updateIndex = imagesCount + index;
const updatePath = this.enableMultiple() ? `${this.props.path}.${updateIndex}` : this.props.path; const updatePath = `${this.props.path}.${updateIndex}`;
// build preview object // build preview object
const previewObject = { secure_url: file.preview, loading: true, preview: true }; const previewObject = { secure_url: file.preview, loading: true, preview: true };
@ -134,6 +158,8 @@ class Upload extends PureComponent {
// eslint-disable-next-line no-console // eslint-disable-next-line no-console
console.log(body.error); console.log(body.error);
this.props.throwError({ id: 'upload.error', path: this.props.path, message: body.error.message }); this.props.throwError({ id: 'upload.error', path: this.props.path, message: body.error.message });
const errorObject = { ...previewObject, loading: false, error: true };
this.props.updateCurrentValues({ [updatePath]: errorObject });
return null; return null;
} else { } else {
// use the https:// url given by cloudinary; or eager property if using transformations // use the https:// url given by cloudinary; or eager property if using transformations
@ -165,28 +191,33 @@ class Upload extends PureComponent {
/* /*
Remove the image at `index` (or just remove image if no index is passed) Remove the image at `index`
*/ */
clearImage = index => { clearImage = index => {
if (this.enableMultiple()) { this.props.updateCurrentValues({ [`${this.props.path}.${index}`]: null });
this.props.addToDeletedValues(`${this.props.path}.${index}`);
} else {
this.props.addToDeletedValues(this.props.path);
}
}; };
getImages = () => { /*
// show the actual uploaded image(s)
return this.enableMultiple() ? this.props.value : [this.props.value];
};
Get images, with or without previews/deleted images
*/
getImages = (args = {}) => {
const { includePreviews = true, includeDeleted = false } = args;
let images = this.props.value;
// remove previews if needed
images = includePreviews ? images : images.filter(image => !image.preview);
// remove deleted images
images = includeDeleted ? images : images.filter((image, index) => !this.isDeleted(index));
return images;
};
render() { render() {
const { uploading } = this.state; const { uploading } = this.state;
const images = this.getImages(); const images = this.getImages({ includeDeleted: true });
return ( return (
<div className="form-group row"> <div className={`form-group row ${this.isDisabled() ? 'upload-disabled' : ''}`}>
<label className="control-label col-sm-3">{this.props.label}</label> <label className="control-label col-sm-3">{this.props.label}</label>
<div className="col-sm-9"> <div className="col-sm-9">
<div className="upload-field"> <div className="upload-field">
@ -198,7 +229,7 @@ class Upload extends PureComponent {
className="dropzone-base" className="dropzone-base"
activeClassName="dropzone-active" activeClassName="dropzone-active"
rejectClassName="dropzone-reject" rejectClassName="dropzone-reject"
disabled={this.state.uploading} disabled={this.isDisabled()}
> >
<div> <div>
<FormattedMessage id="upload.prompt" /> <FormattedMessage id="upload.prompt" />
@ -217,8 +248,7 @@ class Upload extends PureComponent {
<div className="upload-images"> <div className="upload-images">
{images.map( {images.map(
(image, index) => (image, index) =>
!this.isDeleted(index) && !this.isDeleted(index) && (
image && (
<Image <Image
clearImage={this.clearImage} clearImage={this.clearImage}
key={index} key={index}
@ -226,6 +256,7 @@ class Upload extends PureComponent {
image={image} image={image}
loading={image.loading} loading={image.loading}
preview={image.preview} preview={image.preview}
error={image.error}
/> />
) )
)} )}
@ -245,6 +276,10 @@ Upload.propTypes = {
label: PropTypes.string, label: PropTypes.string,
}; };
Upload.contextTypes = {
addToSubmitForm: PropTypes.func,
};
registerComponent('Upload', Upload); registerComponent('Upload', Upload);
export default Upload; export default Upload;

View file

@ -72,3 +72,28 @@
justify-content: center; justify-content: center;
align-items: center; align-items: center;
} }
.upload-disabled{
.dropzone-base{
background-image: url("data:image/svg+xml,%3Csvg width='40' height='40' viewBox='0 0 40 40' xmlns='http://www.w3.org/2000/svg'%3E%3Cg fill='%23cccccc' fill-opacity='0.4' fill-rule='evenodd'%3E%3Cpath d='M0 40L40 0H20L0 20M40 40V20L20 40'/%3E%3C/g%3E%3C/svg%3E");
}
}
.upload-image-error{
.upload-image-contents{
position: relative;
&:after{
content: " ";
display: block;
position: absolute;
height: 100%;
width: 100%;
top: 0;
right: 0;
left: 0;
right: 0;
background-color: rgba(255, 255, 255, 0.6);
background-image: url("data:image/svg+xml,%3Csvg width='40' height='40' viewBox='0 0 40 40' xmlns='http://www.w3.org/2000/svg'%3E%3Cg fill='%23ff0000' fill-opacity='0.4' fill-rule='evenodd'%3E%3Cpath d='M0 40L40 0H20L0 20M40 40V20L20 40'/%3E%3C/g%3E%3C/svg%3E");
}
}
}

View file

@ -372,6 +372,16 @@ class Form extends Component {
})); }));
}; };
/*
Clear errors for a field
*/
clearFieldErrors = path => {
const errors = this.state.errors.filter(error => error.path !== path);
this.setState({ errors });
}
// --------------------------------------------------------------------- // // --------------------------------------------------------------------- //
// ------------------------------- Context ----------------------------- // // ------------------------------- Context ----------------------------- //
// --------------------------------------------------------------------- // // --------------------------------------------------------------------- //
@ -668,6 +678,7 @@ class Form extends Component {
updateCurrentValues={this.updateCurrentValues} updateCurrentValues={this.updateCurrentValues}
deletedValues={this.state.deletedValues} deletedValues={this.state.deletedValues}
addToDeletedValues={this.addToDeletedValues} addToDeletedValues={this.addToDeletedValues}
clearFieldErrors={this.clearFieldErrors}
formType={this.getFormType()} formType={this.getFormType()}
/> />
))} ))}

View file

@ -139,6 +139,7 @@ class FormComponent extends PureComponent {
currentValues, currentValues,
addToDeletedValues, addToDeletedValues,
deletedValues, deletedValues,
clearFieldErrors,
} = this.props; } = this.props;
const value = this.getValue(); const value = this.getValue();
@ -165,6 +166,7 @@ class FormComponent extends PureComponent {
updateCurrentValues, updateCurrentValues,
deletedValues, deletedValues,
addToDeletedValues, addToDeletedValues,
clearFieldErrors,
}; };
// if control is a React component, use it // if control is a React component, use it

View file

@ -52,6 +52,7 @@ class FormGroup extends PureComponent {
updateCurrentValues={this.props.updateCurrentValues} updateCurrentValues={this.props.updateCurrentValues}
deletedValues={this.props.deletedValues} deletedValues={this.props.deletedValues}
addToDeletedValues={this.props.addToDeletedValues} addToDeletedValues={this.props.addToDeletedValues}
clearFieldErrors={this.props.clearFieldErrors}
formType={this.props.formType} formType={this.props.formType}
/> />
))} ))}