mirror of
https://github.com/vale981/Vulcan
synced 2025-03-06 10:01:40 -05:00
Merge branch 'erikdakoda5' of https://github.com/ErikDakoda/Vulcan into ErikDakoda-erikdakoda5
# Conflicts: # packages/vulcan-forms/lib/components/FormComponent.jsx
This commit is contained in:
commit
d60b16ea0d
12 changed files with 506 additions and 234 deletions
|
@ -22,7 +22,10 @@ This component expects:
|
||||||
|
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { registerComponent, Components, runCallbacks, getCollection, getErrors } from 'meteor/vulcan:core';
|
import {
|
||||||
|
registerComponent, Components, runCallbacks, getCollection,
|
||||||
|
getErrors, registerSetting, getSetting, Utils
|
||||||
|
} from 'meteor/vulcan:core';
|
||||||
import React, { Component } from 'react';
|
import React, { Component } from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import { intlShape } from 'meteor/vulcan:i18n';
|
import { intlShape } from 'meteor/vulcan:i18n';
|
||||||
|
@ -34,8 +37,14 @@ import unset from 'lodash/unset';
|
||||||
import compact from 'lodash/compact';
|
import compact from 'lodash/compact';
|
||||||
import update from 'lodash/update';
|
import update from 'lodash/update';
|
||||||
import merge from 'lodash/merge';
|
import merge from 'lodash/merge';
|
||||||
|
import find from 'lodash/find';
|
||||||
|
import isEqualWith from 'lodash/isEqualWith';
|
||||||
|
|
||||||
import { convertSchema, formProperties } from '../modules/schema_utils';
|
import { convertSchema, formProperties } from '../modules/schema_utils';
|
||||||
|
|
||||||
|
registerSetting('forms.warnUnsavedChanges', false,
|
||||||
|
'Warn user about unsaved changes before leaving route', true);
|
||||||
|
|
||||||
// unsetCompact
|
// unsetCompact
|
||||||
const unsetCompact = (object, path) => {
|
const unsetCompact = (object, path) => {
|
||||||
const parentPath = path.slice(0, path.lastIndexOf('.'));
|
const parentPath = path.slice(0, path.lastIndexOf('.'));
|
||||||
|
@ -68,7 +77,7 @@ const computeStateFromProps = (nextProps) => {
|
||||||
*/
|
*/
|
||||||
|
|
||||||
class Form extends Component {
|
class Form extends Component {
|
||||||
constructor(props) {
|
constructor (props) {
|
||||||
super(props);
|
super(props);
|
||||||
|
|
||||||
this.state = {
|
this.state = {
|
||||||
|
@ -119,7 +128,18 @@ class Form extends Component {
|
||||||
|
|
||||||
*/
|
*/
|
||||||
getDocument = () => {
|
getDocument = () => {
|
||||||
const document = merge({}, this.state.initialDocument, this.defaultValues, this.state.currentValues);
|
const deletedValues = {};
|
||||||
|
this.state.deletedValues.forEach(path => {
|
||||||
|
set(deletedValues, path, null);
|
||||||
|
});
|
||||||
|
|
||||||
|
const document = merge(
|
||||||
|
{},
|
||||||
|
this.state.initialDocument,
|
||||||
|
this.defaultValues,
|
||||||
|
this.state.currentValues,
|
||||||
|
deletedValues
|
||||||
|
);
|
||||||
|
|
||||||
return document;
|
return document;
|
||||||
};
|
};
|
||||||
|
@ -223,9 +243,10 @@ class Form extends Component {
|
||||||
|
|
||||||
// remove all hidden fields
|
// remove all hidden fields
|
||||||
if (excludeHiddenFields) {
|
if (excludeHiddenFields) {
|
||||||
|
const document = this.getDocument();
|
||||||
relevantFields = _.reject(relevantFields, fieldName => {
|
relevantFields = _.reject(relevantFields, fieldName => {
|
||||||
const hidden = schema[fieldName].hidden;
|
const hidden = schema[fieldName].hidden;
|
||||||
return typeof hidden === 'function' ? hidden(this.props) : hidden;
|
return typeof hidden === 'function' ? hidden({ ...this.props, document }) : hidden;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -404,7 +425,11 @@ class Form extends Component {
|
||||||
return {
|
return {
|
||||||
throwError: this.throwError,
|
throwError: this.throwError,
|
||||||
clearForm: this.clearForm,
|
clearForm: this.clearForm,
|
||||||
submitForm: this.submitFormContext, //Change in name because we already have a function called submitForm, but no reason for the user to know about that
|
refetchForm: this.refetchForm,
|
||||||
|
isChanged: this.isChanged,
|
||||||
|
submitForm: this.submitFormContext, //Change in name because we already have a function
|
||||||
|
// called submitForm, but no reason for the user to know
|
||||||
|
// about that
|
||||||
addToDeletedValues: this.addToDeletedValues,
|
addToDeletedValues: this.addToDeletedValues,
|
||||||
updateCurrentValues: this.updateCurrentValues,
|
updateCurrentValues: this.updateCurrentValues,
|
||||||
getDocument: this.getDocument,
|
getDocument: this.getDocument,
|
||||||
|
@ -428,7 +453,9 @@ class Form extends Component {
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|
|
||||||
Manually update the current values of one or more fields(i.e. on change or blur).
|
Manually update the current values of one or more fields(i.e. on change or blur).
|
||||||
|
|
||||||
*/
|
*/
|
||||||
updateCurrentValues = newValues => {
|
updateCurrentValues = newValues => {
|
||||||
// keep the previous ones and extend (with possible replacement) with new ones
|
// keep the previous ones and extend (with possible replacement) with new ones
|
||||||
|
@ -450,22 +477,93 @@ class Form extends Component {
|
||||||
return newState;
|
return newState;
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|
|
||||||
|
Warn the user if there are unsaved changes
|
||||||
|
|
||||||
|
*/
|
||||||
|
handleRouteLeave = () => {
|
||||||
|
if (this.isChanged()) {
|
||||||
|
const message = this.context.intl.formatMessage({
|
||||||
|
id: 'forms.confirm_discard',
|
||||||
|
defaultMessage: 'Are you sure you want to discard your changes?'
|
||||||
|
});
|
||||||
|
return message;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/*
|
||||||
|
|
||||||
|
Install a route leave hook to warn the user if there are unsaved changes
|
||||||
|
|
||||||
|
*/
|
||||||
|
componentDidMount = () => {
|
||||||
|
let warnUnsavedChanges = getSetting('forms.warnUnsavedChanges');
|
||||||
|
if (typeof this.props.warnUnsavedChanges === 'boolean') {
|
||||||
|
warnUnsavedChanges = this.props.warnUnsavedChanges;
|
||||||
|
}
|
||||||
|
if (warnUnsavedChanges) {
|
||||||
|
const routes = this.props.router.routes;
|
||||||
|
const currentRoute = routes[routes.length - 1];
|
||||||
|
this.props.router.setRouteLeaveHook(currentRoute, this.handleRouteLeave);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/*
|
||||||
|
|
||||||
|
Returns true if there are any differences between the initial document and the current one
|
||||||
|
|
||||||
|
*/
|
||||||
|
isChanged = () => {
|
||||||
|
const initialDocument = this.state.initialDocument;
|
||||||
|
const changedDocument = this.getDocument();
|
||||||
|
|
||||||
|
const changedValue = find(changedDocument, (value, key, collection) => {
|
||||||
|
return !isEqualWith(value, initialDocument[key], (objValue, othValue) => {
|
||||||
|
if (!objValue && !othValue) return true;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
return typeof changedValue !== 'undefined';
|
||||||
|
};
|
||||||
|
|
||||||
|
/*
|
||||||
|
|
||||||
|
Refetch the document from the database (in case it was updated by another process or to reset the form)
|
||||||
|
|
||||||
|
*/
|
||||||
|
refetchForm = () => {
|
||||||
|
if (this.props.data && this.props.data.refetch) {
|
||||||
|
this.props.data.refetch();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/*
|
||||||
|
|
||||||
Clear and reset the form
|
Clear and reset the form
|
||||||
By default, clear errors and keep current values and deleted values
|
By default, clear errors and keep current values and deleted values
|
||||||
|
|
||||||
*/
|
*/
|
||||||
clearForm = ({ clearErrors = true, clearCurrentValues = false, clearDeletedValues = false }) => {
|
clearForm = ({
|
||||||
|
clearErrors = true,
|
||||||
|
clearCurrentValues = false,
|
||||||
|
clearDeletedValues = false,
|
||||||
|
document,
|
||||||
|
}) => {
|
||||||
|
document = document ? merge({}, this.props.prefilledProps, document) : null;
|
||||||
|
|
||||||
this.setState(prevState => ({
|
this.setState(prevState => ({
|
||||||
errors: clearErrors ? [] : prevState.errors,
|
errors: clearErrors ? [] : prevState.errors,
|
||||||
currentValues: clearCurrentValues ? {} : prevState.currentValues,
|
currentValues: clearCurrentValues ? {} : prevState.currentValues,
|
||||||
deletedValues: clearDeletedValues ? [] : prevState.deletedValues,
|
deletedValues: clearDeletedValues ? [] : prevState.deletedValues,
|
||||||
|
initialDocument: document ? document : prevState.initialDocument,
|
||||||
disabled: false,
|
disabled: false,
|
||||||
}));
|
}));
|
||||||
};
|
};
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|
|
||||||
Key down handler
|
Key down handler
|
||||||
|
|
||||||
*/
|
*/
|
||||||
|
@ -489,15 +587,11 @@ class Form extends Component {
|
||||||
// for new mutation, run refetch function if it exists
|
// for new mutation, run refetch function if it exists
|
||||||
if (mutationType === 'new' && this.props.refetch) this.props.refetch();
|
if (mutationType === 'new' && this.props.refetch) this.props.refetch();
|
||||||
|
|
||||||
// call the clear form method (i.e. trigger setState) only if the form has not been unmounted (we are in an async callback, everything can happen!)
|
// call the clear form method (i.e. trigger setState) only if the form has not been unmounted
|
||||||
|
// (we are in an async callback, everything can happen!)
|
||||||
if (typeof this.refs.form !== 'undefined') {
|
if (typeof this.refs.form !== 'undefined') {
|
||||||
let clearCurrentValues = false;
|
this.refs.form.reset();
|
||||||
// reset form if this is a new document form
|
this.clearForm({ clearErrors: true, clearCurrentValues: true, clearDeletedValues: true, document });
|
||||||
if (this.getFormType() === 'new') {
|
|
||||||
this.refs.form.reset();
|
|
||||||
clearCurrentValues = true;
|
|
||||||
}
|
|
||||||
this.clearForm({ clearErrors: true, clearCurrentValues, clearDeletedValues: true });
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// run document through mutation success callbacks
|
// run document through mutation success callbacks
|
||||||
|
@ -523,9 +617,11 @@ class Form extends Component {
|
||||||
// add error to state
|
// add error to state
|
||||||
this.throwError(error);
|
this.throwError(error);
|
||||||
}
|
}
|
||||||
|
|
||||||
// run error callback if it exists
|
// run error callback if it exists
|
||||||
if (this.props.errorCallback) this.props.errorCallback(document, error);
|
if (this.props.errorCallback) this.props.errorCallback(document, error);
|
||||||
|
|
||||||
|
Utils.scrollIntoView('.flash-message');
|
||||||
};
|
};
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|
@ -632,14 +728,14 @@ class Form extends Component {
|
||||||
// ----------------------------- Render -------------------------------- //
|
// ----------------------------- Render -------------------------------- //
|
||||||
// --------------------------------------------------------------------- //
|
// --------------------------------------------------------------------- //
|
||||||
|
|
||||||
render() {
|
render () {
|
||||||
const fieldGroups = this.getFieldGroups();
|
const fieldGroups = this.getFieldGroups();
|
||||||
const collectionName = this.getCollection()._name;
|
const collectionName = this.getCollection()._name;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={'document-' + this.getFormType()}>
|
<div className={'document-' + this.getFormType()}>
|
||||||
<Formsy.Form onSubmit={this.submitForm} onKeyDown={this.formKeyDown} disabled={this.state.disabled} ref="form">
|
<Formsy.Form onSubmit={this.submitForm} onKeyDown={this.formKeyDown} disabled={this.state.disabled} ref="form">
|
||||||
<Components.FormErrors errors={this.state.errors} />
|
<Components.FormErrors errors={this.state.errors}/>
|
||||||
|
|
||||||
{fieldGroups.map(group => (
|
{fieldGroups.map(group => (
|
||||||
<Components.FormGroup
|
<Components.FormGroup
|
||||||
|
@ -662,7 +758,9 @@ class Form extends Component {
|
||||||
<Components.FormSubmit
|
<Components.FormSubmit
|
||||||
submitLabel={this.props.submitLabel}
|
submitLabel={this.props.submitLabel}
|
||||||
cancelLabel={this.props.cancelLabel}
|
cancelLabel={this.props.cancelLabel}
|
||||||
|
revertLabel={this.props.revertLabel}
|
||||||
cancelCallback={this.props.cancelCallback}
|
cancelCallback={this.props.cancelCallback}
|
||||||
|
revertCallback={this.props.revertCallback}
|
||||||
document={this.getDocument()}
|
document={this.getDocument()}
|
||||||
deleteDocument={(this.getFormType() === 'edit' && this.props.showRemove && this.deleteDocument) || null}
|
deleteDocument={(this.getFormType() === 'edit' && this.props.showRemove && this.deleteDocument) || null}
|
||||||
collectionName={collectionName}
|
collectionName={collectionName}
|
||||||
|
@ -700,7 +798,9 @@ Form.propTypes = {
|
||||||
showRemove: PropTypes.bool,
|
showRemove: PropTypes.bool,
|
||||||
submitLabel: PropTypes.string,
|
submitLabel: PropTypes.string,
|
||||||
cancelLabel: PropTypes.string,
|
cancelLabel: PropTypes.string,
|
||||||
|
revertLabel: PropTypes.string,
|
||||||
repeatErrors: PropTypes.bool,
|
repeatErrors: PropTypes.bool,
|
||||||
|
warnUnsavedChanges: PropTypes.bool,
|
||||||
|
|
||||||
// callbacks
|
// callbacks
|
||||||
submitCallback: PropTypes.func,
|
submitCallback: PropTypes.func,
|
||||||
|
@ -708,6 +808,7 @@ Form.propTypes = {
|
||||||
removeSuccessCallback: PropTypes.func,
|
removeSuccessCallback: PropTypes.func,
|
||||||
errorCallback: PropTypes.func,
|
errorCallback: PropTypes.func,
|
||||||
cancelCallback: PropTypes.func,
|
cancelCallback: PropTypes.func,
|
||||||
|
revertCallback: PropTypes.func,
|
||||||
|
|
||||||
currentUser: PropTypes.object,
|
currentUser: PropTypes.object,
|
||||||
client: PropTypes.object,
|
client: PropTypes.object,
|
||||||
|
@ -734,6 +835,8 @@ Form.childContextTypes = {
|
||||||
setFormState: PropTypes.func,
|
setFormState: PropTypes.func,
|
||||||
throwError: PropTypes.func,
|
throwError: PropTypes.func,
|
||||||
clearForm: PropTypes.func,
|
clearForm: PropTypes.func,
|
||||||
|
refetchForm: PropTypes.func,
|
||||||
|
isChanged: PropTypes.func,
|
||||||
initialDocument: PropTypes.object,
|
initialDocument: PropTypes.object,
|
||||||
getDocument: PropTypes.func,
|
getDocument: PropTypes.func,
|
||||||
submitForm: PropTypes.func,
|
submitForm: PropTypes.func,
|
||||||
|
|
|
@ -1,46 +1,60 @@
|
||||||
import React, { Component } from 'react';
|
import React, { Component } from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import { intlShape } from 'meteor/vulcan:i18n';
|
|
||||||
import classNames from 'classnames';
|
|
||||||
import { Components } from 'meteor/vulcan:core';
|
import { Components } from 'meteor/vulcan:core';
|
||||||
import { registerComponent } from 'meteor/vulcan:core';
|
import { registerComponent } from 'meteor/vulcan:core';
|
||||||
import debounce from 'lodash.debounce';
|
|
||||||
import get from 'lodash/get';
|
import get from 'lodash/get';
|
||||||
import merge from 'lodash/merge';
|
import merge from 'lodash/merge';
|
||||||
import find from 'lodash/find';
|
import find from 'lodash/find';
|
||||||
import isObjectLike from 'lodash/isObjectLike';
|
import isObjectLike from 'lodash/isObjectLike';
|
||||||
|
import isEqual from 'lodash/isEqual';
|
||||||
import { isEmptyValue } from '../modules/utils.js';
|
import { isEmptyValue } from '../modules/utils.js';
|
||||||
|
|
||||||
class FormComponent extends Component {
|
class FormComponent extends Component {
|
||||||
constructor(props) {
|
|
||||||
|
constructor (props) {
|
||||||
super(props);
|
super(props);
|
||||||
|
|
||||||
const value = this.getValue(props);
|
this.state = {};
|
||||||
|
}
|
||||||
|
|
||||||
if (this.showCharsRemaining(props)) {
|
componentWillMount () {
|
||||||
const characterCount = value ? value.length : 0;
|
if (this.showCharsRemaining()) {
|
||||||
this.state = {
|
const value = this.getValue();
|
||||||
charsRemaining: props.max - characterCount,
|
this.updateCharacterCount(value);
|
||||||
};
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// shouldComponentUpdate(nextProps, nextState) {
|
shouldComponentUpdate (nextProps, nextState) {
|
||||||
// const { currentValues, deletedValues, errors } = nextProps;
|
// allow custom controls to determine if they should update
|
||||||
// const { path } = this.props;
|
if (!['nested', 'number', 'url', 'email', 'textarea', 'checkbox',
|
||||||
// const hasChanged = currentValues[path] && currentValues[path] !== this.props.currentValues[path];
|
'checkboxgroup', 'radiogroup', 'select', 'selectmultiple', 'datetime',
|
||||||
// const hasError = !!errors[path];
|
'date', 'time', 'text'].includes(this.getType(nextProps))) {
|
||||||
// const hasBeenDeleted = deletedValues.includes(path) && !this.props.deletedValues.includes(path)
|
return true;
|
||||||
// return hasChanged || hasError || hasBeenDeleted;
|
}
|
||||||
// }
|
|
||||||
|
const { currentValues, deletedValues, errors } = nextProps;
|
||||||
|
const { path } = this.props;
|
||||||
|
|
||||||
|
const valueChanged = currentValues[path] !== this.props.currentValues[path];
|
||||||
|
const errorChanged = !isEqual(this.getErrors(errors), this.getErrors());
|
||||||
|
const deleteChanged = deletedValues.includes(path) !== this.props.deletedValues.includes(path);
|
||||||
|
const charsChanged = nextState.charsRemaining !== this.state.charsRemaining;
|
||||||
|
|
||||||
|
return valueChanged || errorChanged || deleteChanged || charsChanged;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
|
||||||
|
Function passed to form controls (always controlled) to update their value
|
||||||
|
|
||||||
|
*/
|
||||||
handleChange = (name, value) => {
|
handleChange = (name, value) => {
|
||||||
// if value is an empty string, delete the field
|
// if value is an empty string, delete the field
|
||||||
if (value === '') {
|
if (value === '') {
|
||||||
value = null;
|
value = null;
|
||||||
}
|
}
|
||||||
// if this is a number field, convert value before sending it up to Form
|
// if this is a number field, convert value before sending it up to Form
|
||||||
if (this.getType() === 'number') {
|
if (this.getType() === 'number' && value != null) {
|
||||||
value = Number(value);
|
value = Number(value);
|
||||||
}
|
}
|
||||||
this.props.updateCurrentValues({ [this.props.path]: value });
|
this.props.updateCurrentValues({ [this.props.path]: value });
|
||||||
|
@ -52,17 +66,15 @@ class FormComponent extends Component {
|
||||||
};
|
};
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|
|
||||||
Note: not currently used because when function is debounced
|
Updates the state of charsCount and charsRemaining as the users types
|
||||||
some changes might not register if the user submits form too soon
|
|
||||||
|
|
||||||
*/
|
*/
|
||||||
handleChangeDebounced = debounce(this.handleChange, 500, { leading: true });
|
|
||||||
|
|
||||||
updateCharacterCount = value => {
|
updateCharacterCount = value => {
|
||||||
const characterCount = value ? value.length : 0;
|
const characterCount = value ? value.length : 0;
|
||||||
this.setState({
|
this.setState({
|
||||||
charsRemaining: this.props.max - characterCount,
|
charsRemaining: this.props.max - characterCount,
|
||||||
|
charsCount: characterCount,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -90,7 +102,15 @@ class FormComponent extends Component {
|
||||||
value = merge({}, documentValue, currentValue);
|
value = merge({}, documentValue, currentValue);
|
||||||
} else {
|
} else {
|
||||||
// note: value has to default to '' to make component controlled
|
// note: value has to default to '' to make component controlled
|
||||||
value = currentValue || documentValue || '';
|
//value = currentValue || documentValue || '';
|
||||||
|
// note: the previous line does not work when a checkbox is 'false' or a number is '0'
|
||||||
|
value = currentValue;
|
||||||
|
if (typeof value === 'undefined' || value === null) {
|
||||||
|
value = documentValue;
|
||||||
|
}
|
||||||
|
if (typeof value === 'undefined' || value === null) {
|
||||||
|
value = '';
|
||||||
|
}
|
||||||
}
|
}
|
||||||
// replace empty value, which has not been prefilled, by the default value from the schema
|
// replace empty value, which has not been prefilled, by the default value from the schema
|
||||||
if (isEmptyValue(value)) {
|
if (isEmptyValue(value)) {
|
||||||
|
@ -116,8 +136,9 @@ class FormComponent extends Component {
|
||||||
Get errors from Form state through context
|
Get errors from Form state through context
|
||||||
|
|
||||||
*/
|
*/
|
||||||
getErrors = () => {
|
getErrors = (errors) => {
|
||||||
const fieldErrors = this.props.errors.filter(error => error.path === this.props.path);
|
errors = errors || this.props.errors;
|
||||||
|
const fieldErrors = errors.filter(error => error.path === this.props.path);
|
||||||
return fieldErrors;
|
return fieldErrors;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -131,194 +152,49 @@ class FormComponent extends Component {
|
||||||
const p = props || this.props;
|
const p = props || this.props;
|
||||||
const fieldType = p.datatype && p.datatype[0].type;
|
const fieldType = p.datatype && p.datatype[0].type;
|
||||||
const autoType =
|
const autoType =
|
||||||
fieldType === Number ? 'number' : fieldType === Boolean ? 'checkbox' : fieldType === Date ? 'date' : 'text';
|
fieldType === Number ? 'number' :
|
||||||
|
fieldType === Boolean ? 'checkbox' :
|
||||||
|
fieldType === Date ?
|
||||||
|
'date' :
|
||||||
|
'text';
|
||||||
return p.input || autoType;
|
return p.input || autoType;
|
||||||
};
|
};
|
||||||
|
|
||||||
renderComponent() {
|
/*
|
||||||
const {
|
|
||||||
input,
|
Function passed to form controls to clear their contents (set their value to null)
|
||||||
beforeComponent,
|
|
||||||
afterComponent,
|
*/
|
||||||
options,
|
clearField = event => {
|
||||||
name,
|
event.preventDefault();
|
||||||
label,
|
event.stopPropagation();
|
||||||
formType,
|
|
||||||
/*
|
|
||||||
|
|
||||||
note: following properties will be passed as part of `...this.props` in `properties`:
|
|
||||||
|
|
||||||
*/
|
|
||||||
// throwError,
|
|
||||||
// updateCurrentValues,
|
|
||||||
// currentValues,
|
|
||||||
// addToDeletedValues,
|
|
||||||
// deletedValues,
|
|
||||||
// clearFieldErrors,
|
|
||||||
// currentUser,
|
|
||||||
} = this.props;
|
|
||||||
|
|
||||||
const value = this.getValue();
|
|
||||||
const errors = this.getErrors();
|
|
||||||
|
|
||||||
// these properties are whitelisted so that they can be safely passed to the actual form input
|
|
||||||
// and avoid https://facebook.github.io/react/warnings/unknown-prop.html warnings
|
|
||||||
const inputProperties = {
|
|
||||||
name,
|
|
||||||
options,
|
|
||||||
label,
|
|
||||||
onChange: this.handleChange,
|
|
||||||
value,
|
|
||||||
...this.props.inputProperties,
|
|
||||||
};
|
|
||||||
|
|
||||||
// note: we also pass value on props directly
|
|
||||||
const properties = {
|
|
||||||
...this.props,
|
|
||||||
value,
|
|
||||||
errors, // only get errors for the current field
|
|
||||||
inputProperties,
|
|
||||||
};
|
|
||||||
|
|
||||||
// if input is a React component, use it
|
|
||||||
if (typeof input === 'function') {
|
|
||||||
const InputComponent = input;
|
|
||||||
return <InputComponent {...properties} />;
|
|
||||||
} else {
|
|
||||||
// else pick a predefined component
|
|
||||||
|
|
||||||
switch (this.getType()) {
|
|
||||||
case 'nested':
|
|
||||||
return <Components.FormNested {...properties} />;
|
|
||||||
|
|
||||||
case 'number':
|
|
||||||
return <Components.FormComponentNumber {...properties} />;
|
|
||||||
|
|
||||||
case 'url':
|
|
||||||
return <Components.FormComponentUrl {...properties} />;
|
|
||||||
|
|
||||||
case 'email':
|
|
||||||
return <Components.FormComponentEmail {...properties} />;
|
|
||||||
|
|
||||||
case 'textarea':
|
|
||||||
return <Components.FormComponentTextarea {...properties} />;
|
|
||||||
|
|
||||||
case 'checkbox':
|
|
||||||
// formsy-react-components expects a boolean value for checkbox
|
|
||||||
// https://github.com/twisty/formsy-react-components/blob/v0.11.1/src/checkbox.js#L20
|
|
||||||
properties.inputProperties.value = !!properties.inputProperties.value;
|
|
||||||
return <Components.FormComponentCheckbox {...properties} />;
|
|
||||||
|
|
||||||
case 'checkboxgroup':
|
|
||||||
// formsy-react-components expects an array value
|
|
||||||
// https://github.com/twisty/formsy-react-components/blob/v0.11.1/src/checkbox-group.js#L42
|
|
||||||
if (!Array.isArray(properties.inputProperties.value)) {
|
|
||||||
properties.inputProperties.value = [properties.inputProperties.value];
|
|
||||||
}
|
|
||||||
// in case of checkbox groups, check "checked" option to populate value if this is a "new document" form
|
|
||||||
const checkedValues = _.where(properties.options, { checked: true }).map(option => option.value);
|
|
||||||
if (checkedValues.length && !properties.inputProperties.value && formType === 'new') {
|
|
||||||
properties.inputProperties.value = checkedValues;
|
|
||||||
}
|
|
||||||
return <Components.FormComponentCheckboxGroup {...properties} />;
|
|
||||||
|
|
||||||
case 'radiogroup':
|
|
||||||
// TODO: remove this?
|
|
||||||
// formsy-react-compnents RadioGroup expects an onChange callback
|
|
||||||
// https://github.com/twisty/formsy-react-components/blob/v0.11.1/src/radio-group.js#L33
|
|
||||||
// properties.onChange = (name, value) => {
|
|
||||||
// this.context.updateCurrentValues({ [name]: value });
|
|
||||||
// };
|
|
||||||
return <Components.FormComponentRadioGroup {...properties} />;
|
|
||||||
|
|
||||||
case 'select':
|
|
||||||
const noneOption = {
|
|
||||||
label: this.context.intl.formatMessage({ id: 'forms.select_option' }),
|
|
||||||
value: '',
|
|
||||||
disabled: true,
|
|
||||||
};
|
|
||||||
properties.inputProperties.options = [noneOption, ...properties.inputProperties.options];
|
|
||||||
|
|
||||||
return <Components.FormComponentSelect {...properties} />;
|
|
||||||
|
|
||||||
case 'selectmultiple':
|
|
||||||
properties.inputProperties.multiple = true;
|
|
||||||
return <Components.FormComponentSelect {...properties} />;
|
|
||||||
|
|
||||||
case 'datetime':
|
|
||||||
return <Components.FormComponentDateTime {...properties} />;
|
|
||||||
|
|
||||||
case 'date':
|
|
||||||
return <Components.FormComponentDate {...properties} />;
|
|
||||||
|
|
||||||
case 'time':
|
|
||||||
return <Components.FormComponentTime {...properties} />;
|
|
||||||
|
|
||||||
case 'text':
|
|
||||||
return <Components.FormComponentDefault {...properties} />;
|
|
||||||
|
|
||||||
default:
|
|
||||||
const CustomComponent = Components[input];
|
|
||||||
return CustomComponent ? (
|
|
||||||
<CustomComponent {...properties} />
|
|
||||||
) : (
|
|
||||||
<Components.FormComponentDefault {...properties} />
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
showClear = () => {
|
|
||||||
return ['datetime', 'time', 'select', 'radiogroup'].includes(this.props.input);
|
|
||||||
};
|
|
||||||
|
|
||||||
clearField = e => {
|
|
||||||
e.preventDefault();
|
|
||||||
this.props.updateCurrentValues({ [this.props.path]: null });
|
this.props.updateCurrentValues({ [this.props.path]: null });
|
||||||
|
if (this.showCharsRemaining()) {
|
||||||
|
this.updateCharacterCount(null);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
renderClear() {
|
render () {
|
||||||
return (
|
return (
|
||||||
<a
|
<Components.FormComponentInner
|
||||||
href="javascript:void(0)"
|
{...this.props}
|
||||||
className="form-component-clear"
|
{...this.state}
|
||||||
title={this.context.intl.formatMessage({ id: 'forms.clear_field' })}
|
inputType={this.getType()}
|
||||||
onClick={this.clearField}
|
value={this.getValue()}
|
||||||
>
|
errors={this.getErrors()}
|
||||||
<span>✕</span>
|
document={this.context.getDocument()}
|
||||||
</a>
|
showCharsRemaining={!!this.showCharsRemaining()}
|
||||||
|
onChange={this.handleChange}
|
||||||
|
clearField={this.clearField}
|
||||||
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
|
||||||
const { beforeComponent, afterComponent, name, input } = this.props;
|
|
||||||
|
|
||||||
const hasErrors = this.getErrors() && this.getErrors().length;
|
|
||||||
const inputName = typeof input === 'function' ? input.name : input;
|
|
||||||
const inputClass = classNames('form-input', `input-${name}`, `form-component-${inputName || 'default'}`, {
|
|
||||||
'input-error': hasErrors,
|
|
||||||
});
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className={inputClass}>
|
|
||||||
{beforeComponent ? beforeComponent : null}
|
|
||||||
{this.renderComponent()}
|
|
||||||
{hasErrors ? <Components.FieldErrors errors={this.getErrors()} /> : null}
|
|
||||||
{this.showClear() ? this.renderClear() : null}
|
|
||||||
{this.showCharsRemaining() && (
|
|
||||||
<div className={classNames('form-control-limit', { danger: this.state.charsRemaining < 10 })}>
|
|
||||||
{this.state.charsRemaining}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
{afterComponent ? afterComponent : null}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
FormComponent.propTypes = {
|
FormComponent.propTypes = {
|
||||||
document: PropTypes.object,
|
document: PropTypes.object,
|
||||||
name: PropTypes.string,
|
name: PropTypes.string.isRequired,
|
||||||
label: PropTypes.string,
|
label: PropTypes.string,
|
||||||
value: PropTypes.any,
|
value: PropTypes.any,
|
||||||
placeholder: PropTypes.string,
|
placeholder: PropTypes.string,
|
||||||
|
@ -326,19 +202,21 @@ FormComponent.propTypes = {
|
||||||
options: PropTypes.any,
|
options: PropTypes.any,
|
||||||
input: PropTypes.any,
|
input: PropTypes.any,
|
||||||
datatype: PropTypes.any,
|
datatype: PropTypes.any,
|
||||||
path: PropTypes.string,
|
path: PropTypes.string.isRequired,
|
||||||
disabled: PropTypes.bool,
|
disabled: PropTypes.bool,
|
||||||
nestedSchema: PropTypes.object,
|
nestedSchema: PropTypes.object,
|
||||||
currentValues: PropTypes.object,
|
currentValues: PropTypes.object.isRequired,
|
||||||
deletedValues: PropTypes.array,
|
deletedValues: PropTypes.array.isRequired,
|
||||||
updateCurrentValues: PropTypes.func,
|
throwError: PropTypes.func.isRequired,
|
||||||
errors: PropTypes.array,
|
updateCurrentValues: PropTypes.func.isRequired,
|
||||||
|
errors: PropTypes.array.isRequired,
|
||||||
addToDeletedValues: PropTypes.func,
|
addToDeletedValues: PropTypes.func,
|
||||||
|
clearFieldErrors: PropTypes.func.isRequired,
|
||||||
|
currentUser: PropTypes.object,
|
||||||
};
|
};
|
||||||
|
|
||||||
FormComponent.contextTypes = {
|
FormComponent.contextTypes = {
|
||||||
intl: intlShape,
|
getDocument: PropTypes.func.isRequired,
|
||||||
getDocument: PropTypes.func,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
registerComponent('FormComponent', FormComponent);
|
registerComponent('FormComponent', FormComponent);
|
||||||
|
|
|
@ -67,8 +67,16 @@ FormGroup.propTypes = {
|
||||||
name: PropTypes.string,
|
name: PropTypes.string,
|
||||||
label: PropTypes.string,
|
label: PropTypes.string,
|
||||||
order: PropTypes.number,
|
order: PropTypes.number,
|
||||||
fields: PropTypes.array,
|
fields: PropTypes.array.isRequired,
|
||||||
updateCurrentValues: PropTypes.func,
|
errors: PropTypes.array.isRequired,
|
||||||
|
throwError: PropTypes.func.isRequired,
|
||||||
|
currentValues: PropTypes.object.isRequired,
|
||||||
|
updateCurrentValues: PropTypes.func.isRequired,
|
||||||
|
deletedValues: PropTypes.array.isRequired,
|
||||||
|
addToDeletedValues: PropTypes.func.isRequired,
|
||||||
|
clearFieldErrors: PropTypes.func.isRequired,
|
||||||
|
formType: PropTypes.string.isRequired,
|
||||||
|
currentUser: PropTypes.object,
|
||||||
};
|
};
|
||||||
|
|
||||||
registerComponent('FormGroup', FormGroup);
|
registerComponent('FormGroup', FormGroup);
|
||||||
|
|
|
@ -8,10 +8,15 @@ const FormSubmit = ({
|
||||||
submitLabel,
|
submitLabel,
|
||||||
cancelLabel,
|
cancelLabel,
|
||||||
cancelCallback,
|
cancelCallback,
|
||||||
|
revertLabel,
|
||||||
|
revertCallback,
|
||||||
document,
|
document,
|
||||||
deleteDocument,
|
deleteDocument,
|
||||||
collectionName,
|
collectionName,
|
||||||
classes,
|
classes,
|
||||||
|
}, {
|
||||||
|
isChanged,
|
||||||
|
clearForm,
|
||||||
}) => (
|
}) => (
|
||||||
<div className="form-submit">
|
<div className="form-submit">
|
||||||
<Components.Button type="submit" variant="primary">
|
<Components.Button type="submit" variant="primary">
|
||||||
|
@ -29,7 +34,20 @@ const FormSubmit = ({
|
||||||
{cancelLabel ? cancelLabel : <FormattedMessage id="forms.cancel" />}
|
{cancelLabel ? cancelLabel : <FormattedMessage id="forms.cancel" />}
|
||||||
</a>
|
</a>
|
||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
|
{revertCallback ? (
|
||||||
|
<a
|
||||||
|
className="form-cancel"
|
||||||
|
onClick={e => {
|
||||||
|
e.preventDefault();
|
||||||
|
clearForm({ clearErrors: true, clearCurrentValues: true, clearDeletedValues: true });
|
||||||
|
revertCallback(document);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{revertLabel ? revertLabel : <FormattedMessage id="forms.revert"/>}
|
||||||
|
</a>
|
||||||
|
) : null}
|
||||||
|
|
||||||
{deleteDocument ? (
|
{deleteDocument ? (
|
||||||
<div>
|
<div>
|
||||||
<hr />
|
<hr />
|
||||||
|
@ -45,10 +63,18 @@ FormSubmit.propTypes = {
|
||||||
submitLabel: PropTypes.string,
|
submitLabel: PropTypes.string,
|
||||||
cancelLabel: PropTypes.string,
|
cancelLabel: PropTypes.string,
|
||||||
cancelCallback: PropTypes.func,
|
cancelCallback: PropTypes.func,
|
||||||
|
revertLabel: PropTypes.string,
|
||||||
|
revertCallback: PropTypes.func,
|
||||||
document: PropTypes.object,
|
document: PropTypes.object,
|
||||||
deleteDocument: PropTypes.func,
|
deleteDocument: PropTypes.func,
|
||||||
collectionName: PropTypes.string,
|
collectionName: PropTypes.string,
|
||||||
classes: PropTypes.object,
|
classes: PropTypes.object,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
FormSubmit.contextTypes = {
|
||||||
|
isChanged: PropTypes.func,
|
||||||
|
clearForm: PropTypes.func,
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
registerComponent('FormSubmit', FormSubmit);
|
registerComponent('FormSubmit', FormSubmit);
|
||||||
|
|
|
@ -27,6 +27,7 @@ component is also added to wait for withDocument's loading prop to be false)
|
||||||
import React, { PureComponent } from 'react';
|
import React, { PureComponent } from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import { intlShape } from 'meteor/vulcan:i18n';
|
import { intlShape } from 'meteor/vulcan:i18n';
|
||||||
|
import { withRouter } from 'react-router'
|
||||||
import { withApollo, compose } from 'react-apollo';
|
import { withApollo, compose } from 'react-apollo';
|
||||||
import {
|
import {
|
||||||
Components,
|
Components,
|
||||||
|
@ -265,7 +266,13 @@ FormWrapper.propTypes = {
|
||||||
prefilledProps: PropTypes.object,
|
prefilledProps: PropTypes.object,
|
||||||
layout: PropTypes.string,
|
layout: PropTypes.string,
|
||||||
fields: PropTypes.arrayOf(PropTypes.string),
|
fields: PropTypes.arrayOf(PropTypes.string),
|
||||||
|
hideFields: PropTypes.arrayOf(PropTypes.string),
|
||||||
showRemove: PropTypes.bool,
|
showRemove: PropTypes.bool,
|
||||||
|
submitLabel: PropTypes.string,
|
||||||
|
cancelLabel: PropTypes.string,
|
||||||
|
revertLabel: PropTypes.string,
|
||||||
|
repeatErrors: PropTypes.bool,
|
||||||
|
warnUnsavedChanges: PropTypes.bool,
|
||||||
|
|
||||||
// callbacks
|
// callbacks
|
||||||
submitCallback: PropTypes.func,
|
submitCallback: PropTypes.func,
|
||||||
|
@ -273,6 +280,7 @@ FormWrapper.propTypes = {
|
||||||
removeSuccessCallback: PropTypes.func,
|
removeSuccessCallback: PropTypes.func,
|
||||||
errorCallback: PropTypes.func,
|
errorCallback: PropTypes.func,
|
||||||
cancelCallback: PropTypes.func,
|
cancelCallback: PropTypes.func,
|
||||||
|
revertCallback: PropTypes.func,
|
||||||
|
|
||||||
currentUser: PropTypes.object,
|
currentUser: PropTypes.object,
|
||||||
client: PropTypes.object,
|
client: PropTypes.object,
|
||||||
|
@ -287,4 +295,4 @@ FormWrapper.contextTypes = {
|
||||||
intl: intlShape
|
intl: intlShape
|
||||||
}
|
}
|
||||||
|
|
||||||
registerComponent('SmartForm', FormWrapper, withCurrentUser, withApollo);
|
registerComponent('SmartForm', FormWrapper, withCurrentUser, withApollo, withRouter);
|
||||||
|
|
|
@ -43,7 +43,9 @@ addStrings('en', {
|
||||||
"forms.select_option": "-- select option --",
|
"forms.select_option": "-- select option --",
|
||||||
"forms.delete": "Delete",
|
"forms.delete": "Delete",
|
||||||
"forms.delete_confirm": "Delete document?",
|
"forms.delete_confirm": "Delete document?",
|
||||||
|
"forms.revert": "Revert",
|
||||||
|
"forms.confirm_discard": "Discard changes?",
|
||||||
|
|
||||||
"users.profile": "Profile",
|
"users.profile": "Profile",
|
||||||
"users.complete_profile": "Complete your Profile",
|
"users.complete_profile": "Complete your Profile",
|
||||||
"users.profile_completed": "Profile completed.",
|
"users.profile_completed": "Profile completed.",
|
||||||
|
|
|
@ -43,6 +43,8 @@ addStrings('es', {
|
||||||
"forms.select_option": "-- seleccionar opción --",
|
"forms.select_option": "-- seleccionar opción --",
|
||||||
"forms.delete": "Eliminar",
|
"forms.delete": "Eliminar",
|
||||||
"forms.delete_confirm": "¿Eliminar documento?",
|
"forms.delete_confirm": "¿Eliminar documento?",
|
||||||
|
"forms.revert": "Revert", // TODO: translate
|
||||||
|
"forms.confirm_discard": "Discard changes?", // TODO: translate
|
||||||
|
|
||||||
"users.profile": "Perfil",
|
"users.profile": "Perfil",
|
||||||
"users.complete_profile": "Complete su perfil",
|
"users.complete_profile": "Complete su perfil",
|
||||||
|
|
|
@ -45,6 +45,8 @@ addStrings('fr', {
|
||||||
"forms.delete_confirm": "Supprimer le document?",
|
"forms.delete_confirm": "Supprimer le document?",
|
||||||
"forms.next": "Suivant",
|
"forms.next": "Suivant",
|
||||||
"forms.previous": "Précédent",
|
"forms.previous": "Précédent",
|
||||||
|
"forms.revert": "Revert", // TODO: translate
|
||||||
|
"forms.confirm_discard": "Discard changes?", // TODO: translate
|
||||||
|
|
||||||
"users.profile": "Profil",
|
"users.profile": "Profil",
|
||||||
"users.complete_profile": "Complétez votre profil",
|
"users.complete_profile": "Complétez votre profil",
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
import { compose } from 'react-apollo'; // note: at the moment, compose@react-apollo === compose@redux ; see https://github.com/apollostack/react-apollo/blob/master/src/index.ts#L4-L7
|
import { compose } from 'react-apollo'; // note: at the moment, compose@react-apollo === compose@redux ; see https://github.com/apollostack/react-apollo/blob/master/src/index.ts#L4-L7
|
||||||
|
import React from 'react';
|
||||||
|
|
||||||
export const Components = {}; // will be populated on startup (see vulcan:routing)
|
export const Components = {}; // will be populated on startup (see vulcan:routing)
|
||||||
export const ComponentsTable = {} // storage for infos about components
|
export const ComponentsTable = {} // storage for infos about components
|
||||||
|
@ -112,3 +113,27 @@ export const populateComponentsApp = () => {
|
||||||
export const copyHoCs = (sourceComponent, targetComponent) => {
|
export const copyHoCs = (sourceComponent, targetComponent) => {
|
||||||
return compose(...sourceComponent.hocs)(targetComponent);
|
return compose(...sourceComponent.hocs)(targetComponent);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns an instance of the given component name of function
|
||||||
|
* @param {string|function} component A component or registered component name
|
||||||
|
* @param {Object} [props] Optional properties to pass to the component
|
||||||
|
*/
|
||||||
|
//eslint-disable-next-line react/display-name
|
||||||
|
export const instantiateComponent = (component, props) => {
|
||||||
|
if (!component) {
|
||||||
|
return null;
|
||||||
|
} else if (typeof component === 'string') {
|
||||||
|
const Component = getComponent(component);
|
||||||
|
return <Component {...props}/>
|
||||||
|
} else if (typeof component === 'function' && component.prototype && component.prototype.isReactComponent) {
|
||||||
|
const Component = component;
|
||||||
|
return <Component {...props}/>
|
||||||
|
} else if (typeof component === 'function') {
|
||||||
|
return component(props);
|
||||||
|
} else {
|
||||||
|
return component;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -112,6 +112,15 @@ Utils.scrollPageTo = function(selector) {
|
||||||
$('body').scrollTop($(selector).offset().top);
|
$('body').scrollTop($(selector).offset().top);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
Utils.scrollIntoView = function (selector) {
|
||||||
|
if (!document) return;
|
||||||
|
|
||||||
|
const element = document.querySelector(selector);
|
||||||
|
if (element) {
|
||||||
|
element.scrollIntoView();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
Utils.getDateRange = function(pageNumber) {
|
Utils.getDateRange = function(pageNumber) {
|
||||||
var now = moment(new Date());
|
var now = moment(new Date());
|
||||||
var dayToDisplay=now.subtract(pageNumber-1, 'days');
|
var dayToDisplay=now.subtract(pageNumber-1, 'days');
|
||||||
|
@ -481,4 +490,4 @@ Utils.getRoutePath = routeName => {
|
||||||
String.prototype.replaceAll = function(search, replacement) {
|
String.prototype.replaceAll = function(search, replacement) {
|
||||||
var target = this;
|
var target = this;
|
||||||
return target.replace(new RegExp(search, 'g'), replacement);
|
return target.replace(new RegExp(search, 'g'), replacement);
|
||||||
};
|
};
|
||||||
|
|
|
@ -0,0 +1,208 @@
|
||||||
|
import React, { PureComponent } from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import { intlShape } from 'meteor/vulcan:i18n';
|
||||||
|
import { Components, registerComponent, instantiateComponent } from 'meteor/vulcan:core';
|
||||||
|
import classNames from 'classnames';
|
||||||
|
|
||||||
|
|
||||||
|
class FormComponentInner extends PureComponent {
|
||||||
|
|
||||||
|
renderClear = () => {
|
||||||
|
if (['datetime', 'time', 'select', 'radiogroup'].includes(this.props.input)) {
|
||||||
|
return (
|
||||||
|
<a
|
||||||
|
href="javascript:void(0)"
|
||||||
|
className="form-component-clear"
|
||||||
|
title={this.context.intl.formatMessage({ id: 'forms.clear_field' })}
|
||||||
|
onClick={this.props.clearField}
|
||||||
|
>
|
||||||
|
<span>✕</span>
|
||||||
|
</a>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
getProperties = () => {
|
||||||
|
const { name, options, label, onChange, value } = this.props;
|
||||||
|
|
||||||
|
// these properties are whitelisted so that they can be safely passed to the actual form input
|
||||||
|
// and avoid https://facebook.github.io/react/warnings/unknown-prop.html warnings
|
||||||
|
const inputProperties = {
|
||||||
|
name,
|
||||||
|
options,
|
||||||
|
label,
|
||||||
|
onChange,
|
||||||
|
value,
|
||||||
|
...this.props.inputProperties,
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
...this.props,
|
||||||
|
inputProperties,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
renderExtraComponent = (extraComponent) => {
|
||||||
|
if (!extraComponent) return null;
|
||||||
|
|
||||||
|
const properties = this.getProperties();
|
||||||
|
|
||||||
|
return instantiateComponent(extraComponent, properties);
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
renderComponent = () => {
|
||||||
|
const { input, inputType, formType } = this.props;
|
||||||
|
const properties = this.getProperties();
|
||||||
|
|
||||||
|
// if input is a React component, use it
|
||||||
|
if (typeof input === 'function') {
|
||||||
|
const InputComponent = input;
|
||||||
|
return <InputComponent {...properties} />;
|
||||||
|
} else {
|
||||||
|
// else pick a predefined component
|
||||||
|
|
||||||
|
switch (inputType) {
|
||||||
|
case 'nested':
|
||||||
|
return <Components.FormNested {...properties} />;
|
||||||
|
|
||||||
|
case 'number':
|
||||||
|
return <Components.FormComponentNumber {...properties} />;
|
||||||
|
|
||||||
|
case 'url':
|
||||||
|
return <Components.FormComponentUrl {...properties} />;
|
||||||
|
|
||||||
|
case 'email':
|
||||||
|
return <Components.FormComponentEmail {...properties} />;
|
||||||
|
|
||||||
|
case 'textarea':
|
||||||
|
return <Components.FormComponentTextarea {...properties} />;
|
||||||
|
|
||||||
|
case 'checkbox':
|
||||||
|
// formsy-react-components expects a boolean value for checkbox
|
||||||
|
// https://github.com/twisty/formsy-react-components/blob/v0.11.1/src/checkbox.js#L20
|
||||||
|
properties.inputProperties.value = !!properties.inputProperties.value;
|
||||||
|
return <Components.FormComponentCheckbox {...properties} />;
|
||||||
|
|
||||||
|
case 'checkboxgroup':
|
||||||
|
// formsy-react-components expects an array value
|
||||||
|
// https://github.com/twisty/formsy-react-components/blob/v0.11.1/src/checkbox-group.js#L42
|
||||||
|
if (!Array.isArray(properties.inputProperties.value)) {
|
||||||
|
properties.inputProperties.value = [properties.inputProperties.value];
|
||||||
|
}
|
||||||
|
// in case of checkbox groups, check "checked" option to populate value if this is a "new
|
||||||
|
// document" form
|
||||||
|
const checkedValues = _.where(properties.options, { checked: true })
|
||||||
|
.map(option => option.value);
|
||||||
|
if (checkedValues.length && !properties.inputProperties.value && formType === 'new') {
|
||||||
|
properties.inputProperties.value = checkedValues;
|
||||||
|
}
|
||||||
|
return <Components.FormComponentCheckboxGroup {...properties} />;
|
||||||
|
|
||||||
|
case 'radiogroup':
|
||||||
|
// TODO: remove this?
|
||||||
|
// formsy-react-compnents RadioGroup expects an onChange callback
|
||||||
|
// https://github.com/twisty/formsy-react-components/blob/v0.11.1/src/radio-group.js#L33
|
||||||
|
// properties.onChange = (name, value) => {
|
||||||
|
// this.context.updateCurrentValues({ [name]: value });
|
||||||
|
// };
|
||||||
|
return <Components.FormComponentRadioGroup {...properties} />;
|
||||||
|
|
||||||
|
case 'select':
|
||||||
|
const noneOption = {
|
||||||
|
label: this.context.intl.formatMessage({ id: 'forms.select_option' }),
|
||||||
|
value: '',
|
||||||
|
disabled: true,
|
||||||
|
};
|
||||||
|
properties.inputProperties.options = [noneOption, ...properties.inputProperties.options];
|
||||||
|
|
||||||
|
return <Components.FormComponentSelect {...properties} />;
|
||||||
|
|
||||||
|
case 'selectmultiple':
|
||||||
|
properties.inputProperties.multiple = true;
|
||||||
|
return <Components.FormComponentSelect {...properties} />;
|
||||||
|
|
||||||
|
case 'datetime':
|
||||||
|
return <Components.FormComponentDateTime {...properties} />;
|
||||||
|
|
||||||
|
case 'date':
|
||||||
|
return <Components.FormComponentDate {...properties} />;
|
||||||
|
|
||||||
|
case 'time':
|
||||||
|
return <Components.FormComponentTime {...properties} />;
|
||||||
|
|
||||||
|
case 'text':
|
||||||
|
return <Components.FormComponentDefault {...properties} />;
|
||||||
|
|
||||||
|
default:
|
||||||
|
const CustomComponent = Components[input];
|
||||||
|
return CustomComponent ? (
|
||||||
|
<CustomComponent {...properties} />
|
||||||
|
) : (
|
||||||
|
<Components.FormComponentDefault {...properties} />
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
render () {
|
||||||
|
const {
|
||||||
|
inputClassName,
|
||||||
|
name,
|
||||||
|
input,
|
||||||
|
beforeComponent,
|
||||||
|
afterComponent,
|
||||||
|
errors,
|
||||||
|
showCharsRemaining,
|
||||||
|
charsRemaining,
|
||||||
|
} = this.props;
|
||||||
|
|
||||||
|
const hasErrors = errors && errors.length;
|
||||||
|
|
||||||
|
const inputName = typeof input === 'function' ? input.name : input;
|
||||||
|
const inputClass = classNames('form-input', inputClassName, `input-${name}`,
|
||||||
|
`form-component-${inputName || 'default'}`, { 'input-error': hasErrors, });
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={inputClass}>
|
||||||
|
{this.renderExtraComponent(beforeComponent)}
|
||||||
|
{this.renderComponent()}
|
||||||
|
{hasErrors ? <Components.FieldErrors errors={errors}/> : null}
|
||||||
|
{this.renderClear()}
|
||||||
|
{showCharsRemaining &&
|
||||||
|
<div className={classNames('form-control-limit', { danger: charsRemaining < 10 })}>
|
||||||
|
{charsRemaining}
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
{this.renderExtraComponent(afterComponent)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
FormComponentInner.propTypes = {
|
||||||
|
inputClassName: PropTypes.string,
|
||||||
|
name: PropTypes.string.isRequired,
|
||||||
|
input: PropTypes.any,
|
||||||
|
beforeComponent: PropTypes.oneOfType([PropTypes.string, PropTypes.node]),
|
||||||
|
afterComponent: PropTypes.oneOfType([PropTypes.string, PropTypes.node]),
|
||||||
|
clearField: PropTypes.func.isRequired,
|
||||||
|
errors: PropTypes.array.isRequired,
|
||||||
|
help: PropTypes.node,
|
||||||
|
onChange: PropTypes.func.isRequired,
|
||||||
|
showCharsRemaining: PropTypes.bool.isRequired,
|
||||||
|
charsRemaining: PropTypes.number,
|
||||||
|
charsCount: PropTypes.number,
|
||||||
|
charsMax: PropTypes.number,
|
||||||
|
};
|
||||||
|
|
||||||
|
FormComponentInner.contextTypes = {
|
||||||
|
intl: intlShape,
|
||||||
|
};
|
||||||
|
|
||||||
|
FormComponentInner.displayName = 'FormComponentInner';
|
||||||
|
|
||||||
|
|
||||||
|
registerComponent('FormComponentInner', FormComponentInner);
|
|
@ -10,6 +10,7 @@ import '../components/forms/Textarea.jsx';
|
||||||
import '../components/forms/Time.jsx';
|
import '../components/forms/Time.jsx';
|
||||||
import '../components/forms/Date.jsx';
|
import '../components/forms/Date.jsx';
|
||||||
import '../components/forms/Url.jsx';
|
import '../components/forms/Url.jsx';
|
||||||
|
import '../components/forms/FormComponentInner.jsx';
|
||||||
import '../components/forms/FormControl.jsx'; // note: only used by old accounts package, remove soon?
|
import '../components/forms/FormControl.jsx'; // note: only used by old accounts package, remove soon?
|
||||||
|
|
||||||
import '../components/ui/Button.jsx';
|
import '../components/ui/Button.jsx';
|
||||||
|
|
Loading…
Add table
Reference in a new issue