app/react/Markdown/components/PublicForm.js
/* eslint-disable react/jsx-pascal-case */
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { actions, Control } from 'react-redux-form';
import { bindActionCreators } from 'redux';
import { BrowserView, MobileView } from 'react-device-detect';
import Immutable from 'immutable';
import Dropzone from 'react-dropzone';
import PropTypes from 'prop-types';
import { LocalForm } from 'app/Forms/Form';
import { MetadataFormFields, validator, prepareMetadataAndFiles } from 'app/Metadata';
import { Translate } from 'app/I18N';
import { publicSubmit } from 'app/Uploads/actions/uploadsActions';
import { FormGroup } from 'app/Forms';
import { Icon } from 'UI';
import { Loader } from 'app/components/Elements/Loader';
import './scss/public-form.scss';
import { generateID } from 'shared/IDGenerator';
import { FormCaptcha } from './FormCaptcha';
class PublicFormComponent extends Component {
static renderTitle(template) {
const titleProperty = template.get('commonProperties').find(p => p.get('name') === 'title');
const useGeneratedId = Boolean(titleProperty.get('generatedId'));
let input = <Control.text id="title" className="form-control" model=".title" />;
if (useGeneratedId) {
input = React.cloneElement(input, { defaultValue: generateID(3, 4, 4) });
}
return (
<FormGroup key="title" model=".title">
<ul className="search__filter">
<li>
<label htmlFor="title">
<Translate context={template.get('_id')}>{titleProperty.get('label')}</Translate>
<span className="required">*</span>
</label>
</li>
<li className="wide">{input}</li>
</ul>
</FormGroup>
);
}
static renderSubmitState() {
return (
<div className="public-form submiting">
<h3>
<Translate>Submitting</Translate>
</h3>
<Loader />
</div>
);
}
constructor(props) {
super(props);
this.initLocalState(props);
this.refreshFn = refresh => {
this.refreshCaptcha = refresh;
};
}
async handleSubmit(_values) {
const { submit, remote } = this.props;
const values = await prepareMetadataAndFiles(
_values,
this.state.files,
this.template,
this.mediaProperties
);
try {
this.setState({ submiting: true });
const submitResult = await submit(values, remote);
await submitResult.promise;
this.resetForm(_values);
this.setState({ submiting: false, files: [] });
} catch (e) {
this.setState({ submiting: false });
}
this.refreshCaptcha();
}
attachDispatch(dispatch) {
this.formDispatch = dispatch;
}
initLocalState(props) {
this.removeAttachment = this.removeAttachment.bind(this);
this.fileDropped = this.fileDropped.bind(this);
this.handleSubmit = this.handleSubmit.bind(this);
const template = props.template.toJS();
this.template = template;
this.mediaProperties = template.properties.filter(
p => p.type === 'image' || p.type === 'media'
);
const generatedIdTitle = props.template
.get('commonProperties')
.find(p => p.get('name') === 'title' && p.get('generatedId') === true);
this.state = { submiting: false, files: [], generatedIdTitle };
this.validators = {
captcha: { required: val => val && val.text.length },
...validator.generate(template),
};
}
async removeAttachment(removedFile) {
await this.setState(prevState => ({
files: prevState.files.filter(file => file !== removedFile),
}));
if (!this.state.files.length) {
const input = document.querySelector('#upload-button');
input.value = '';
}
}
resetForm(_values) {
this.mediaProperties.forEach(property => {
URL.revokeObjectURL(_values.metadata?.[property.name]?.data || '');
});
this.formDispatch(actions.reset('publicform'));
if (this.state.generatedIdTitle) {
this.formDispatch(actions.load('publicform', { title: generateID(3, 4, 4) }));
}
}
async fileDropped(files) {
const uploadedFiles = files;
this.state.files.forEach(file => uploadedFiles.push(file));
this.setState(prevState => ({ ...prevState, files: uploadedFiles }));
}
renderFileField(id, options) {
const defaults = { className: 'form-control on-mobile', model: `.${id}` };
const props = Object.assign(defaults, options);
return (
<div className="form-group">
<ul className="search__filter">
<li className="attachments-list">
<Translate>{id === 'file' ? 'Document' : 'Attachments'}</Translate>
<BrowserView>
<Dropzone
onDrop={this.fileDropped}
className="dropzone"
accept={id === 'file' ? { 'application/pdf': ['.pdf'] } : undefined}
>
{({ getRootProps, getInputProps }) => (
// eslint-disable-next-line react/jsx-props-no-spreading
<div {...getRootProps()}>
<div className="drop-zone">
<label>
<div className="text-content">
<div id="icon">
<Icon icon="cloud-upload-alt" />
</div>
<div id="upload-text">
<Translate>Drop your files here to upload or</Translate>
</div>
</div>
</label>
{/* eslint-disable-next-line react/jsx-props-no-spreading */}
<input id="upload-button" {...getInputProps()} />
<button id="button" type="button">
<Translate>Select files on your device</Translate>
</button>
</div>
</div>
)}
</Dropzone>
</BrowserView>
<MobileView>
<Control.file
id={id}
{...props}
onChange={e => this.fileDropped([...e.target.files])}
/>
</MobileView>
<div className="preview-list">
<ul>
{this.state.files.map(file => (
<li key={file.preview}>
<div className="preview-title">{file.name}</div>
<div>
<span onClick={() => this.removeAttachment(file)}>
<Icon icon="times" />
<Translate>Remove</Translate>
</span>
</div>
</li>
))}
</ul>
</div>
</li>
</ul>
</div>
);
}
render() {
const { template, thesauris, file, attachments } = this.props;
const { submiting } = this.state;
return (
<LocalForm
validators={this.validators}
model="publicform"
getDispatch={dispatch => this.attachDispatch(dispatch)}
onSubmit={this.handleSubmit}
>
{submiting && PublicFormComponent.renderSubmitState()}
{!submiting && (
<div className="public-form">
{PublicFormComponent.renderTitle(template)}
<MetadataFormFields
thesauris={thesauris}
model="publicform"
template={template}
boundChange={(formModel, value) =>
this.formDispatch(actions.change(formModel, value))
}
/>
{file ? this.renderFileField('file', { accept: '.pdf' }) : false}
{attachments ? this.renderFileField('attachments', { multiple: 'multiple' }) : false}
<FormCaptcha remote={this.props.remote} refresh={this.refreshFn} />
<button type="submit" className="btn btn-success">
<Translate>Submit</Translate>
</button>
</div>
)}
</LocalForm>
);
}
}
PublicFormComponent.propTypes = {
file: PropTypes.bool.isRequired,
attachments: PropTypes.bool.isRequired,
remote: PropTypes.bool.isRequired,
template: PropTypes.instanceOf(Immutable.Map).isRequired,
thesauris: PropTypes.instanceOf(Immutable.List).isRequired,
submit: PropTypes.func.isRequired,
};
export const mapStateToProps = (state, props) => {
const template = state.templates.find(tpl => tpl.get('_id') === props.template);
if (template === undefined) {
throw new Error('The template is not valid');
}
return {
template,
thesauris: state.thesauris,
file: props.file !== undefined,
remote: props.remote !== undefined,
attachments: props.attachments !== undefined,
};
};
export function mapDispatchToProps(dispatch) {
return bindActionCreators({ submit: publicSubmit }, dispatch);
}
export { PublicFormComponent };
export default connect(mapStateToProps, mapDispatchToProps)(PublicFormComponent);