2016-11-26 11:33:27 +09:00
|
|
|
/*
|
|
|
|
|
|
|
|
Main form component.
|
|
|
|
|
|
|
|
This component expects:
|
|
|
|
|
|
|
|
### All Forms:
|
|
|
|
|
|
|
|
- collection
|
|
|
|
- currentUser
|
|
|
|
- client (Apollo client)
|
|
|
|
|
|
|
|
### New Form:
|
|
|
|
|
|
|
|
- newMutation
|
|
|
|
|
|
|
|
### Edit Form:
|
|
|
|
|
|
|
|
- editMutation
|
|
|
|
- removeMutation
|
|
|
|
- document
|
|
|
|
|
|
|
|
*/
|
|
|
|
|
2018-03-26 17:50:03 +09:00
|
|
|
import { registerComponent, Components, runCallbacks, getCollection } from 'meteor/vulcan:core';
|
2017-06-01 10:00:16 +09:00
|
|
|
import React, { Component } from 'react';
|
|
|
|
import PropTypes from 'prop-types';
|
2017-10-17 09:35:41 -04:00
|
|
|
import { intlShape } from 'meteor/vulcan:i18n';
|
2016-11-23 17:22:29 +09:00
|
|
|
import Formsy from 'formsy-react';
|
2018-03-26 14:27:45 +09:00
|
|
|
import { getEditableFields, getInsertableFields } from '../modules/utils.js';
|
2018-03-24 11:16:11 +09:00
|
|
|
import cloneDeep from 'lodash/cloneDeep';
|
|
|
|
import set from 'lodash/set';
|
|
|
|
import unset from 'lodash/unset';
|
|
|
|
import compact from 'lodash/compact';
|
|
|
|
import update from 'lodash/update';
|
2018-03-26 14:27:45 +09:00
|
|
|
import merge from 'lodash/merge';
|
|
|
|
import { convertSchema, formProperties } from '../modules/schema_utils';
|
2018-03-24 11:16:11 +09:00
|
|
|
|
|
|
|
// unsetCompact
|
|
|
|
const unsetCompact = (object, path) => {
|
2018-03-24 11:33:28 +09:00
|
|
|
const parentPath = path.slice(0, path.lastIndexOf('.'));
|
|
|
|
unset(object, path);
|
2018-03-24 11:16:11 +09:00
|
|
|
update(object, parentPath, compact);
|
2018-03-24 11:33:28 +09:00
|
|
|
};
|
2016-11-23 17:22:29 +09:00
|
|
|
|
|
|
|
/*
|
|
|
|
|
|
|
|
1. Constructor
|
|
|
|
2. Helpers
|
|
|
|
3. Errors
|
|
|
|
4. Context
|
|
|
|
4. Method & Callback
|
|
|
|
5. Render
|
|
|
|
|
|
|
|
*/
|
|
|
|
|
2016-12-08 23:48:16 +01:00
|
|
|
class Form extends Component {
|
2018-03-26 11:51:08 +09:00
|
|
|
constructor(props) {
|
2018-03-25 12:13:30 +09:00
|
|
|
super(props);
|
|
|
|
|
|
|
|
this.state = {
|
|
|
|
disabled: false,
|
|
|
|
errors: [],
|
|
|
|
deletedValues: [],
|
|
|
|
currentValues: {},
|
|
|
|
};
|
2018-03-26 11:51:08 +09:00
|
|
|
|
2018-03-26 14:27:45 +09:00
|
|
|
// convert SimpleSchema schema into JSON object
|
2018-03-29 12:15:21 +02:00
|
|
|
this.schema = convertSchema(this.getCollection().simpleSchema());
|
2018-03-26 11:51:08 +09:00
|
|
|
// Also store all field schemas (including nested schemas) in a flat structure
|
2018-03-29 12:15:21 +02:00
|
|
|
this.flatSchema = convertSchema(this.getCollection().simpleSchema(), true);
|
2018-03-26 14:27:45 +09:00
|
|
|
|
|
|
|
// the initial document passed as props
|
2018-03-26 16:24:01 +09:00
|
|
|
this.initialDocument = merge({}, this.props.prefilledProps, this.props.document);
|
2018-03-25 12:13:30 +09:00
|
|
|
}
|
|
|
|
|
2018-03-24 11:21:39 +09:00
|
|
|
submitFormCallbacks = [];
|
|
|
|
successFormCallbacks = [];
|
|
|
|
failureFormCallbacks = [];
|
|
|
|
|
2016-11-23 17:22:29 +09:00
|
|
|
// --------------------------------------------------------------------- //
|
|
|
|
// ------------------------------- Helpers ----------------------------- //
|
|
|
|
// --------------------------------------------------------------------- //
|
|
|
|
|
2018-03-24 11:33:28 +09:00
|
|
|
/*
|
|
|
|
|
|
|
|
Get the current collection
|
|
|
|
|
|
|
|
*/
|
2018-03-24 11:21:39 +09:00
|
|
|
getCollection = () => {
|
2018-01-26 17:41:15 -06:00
|
|
|
return this.props.collection || getCollection(this.props.collectionName);
|
2018-03-24 11:33:28 +09:00
|
|
|
};
|
|
|
|
|
|
|
|
/*
|
|
|
|
|
|
|
|
If a document is being passed, this is an edit form
|
|
|
|
|
|
|
|
*/
|
|
|
|
getFormType = () => {
|
|
|
|
return this.props.document ? 'edit' : 'new';
|
|
|
|
};
|
|
|
|
|
|
|
|
/*
|
|
|
|
|
2018-03-25 12:13:30 +09:00
|
|
|
Get the document initially passed as props
|
|
|
|
|
|
|
|
*/
|
2018-03-26 14:27:45 +09:00
|
|
|
|
2018-03-25 12:13:30 +09:00
|
|
|
/*
|
|
|
|
|
2018-03-24 11:33:28 +09:00
|
|
|
Get the current document (for edit forms)
|
|
|
|
|
|
|
|
for each field, we apply the following logic:
|
2018-03-25 12:13:30 +09:00
|
|
|
- if its value was provided by prefilledProps, use that
|
|
|
|
- unless its value was provided by the db (i.e. props.document)
|
|
|
|
- unless its value is currently being inputted
|
2018-03-24 11:33:28 +09:00
|
|
|
|
|
|
|
*/
|
|
|
|
getDocument = () => {
|
2018-03-29 11:58:24 +09:00
|
|
|
const document = merge({}, this.initialDocument, this.state.currentValues);
|
2018-03-26 17:50:03 +09:00
|
|
|
|
2018-03-26 14:27:45 +09:00
|
|
|
return document;
|
2018-03-24 11:33:28 +09:00
|
|
|
};
|
|
|
|
|
|
|
|
/*
|
|
|
|
|
|
|
|
Like getDocument, but cross-reference with getFieldNames()
|
|
|
|
to only return fields that actually need to be submitted
|
|
|
|
|
|
|
|
Also remove any deleted values.
|
|
|
|
|
|
|
|
*/
|
|
|
|
getData = () => {
|
|
|
|
// only keep relevant fields
|
2018-04-07 10:09:38 +09:00
|
|
|
const fields = this.getFieldNames({ excludeHiddenFields: false });
|
2018-03-24 11:33:28 +09:00
|
|
|
let data = cloneDeep(_.pick(this.getDocument(), ...fields));
|
|
|
|
|
|
|
|
// remove any deleted values
|
|
|
|
// (deleted nested fields cannot be added to $unset, instead we need to modify their value directly)
|
|
|
|
this.state.deletedValues.forEach(path => {
|
|
|
|
unsetCompact(data, path);
|
|
|
|
});
|
|
|
|
|
|
|
|
// run data object through submitForm callbacks
|
|
|
|
data = runCallbacks(this.submitFormCallbacks, data);
|
|
|
|
|
|
|
|
return data;
|
|
|
|
};
|
|
|
|
|
|
|
|
// --------------------------------------------------------------------- //
|
|
|
|
// -------------------------------- Fields ----------------------------- //
|
|
|
|
// --------------------------------------------------------------------- //
|
|
|
|
|
|
|
|
/*
|
|
|
|
|
|
|
|
Get all field groups
|
2018-03-22 19:22:54 +09:00
|
|
|
|
2018-03-24 11:33:28 +09:00
|
|
|
*/
|
|
|
|
getFieldGroups = () => {
|
2018-03-22 19:22:54 +09:00
|
|
|
// build fields array by iterating over the list of field names
|
2018-03-26 11:51:08 +09:00
|
|
|
let fields = this.getFieldNames().map(fieldName => {
|
2018-03-22 19:22:54 +09:00
|
|
|
// get schema for the current field
|
2018-03-26 11:51:08 +09:00
|
|
|
return this.createField(fieldName, this.schema);
|
2016-11-23 17:22:29 +09:00
|
|
|
});
|
|
|
|
|
2018-03-24 11:33:28 +09:00
|
|
|
fields = _.sortBy(fields, 'order');
|
2016-11-23 17:22:29 +09:00
|
|
|
|
2016-12-12 09:55:24 +09:00
|
|
|
// get list of all unique groups (based on their name) used in current fields
|
2018-03-24 11:33:28 +09:00
|
|
|
let groups = _.compact(_.unique(_.pluck(fields, 'group'), false, g => g && g.name));
|
2016-11-23 17:22:29 +09:00
|
|
|
|
|
|
|
// for each group, add relevant fields
|
|
|
|
groups = groups.map(group => {
|
2018-03-24 11:33:28 +09:00
|
|
|
group.label = group.label || this.context.intl.formatMessage({ id: group.name });
|
|
|
|
group.fields = _.filter(fields, field => {
|
|
|
|
return field.group && field.group.name === group.name;
|
|
|
|
});
|
2016-11-23 17:22:29 +09:00
|
|
|
return group;
|
|
|
|
});
|
|
|
|
|
|
|
|
// add default group
|
2018-03-24 11:33:28 +09:00
|
|
|
groups = [
|
|
|
|
{
|
|
|
|
name: 'default',
|
|
|
|
label: 'default',
|
|
|
|
order: 0,
|
|
|
|
fields: _.filter(fields, field => {
|
|
|
|
return !field.group;
|
|
|
|
}),
|
|
|
|
},
|
|
|
|
].concat(groups);
|
2016-11-23 17:22:29 +09:00
|
|
|
|
|
|
|
// sort by order
|
2018-03-24 11:33:28 +09:00
|
|
|
groups = _.sortBy(groups, 'order');
|
2016-11-23 17:22:29 +09:00
|
|
|
|
|
|
|
// console.log(groups);
|
|
|
|
|
|
|
|
return groups;
|
2018-03-24 11:33:28 +09:00
|
|
|
};
|
2016-11-23 17:22:29 +09:00
|
|
|
|
2018-03-24 11:33:28 +09:00
|
|
|
/*
|
|
|
|
|
|
|
|
Get a list of the fields to be included in the current form
|
2016-11-23 17:22:29 +09:00
|
|
|
|
2018-03-24 11:33:28 +09:00
|
|
|
*/
|
2018-04-07 11:53:40 +09:00
|
|
|
getFieldNames = (args = {}) => {
|
|
|
|
|
|
|
|
const { schema = this.schema, excludeHiddenFields = true } = args;
|
|
|
|
|
2017-10-01 11:49:19 +09:00
|
|
|
const { fields, hideFields } = this.props;
|
2016-11-23 17:22:29 +09:00
|
|
|
|
|
|
|
// get all editable/insertable fields (depending on current form type)
|
2018-03-24 11:33:28 +09:00
|
|
|
let relevantFields =
|
|
|
|
this.getFormType() === 'edit'
|
2018-03-26 14:27:45 +09:00
|
|
|
? getEditableFields(schema, this.props.currentUser, this.initialDocument)
|
2018-03-24 11:33:28 +09:00
|
|
|
: getInsertableFields(schema, this.props.currentUser);
|
2016-11-23 17:22:29 +09:00
|
|
|
|
|
|
|
// if "fields" prop is specified, restrict list of fields to it
|
2018-03-24 11:33:28 +09:00
|
|
|
if (typeof fields !== 'undefined' && fields.length > 0) {
|
2016-11-23 17:22:29 +09:00
|
|
|
relevantFields = _.intersection(relevantFields, fields);
|
2017-10-01 11:49:19 +09:00
|
|
|
}
|
|
|
|
// if "hideFields" prop is specified, remove its fields
|
2018-03-24 11:33:28 +09:00
|
|
|
if (typeof hideFields !== 'undefined' && hideFields.length > 0) {
|
2017-10-01 11:49:19 +09:00
|
|
|
relevantFields = _.difference(relevantFields, hideFields);
|
2016-11-23 17:22:29 +09:00
|
|
|
}
|
|
|
|
|
2018-04-07 10:09:38 +09:00
|
|
|
// remove all hidden fields
|
|
|
|
if (excludeHiddenFields) {
|
|
|
|
relevantFields = _.reject(relevantFields, fieldName => {
|
|
|
|
const hidden = schema[fieldName].hidden;
|
|
|
|
return typeof hidden === 'function' ? hidden(this.props) : hidden;
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
2016-11-23 17:22:29 +09:00
|
|
|
return relevantFields;
|
2018-03-24 11:33:28 +09:00
|
|
|
};
|
2016-11-23 17:22:29 +09:00
|
|
|
|
2018-03-26 14:27:45 +09:00
|
|
|
/*
|
|
|
|
|
|
|
|
Given a field's name, the containing schema, and parent, create the
|
|
|
|
complete field object to be passed to the component
|
|
|
|
|
|
|
|
*/
|
|
|
|
createField = (fieldName, schema, parentFieldName, parentPath) => {
|
|
|
|
const fieldPath = parentPath ? `${parentPath}.${fieldName}` : fieldName;
|
|
|
|
const fieldSchema = schema[fieldName];
|
|
|
|
|
|
|
|
// intialize properties
|
|
|
|
let field = {
|
|
|
|
..._.pick(fieldSchema, formProperties),
|
|
|
|
document: this.initialDocument,
|
|
|
|
name: fieldName,
|
|
|
|
path: fieldPath,
|
|
|
|
datatype: fieldSchema.type,
|
|
|
|
layout: this.props.layout,
|
|
|
|
};
|
|
|
|
|
|
|
|
// if field has a parent field, pass it on
|
|
|
|
if (parentFieldName) {
|
|
|
|
field.parentFieldName = parentFieldName;
|
|
|
|
}
|
|
|
|
|
|
|
|
field.label = this.getLabel(fieldName);
|
|
|
|
|
|
|
|
// // replace value by prefilled value if value is empty
|
|
|
|
// const prefill = fieldSchema.prefill || (fieldSchema.form && fieldSchema.form.prefill);
|
|
|
|
// if (prefill) {
|
|
|
|
// const prefilledValue = typeof prefill === 'function' ? prefill.call(fieldSchema) : prefill;
|
|
|
|
// if (!!prefilledValue && !field.value) {
|
|
|
|
// field.prefilledValue = prefilledValue;
|
|
|
|
// field.value = prefilledValue;
|
|
|
|
// }
|
|
|
|
// }
|
|
|
|
|
|
|
|
// if options are a function, call it
|
|
|
|
if (typeof field.options === 'function') {
|
|
|
|
field.options = field.options.call(fieldSchema, this.props);
|
|
|
|
}
|
|
|
|
|
2018-03-26 17:50:03 +09:00
|
|
|
// add any properties specified in fieldSchema.form as extra props passed on
|
|
|
|
// to the form component, calling them if they are functions
|
|
|
|
if (fieldSchema.form) {
|
|
|
|
for (const prop in fieldSchema.form) {
|
|
|
|
field[prop] =
|
|
|
|
typeof fieldSchema.form[prop] === 'function'
|
|
|
|
? fieldSchema.form[prop].call(fieldSchema, this.props)
|
|
|
|
: fieldSchema.form[prop];
|
2018-03-26 14:27:45 +09:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// add description as help prop
|
|
|
|
if (fieldSchema.description) {
|
|
|
|
field.help = fieldSchema.description;
|
|
|
|
}
|
|
|
|
|
|
|
|
// nested fields: set control to "nested"
|
|
|
|
if (fieldSchema.schema) {
|
|
|
|
field.nestedSchema = fieldSchema.schema;
|
|
|
|
field.control = 'nested';
|
|
|
|
// get nested schema
|
|
|
|
// for each nested field, get field object by calling createField recursively
|
2018-04-07 10:09:38 +09:00
|
|
|
field.nestedFields = this.getFieldNames({ schema: field.nestedSchema }).map(subFieldName => {
|
2018-03-26 14:27:45 +09:00
|
|
|
return this.createField(subFieldName, field.nestedSchema, fieldName, fieldPath);
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
return field;
|
|
|
|
};
|
|
|
|
|
2018-03-24 11:33:28 +09:00
|
|
|
/*
|
2018-03-24 11:16:11 +09:00
|
|
|
|
2018-03-24 11:33:28 +09:00
|
|
|
Get a field's label
|
2018-03-24 11:16:11 +09:00
|
|
|
|
2018-03-24 11:33:28 +09:00
|
|
|
*/
|
|
|
|
getLabel = fieldName => {
|
|
|
|
return this.context.intl.formatMessage({
|
|
|
|
id: this.getCollection()._name + '.' + fieldName,
|
2018-03-26 14:27:45 +09:00
|
|
|
defaultMessage: this.flatSchema[fieldName].label,
|
2018-03-24 11:16:11 +09:00
|
|
|
});
|
2018-03-24 11:33:28 +09:00
|
|
|
};
|
2018-03-24 11:16:11 +09:00
|
|
|
|
2018-03-24 11:33:28 +09:00
|
|
|
// --------------------------------------------------------------------- //
|
|
|
|
// ------------------------------- Errors ------------------------------ //
|
|
|
|
// --------------------------------------------------------------------- //
|
2017-08-16 16:18:40 +09:00
|
|
|
|
2018-04-09 13:10:42 +09:00
|
|
|
/*
|
|
|
|
|
|
|
|
Convert GraphQL error into SmartForm-compatible error
|
|
|
|
|
|
|
|
*/
|
|
|
|
convertError = error => ({
|
|
|
|
id: error.id,
|
|
|
|
path: error.data.name,
|
|
|
|
data: error.data,
|
|
|
|
});
|
|
|
|
|
|
|
|
/*
|
|
|
|
|
|
|
|
Add error to form state
|
|
|
|
|
|
|
|
Errors can have the following properties:
|
|
|
|
- id: used as an internationalization key, for example `errors.required`
|
|
|
|
- path: for field-specific errors, the path of the field with the issue
|
|
|
|
- data: additional data. Will be passed to vulcan-i18n as values
|
|
|
|
|
|
|
|
*/
|
2018-03-24 11:33:28 +09:00
|
|
|
throwError = error => {
|
2018-04-09 13:10:42 +09:00
|
|
|
let formErrors = [error];
|
|
|
|
// if this is one or more GraphQL errors, extract and convert them
|
2018-03-29 11:58:24 +09:00
|
|
|
if (error.graphQLErrors) {
|
|
|
|
// get graphQL error (see https://github.com/thebigredgeek/apollo-errors/issues/12)
|
|
|
|
const graphQLError = error.graphQLErrors[0];
|
2018-04-09 13:10:42 +09:00
|
|
|
formErrors = graphQLError.data && graphQLError.data.errors;
|
|
|
|
formErrors = formErrors.map(this.convertError);
|
2018-03-29 11:58:24 +09:00
|
|
|
}
|
2018-04-09 13:10:42 +09:00
|
|
|
|
2018-01-25 15:03:03 -06:00
|
|
|
// eslint-disable-next-line no-console
|
2018-03-29 11:58:24 +09:00
|
|
|
console.log(formErrors);
|
2018-04-09 13:10:42 +09:00
|
|
|
|
2018-03-29 11:58:24 +09:00
|
|
|
// add error(s) to state
|
2017-02-02 15:15:51 +01:00
|
|
|
this.setState(prevState => ({
|
2018-03-29 11:58:24 +09:00
|
|
|
errors: [...prevState.errors, ...formErrors],
|
2017-02-02 15:15:51 +01:00
|
|
|
}));
|
2018-03-24 11:33:28 +09:00
|
|
|
};
|
2016-11-23 17:22:29 +09:00
|
|
|
|
2018-03-26 14:27:45 +09:00
|
|
|
// --------------------------------------------------------------------- //
|
|
|
|
// ------------------------------- Context ----------------------------- //
|
|
|
|
// --------------------------------------------------------------------- //
|
|
|
|
|
2017-05-06 16:08:01 +09:00
|
|
|
// add something to deleted values
|
2018-03-24 11:33:28 +09:00
|
|
|
addToDeletedValues = name => {
|
2017-05-06 16:08:01 +09:00
|
|
|
this.setState(prevState => ({
|
2018-03-24 11:33:28 +09:00
|
|
|
deletedValues: [...prevState.deletedValues, name],
|
2017-05-06 16:08:01 +09:00
|
|
|
}));
|
2018-03-24 11:33:28 +09:00
|
|
|
};
|
2017-05-06 16:08:01 +09:00
|
|
|
|
2017-05-30 09:49:38 +09:00
|
|
|
// add a callback to the form submission
|
2018-03-24 11:33:28 +09:00
|
|
|
addToSubmitForm = callback => {
|
2017-05-30 09:49:38 +09:00
|
|
|
this.submitFormCallbacks.push(callback);
|
2018-03-24 11:33:28 +09:00
|
|
|
};
|
2017-05-30 09:49:38 +09:00
|
|
|
|
2017-07-06 12:49:28 -07:00
|
|
|
// add a callback to form submission success
|
2018-03-24 11:33:28 +09:00
|
|
|
addToSuccessForm = callback => {
|
2017-07-06 12:49:28 -07:00
|
|
|
this.successFormCallbacks.push(callback);
|
2018-03-24 11:33:28 +09:00
|
|
|
};
|
2017-07-06 12:49:28 -07:00
|
|
|
|
|
|
|
// add a callback to form submission failure
|
2018-03-24 11:33:28 +09:00
|
|
|
addToFailureForm = callback => {
|
2017-07-06 12:49:28 -07:00
|
|
|
this.failureFormCallbacks.push(callback);
|
2018-03-24 11:33:28 +09:00
|
|
|
};
|
2017-07-06 12:49:28 -07:00
|
|
|
|
2018-03-24 11:33:28 +09:00
|
|
|
setFormState = fn => {
|
2017-04-20 16:04:24 +09:00
|
|
|
this.setState(fn);
|
2018-03-24 11:33:28 +09:00
|
|
|
};
|
2017-04-20 16:04:24 +09:00
|
|
|
|
2018-03-24 11:33:28 +09:00
|
|
|
submitFormContext = newValues => {
|
2017-09-08 22:52:54 -07:00
|
|
|
// keep the previous ones and extend (with possible replacement) with new ones
|
2018-03-24 11:33:28 +09:00
|
|
|
this.setState(
|
|
|
|
prevState => ({
|
|
|
|
currentValues: {
|
|
|
|
...prevState.currentValues,
|
|
|
|
...newValues,
|
|
|
|
}, // Submit form after setState update completed
|
|
|
|
}),
|
|
|
|
() => this.submitForm(this.refs.form.getModel())
|
|
|
|
);
|
|
|
|
};
|
2017-09-08 22:52:54 -07:00
|
|
|
|
2016-11-23 17:22:29 +09:00
|
|
|
// pass on context to all child components
|
2018-03-24 11:21:39 +09:00
|
|
|
getChildContext = () => {
|
2016-11-23 17:22:29 +09:00
|
|
|
return {
|
|
|
|
throwError: this.throwError,
|
2017-02-02 15:15:51 +01:00
|
|
|
clearForm: this.clearForm,
|
2017-09-08 22:52:54 -07:00
|
|
|
submitForm: this.submitFormContext, //Change in name because we already have a function called submitForm, but no reason for the user to know about that
|
2017-05-06 16:08:01 +09:00
|
|
|
addToDeletedValues: this.addToDeletedValues,
|
2017-01-23 15:50:55 +01:00
|
|
|
updateCurrentValues: this.updateCurrentValues,
|
2016-11-23 17:22:29 +09:00
|
|
|
getDocument: this.getDocument,
|
2018-03-26 14:27:45 +09:00
|
|
|
initialDocument: this.initialDocument,
|
2017-04-20 16:04:24 +09:00
|
|
|
setFormState: this.setFormState,
|
2017-06-01 11:50:47 +09:00
|
|
|
addToSubmitForm: this.addToSubmitForm,
|
2017-07-06 12:49:28 -07:00
|
|
|
addToSuccessForm: this.addToSuccessForm,
|
|
|
|
addToFailureForm: this.addToFailureForm,
|
2018-03-25 10:54:45 +09:00
|
|
|
errors: this.state.errors,
|
2018-03-25 12:13:30 +09:00
|
|
|
currentValues: this.state.currentValues,
|
|
|
|
deletedValues: this.state.deletedValues,
|
2016-11-23 17:22:29 +09:00
|
|
|
};
|
2018-03-24 11:33:28 +09:00
|
|
|
};
|
2016-11-23 17:22:29 +09:00
|
|
|
|
|
|
|
// --------------------------------------------------------------------- //
|
2018-03-24 11:33:28 +09:00
|
|
|
// ------------------------------ Lifecycle ---------------------------- //
|
2016-11-23 17:22:29 +09:00
|
|
|
// --------------------------------------------------------------------- //
|
|
|
|
|
2018-03-24 11:33:28 +09:00
|
|
|
/*
|
|
|
|
|
|
|
|
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
|
|
|
|
this.setState(prevState => {
|
2018-03-25 12:13:30 +09:00
|
|
|
const newState = cloneDeep(prevState);
|
2018-03-24 11:33:28 +09:00
|
|
|
Object.keys(newValues).forEach(key => {
|
|
|
|
const path = key;
|
|
|
|
const value = newValues[key];
|
|
|
|
if (value === null) {
|
|
|
|
// delete value
|
2018-03-26 14:27:45 +09:00
|
|
|
unset(newState.currentValues, path);
|
2018-03-28 11:51:18 +09:00
|
|
|
newState.deletedValues = [...prevState.deletedValues, path];
|
2018-03-24 11:33:28 +09:00
|
|
|
} else {
|
2018-03-28 11:51:18 +09:00
|
|
|
// in case value had previously been deleted, "undelete" it
|
2018-03-25 12:13:30 +09:00
|
|
|
set(newState.currentValues, path, value);
|
2018-03-28 11:51:18 +09:00
|
|
|
newState.deletedValues = _.without(prevState.deletedValues, path);
|
2018-03-24 11:33:28 +09:00
|
|
|
}
|
|
|
|
});
|
2018-03-25 12:13:30 +09:00
|
|
|
return newState;
|
2018-03-24 11:33:28 +09:00
|
|
|
});
|
|
|
|
};
|
|
|
|
|
|
|
|
/*
|
|
|
|
|
|
|
|
Clear and reset the form
|
|
|
|
By default, clear errors and keep current values and deleted values
|
|
|
|
|
|
|
|
*/
|
|
|
|
clearForm = ({ clearErrors = true, clearCurrentValues = false, clearDeletedValues = false }) => {
|
|
|
|
this.setState(prevState => ({
|
|
|
|
errors: clearErrors ? [] : prevState.errors,
|
|
|
|
currentValues: clearCurrentValues ? {} : prevState.currentValues,
|
|
|
|
deletedValues: clearDeletedValues ? [] : prevState.deletedValues,
|
|
|
|
disabled: false,
|
|
|
|
}));
|
|
|
|
};
|
|
|
|
|
|
|
|
/*
|
|
|
|
|
|
|
|
Key down handler
|
|
|
|
|
|
|
|
*/
|
|
|
|
formKeyDown = event => {
|
|
|
|
if ((event.ctrlKey || event.metaKey) && event.keyCode === 13) {
|
|
|
|
this.submitForm(this.refs.form.getModel());
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
newMutationSuccessCallback = result => {
|
2016-11-25 12:22:13 +09:00
|
|
|
this.mutationSuccessCallback(result, 'new');
|
2018-03-24 11:33:28 +09:00
|
|
|
};
|
2016-11-25 12:22:13 +09:00
|
|
|
|
2018-03-24 11:33:28 +09:00
|
|
|
editMutationSuccessCallback = result => {
|
2016-11-25 12:22:13 +09:00
|
|
|
this.mutationSuccessCallback(result, 'edit');
|
2018-03-24 11:33:28 +09:00
|
|
|
};
|
2016-11-25 12:22:13 +09:00
|
|
|
|
2018-03-24 11:21:39 +09:00
|
|
|
mutationSuccessCallback = (result, mutationType) => {
|
2016-11-23 17:22:29 +09:00
|
|
|
const document = result.data[Object.keys(result.data)[0]]; // document is always on first property
|
|
|
|
|
2016-11-25 12:22:13 +09:00
|
|
|
// for new mutation, run refetch function if it exists
|
|
|
|
if (mutationType === 'new' && this.props.refetch) this.props.refetch();
|
|
|
|
|
2017-01-13 18:17:08 +01:00
|
|
|
// 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
|
2018-03-24 11:33:28 +09:00
|
|
|
if (this.getFormType() === 'new') {
|
2017-01-13 18:17:08 +01:00
|
|
|
this.refs.form.reset();
|
|
|
|
clearCurrentValues = true;
|
|
|
|
}
|
2018-03-24 11:33:28 +09:00
|
|
|
this.clearForm({ clearErrors: true, clearCurrentValues, clearDeletedValues: true });
|
2017-01-13 18:17:08 +01:00
|
|
|
}
|
2017-01-10 17:49:03 +09:00
|
|
|
|
2017-07-06 12:49:28 -07:00
|
|
|
// run document through mutation success callbacks
|
|
|
|
result = runCallbacks(this.successFormCallbacks, result);
|
|
|
|
|
2016-11-23 17:22:29 +09:00
|
|
|
// run success callback if it exists
|
|
|
|
if (this.props.successCallback) this.props.successCallback(document);
|
2018-03-24 11:33:28 +09:00
|
|
|
};
|
2016-11-23 17:22:29 +09:00
|
|
|
|
|
|
|
// catch graphql errors
|
2018-03-24 11:33:28 +09:00
|
|
|
mutationErrorCallback = error => {
|
|
|
|
this.setState(prevState => ({ disabled: false }));
|
2016-11-23 17:22:29 +09:00
|
|
|
|
2018-01-25 15:03:03 -06:00
|
|
|
// eslint-disable-next-line no-console
|
2018-03-24 11:33:28 +09:00
|
|
|
console.log('// graphQL Error');
|
2018-01-25 15:03:03 -06:00
|
|
|
// eslint-disable-next-line no-console
|
|
|
|
console.log(error);
|
2017-07-06 12:49:28 -07:00
|
|
|
|
|
|
|
// run mutation failure callbacks on error, we do not allow the callbacks to change the error
|
|
|
|
runCallbacks(this.failureFormCallbacks, error);
|
|
|
|
|
2016-11-23 17:22:29 +09:00
|
|
|
if (!_.isEmpty(error)) {
|
|
|
|
// add error to state
|
2017-07-07 10:21:15 +09:00
|
|
|
this.throwError(error);
|
2016-11-23 17:22:29 +09:00
|
|
|
}
|
|
|
|
|
|
|
|
// note: we don't have access to the document here :( maybe use redux-forms and get it from the store?
|
|
|
|
// run error callback if it exists
|
|
|
|
// if (this.props.errorCallback) this.props.errorCallback(document, error);
|
2018-03-24 11:33:28 +09:00
|
|
|
};
|
2016-11-23 17:22:29 +09:00
|
|
|
|
2018-03-24 11:33:28 +09:00
|
|
|
/*
|
|
|
|
|
|
|
|
Submit form handler
|
2017-06-22 16:41:56 +09:00
|
|
|
|
2018-03-24 11:33:28 +09:00
|
|
|
*/
|
|
|
|
submitForm = data => {
|
2018-03-23 15:46:31 +09:00
|
|
|
// note: we can discard the data collected by Formsy because all the data we need is already available via getDocument()
|
|
|
|
|
2017-06-22 16:41:56 +09:00
|
|
|
// if form is disabled (there is already a submit handler running) don't do anything
|
|
|
|
if (this.state.disabled) {
|
|
|
|
return;
|
|
|
|
}
|
2017-07-06 12:49:28 -07:00
|
|
|
|
2017-08-16 16:24:50 +09:00
|
|
|
// clear errors and disable form while it's submitting
|
2018-03-24 11:33:28 +09:00
|
|
|
this.setState(prevState => ({ errors: [], disabled: true }));
|
2016-11-23 17:22:29 +09:00
|
|
|
|
|
|
|
// complete the data with values from custom components which are not being catched by Formsy mixin
|
2016-12-20 09:27:16 +09:00
|
|
|
// note: it follows the same logic as SmartForm's getDocument method
|
2018-03-23 15:46:31 +09:00
|
|
|
data = this.getData();
|
2016-11-23 17:22:29 +09:00
|
|
|
|
2018-03-23 15:46:31 +09:00
|
|
|
// console.log(data)
|
2017-07-06 12:49:28 -07:00
|
|
|
|
2018-03-26 11:51:08 +09:00
|
|
|
const fields = this.getFieldNames();
|
2016-11-23 17:22:29 +09:00
|
|
|
|
|
|
|
// if there's a submit callback, run it
|
|
|
|
if (this.props.submitCallback) {
|
|
|
|
data = this.props.submitCallback(data);
|
|
|
|
}
|
|
|
|
|
2018-03-24 11:33:28 +09:00
|
|
|
if (this.getFormType() === 'new') {
|
|
|
|
// new document form
|
2016-11-23 17:22:29 +09:00
|
|
|
|
|
|
|
// remove any empty properties
|
2017-06-20 10:25:34 +09:00
|
|
|
let document = _.compactObject(data);
|
2016-11-23 17:22:29 +09:00
|
|
|
// call method with new document
|
2018-03-24 11:33:28 +09:00
|
|
|
this.props
|
|
|
|
.newMutation({ document })
|
|
|
|
.then(this.newMutationSuccessCallback)
|
|
|
|
.catch(this.mutationErrorCallback);
|
|
|
|
} else {
|
|
|
|
// edit document form
|
2016-11-23 17:22:29 +09:00
|
|
|
|
|
|
|
const document = this.getDocument();
|
|
|
|
|
|
|
|
// put all keys with data on $set
|
2017-06-20 10:25:34 +09:00
|
|
|
const set = _.compactObject(data);
|
2016-11-23 17:22:29 +09:00
|
|
|
|
|
|
|
// put all keys without data on $unset
|
2017-05-06 16:08:01 +09:00
|
|
|
const setKeys = _.keys(set);
|
|
|
|
let unsetKeys = _.difference(fields, setKeys);
|
|
|
|
|
|
|
|
// add all keys to delete (minus those that have data associated)
|
|
|
|
unsetKeys = _.unique(unsetKeys.concat(_.difference(this.state.deletedValues, setKeys)));
|
2016-11-23 17:22:29 +09:00
|
|
|
|
2018-03-24 11:16:11 +09:00
|
|
|
// only keep unset keys that correspond to a field (get rid of nested keys)
|
2018-03-26 11:51:08 +09:00
|
|
|
unsetKeys = _.intersection(unsetKeys, this.getFieldNames());
|
2018-03-24 11:16:11 +09:00
|
|
|
|
2017-05-06 16:08:01 +09:00
|
|
|
// build mutation arguments object
|
2018-03-24 11:33:28 +09:00
|
|
|
const args = { documentId: document._id, set: set, unset: {} };
|
2017-05-06 16:08:01 +09:00
|
|
|
if (unsetKeys.length > 0) {
|
2017-05-07 22:01:52 +09:00
|
|
|
args.unset = _.object(unsetKeys, unsetKeys.map(() => true));
|
2017-05-06 16:08:01 +09:00
|
|
|
}
|
2016-11-23 17:22:29 +09:00
|
|
|
// call method with _id of document being edited and modifier
|
2018-03-24 11:33:28 +09:00
|
|
|
this.props
|
|
|
|
.editMutation(args)
|
|
|
|
.then(this.editMutationSuccessCallback)
|
|
|
|
.catch(this.mutationErrorCallback);
|
2016-11-23 17:22:29 +09:00
|
|
|
}
|
2018-03-24 11:33:28 +09:00
|
|
|
};
|
2016-11-23 17:22:29 +09:00
|
|
|
|
2018-03-24 11:33:28 +09:00
|
|
|
/*
|
2016-11-23 17:22:29 +09:00
|
|
|
|
2018-03-24 11:33:28 +09:00
|
|
|
Delete document handler
|
|
|
|
|
|
|
|
*/
|
2018-03-24 11:21:39 +09:00
|
|
|
deleteDocument = () => {
|
2016-11-23 17:22:29 +09:00
|
|
|
const document = this.getDocument();
|
2016-11-25 12:22:13 +09:00
|
|
|
const documentId = this.props.document._id;
|
2016-11-27 08:39:25 +09:00
|
|
|
const documentTitle = document.title || document.name || '';
|
2016-11-23 17:22:29 +09:00
|
|
|
|
2018-03-24 11:33:28 +09:00
|
|
|
const deleteDocumentConfirm = this.context.intl.formatMessage(
|
|
|
|
{ id: 'forms.delete_confirm' },
|
|
|
|
{ title: documentTitle }
|
|
|
|
);
|
2016-11-23 17:22:29 +09:00
|
|
|
|
2016-12-08 23:48:16 +01:00
|
|
|
if (window.confirm(deleteDocumentConfirm)) {
|
2018-03-24 11:33:28 +09:00
|
|
|
this.props
|
|
|
|
.removeMutation({ documentId })
|
|
|
|
.then(mutationResult => {
|
|
|
|
// the mutation result looks like {data:{collectionRemove: null}} if succeeded
|
|
|
|
if (this.props.removeSuccessCallback) this.props.removeSuccessCallback({ documentId, documentTitle });
|
2016-11-25 12:22:13 +09:00
|
|
|
if (this.props.refetch) this.props.refetch();
|
2016-11-23 17:22:29 +09:00
|
|
|
})
|
2018-03-24 11:33:28 +09:00
|
|
|
.catch(error => {
|
2018-01-25 15:03:03 -06:00
|
|
|
// eslint-disable-next-line no-console
|
2016-11-24 15:47:51 +09:00
|
|
|
console.log(error);
|
2016-11-23 17:22:29 +09:00
|
|
|
});
|
|
|
|
}
|
2018-03-24 11:33:28 +09:00
|
|
|
};
|
2016-11-23 17:22:29 +09:00
|
|
|
|
|
|
|
// --------------------------------------------------------------------- //
|
2018-03-24 11:33:28 +09:00
|
|
|
// ----------------------------- Render -------------------------------- //
|
2016-11-23 17:22:29 +09:00
|
|
|
// --------------------------------------------------------------------- //
|
|
|
|
|
|
|
|
render() {
|
|
|
|
const fieldGroups = this.getFieldGroups();
|
2018-01-26 17:41:15 -06:00
|
|
|
const collectionName = this.getCollection()._name;
|
2016-11-23 17:22:29 +09:00
|
|
|
|
|
|
|
return (
|
2018-03-24 11:33:28 +09:00
|
|
|
<div className={'document-' + this.getFormType()}>
|
|
|
|
<Formsy.Form onSubmit={this.submitForm} onKeyDown={this.formKeyDown} disabled={this.state.disabled} ref="form">
|
2018-03-26 11:51:08 +09:00
|
|
|
<Components.FormErrors errors={this.state.errors} />
|
2018-01-26 17:41:15 -06:00
|
|
|
|
2018-03-24 11:33:28 +09:00
|
|
|
{fieldGroups.map(group => (
|
2018-03-26 17:50:03 +09:00
|
|
|
<Components.FormGroup
|
|
|
|
key={group.name}
|
|
|
|
{...group}
|
2018-03-28 11:14:36 +09:00
|
|
|
errors={this.state.errors}
|
2018-04-06 17:56:25 +09:00
|
|
|
throwError={this.throwError}
|
2018-03-27 10:45:17 +09:00
|
|
|
currentValues={this.state.currentValues}
|
2018-03-26 17:50:03 +09:00
|
|
|
updateCurrentValues={this.updateCurrentValues}
|
2018-04-06 17:56:25 +09:00
|
|
|
deletedValues={this.state.deletedValues}
|
|
|
|
addToDeletedValues={this.addToDeletedValues}
|
2018-03-26 17:50:03 +09:00
|
|
|
formType={this.getFormType()}
|
|
|
|
/>
|
2018-03-24 11:33:28 +09:00
|
|
|
))}
|
2018-01-26 17:41:15 -06:00
|
|
|
|
2017-11-09 10:01:22 +09:00
|
|
|
{this.props.repeatErrors && this.renderErrors()}
|
|
|
|
|
2018-03-24 11:33:28 +09:00
|
|
|
<Components.FormSubmit
|
|
|
|
submitLabel={this.props.submitLabel}
|
|
|
|
cancelLabel={this.props.cancelLabel}
|
|
|
|
cancelCallback={this.props.cancelCallback}
|
|
|
|
document={this.getDocument()}
|
|
|
|
deleteDocument={(this.getFormType() === 'edit' && this.props.showRemove && this.deleteDocument) || null}
|
|
|
|
collectionName={collectionName}
|
2017-10-17 09:35:41 -04:00
|
|
|
/>
|
2016-11-23 17:22:29 +09:00
|
|
|
</Formsy.Form>
|
|
|
|
</div>
|
2018-03-24 11:33:28 +09:00
|
|
|
);
|
2016-11-23 17:22:29 +09:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2016-12-08 23:48:16 +01:00
|
|
|
Form.propTypes = {
|
2016-11-23 17:22:29 +09:00
|
|
|
// main options
|
2017-02-02 15:15:51 +01:00
|
|
|
collection: PropTypes.object,
|
2018-01-26 17:41:15 -06:00
|
|
|
collectionName: (props, propName, componentName) => {
|
|
|
|
if (!props.collection && !props.collectionName) {
|
|
|
|
return new Error(`One of props 'collection' or 'collectionName' was not specified in '${componentName}'.`);
|
|
|
|
}
|
|
|
|
if (!props.collection && typeof props['collectionName'] !== 'string') {
|
|
|
|
return new Error(`Prop collectionName was not of type string in '${componentName}`);
|
|
|
|
}
|
|
|
|
},
|
2017-02-02 15:15:51 +01:00
|
|
|
document: PropTypes.object, // if a document is passed, this will be an edit form
|
|
|
|
schema: PropTypes.object, // usually not needed
|
2016-11-23 17:22:29 +09:00
|
|
|
|
|
|
|
// graphQL
|
2017-02-02 15:15:51 +01:00
|
|
|
newMutation: PropTypes.func, // the new mutation
|
|
|
|
editMutation: PropTypes.func, // the edit mutation
|
|
|
|
removeMutation: PropTypes.func, // the remove mutation
|
2016-11-23 17:22:29 +09:00
|
|
|
|
|
|
|
// form
|
2017-02-02 15:15:51 +01:00
|
|
|
prefilledProps: PropTypes.object,
|
|
|
|
layout: PropTypes.string,
|
|
|
|
fields: PropTypes.arrayOf(PropTypes.string),
|
2017-10-01 11:49:19 +09:00
|
|
|
hideFields: PropTypes.arrayOf(PropTypes.string),
|
2017-02-02 15:15:51 +01:00
|
|
|
showRemove: PropTypes.bool,
|
2017-06-01 11:50:47 +09:00
|
|
|
submitLabel: PropTypes.string,
|
|
|
|
cancelLabel: PropTypes.string,
|
2017-11-09 10:01:22 +09:00
|
|
|
repeatErrors: PropTypes.bool,
|
2016-11-23 17:22:29 +09:00
|
|
|
|
|
|
|
// callbacks
|
2017-02-02 15:15:51 +01:00
|
|
|
submitCallback: PropTypes.func,
|
|
|
|
successCallback: PropTypes.func,
|
|
|
|
removeSuccessCallback: PropTypes.func,
|
|
|
|
errorCallback: PropTypes.func,
|
|
|
|
cancelCallback: PropTypes.func,
|
|
|
|
|
|
|
|
currentUser: PropTypes.object,
|
|
|
|
client: PropTypes.object,
|
2018-03-24 11:33:28 +09:00
|
|
|
};
|
2016-11-23 17:22:29 +09:00
|
|
|
|
2016-12-08 23:48:16 +01:00
|
|
|
Form.defaultProps = {
|
2017-11-09 10:01:22 +09:00
|
|
|
layout: 'horizontal',
|
2018-03-23 08:51:24 +09:00
|
|
|
prefilledProps: {},
|
2017-11-09 10:01:22 +09:00
|
|
|
repeatErrors: false,
|
2018-03-22 16:54:50 +09:00
|
|
|
showRemove: true,
|
2018-03-24 11:33:28 +09:00
|
|
|
};
|
2016-11-23 17:22:29 +09:00
|
|
|
|
2016-12-08 23:48:16 +01:00
|
|
|
Form.contextTypes = {
|
2018-03-24 11:33:28 +09:00
|
|
|
intl: intlShape,
|
|
|
|
};
|
2016-11-23 17:22:29 +09:00
|
|
|
|
2016-12-08 23:48:16 +01:00
|
|
|
Form.childContextTypes = {
|
2017-05-06 16:08:01 +09:00
|
|
|
addToDeletedValues: PropTypes.func,
|
2018-03-24 11:16:11 +09:00
|
|
|
deletedValues: PropTypes.array,
|
2017-05-30 09:49:38 +09:00
|
|
|
addToSubmitForm: PropTypes.func,
|
2017-07-06 12:49:28 -07:00
|
|
|
addToFailureForm: PropTypes.func,
|
|
|
|
addToSuccessForm: PropTypes.func,
|
2017-02-02 15:15:51 +01:00
|
|
|
updateCurrentValues: PropTypes.func,
|
2017-04-20 16:04:24 +09:00
|
|
|
setFormState: PropTypes.func,
|
2017-02-02 15:15:51 +01:00
|
|
|
throwError: PropTypes.func,
|
|
|
|
clearForm: PropTypes.func,
|
2018-03-26 14:27:45 +09:00
|
|
|
initialDocument: PropTypes.object,
|
2017-09-08 22:52:54 -07:00
|
|
|
getDocument: PropTypes.func,
|
|
|
|
submitForm: PropTypes.func,
|
2018-03-25 10:54:45 +09:00
|
|
|
errors: PropTypes.array,
|
2018-03-25 12:13:30 +09:00
|
|
|
currentValues: PropTypes.object,
|
2018-03-24 11:33:28 +09:00
|
|
|
};
|
2016-11-23 17:22:29 +09:00
|
|
|
|
2018-03-24 11:33:28 +09:00
|
|
|
module.exports = Form;
|
2018-03-26 17:50:03 +09:00
|
|
|
|
2018-03-29 11:58:24 +09:00
|
|
|
registerComponent('Form', Form);
|