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-05-08 20:09:42 -04:00
import {
2018-05-11 09:52:04 +09:00
registerComponent ,
Components ,
runCallbacks ,
getCollection ,
getErrors ,
getSetting ,
Utils ,
isIntlField ,
2018-05-08 20:09:42 -04:00
} 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' ;
2018-06-20 10:23:54 +09:00
import { Form } from 'formsy-react-components' ;
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' ;
2018-05-08 20:09:42 -04:00
import find from 'lodash/find' ;
2018-05-23 16:02:36 -04:00
import pick from 'lodash/pick' ;
2018-05-08 20:09:42 -04:00
import isEqualWith from 'lodash/isEqualWith' ;
2018-03-26 14:27:45 +09:00
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 ( '.' ) ) ;
2018-05-23 17:09:32 +09:00
2018-03-24 11:33:28 +09:00
unset ( object , path ) ;
2018-05-23 17:09:32 +09:00
// note: we only want to compact arrays, not objects
const compactIfArray = x => Array . isArray ( x ) ? compact ( x ) : x ;
update ( object , parentPath , compactIfArray ) ;
2018-03-24 11:33:28 +09:00
} ;
2016-11-23 17:22:29 +09:00
2018-05-11 09:52:04 +09:00
const computeStateFromProps = nextProps => {
2018-04-20 14:35:53 -05:00
const collection = nextProps . collection || getCollection ( nextProps . collectionName ) ;
const schema = collection . simpleSchema ( ) ;
return {
// convert SimpleSchema schema into JSON object
schema : convertSchema ( schema ) ,
// Also store all field schemas (including nested schemas) in a flat structure
flatSchema : convertSchema ( schema , true ) ,
// the initial document passed as props
initialDocument : merge ( { } , nextProps . prefilledProps , nextProps . document ) ,
} ;
} ;
2016-11-23 17:22:29 +09:00
/ *
1. Constructor
2. Helpers
3. Errors
4. Context
4. Method & Callback
5. Render
* /
2018-06-20 10:23:54 +09:00
class SmartForm extends Component {
2018-05-11 09:52:04 +09:00
constructor ( props ) {
2018-03-25 12:13:30 +09:00
super ( props ) ;
this . state = {
disabled : false ,
errors : [ ] ,
deletedValues : [ ] ,
currentValues : { } ,
2018-04-20 14:35:53 -05:00
... computeStateFromProps ( props ) ,
2018-03-25 12:13:30 +09:00
} ;
}
2018-04-10 17:23:23 +09:00
defaultValues = { } ;
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-04-10 17:23:23 +09:00
Get the current document
2018-03-24 11:33:28 +09:00
* /
getDocument = ( ) => {
2018-05-08 20:09:42 -04:00
const deletedValues = { } ;
this . state . deletedValues . forEach ( path => {
set ( deletedValues , path , null ) ;
} ) ;
2018-05-11 09:52:04 +09:00
const document = merge ( { } , this . state . initialDocument , this . defaultValues , this . state . currentValues , deletedValues ) ;
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
} ;
/ *
2018-04-20 14:35:53 -05:00
Like getDocument , but cross - reference with getFieldNames ( )
2018-03-24 11:33:28 +09:00
to only return fields that actually need to be submitted
2018-04-20 14:35:53 -05:00
Also remove any deleted values .
2018-03-24 11:33:28 +09:00
* /
getData = ( ) => {
// only keep relevant fields
2018-05-21 09:42:08 +09:00
// for intl fields, make sure we look in foo_intl and not foo
const fields = this . getFieldNames ( { excludeHiddenFields : false , replaceIntlFields : true } ) ;
2018-05-23 16:02:36 -04:00
let data = pick ( this . getDocument ( ) , ... fields ) ;
2018-03-24 11:33:28 +09:00
// 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-04-20 14:35:53 -05:00
return this . createField ( fieldName , this . state . 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 = { } ) => {
2018-05-21 09:42:08 +09:00
const { schema = this . state . schema , excludeHiddenFields = true , replaceIntlFields = false } = args ;
2018-04-07 11:53:40 +09:00
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-04-20 14:35:53 -05:00
? getEditableFields ( schema , this . props . currentUser , this . state . 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 ) {
2018-05-08 20:09:42 -04:00
const document = this . getDocument ( ) ;
2018-04-07 10:09:38 +09:00
relevantFields = _ . reject ( relevantFields , fieldName => {
const hidden = schema [ fieldName ] . hidden ;
2018-05-08 20:09:42 -04:00
return typeof hidden === 'function' ? hidden ( { ... this . props , document } ) : hidden ;
2018-04-07 10:09:38 +09:00
} ) ;
}
2018-05-21 09:42:08 +09:00
// replace intl fields
if ( replaceIntlFields ) {
relevantFields = relevantFields . map ( fieldName => isIntlField ( schema [ fieldName ] ) ? ` ${ fieldName } _intl ` : fieldName ) ;
}
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
/ *
2018-04-20 14:35:53 -05:00
Given a field ' s name , the containing schema , and parent , create the
2018-03-26 14:27:45 +09:00
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 ) ,
2018-04-20 14:35:53 -05:00
document : this . state . initialDocument ,
2018-03-26 14:27:45 +09:00
name : fieldName ,
path : fieldPath ,
datatype : fieldSchema . type ,
layout : this . props . layout ,
2018-04-14 18:09:35 +09:00
input : fieldSchema . input || fieldSchema . control ,
2018-03-26 14:27:45 +09:00
} ;
2018-05-07 17:41:22 +09:00
// if this an intl'd field, use a special intlInput
2018-05-08 12:23:42 +09:00
if ( isIntlField ( fieldSchema ) ) {
2018-05-07 17:41:22 +09:00
field . intlInput = true ;
}
2018-04-10 17:23:23 +09:00
if ( field . defaultValue ) {
set ( this . defaultValues , fieldPath , field . defaultValue ) ;
}
2018-03-26 14:27:45 +09:00
// 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
2018-04-14 18:09:35 +09:00
const inputProperties = fieldSchema . form || fieldSchema . inputProperties ;
if ( inputProperties ) {
for ( const prop in inputProperties ) {
const property = inputProperties [ prop ] ;
2018-04-21 17:57:53 +09:00
field [ prop ] = typeof property === 'function' ? property . call ( fieldSchema , this . props ) : property ;
2018-03-26 14:27:45 +09:00
}
}
// add description as help prop
if ( fieldSchema . description ) {
field . help = fieldSchema . description ;
}
2018-04-20 16:25:11 +02:00
// nested fields: set input to "nested"
2018-03-26 14:27:45 +09:00
if ( fieldSchema . schema ) {
field . nestedSchema = fieldSchema . schema ;
2018-05-23 22:04:32 +09:00
field . nestedInput = true ;
2018-03-26 14:27:45 +09:00
// 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-04-20 14:35:53 -05:00
Get a field ' s label
2018-03-24 11:16:11 +09:00
2018-04-20 14:35:53 -05:00
* /
2018-03-24 11:33:28 +09:00
getLabel = fieldName => {
return this . context . intl . formatMessage ( {
id : this . getCollection ( ) . _name + '.' + fieldName ,
2018-04-20 14:35:53 -05:00
defaultMessage : this . state . 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
/ *
2018-04-20 14:35:53 -05:00
2018-04-09 13:10:42 +09:00
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
2018-04-21 17:57:53 +09:00
- properties : additional data . Will be passed to vulcan - i18n as values
2018-04-14 18:09:35 +09:00
- message : if id cannot be used as i81n key , message will be used
2018-04-21 17:57:53 +09:00
2018-04-09 13:10:42 +09:00
* /
2018-03-24 11:33:28 +09:00
throwError = error => {
2018-04-28 10:54:03 +09:00
let formErrors = getErrors ( error ) ;
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-04-14 17:21:10 +09:00
/ *
Clear errors for a field
* /
clearFieldErrors = path => {
const errors = this . state . errors . filter ( error => error . path !== path ) ;
this . setState ( { errors } ) ;
2018-04-21 17:57:53 +09:00
} ;
2018-04-14 17:21:10 +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
} ) ,
2018-06-20 10:23:54 +09:00
( ) => this . submitForm ( this . refs . form . formsyForm . getModel ( ) )
2018-03-24 11:33:28 +09:00
) ;
} ;
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 ,
2018-05-08 20:09:42 -04:00
refetchForm : this . refetchForm ,
isChanged : this . isChanged ,
submitForm : this . submitFormContext , //Change in name because we already have a function
2018-05-11 09:52:04 +09:00
// 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-04-20 14:35:53 -05:00
initialDocument : this . state . 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-04-20 14:35:53 -05:00
static getDerivedStateFromProps ( nextProps , prevState ) {
return computeStateFromProps ( nextProps ) ;
}
2018-03-24 11:33:28 +09:00
/ *
2018-05-08 20:09:42 -04:00
2018-04-20 14:35:53 -05:00
Manually update the current values of one or more fields ( i . e . on change or blur ) .
2018-05-08 20:09:42 -04:00
2018-03-24 11:33:28 +09:00
* /
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
} ) ;
} ;
2018-05-11 09:52:04 +09:00
2018-05-08 20:09:42 -04:00
/ *
Warn the user if there are unsaved changes
* /
handleRouteLeave = ( ) => {
if ( this . isChanged ( ) ) {
const message = this . context . intl . formatMessage ( {
id : 'forms.confirm_discard' ,
2018-05-11 09:52:04 +09:00
defaultMessage : 'Are you sure you want to discard your changes?' ,
2018-05-08 20:09:42 -04:00
} ) ;
return message ;
}
} ;
2018-05-22 09:51:36 +02:00
//see https://developer.mozilla.org/en-US/docs/Web/API/WindowEventHandlers/onbeforeunload
//the message returned is actually ignored by most browsers and a default message 'Are you sure you want to leave this page? You might have unsaved changes' is displayed. See the Notes section on the mozilla docs above
handlePageLeave = ( event ) => {
if ( this . isChanged ( ) ) {
const message = this . context . intl . formatMessage ( {
id : 'forms.confirm_discard' ,
defaultMessage : 'Are you sure you want to discard your changes?'
} ) ;
if ( event ) {
event . returnValue = message ;
}
return message ;
}
} ;
2018-05-08 20:09:42 -04:00
/ *
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 ) ;
2018-05-22 09:51:36 +02:00
//check for closing the browser with unsaved changes
window . onbeforeunload = this . handlePageLeave ;
2018-05-08 20:09:42 -04:00
}
} ;
2018-05-11 09:52:04 +09:00
2018-05-22 09:51:36 +02:00
/ *
Remove the closing browser check on component unmount
see https : //gist.github.com/mknabe/bfcb6db12ef52323954a28655801792d
* /
componentWillUnmount = ( ) => {
let warnUnsavedChanges = getSetting ( 'forms.warnUnsavedChanges' ) ;
if ( typeof this . props . warnUnsavedChanges === 'boolean' ) {
warnUnsavedChanges = this . props . warnUnsavedChanges ;
}
if ( warnUnsavedChanges ) {
window . onbeforeunload = undefined ; //undefined instead of null to support IE
}
} ;
2018-04-20 14:35:53 -05:00
/ *
2018-05-08 20:09:42 -04:00
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 ( ) ;
2018-05-11 09:52:04 +09:00
2018-05-08 20:09:42 -04:00
const changedValue = find ( changedDocument , ( value , key , collection ) => {
return ! isEqualWith ( value , initialDocument [ key ] , ( objValue , othValue ) => {
if ( ! objValue && ! othValue ) return true ;
} ) ;
} ) ;
2018-05-11 09:52:04 +09:00
2018-05-08 20:09:42 -04:00
return typeof changedValue !== 'undefined' ;
} ;
2018-05-11 09:52:04 +09:00
2018-05-08 20:09:42 -04:00
/ *
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 ( ) ;
}
} ;
2018-05-11 09:52:04 +09:00
2018-05-08 20:09:42 -04:00
/ *
2018-03-24 11:33:28 +09:00
Clear and reset the form
By default , clear errors and keep current values and deleted values
* /
2018-05-11 09:52:04 +09:00
clearForm = ( { clearErrors = true , clearCurrentValues = false , clearDeletedValues = false , document } ) => {
2018-05-08 20:09:42 -04:00
document = document ? merge ( { } , this . props . prefilledProps , document ) : null ;
2018-05-11 09:52:04 +09:00
2018-03-24 11:33:28 +09:00
this . setState ( prevState => ( {
errors : clearErrors ? [ ] : prevState . errors ,
currentValues : clearCurrentValues ? { } : prevState . currentValues ,
deletedValues : clearDeletedValues ? [ ] : prevState . deletedValues ,
2018-05-08 20:09:42 -04:00
initialDocument : document ? document : prevState . initialDocument ,
2018-03-24 11:33:28 +09:00
disabled : false ,
} ) ) ;
} ;
/ *
2018-05-08 20:09:42 -04:00
2018-03-24 11:33:28 +09:00
Key down handler
* /
formKeyDown = event => {
if ( ( event . ctrlKey || event . metaKey ) && event . keyCode === 13 ) {
2018-06-20 10:23:54 +09:00
this . submitForm ( this . refs . form . formsyForm . getModel ( ) ) ;
2018-03-24 11:33:28 +09:00
}
} ;
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 ) => {
2018-06-13 18:28:01 +09:00
this . setState ( prevState => ( { disabled : false } ) ) ;
2018-06-20 10:23:54 +09:00
const document = result . data [ Object . keys ( result . data ) [ 0 ] ] . data ; // document is always on first property
2016-11-23 17:22:29 +09:00
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 ( ) ;
2018-05-11 09:52:04 +09:00
// call the clear form method (i.e. trigger setState) only if the form has not been unmounted
2018-05-08 20:09:42 -04:00
// (we are in an async callback, everything can happen!)
2017-01-13 18:17:08 +01:00
if ( typeof this . refs . form !== 'undefined' ) {
2018-06-20 10:23:54 +09:00
this . refs . form . formsyForm . reset ( ) ;
2018-05-08 20:09:42 -04:00
this . clearForm ( { clearErrors : true , clearCurrentValues : true , clearDeletedValues : true , document } ) ;
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-04-30 15:39:33 -03:00
mutationErrorCallback = ( document , error ) => {
2018-03-24 11:33:28 +09:00
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
}
2018-05-11 09:52:04 +09:00
2016-11-23 17:22:29 +09:00
// run error callback if it exists
2018-04-30 15:39:33 -03:00
if ( this . props . errorCallback ) this . props . errorCallback ( document , error ) ;
2018-05-11 09:52:04 +09:00
2018-05-10 10:18:55 +09:00
// scroll back up to show error messages
2018-05-08 20:09:42 -04:00
Utils . scrollIntoView ( '.flash-message' ) ;
2018-03-24 11:33:28 +09:00
} ;
2016-11-23 17:22:29 +09:00
2018-04-20 14:35:53 -05: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-05-22 08:43:27 +09:00
const fields = this . getFieldNames ( { replaceIntlFields : true } ) ;
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 )
2018-04-30 15:39:33 -03:00
. catch ( error => this . mutationErrorCallback ( document , error ) ) ;
2018-03-24 11:33:28 +09:00
} 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
2018-05-23 16:02:36 -04:00
unsetKeys = unsetKeys . filter ( key => ! key . includes ( '.' ) ) ;
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 )
2018-05-02 10:35:13 -03:00
. catch ( error => this . mutationErrorCallback ( document , 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
/ *
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-05-02 13:39:52 -03:00
. catch ( error => {
// eslint-disable-next-line no-console
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
// --------------------------------------------------------------------- //
2018-05-11 09:52:04 +09:00
render ( ) {
2016-11-23 17:22:29 +09:00
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 ( ) } >
2018-06-20 10:23:54 +09:00
< Form onSubmit = { this . submitForm } onKeyDown = { this . formKeyDown } disabled = { this . state . disabled } ref = "form" >
2018-05-11 09:52:04 +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-04-14 17:21:10 +09:00
clearFieldErrors = { this . clearFieldErrors }
2018-03-26 17:50:03 +09:00
formType = { this . getFormType ( ) }
2018-04-28 10:54:03 +09:00
currentUser = { this . props . currentUser }
2018-03-26 17:50:03 +09:00
/ >
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 }
2018-05-08 20:09:42 -04:00
revertLabel = { this . props . revertLabel }
2018-03-24 11:33:28 +09:00
cancelCallback = { this . props . cancelCallback }
2018-05-08 20:09:42 -04:00
revertCallback = { this . props . revertCallback }
2018-03-24 11:33:28 +09:00
document = { this . getDocument ( ) }
deleteDocument = { ( this . getFormType ( ) === 'edit' && this . props . showRemove && this . deleteDocument ) || null }
collectionName = { collectionName }
2017-10-17 09:35:41 -04:00
/ >
2018-06-20 10:23:54 +09:00
< / Form >
2016-11-23 17:22:29 +09:00
< / div >
2018-03-24 11:33:28 +09:00
) ;
2016-11-23 17:22:29 +09:00
}
}
2018-06-20 10:23:54 +09:00
SmartForm . 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 ,
2018-05-08 20:09:42 -04:00
revertLabel : PropTypes . string ,
2017-11-09 10:01:22 +09:00
repeatErrors : PropTypes . bool ,
2018-05-08 20:09:42 -04:00
warnUnsavedChanges : 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 ,
2018-05-08 20:09:42 -04:00
revertCallback : PropTypes . func ,
2017-02-02 15:15:51 +01:00
currentUser : PropTypes . object ,
client : PropTypes . object ,
2018-03-24 11:33:28 +09:00
} ;
2016-11-23 17:22:29 +09:00
2018-06-20 10:23:54 +09:00
SmartForm . 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
2018-06-20 10:23:54 +09:00
SmartForm . contextTypes = {
2018-03-24 11:33:28 +09:00
intl : intlShape ,
} ;
2016-11-23 17:22:29 +09:00
2018-06-20 10:23:54 +09:00
SmartForm . 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-05-08 20:09:42 -04:00
refetchForm : PropTypes . func ,
isChanged : 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-06-20 10:23:54 +09:00
module . exports = SmartForm ;
2018-03-26 17:50:03 +09:00
2018-06-20 10:23:54 +09:00
registerComponent ( 'Form' , SmartForm ) ;