Vulcan/packages/vulcan-forms/lib/components/FormWrapper.jsx

333 lines
9.7 KiB
React
Raw Normal View History

/*
Generate the appropriate fragment for the current form, then
wrap the main Form component with the necessary HoCs while passing
them the fragment.
This component is itself wrapped with:
- withCurrentUser
- withApollo (used to access the Apollo client for form pre-population)
And wraps the Form component with:
- withNew
Or:
2018-08-29 20:36:36 +09:00
- withSingle
- withUpdate
- withDelete
2018-08-29 20:36:36 +09:00
(When wrapping with withSingle, withUpdate, and withDelete, a special Loader
component is also added to wait for withSingle's loading prop to be false)
*/
import React, { PureComponent } from 'react';
2017-05-19 14:42:43 -06:00
import PropTypes from 'prop-types';
2017-06-01 11:42:30 +09:00
import { intlShape } from 'meteor/vulcan:i18n';
2018-10-26 13:01:35 +02:00
import { withRouter } from 'react-router';
import { withApollo, compose } from 'react-apollo';
import {
Components,
registerComponent,
withCurrentUser,
Utils,
withNew,
2018-08-29 20:36:36 +09:00
withUpdate,
withDelete,
getFragment
} from 'meteor/vulcan:core';
import gql from 'graphql-tag';
2018-08-29 20:36:36 +09:00
import { withSingle } from 'meteor/vulcan:core';
import { graphql } from 'react-apollo';
import {
getReadableFields,
getCreateableFields,
getUpdateableFields
} from '../modules/schema_utils';
import withCollectionProps from './withCollectionProps';
import { callbackProps } from './propTypes';
class FormWrapper extends PureComponent {
constructor(props) {
super(props);
// instantiate the wrapped component in constructor, not in render
// see https://reactjs.org/docs/higher-order-components.html#dont-use-hocs-inside-the-render-method
this.FormComponent = this.getComponent(props);
}
// return the current schema based on either the schema or collection prop
getSchema() {
2018-10-26 13:01:35 +02:00
return this.props.schema
? this.props.schema
: this.props.collection.simpleSchema()._schema;
}
// if a document is being passed, this is an edit form
getFormType() {
2018-09-16 11:48:38 +09:00
return this.props.documentId || this.props.slug ? 'edit' : 'new';
}
// get fragment used to decide what data to load from the server to populate the form,
// as well as what data to ask for as return value for the mutation
getFragments() {
const prefix = `${this.props.collectionName}${Utils.capitalize(
2018-10-26 13:01:35 +02:00
this.getFormType()
)}`;
const fragmentName = `${prefix}FormFragment`;
const fields = this.props.fields;
const readableFields = getReadableFields(this.getSchema());
const createableFields = getCreateableFields(this.getSchema());
const updatetableFields = getUpdateableFields(this.getSchema());
// get all editable/insertable fields (depending on current form type)
2018-10-26 13:01:35 +02:00
let queryFields =
this.getFormType() === 'new' ? createableFields : updatetableFields;
2017-07-26 07:26:57 +09:00
// for the mutations's return value, also get non-editable but viewable fields (such as createdAt, userId, etc.)
2018-10-26 13:01:35 +02:00
let mutationFields =
this.getFormType() === 'new'
? _.unique(createableFields.concat(readableFields))
: _.unique(createableFields.concat(updatetableFields));
// if "fields" prop is specified, restrict list of fields to it
2018-09-16 11:48:38 +09:00
if (typeof fields !== 'undefined' && fields.length > 0) {
2017-07-26 07:26:57 +09:00
queryFields = _.intersection(queryFields, fields);
mutationFields = _.intersection(mutationFields, fields);
}
2018-10-26 13:01:35 +02:00
// add "addFields" prop contents to list of fields
2018-11-24 09:56:51 +09:00
if (this.props.addFields && this.props.addFields.length) {
queryFields = queryFields.concat(this.props.addFields);
mutationFields = mutationFields.concat(this.props.addFields);
}
const convertFields = field => {
2018-10-26 13:01:35 +02:00
return field.slice(-5) === '_intl' ? `${field}{ locale value }` : field;
};
2017-07-26 07:26:57 +09:00
// generate query fragment based on the fields that can be edited. Note: always add _id.
const generatedQueryFragment = gql`
fragment ${fragmentName} on ${this.props.typeName} {
2017-07-26 07:26:57 +09:00
_id
${queryFields.map(convertFields).join('\n')}
2017-07-26 07:26:57 +09:00
}
2018-10-26 13:01:35 +02:00
`;
2017-07-26 07:26:57 +09:00
// generate mutation fragment based on the fields that can be edited and/or viewed. Note: always add _id.
const generatedMutationFragment = gql`
fragment ${fragmentName} on ${this.props.typeName} {
_id
${mutationFields.map(convertFields).join('\n')}
}
2018-10-26 13:01:35 +02:00
`;
// default to generated fragments
let queryFragment = generatedQueryFragment;
let mutationFragment = generatedMutationFragment;
// if queryFragment or mutationFragment props are specified, accept either fragment object or fragment string
if (this.props.queryFragment) {
2018-10-26 13:01:35 +02:00
queryFragment =
typeof this.props.queryFragment === 'string'
? gql`
${this.props.queryFragment}
`
: this.props.queryFragment;
}
if (this.props.mutationFragment) {
2018-10-26 13:01:35 +02:00
mutationFragment =
typeof this.props.mutationFragment === 'string'
? gql`
${this.props.mutationFragment}
`
: this.props.mutationFragment;
}
// same with queryFragmentName and mutationFragmentName
if (this.props.queryFragmentName) {
queryFragment = getFragment(this.props.queryFragmentName);
}
if (this.props.mutationFragmentName) {
mutationFragment = getFragment(this.props.mutationFragmentName);
}
// if any field specifies extra queries, add them
2018-10-26 13:01:35 +02:00
const extraQueries = _.compact(
queryFields.map(fieldName => {
const field = this.getSchema()[fieldName];
return field.query;
})
);
// get query & mutation fragments from props or else default to same as generatedFragment
return {
queryFragment,
mutationFragment,
2018-10-26 13:01:35 +02:00
extraQueries
};
}
getComponent() {
let WrappedComponent;
const prefix = `${this.props.collectionName}${Utils.capitalize(
2018-10-26 13:01:35 +02:00
this.getFormType()
)}`;
2018-10-26 13:01:35 +02:00
const {
queryFragment,
mutationFragment,
extraQueries
} = this.getFragments();
// props to pass on to child component (i.e. <Form />)
const childProps = {
formType: this.getFormType(),
2018-10-26 13:01:35 +02:00
schema: this.getSchema()
};
2018-08-29 20:36:36 +09:00
// options for withSingle HoC
const queryOptions = {
queryName: `${prefix}FormQuery`,
collection: this.props.collection,
fragment: queryFragment,
extraQueries,
fetchPolicy: 'network-only', // we always want to load a fresh copy of the document
enableCache: false,
2018-10-26 13:01:35 +02:00
pollInterval: 0 // no polling, only load data once
};
2018-08-29 20:36:36 +09:00
// options for withNew, withUpdate, and withDelete HoCs
const mutationOptions = {
collection: this.props.collection,
2018-10-26 13:01:35 +02:00
fragment: mutationFragment
};
// create a stateless loader component,
// displays the loading state if needed, and passes on loading and document/data
const Loader = props => {
const { document, loading } = props;
2018-10-26 13:01:35 +02:00
return loading ? (
<Components.Loading />
) : (
2018-03-26 17:50:03 +09:00
<Components.Form
document={document}
loading={loading}
{...childProps}
{...props}
2018-10-26 13:01:35 +02:00
/>
);
};
2018-09-16 11:48:38 +09:00
Loader.displayName = 'withLoader(Form)';
2018-08-29 20:36:36 +09:00
// if this is an edit from, load the necessary data using the withSingle HoC
2017-05-19 14:42:43 -06:00
if (this.getFormType() === 'edit') {
WrappedComponent = compose(
2018-08-29 20:36:36 +09:00
withSingle(queryOptions),
withUpdate(mutationOptions),
withDelete(mutationOptions)
)(Loader);
2018-10-26 13:01:35 +02:00
return (
<WrappedComponent
selector={{
documentId: this.props.documentId,
slug: this.props.slug
}}
/>
);
} else {
2018-01-04 08:58:50 +09:00
if (extraQueries && extraQueries.length) {
2018-10-26 13:01:35 +02:00
const extraQueriesHoC = graphql(
gql`
query formNewExtraQuery {
${extraQueries}
2018-10-26 13:01:35 +02:00
}`,
{
alias: 'withExtraQueries',
props: returnedProps => {
2018-01-25 15:03:03 -06:00
const { /* ownProps, */ data } = returnedProps;
const props = {
loading: data.loading,
2018-10-26 13:01:35 +02:00
data
};
return props;
2018-10-26 13:01:35 +02:00
}
}
);
WrappedComponent = compose(
extraQueriesHoC,
withNew(mutationOptions)
)(Loader);
} else {
2018-10-26 13:01:35 +02:00
WrappedComponent = compose(withNew(mutationOptions))(Components.Form);
}
return <WrappedComponent {...childProps} />;
}
}
render() {
const component = this.FormComponent;
const componentWithParentProps = React.cloneElement(component, this.props);
return componentWithParentProps;
}
}
FormWrapper.propTypes = {
// main options
collection: PropTypes.object.isRequired,
collectionName: PropTypes.string.isRequired,
typeName: PropTypes.string.isRequired,
2017-05-19 14:42:43 -06:00
documentId: PropTypes.string, // if a document is passed, this will be an edit form
schema: PropTypes.object, // usually not needed
queryFragment: PropTypes.object,
queryFragmentName: PropTypes.string,
2017-05-19 14:42:43 -06:00
mutationFragment: PropTypes.object,
mutationFragmentName: PropTypes.string,
// graphQL
2017-05-19 14:42:43 -06:00
newMutation: PropTypes.func, // the new mutation
editMutation: PropTypes.func, // the edit mutation
removeMutation: PropTypes.func, // the remove mutation
// form
2017-05-19 14:42:43 -06:00
prefilledProps: PropTypes.object,
layout: PropTypes.string,
fields: PropTypes.arrayOf(PropTypes.string),
hideFields: PropTypes.arrayOf(PropTypes.string),
addFields: PropTypes.arrayOf(PropTypes.string),
2017-05-19 14:42:43 -06:00
showRemove: PropTypes.bool,
submitLabel: PropTypes.node,
cancelLabel: PropTypes.node,
revertLabel: PropTypes.node,
repeatErrors: PropTypes.bool,
warnUnsavedChanges: PropTypes.bool,
// callbacks
...callbackProps,
2017-05-19 14:42:43 -06:00
currentUser: PropTypes.object,
2018-10-26 13:01:35 +02:00
client: PropTypes.object
};
FormWrapper.defaultProps = {
2018-10-26 13:01:35 +02:00
layout: 'horizontal'
};
FormWrapper.contextTypes = {
2017-05-19 14:42:43 -06:00
closeCallback: PropTypes.func,
intl: intlShape
2018-10-26 13:01:35 +02:00
};
registerComponent({
name: 'SmartForm',
component: FormWrapper,
hocs: [withCurrentUser, withApollo, withRouter, withCollectionProps]
});