mirror of
https://github.com/vale981/Vulcan
synced 2025-03-06 01:51:40 -05:00
Improve upload error handling; add clearFieldErrors;
This commit is contained in:
parent
e37704a94c
commit
bef525eea8
5 changed files with 102 additions and 28 deletions
|
@ -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;
|
||||||
|
|
|
@ -71,4 +71,29 @@
|
||||||
display: flex;
|
display: flex;
|
||||||
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");
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
|
@ -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()}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
|
|
Loading…
Add table
Reference in a new issue