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 PropTypes from 'prop-types';
|
||||
import { intlShape } from 'meteor/vulcan:i18n';
|
||||
|
@ -34,8 +37,14 @@ import unset from 'lodash/unset';
|
|||
import compact from 'lodash/compact';
|
||||
import update from 'lodash/update';
|
||||
import merge from 'lodash/merge';
|
||||
import find from 'lodash/find';
|
||||
import isEqualWith from 'lodash/isEqualWith';
|
||||
|
||||
import { convertSchema, formProperties } from '../modules/schema_utils';
|
||||
|
||||
registerSetting('forms.warnUnsavedChanges', false,
|
||||
'Warn user about unsaved changes before leaving route', true);
|
||||
|
||||
// unsetCompact
|
||||
const unsetCompact = (object, path) => {
|
||||
const parentPath = path.slice(0, path.lastIndexOf('.'));
|
||||
|
@ -68,7 +77,7 @@ const computeStateFromProps = (nextProps) => {
|
|||
*/
|
||||
|
||||
class Form extends Component {
|
||||
constructor(props) {
|
||||
constructor (props) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
|
@ -119,7 +128,18 @@ class Form extends Component {
|
|||
|
||||
*/
|
||||
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;
|
||||
};
|
||||
|
@ -223,9 +243,10 @@ class Form extends Component {
|
|||
|
||||
// remove all hidden fields
|
||||
if (excludeHiddenFields) {
|
||||
const document = this.getDocument();
|
||||
relevantFields = _.reject(relevantFields, fieldName => {
|
||||
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 {
|
||||
throwError: this.throwError,
|
||||
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,
|
||||
updateCurrentValues: this.updateCurrentValues,
|
||||
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).
|
||||
|
||||
*/
|
||||
updateCurrentValues = newValues => {
|
||||
// keep the previous ones and extend (with possible replacement) with new ones
|
||||
|
@ -452,20 +479,91 @@ class Form extends Component {
|
|||
};
|
||||
|
||||
/*
|
||||
|
||||
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
|
||||
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 => ({
|
||||
errors: clearErrors ? [] : prevState.errors,
|
||||
currentValues: clearCurrentValues ? {} : prevState.currentValues,
|
||||
deletedValues: clearDeletedValues ? [] : prevState.deletedValues,
|
||||
initialDocument: document ? document : prevState.initialDocument,
|
||||
disabled: false,
|
||||
}));
|
||||
};
|
||||
|
||||
/*
|
||||
|
||||
Key down handler
|
||||
|
||||
*/
|
||||
|
@ -489,15 +587,11 @@ class Form extends Component {
|
|||
// for new mutation, run refetch function if it exists
|
||||
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') {
|
||||
let clearCurrentValues = false;
|
||||
// reset form if this is a new document form
|
||||
if (this.getFormType() === 'new') {
|
||||
this.refs.form.reset();
|
||||
clearCurrentValues = true;
|
||||
}
|
||||
this.clearForm({ clearErrors: true, clearCurrentValues, clearDeletedValues: true });
|
||||
this.refs.form.reset();
|
||||
this.clearForm({ clearErrors: true, clearCurrentValues: true, clearDeletedValues: true, document });
|
||||
}
|
||||
|
||||
// run document through mutation success callbacks
|
||||
|
@ -526,6 +620,8 @@ class Form extends Component {
|
|||
|
||||
// run error callback if it exists
|
||||
if (this.props.errorCallback) this.props.errorCallback(document, error);
|
||||
|
||||
Utils.scrollIntoView('.flash-message');
|
||||
};
|
||||
|
||||
/*
|
||||
|
@ -632,14 +728,14 @@ class Form extends Component {
|
|||
// ----------------------------- Render -------------------------------- //
|
||||
// --------------------------------------------------------------------- //
|
||||
|
||||
render() {
|
||||
render () {
|
||||
const fieldGroups = this.getFieldGroups();
|
||||
const collectionName = this.getCollection()._name;
|
||||
|
||||
return (
|
||||
<div className={'document-' + this.getFormType()}>
|
||||
<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 => (
|
||||
<Components.FormGroup
|
||||
|
@ -662,7 +758,9 @@ class Form extends Component {
|
|||
<Components.FormSubmit
|
||||
submitLabel={this.props.submitLabel}
|
||||
cancelLabel={this.props.cancelLabel}
|
||||
revertLabel={this.props.revertLabel}
|
||||
cancelCallback={this.props.cancelCallback}
|
||||
revertCallback={this.props.revertCallback}
|
||||
document={this.getDocument()}
|
||||
deleteDocument={(this.getFormType() === 'edit' && this.props.showRemove && this.deleteDocument) || null}
|
||||
collectionName={collectionName}
|
||||
|
@ -700,7 +798,9 @@ Form.propTypes = {
|
|||
showRemove: PropTypes.bool,
|
||||
submitLabel: PropTypes.string,
|
||||
cancelLabel: PropTypes.string,
|
||||
revertLabel: PropTypes.string,
|
||||
repeatErrors: PropTypes.bool,
|
||||
warnUnsavedChanges: PropTypes.bool,
|
||||
|
||||
// callbacks
|
||||
submitCallback: PropTypes.func,
|
||||
|
@ -708,6 +808,7 @@ Form.propTypes = {
|
|||
removeSuccessCallback: PropTypes.func,
|
||||
errorCallback: PropTypes.func,
|
||||
cancelCallback: PropTypes.func,
|
||||
revertCallback: PropTypes.func,
|
||||
|
||||
currentUser: PropTypes.object,
|
||||
client: PropTypes.object,
|
||||
|
@ -734,6 +835,8 @@ Form.childContextTypes = {
|
|||
setFormState: PropTypes.func,
|
||||
throwError: PropTypes.func,
|
||||
clearForm: PropTypes.func,
|
||||
refetchForm: PropTypes.func,
|
||||
isChanged: PropTypes.func,
|
||||
initialDocument: PropTypes.object,
|
||||
getDocument: PropTypes.func,
|
||||
submitForm: PropTypes.func,
|
||||
|
|
|
@ -1,46 +1,60 @@
|
|||
import React, { Component } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { intlShape } from 'meteor/vulcan:i18n';
|
||||
import classNames from 'classnames';
|
||||
import { Components } from 'meteor/vulcan:core';
|
||||
import { registerComponent } from 'meteor/vulcan:core';
|
||||
import debounce from 'lodash.debounce';
|
||||
import get from 'lodash/get';
|
||||
import merge from 'lodash/merge';
|
||||
import find from 'lodash/find';
|
||||
import isObjectLike from 'lodash/isObjectLike';
|
||||
import isEqual from 'lodash/isEqual';
|
||||
import { isEmptyValue } from '../modules/utils.js';
|
||||
|
||||
class FormComponent extends Component {
|
||||
constructor(props) {
|
||||
|
||||
constructor (props) {
|
||||
super(props);
|
||||
|
||||
const value = this.getValue(props);
|
||||
this.state = {};
|
||||
}
|
||||
|
||||
if (this.showCharsRemaining(props)) {
|
||||
const characterCount = value ? value.length : 0;
|
||||
this.state = {
|
||||
charsRemaining: props.max - characterCount,
|
||||
};
|
||||
componentWillMount () {
|
||||
if (this.showCharsRemaining()) {
|
||||
const value = this.getValue();
|
||||
this.updateCharacterCount(value);
|
||||
}
|
||||
}
|
||||
|
||||
// shouldComponentUpdate(nextProps, nextState) {
|
||||
// const { currentValues, deletedValues, errors } = nextProps;
|
||||
// const { path } = this.props;
|
||||
// const hasChanged = currentValues[path] && currentValues[path] !== this.props.currentValues[path];
|
||||
// const hasError = !!errors[path];
|
||||
// const hasBeenDeleted = deletedValues.includes(path) && !this.props.deletedValues.includes(path)
|
||||
// return hasChanged || hasError || hasBeenDeleted;
|
||||
// }
|
||||
shouldComponentUpdate (nextProps, nextState) {
|
||||
// allow custom controls to determine if they should update
|
||||
if (!['nested', 'number', 'url', 'email', 'textarea', 'checkbox',
|
||||
'checkboxgroup', 'radiogroup', 'select', 'selectmultiple', 'datetime',
|
||||
'date', 'time', 'text'].includes(this.getType(nextProps))) {
|
||||
return true;
|
||||
}
|
||||
|
||||
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) => {
|
||||
// if value is an empty string, delete the field
|
||||
if (value === '') {
|
||||
value = null;
|
||||
}
|
||||
// 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);
|
||||
}
|
||||
this.props.updateCurrentValues({ [this.props.path]: value });
|
||||
|
@ -53,16 +67,14 @@ class FormComponent extends Component {
|
|||
|
||||
/*
|
||||
|
||||
Note: not currently used because when function is debounced
|
||||
some changes might not register if the user submits form too soon
|
||||
Updates the state of charsCount and charsRemaining as the users types
|
||||
|
||||
*/
|
||||
handleChangeDebounced = debounce(this.handleChange, 500, { leading: true });
|
||||
|
||||
updateCharacterCount = value => {
|
||||
const characterCount = value ? value.length : 0;
|
||||
this.setState({
|
||||
charsRemaining: this.props.max - characterCount,
|
||||
charsCount: characterCount,
|
||||
});
|
||||
};
|
||||
|
||||
|
@ -90,7 +102,15 @@ class FormComponent extends Component {
|
|||
value = merge({}, documentValue, currentValue);
|
||||
} else {
|
||||
// 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
|
||||
if (isEmptyValue(value)) {
|
||||
|
@ -116,8 +136,9 @@ class FormComponent extends Component {
|
|||
Get errors from Form state through context
|
||||
|
||||
*/
|
||||
getErrors = () => {
|
||||
const fieldErrors = this.props.errors.filter(error => error.path === this.props.path);
|
||||
getErrors = (errors) => {
|
||||
errors = errors || this.props.errors;
|
||||
const fieldErrors = errors.filter(error => error.path === this.props.path);
|
||||
return fieldErrors;
|
||||
};
|
||||
|
||||
|
@ -131,194 +152,49 @@ class FormComponent extends Component {
|
|||
const p = props || this.props;
|
||||
const fieldType = p.datatype && p.datatype[0].type;
|
||||
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;
|
||||
};
|
||||
|
||||
renderComponent() {
|
||||
const {
|
||||
input,
|
||||
beforeComponent,
|
||||
afterComponent,
|
||||
options,
|
||||
name,
|
||||
label,
|
||||
formType,
|
||||
/*
|
||||
/*
|
||||
|
||||
note: following properties will be passed as part of `...this.props` in `properties`:
|
||||
Function passed to form controls to clear their contents (set their value to null)
|
||||
|
||||
*/
|
||||
// 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();
|
||||
*/
|
||||
clearField = event => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
this.props.updateCurrentValues({ [this.props.path]: null });
|
||||
if (this.showCharsRemaining()) {
|
||||
this.updateCharacterCount(null);
|
||||
}
|
||||
};
|
||||
|
||||
renderClear() {
|
||||
render () {
|
||||
return (
|
||||
<a
|
||||
href="javascript:void(0)"
|
||||
className="form-component-clear"
|
||||
title={this.context.intl.formatMessage({ id: 'forms.clear_field' })}
|
||||
onClick={this.clearField}
|
||||
>
|
||||
<span>✕</span>
|
||||
</a>
|
||||
<Components.FormComponentInner
|
||||
{...this.props}
|
||||
{...this.state}
|
||||
inputType={this.getType()}
|
||||
value={this.getValue()}
|
||||
errors={this.getErrors()}
|
||||
document={this.context.getDocument()}
|
||||
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 = {
|
||||
document: PropTypes.object,
|
||||
name: PropTypes.string,
|
||||
name: PropTypes.string.isRequired,
|
||||
label: PropTypes.string,
|
||||
value: PropTypes.any,
|
||||
placeholder: PropTypes.string,
|
||||
|
@ -326,19 +202,21 @@ FormComponent.propTypes = {
|
|||
options: PropTypes.any,
|
||||
input: PropTypes.any,
|
||||
datatype: PropTypes.any,
|
||||
path: PropTypes.string,
|
||||
path: PropTypes.string.isRequired,
|
||||
disabled: PropTypes.bool,
|
||||
nestedSchema: PropTypes.object,
|
||||
currentValues: PropTypes.object,
|
||||
deletedValues: PropTypes.array,
|
||||
updateCurrentValues: PropTypes.func,
|
||||
errors: PropTypes.array,
|
||||
currentValues: PropTypes.object.isRequired,
|
||||
deletedValues: PropTypes.array.isRequired,
|
||||
throwError: PropTypes.func.isRequired,
|
||||
updateCurrentValues: PropTypes.func.isRequired,
|
||||
errors: PropTypes.array.isRequired,
|
||||
addToDeletedValues: PropTypes.func,
|
||||
clearFieldErrors: PropTypes.func.isRequired,
|
||||
currentUser: PropTypes.object,
|
||||
};
|
||||
|
||||
FormComponent.contextTypes = {
|
||||
intl: intlShape,
|
||||
getDocument: PropTypes.func,
|
||||
getDocument: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
registerComponent('FormComponent', FormComponent);
|
||||
|
|
|
@ -67,8 +67,16 @@ FormGroup.propTypes = {
|
|||
name: PropTypes.string,
|
||||
label: PropTypes.string,
|
||||
order: PropTypes.number,
|
||||
fields: PropTypes.array,
|
||||
updateCurrentValues: PropTypes.func,
|
||||
fields: PropTypes.array.isRequired,
|
||||
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);
|
||||
|
|
|
@ -8,10 +8,15 @@ const FormSubmit = ({
|
|||
submitLabel,
|
||||
cancelLabel,
|
||||
cancelCallback,
|
||||
revertLabel,
|
||||
revertCallback,
|
||||
document,
|
||||
deleteDocument,
|
||||
collectionName,
|
||||
classes,
|
||||
}, {
|
||||
isChanged,
|
||||
clearForm,
|
||||
}) => (
|
||||
<div className="form-submit">
|
||||
<Components.Button type="submit" variant="primary">
|
||||
|
@ -30,6 +35,19 @@ const FormSubmit = ({
|
|||
</a>
|
||||
) : 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 ? (
|
||||
<div>
|
||||
<hr />
|
||||
|
@ -45,10 +63,18 @@ FormSubmit.propTypes = {
|
|||
submitLabel: PropTypes.string,
|
||||
cancelLabel: PropTypes.string,
|
||||
cancelCallback: PropTypes.func,
|
||||
revertLabel: PropTypes.string,
|
||||
revertCallback: PropTypes.func,
|
||||
document: PropTypes.object,
|
||||
deleteDocument: PropTypes.func,
|
||||
collectionName: PropTypes.string,
|
||||
classes: PropTypes.object,
|
||||
};
|
||||
|
||||
FormSubmit.contextTypes = {
|
||||
isChanged: PropTypes.func,
|
||||
clearForm: PropTypes.func,
|
||||
};
|
||||
|
||||
|
||||
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 PropTypes from 'prop-types';
|
||||
import { intlShape } from 'meteor/vulcan:i18n';
|
||||
import { withRouter } from 'react-router'
|
||||
import { withApollo, compose } from 'react-apollo';
|
||||
import {
|
||||
Components,
|
||||
|
@ -265,7 +266,13 @@ FormWrapper.propTypes = {
|
|||
prefilledProps: PropTypes.object,
|
||||
layout: PropTypes.string,
|
||||
fields: PropTypes.arrayOf(PropTypes.string),
|
||||
hideFields: PropTypes.arrayOf(PropTypes.string),
|
||||
showRemove: PropTypes.bool,
|
||||
submitLabel: PropTypes.string,
|
||||
cancelLabel: PropTypes.string,
|
||||
revertLabel: PropTypes.string,
|
||||
repeatErrors: PropTypes.bool,
|
||||
warnUnsavedChanges: PropTypes.bool,
|
||||
|
||||
// callbacks
|
||||
submitCallback: PropTypes.func,
|
||||
|
@ -273,6 +280,7 @@ FormWrapper.propTypes = {
|
|||
removeSuccessCallback: PropTypes.func,
|
||||
errorCallback: PropTypes.func,
|
||||
cancelCallback: PropTypes.func,
|
||||
revertCallback: PropTypes.func,
|
||||
|
||||
currentUser: PropTypes.object,
|
||||
client: PropTypes.object,
|
||||
|
@ -287,4 +295,4 @@ FormWrapper.contextTypes = {
|
|||
intl: intlShape
|
||||
}
|
||||
|
||||
registerComponent('SmartForm', FormWrapper, withCurrentUser, withApollo);
|
||||
registerComponent('SmartForm', FormWrapper, withCurrentUser, withApollo, withRouter);
|
||||
|
|
|
@ -43,6 +43,8 @@ addStrings('en', {
|
|||
"forms.select_option": "-- select option --",
|
||||
"forms.delete": "Delete",
|
||||
"forms.delete_confirm": "Delete document?",
|
||||
"forms.revert": "Revert",
|
||||
"forms.confirm_discard": "Discard changes?",
|
||||
|
||||
"users.profile": "Profile",
|
||||
"users.complete_profile": "Complete your Profile",
|
||||
|
|
|
@ -43,6 +43,8 @@ addStrings('es', {
|
|||
"forms.select_option": "-- seleccionar opción --",
|
||||
"forms.delete": "Eliminar",
|
||||
"forms.delete_confirm": "¿Eliminar documento?",
|
||||
"forms.revert": "Revert", // TODO: translate
|
||||
"forms.confirm_discard": "Discard changes?", // TODO: translate
|
||||
|
||||
"users.profile": "Perfil",
|
||||
"users.complete_profile": "Complete su perfil",
|
||||
|
|
|
@ -45,6 +45,8 @@ addStrings('fr', {
|
|||
"forms.delete_confirm": "Supprimer le document?",
|
||||
"forms.next": "Suivant",
|
||||
"forms.previous": "Précédent",
|
||||
"forms.revert": "Revert", // TODO: translate
|
||||
"forms.confirm_discard": "Discard changes?", // TODO: translate
|
||||
|
||||
"users.profile": "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 React from 'react';
|
||||
|
||||
export const Components = {}; // will be populated on startup (see vulcan:routing)
|
||||
export const ComponentsTable = {} // storage for infos about components
|
||||
|
@ -112,3 +113,27 @@ export const populateComponentsApp = () => {
|
|||
export const copyHoCs = (sourceComponent, 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);
|
||||
};
|
||||
|
||||
Utils.scrollIntoView = function (selector) {
|
||||
if (!document) return;
|
||||
|
||||
const element = document.querySelector(selector);
|
||||
if (element) {
|
||||
element.scrollIntoView();
|
||||
}
|
||||
};
|
||||
|
||||
Utils.getDateRange = function(pageNumber) {
|
||||
var now = moment(new Date());
|
||||
var dayToDisplay=now.subtract(pageNumber-1, 'days');
|
||||
|
|
|
@ -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/Date.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/ui/Button.jsx';
|
||||
|
|
Loading…
Add table
Reference in a new issue