mirror of
https://github.com/vale981/Vulcan
synced 2025-03-05 17:41:43 -05:00
Merge branch 'devel' into single-documentId
This commit is contained in:
commit
e981503a4b
31 changed files with 724 additions and 459 deletions
|
@ -30,7 +30,7 @@ import React, { Component } from 'react';
|
|||
import { graphql } from 'react-apollo';
|
||||
import gql from 'graphql-tag';
|
||||
import { createClientTemplate } from 'meteor/vulcan:core';
|
||||
import { extractCollectionInfo, extractFragmentInfo } from './handleOptions';
|
||||
import { extractCollectionInfo, extractFragmentInfo } from 'meteor/vulcan:lib';
|
||||
|
||||
const withCreate = options => {
|
||||
const { collectionName, collection } = extractCollectionInfo(options);
|
||||
|
|
|
@ -30,7 +30,7 @@ import React, { Component } from 'react';
|
|||
import { graphql } from 'react-apollo';
|
||||
import gql from 'graphql-tag';
|
||||
import { deleteClientTemplate } from 'meteor/vulcan:core';
|
||||
import { extractCollectionInfo, extractFragmentInfo } from './handleOptions';
|
||||
import { extractCollectionInfo, extractFragmentInfo } from 'meteor/vulcan:lib';
|
||||
|
||||
const withDelete = options => {
|
||||
const { collectionName, collection } = extractCollectionInfo(options);
|
||||
|
|
|
@ -38,14 +38,12 @@ import React, { Component } from 'react';
|
|||
import { withApollo, graphql } from 'react-apollo';
|
||||
import gql from 'graphql-tag';
|
||||
import update from 'immutability-helper';
|
||||
import { getSetting, Utils, multiClientTemplate } from 'meteor/vulcan:lib';
|
||||
import { getSetting, Utils, multiClientTemplate, extractCollectionInfo, extractFragmentInfo } from 'meteor/vulcan:lib';
|
||||
import Mingo from 'mingo';
|
||||
import compose from 'recompose/compose';
|
||||
import withState from 'recompose/withState';
|
||||
import find from 'lodash/find';
|
||||
|
||||
import { extractCollectionInfo, extractFragmentInfo } from './handleOptions';
|
||||
|
||||
export default function withMulti(options) {
|
||||
// console.log(options)
|
||||
|
||||
|
@ -172,8 +170,8 @@ export default function withMulti(options) {
|
|||
typeof providedTerms === 'undefined'
|
||||
? {
|
||||
/*...props.ownProps.terms,*/ ...props.ownProps.paginationTerms,
|
||||
limit: results.length + props.ownProps.paginationTerms.itemsPerPage
|
||||
}
|
||||
limit: results.length + props.ownProps.paginationTerms.itemsPerPage
|
||||
}
|
||||
: providedTerms;
|
||||
|
||||
props.ownProps.setPaginationTerms(newTerms);
|
||||
|
|
|
@ -2,9 +2,7 @@ import React, { Component } from 'react';
|
|||
import PropTypes from 'prop-types';
|
||||
import { graphql } from 'react-apollo';
|
||||
import gql from 'graphql-tag';
|
||||
import { getSetting, singleClientTemplate, Utils } from 'meteor/vulcan:lib';
|
||||
|
||||
import { extractCollectionInfo, extractFragmentInfo } from './handleOptions';
|
||||
import { getSetting, singleClientTemplate, Utils, extractCollectionInfo, extractFragmentInfo } from 'meteor/vulcan:lib';
|
||||
|
||||
export default function withSingle(options) {
|
||||
const { pollInterval = getSetting('pollInterval', 20000), enableCache = false, extraQueries } = options;
|
||||
|
|
|
@ -30,11 +30,9 @@ Child Props:
|
|||
import React, { Component } from 'react';
|
||||
import { graphql } from 'react-apollo';
|
||||
import gql from 'graphql-tag';
|
||||
import { updateClientTemplate } from 'meteor/vulcan:lib';
|
||||
import { updateClientTemplate, extractCollectionInfo, extractFragmentInfo } from 'meteor/vulcan:lib';
|
||||
import clone from 'lodash/clone';
|
||||
|
||||
import { extractCollectionInfo, extractFragmentInfo } from './handleOptions';
|
||||
|
||||
const withUpdate = options => {
|
||||
const { collectionName, collection } = extractCollectionInfo(options);
|
||||
const { fragmentName, fragment } = extractFragmentInfo(options, collectionName);
|
||||
|
|
|
@ -33,7 +33,7 @@ import gql from 'graphql-tag';
|
|||
import { upsertClientTemplate } from 'meteor/vulcan:core';
|
||||
import clone from 'lodash/clone';
|
||||
|
||||
import { extractCollectionInfo, extractFragmentInfo } from './handleOptions';
|
||||
import { extractCollectionInfo, extractFragmentInfo } from 'meteor/vulcan:lib';
|
||||
|
||||
const withUpsert = options => {
|
||||
const { collectionName, collection } = extractCollectionInfo(options);
|
||||
|
|
|
@ -1,46 +0,0 @@
|
|||
import { extractCollectionInfo, extractFragmentInfo } from '../lib/modules/containers/handleOptions';
|
||||
import expect from 'expect';
|
||||
|
||||
describe('vulcan-core/containers', function() {
|
||||
describe('handleOptions', function() {
|
||||
const expectedCollectionName = 'COLLECTION_NAME';
|
||||
const expectedCollection = { options: { collectionName: expectedCollectionName } };
|
||||
|
||||
it('get collectionName from collection', function() {
|
||||
const options = { collection: expectedCollection };
|
||||
const { collection, collectionName } = extractCollectionInfo(options);
|
||||
expect(collection).toEqual(expectedCollection);
|
||||
expect(collectionName).toEqual(expectedCollectionName);
|
||||
});
|
||||
it.skip('get collection from collectionName', function() {
|
||||
// TODO: mock getCollection
|
||||
const options = { collectionName: expectedCollectionName };
|
||||
const { collection, collectionName } = extractCollectionInfo(options);
|
||||
expect(collection).toEqual(expectedCollection);
|
||||
expect(collectionName).toEqual(expectedCollectionName);
|
||||
});
|
||||
const expectedFragmentName = 'FRAGMENT_NAME';
|
||||
const expectedFragment = { definitions: [{ name: { value: expectedFragmentName } }] };
|
||||
it.skip('get fragment from fragmentName', function() {
|
||||
// TODO: mock getCollection
|
||||
const options = { fragmentName: expectedFragmentName };
|
||||
const { fragment, fragmentName } = extractFragmentInfo(options);
|
||||
expect(fragment).toEqual(expectedFragment);
|
||||
expect(fragmentName).toEqual(expectedFragmentName);
|
||||
});
|
||||
it('get fragmentName from fragment', function() {
|
||||
const options = { fragment: expectedFragment };
|
||||
const { fragment, fragmentName } = extractFragmentInfo(options);
|
||||
expect(fragment).toEqual(expectedFragment);
|
||||
expect(fragmentName).toEqual(expectedFragmentName);
|
||||
});
|
||||
it.skip('get fragmentName and fragment from collectionName', function() {
|
||||
// TODO: mock getFragment
|
||||
// if options does not contain fragment, we get the collection default fragment based on its name
|
||||
const options = {};
|
||||
const { fragment, fragmentName } = extractFragmentInfo(options, expectedCollectionName);
|
||||
expect(fragment).toEqual(expectedFragment);
|
||||
expect(fragmentName).toEqual(expectedFragmentName);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -1,3 +1 @@
|
|||
import './containers.test.js';
|
||||
|
||||
import './resolvers.test';
|
||||
|
|
|
@ -11,4 +11,7 @@ const FieldErrors = ({ errors }) => (
|
|||
))}
|
||||
</ul>
|
||||
);
|
||||
FieldErrors.propTypes = {
|
||||
errors: PropTypes.array.isRequired
|
||||
};
|
||||
registerComponent('FieldErrors', FieldErrors);
|
||||
|
|
|
@ -37,7 +37,6 @@ import SimpleSchema from 'simpl-schema';
|
|||
import PropTypes from 'prop-types';
|
||||
import { intlShape } from 'meteor/vulcan:i18n';
|
||||
import Formsy from 'formsy-react';
|
||||
import { getEditableFields, getInsertableFields } from '../modules/utils.js';
|
||||
import cloneDeep from 'lodash/cloneDeep';
|
||||
import get from 'lodash/get';
|
||||
import set from 'lodash/set';
|
||||
|
@ -58,6 +57,13 @@ import pickBy from 'lodash/pickBy';
|
|||
import { convertSchema, formProperties } from '../modules/schema_utils';
|
||||
import { isEmptyValue } from '../modules/utils';
|
||||
import { getParentPath } from '../modules/path_utils';
|
||||
import mergeWithComponents from '../modules/mergeWithComponents';
|
||||
import {
|
||||
getEditableFields,
|
||||
getInsertableFields
|
||||
} from '../modules/schema_utils.js';
|
||||
import withCollectionProps from './withCollectionProps';
|
||||
import { callbackProps } from './propTypes';
|
||||
|
||||
const compactParent = (object, path) => {
|
||||
const parentPath = getParentPath(path);
|
||||
|
@ -77,8 +83,7 @@ const getDefaultValues = convertedSchema => {
|
|||
};
|
||||
|
||||
const getInitialStateFromProps = nextProps => {
|
||||
const collection =
|
||||
nextProps.collection || getCollection(nextProps.collectionName);
|
||||
const collection = nextProps.collection;
|
||||
const schema = nextProps.schema
|
||||
? new SimpleSchema(nextProps.schema)
|
||||
: collection.simpleSchema();
|
||||
|
@ -143,36 +148,14 @@ class SmartForm extends Component {
|
|||
// --------------------------------------------------------------------- //
|
||||
|
||||
/*
|
||||
|
||||
Get the current collection
|
||||
|
||||
*/
|
||||
getCollection = () => {
|
||||
return this.props.collection || getCollection(this.props.collectionName);
|
||||
};
|
||||
|
||||
/*
|
||||
|
||||
Get current typeName
|
||||
|
||||
*/
|
||||
getTypeName = () => {
|
||||
return this.getCollection().options.typeName;
|
||||
};
|
||||
|
||||
/*
|
||||
|
||||
If a document is being passed, this is an edit form
|
||||
|
||||
*/
|
||||
getFormType = () => {
|
||||
return this.props.document ? 'edit' : 'new';
|
||||
};
|
||||
|
||||
/*
|
||||
|
||||
Get a list of all insertable fields
|
||||
|
||||
*/
|
||||
getInsertableFields = schema => {
|
||||
return getInsertableFields(
|
||||
|
@ -182,9 +165,7 @@ class SmartForm extends Component {
|
|||
};
|
||||
|
||||
/*
|
||||
|
||||
Get a list of all editable fields
|
||||
|
||||
*/
|
||||
getEditableFields = schema => {
|
||||
return getEditableFields(
|
||||
|
@ -262,9 +243,6 @@ class SmartForm extends Component {
|
|||
Get form components, in case any has been overwritten for this specific form
|
||||
|
||||
*/
|
||||
getFormComponents = () => {
|
||||
return { ...Components, ...this.props.formComponents };
|
||||
};
|
||||
// --------------------------------------------------------------------- //
|
||||
// -------------------------------- Fields ----------------------------- //
|
||||
// --------------------------------------------------------------------- //
|
||||
|
@ -519,7 +497,7 @@ class SmartForm extends Component {
|
|||
|
||||
*/
|
||||
getLabel = (fieldName, fieldLocale) => {
|
||||
const collectionName = this.getCollection().options.collectionName.toLowerCase();
|
||||
const collectionName = this.props.collectionName.toLowerCase();
|
||||
const defaultMessage = '|*|*|';
|
||||
let id = `${collectionName}.${fieldName}`;
|
||||
let intlLabel;
|
||||
|
@ -678,6 +656,7 @@ class SmartForm extends Component {
|
|||
updateCurrentValues = (newValues, options = {}) => {
|
||||
// default to overwriting old value with new
|
||||
const { mode = 'overwrite' } = options;
|
||||
const { changeCallback } = this.props;
|
||||
|
||||
// keep the previous ones and extend (with possible replacement) with new ones
|
||||
this.setState(prevState => {
|
||||
|
@ -717,6 +696,7 @@ class SmartForm extends Component {
|
|||
newState.deletedValues = _.without(prevState.deletedValues, path);
|
||||
}
|
||||
});
|
||||
if (changeCallback) changeCallback(newState.currentDocument);
|
||||
return newState;
|
||||
});
|
||||
};
|
||||
|
@ -943,13 +923,13 @@ class SmartForm extends Component {
|
|||
|
||||
if (this.getFormType() === 'new') {
|
||||
// create document form
|
||||
this.props[`create${this.getTypeName()}`]({ data })
|
||||
this.props[`create${this.props.typeName}`]({ data })
|
||||
.then(this.newMutationSuccessCallback)
|
||||
.catch(error => this.mutationErrorCallback(document, error));
|
||||
} else {
|
||||
// update document form
|
||||
const documentId = this.getDocument()._id;
|
||||
this.props[`update${this.getTypeName()}`]({
|
||||
this.props[`update${this.props.typeName}`]({
|
||||
selector: { documentId },
|
||||
data
|
||||
})
|
||||
|
@ -995,8 +975,8 @@ class SmartForm extends Component {
|
|||
|
||||
render() {
|
||||
const fieldGroups = this.getFieldGroups();
|
||||
const collectionName = this.getCollection()._name;
|
||||
const FormComponents = this.getFormComponents();
|
||||
const collectionName = this.props.collectionName;
|
||||
const FormComponents = mergeWithComponents(this.props.formComponents);
|
||||
|
||||
return (
|
||||
<div className={'document-' + this.getFormType()}>
|
||||
|
@ -1055,19 +1035,10 @@ class SmartForm extends Component {
|
|||
|
||||
SmartForm.propTypes = {
|
||||
// main options
|
||||
collection: PropTypes.object,
|
||||
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}`
|
||||
);
|
||||
}
|
||||
},
|
||||
collection: PropTypes.object.isRequired,
|
||||
collectionName: PropTypes.string.isRequired,
|
||||
typeName: PropTypes.string.isRequired,
|
||||
|
||||
document: PropTypes.object, // if a document is passed, this will be an edit form
|
||||
schema: PropTypes.object, // usually not needed
|
||||
|
||||
|
@ -1092,12 +1063,7 @@ SmartForm.propTypes = {
|
|||
formComponents: PropTypes.object,
|
||||
|
||||
// callbacks
|
||||
submitCallback: PropTypes.func,
|
||||
successCallback: PropTypes.func,
|
||||
removeSuccessCallback: PropTypes.func,
|
||||
errorCallback: PropTypes.func,
|
||||
cancelCallback: PropTypes.func,
|
||||
revertCallback: PropTypes.func,
|
||||
...callbackProps,
|
||||
|
||||
currentUser: PropTypes.object,
|
||||
client: PropTypes.object
|
||||
|
@ -1136,4 +1102,8 @@ SmartForm.childContextTypes = {
|
|||
|
||||
module.exports = SmartForm;
|
||||
|
||||
registerComponent('Form', SmartForm);
|
||||
registerComponent({
|
||||
name: 'Form',
|
||||
component: SmartForm,
|
||||
hocs: [withCollectionProps]
|
||||
});
|
||||
|
|
|
@ -6,6 +6,7 @@ import get from 'lodash/get';
|
|||
import isEqual from 'lodash/isEqual';
|
||||
import SimpleSchema from 'simpl-schema';
|
||||
import { isEmptyValue, getNullValue } from '../modules/utils.js';
|
||||
import mergeWithComponents from '../modules/mergeWithComponents';
|
||||
|
||||
class FormComponent extends Component {
|
||||
constructor(props) {
|
||||
|
@ -14,10 +15,6 @@ class FormComponent extends Component {
|
|||
this.state = {};
|
||||
}
|
||||
|
||||
static defaultProps = {
|
||||
formComponents: {}
|
||||
};
|
||||
|
||||
componentWillMount() {
|
||||
if (this.showCharsRemaining()) {
|
||||
const value = this.getValue();
|
||||
|
@ -216,10 +213,6 @@ class FormComponent extends Component {
|
|||
}
|
||||
};
|
||||
|
||||
getFormComponents = () => {
|
||||
return { ...Components, ...this.props.formComponents };
|
||||
};
|
||||
|
||||
/*
|
||||
|
||||
Function passed to FormComponentInner to help with rendering the component
|
||||
|
@ -227,7 +220,7 @@ class FormComponent extends Component {
|
|||
*/
|
||||
getFormInput = () => {
|
||||
const inputType = this.getType();
|
||||
const FormComponents = this.getFormComponents();
|
||||
const FormComponents = mergeWithComponents(this.props.formComponents);
|
||||
|
||||
// if input is a React component, use it
|
||||
if (typeof this.props.input === 'function') {
|
||||
|
@ -301,7 +294,7 @@ class FormComponent extends Component {
|
|||
return this.getFieldType() instanceof SimpleSchema;
|
||||
};
|
||||
render() {
|
||||
const FormComponents = this.getFormComponents();
|
||||
const FormComponents = mergeWithComponents(this.props.formComponents);
|
||||
|
||||
if (this.props.intlInput) {
|
||||
return <FormComponents.FormIntl {...this.props} />;
|
||||
|
|
|
@ -3,6 +3,46 @@ import PropTypes from 'prop-types';
|
|||
import { Components } from 'meteor/vulcan:core';
|
||||
import classNames from 'classnames';
|
||||
import { registerComponent } from 'meteor/vulcan:core';
|
||||
import mergeWithComponents from '../modules/mergeWithComponents';
|
||||
|
||||
const FormGroupHeader = ({ toggle, collapsed, label }) => (
|
||||
<div className="form-section-heading" onClick={toggle}>
|
||||
<h3 className="form-section-heading-title">{label}</h3>
|
||||
<span className="form-section-heading-toggle">
|
||||
{collapsed ? (
|
||||
<Components.IconRight height={16} width={16} />
|
||||
) : (
|
||||
<Components.IconDown height={16} width={16} />
|
||||
)}
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
FormGroupHeader.propTypes = {
|
||||
toggle: PropTypes.func.isRequired,
|
||||
label: PropTypes.string.isRequired,
|
||||
collapsed: PropTypes.bool
|
||||
};
|
||||
registerComponent({ name: 'FormGroupHeader', component: FormGroupHeader });
|
||||
|
||||
const FormGroupLayout = ({ children, heading, collapsed, hasErrors }) => (
|
||||
<div className="form-section">
|
||||
{heading}
|
||||
<div
|
||||
className={classNames({
|
||||
'form-section-collapsed': collapsed && !hasErrors
|
||||
})}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
FormGroupLayout.propTypes = {
|
||||
hasErrors: PropTypes.bool,
|
||||
collapsed: PropTypes.bool,
|
||||
heading: PropTypes.node,
|
||||
children: PropTypes.node
|
||||
};
|
||||
registerComponent({ name: 'FormGroupLayout', component: FormGroupLayout });
|
||||
|
||||
class FormGroup extends PureComponent {
|
||||
constructor(props) {
|
||||
|
@ -10,59 +50,63 @@ class FormGroup extends PureComponent {
|
|||
this.toggle = this.toggle.bind(this);
|
||||
this.renderHeading = this.renderHeading.bind(this);
|
||||
this.state = {
|
||||
collapsed: props.startCollapsed || false,
|
||||
collapsed: props.startCollapsed || false
|
||||
};
|
||||
}
|
||||
|
||||
toggle() {
|
||||
this.setState({
|
||||
collapsed: !this.state.collapsed,
|
||||
collapsed: !this.state.collapsed
|
||||
});
|
||||
}
|
||||
|
||||
renderHeading() {
|
||||
renderHeading(FormComponents) {
|
||||
return (
|
||||
<div className="form-section-heading" onClick={this.toggle}>
|
||||
<h3 className="form-section-heading-title">{this.props.label}</h3>
|
||||
<span className="form-section-heading-toggle">
|
||||
{this.state.collapsed ? <Components.IconRight height={16} width={16} /> : <Components.IconDown height={16} width={16} />}
|
||||
</span>
|
||||
</div>
|
||||
<FormComponents.FormGroupHeader
|
||||
toggle={this.toggle}
|
||||
label={this.props.label}
|
||||
collapsed={this.state.collapsed}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
// if at least one of the fields in the group has an error, the group as a whole has an error
|
||||
hasErrors = () => _.some(this.props.fields, field => {
|
||||
return !!this.props.errors.filter(error => error.path === field.path).length
|
||||
});
|
||||
hasErrors = () =>
|
||||
_.some(this.props.fields, field => {
|
||||
return !!this.props.errors.filter(error => error.path === field.path)
|
||||
.length;
|
||||
});
|
||||
|
||||
render() {
|
||||
|
||||
const FormComponents = this.props.formComponents;
|
||||
const { name, fields, formComponents } = this.props;
|
||||
const { collapsed } = this.state;
|
||||
const FormComponents = mergeWithComponents(formComponents);
|
||||
|
||||
return (
|
||||
<div className="form-section">
|
||||
{this.props.name === 'default' ? null : this.renderHeading()}
|
||||
<div className={classNames({ 'form-section-collapsed': this.state.collapsed && !this.hasErrors() })}>
|
||||
{this.props.fields.map(field => (
|
||||
<FormComponents.FormComponent
|
||||
key={field.name}
|
||||
disabled={this.props.disabled}
|
||||
{...field}
|
||||
errors={this.props.errors}
|
||||
throwError={this.props.throwError}
|
||||
currentValues={this.props.currentValues}
|
||||
updateCurrentValues={this.props.updateCurrentValues}
|
||||
deletedValues={this.props.deletedValues}
|
||||
addToDeletedValues={this.props.addToDeletedValues}
|
||||
clearFieldErrors={this.props.clearFieldErrors}
|
||||
formType={this.props.formType}
|
||||
currentUser={this.props.currentUser}
|
||||
formComponents={FormComponents}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<FormComponents.FormGroupLayout
|
||||
toggle={this.toggle}
|
||||
collapsed={collapsed}
|
||||
heading={name === 'default' ? null : this.renderHeading(FormComponents)}
|
||||
hasErrors={this.hasErrors()}
|
||||
>
|
||||
{fields.map(field => (
|
||||
<FormComponents.FormComponent
|
||||
key={field.name}
|
||||
disabled={this.props.disabled}
|
||||
{...field}
|
||||
errors={this.props.errors}
|
||||
throwError={this.props.throwError}
|
||||
currentValues={this.props.currentValues}
|
||||
updateCurrentValues={this.props.updateCurrentValues}
|
||||
deletedValues={this.props.deletedValues}
|
||||
addToDeletedValues={this.props.addToDeletedValues}
|
||||
clearFieldErrors={this.props.clearFieldErrors}
|
||||
formType={this.props.formType}
|
||||
currentUser={this.props.currentUser}
|
||||
formComponents={FormComponents}
|
||||
/>
|
||||
))}
|
||||
</FormComponents.FormGroupLayout>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -80,10 +124,10 @@ FormGroup.propTypes = {
|
|||
addToDeletedValues: PropTypes.func.isRequired,
|
||||
clearFieldErrors: PropTypes.func.isRequired,
|
||||
formType: PropTypes.string.isRequired,
|
||||
currentUser: PropTypes.object,
|
||||
currentUser: PropTypes.object
|
||||
};
|
||||
|
||||
module.exports = FormGroup
|
||||
module.exports = FormGroup;
|
||||
|
||||
registerComponent('FormGroup', FormGroup);
|
||||
|
||||
|
|
|
@ -3,9 +3,24 @@ import PropTypes from 'prop-types';
|
|||
import { Components, registerComponent, Locales } from 'meteor/vulcan:core';
|
||||
import omit from 'lodash/omit';
|
||||
import getContext from 'recompose/getContext';
|
||||
import mergeWithComponents from '../modules/mergeWithComponents';
|
||||
|
||||
// replaceable layout
|
||||
const FormIntlLayout = ({ children }) => (
|
||||
<div className="form-intl">{children}</div>
|
||||
);
|
||||
registerComponent({ name: 'FormIntlLayout', component: FormIntlLayout });
|
||||
const FormIntlItemLayout = ({ locale, children }) => (
|
||||
<div className={`form-intl-${locale.id}`} key={locale.id}>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
registerComponent({
|
||||
name: 'FormIntlItemLayout',
|
||||
component: FormIntlItemLayout
|
||||
});
|
||||
|
||||
class FormIntl extends PureComponent {
|
||||
|
||||
/*
|
||||
|
||||
Note: ideally we'd try to make sure to return the right path no matter
|
||||
|
@ -13,29 +28,49 @@ class FormIntl extends PureComponent {
|
|||
so we just use the order of the Locales array.
|
||||
|
||||
*/
|
||||
getLocalePath = (defaultIndex) => {
|
||||
getLocalePath = defaultIndex => {
|
||||
return `${this.props.path}_intl.${defaultIndex}`;
|
||||
}
|
||||
|
||||
render() {
|
||||
};
|
||||
|
||||
const FormComponents = this.props.formComponents;
|
||||
render() {
|
||||
const { name, formComponents } = this.props;
|
||||
const FormComponents = mergeWithComponents(formComponents);
|
||||
|
||||
// do not pass FormIntl's own value, inputProperties, and intlInput props down
|
||||
const properties = omit(this.props, 'value', 'inputProperties', 'intlInput', 'nestedInput');
|
||||
|
||||
const properties = omit(
|
||||
this.props,
|
||||
'value',
|
||||
'inputProperties',
|
||||
'intlInput',
|
||||
'nestedInput'
|
||||
);
|
||||
return (
|
||||
<div className="form-intl">
|
||||
<FormComponents.FormIntlLayout>
|
||||
{Locales.map((locale, i) => (
|
||||
<div className={`form-intl-${locale.id}`} key={locale.id}>
|
||||
<FormComponents.FormComponent {...properties} label={this.props.getLabel(this.props.name, locale.id)} path={this.getLocalePath(i)} locale={locale.id} />
|
||||
</div>
|
||||
<FormComponents.FormIntlItemLayout locale={locale}>
|
||||
<FormComponents.FormComponent
|
||||
{...properties}
|
||||
label={this.props.getLabel(name, locale.id)}
|
||||
path={this.getLocalePath(i)}
|
||||
locale={locale.id}
|
||||
/>
|
||||
</FormComponents.FormIntlItemLayout>
|
||||
))}
|
||||
</div>
|
||||
</FormComponents.FormIntlLayout>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
registerComponent('FormIntl', FormIntl, getContext({
|
||||
getLabel: PropTypes.func,
|
||||
}));
|
||||
FormIntl.propTypes = {
|
||||
name: PropTypes.string.isRequired,
|
||||
path: PropTypes.string.isRequired,
|
||||
formComponents: PropTypes.object
|
||||
};
|
||||
|
||||
registerComponent(
|
||||
'FormIntl',
|
||||
FormIntl,
|
||||
getContext({
|
||||
getLabel: PropTypes.func
|
||||
})
|
||||
);
|
||||
|
|
|
@ -2,6 +2,25 @@ import React, { PureComponent } from 'react';
|
|||
import PropTypes from 'prop-types';
|
||||
import { Components, registerComponent } from 'meteor/vulcan:core';
|
||||
|
||||
// Replaceable layout
|
||||
const FormNestedArrayLayout = ({ hasErrors, label, content }) => (
|
||||
<div
|
||||
className={`form-group row form-nested ${hasErrors ? 'input-error' : ''}`}
|
||||
>
|
||||
<label className="control-label col-sm-3">{label}</label>
|
||||
<div className="col-sm-9">{content}</div>
|
||||
</div>
|
||||
);
|
||||
FormNestedArrayLayout.propTypes = {
|
||||
hasErrors: PropTypes.bool,
|
||||
label: PropTypes.node,
|
||||
content: PropTypes.node
|
||||
};
|
||||
registerComponent({
|
||||
name: 'FormNestedArrayLayout',
|
||||
component: FormNestedArrayLayout
|
||||
});
|
||||
|
||||
class FormNestedArray extends PureComponent {
|
||||
getCurrentValue() {
|
||||
return this.props.value || [];
|
||||
|
@ -39,7 +58,7 @@ class FormNestedArray extends PureComponent {
|
|||
'inputProperties',
|
||||
'nestedInput'
|
||||
);
|
||||
const { errors, path, formComponents } = this.props;
|
||||
const { errors, path, label, formComponents } = this.props;
|
||||
const FormComponents = formComponents;
|
||||
// only keep errors specific to the nested array (and not its subfields)
|
||||
const nestedArrayErrors = errors.filter(
|
||||
|
@ -48,14 +67,10 @@ class FormNestedArray extends PureComponent {
|
|||
const hasErrors = nestedArrayErrors && nestedArrayErrors.length;
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`form-group row form-nested ${
|
||||
hasErrors ? 'input-error' : ''
|
||||
}`}
|
||||
>
|
||||
<label className="control-label col-sm-3">{this.props.label}</label>
|
||||
<div className="col-sm-9">
|
||||
{value.map(
|
||||
<FormComponents.FormNestedArrayLayout
|
||||
label={label}
|
||||
content={[
|
||||
value.map(
|
||||
(subDocument, i) =>
|
||||
!this.isDeleted(i) && (
|
||||
<React.Fragment key={i}>
|
||||
|
@ -73,20 +88,24 @@ class FormNestedArray extends PureComponent {
|
|||
/>
|
||||
</React.Fragment>
|
||||
)
|
||||
)}
|
||||
),
|
||||
<Components.Button
|
||||
key="add-button"
|
||||
size="small"
|
||||
variant="success"
|
||||
onClick={this.addItem}
|
||||
className="form-nested-button"
|
||||
>
|
||||
<Components.IconAdd height={12} width={12} />
|
||||
</Components.Button>
|
||||
{hasErrors ? (
|
||||
<FormComponents.FieldErrors errors={nestedArrayErrors} />
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
</Components.Button>,
|
||||
hasErrors ? (
|
||||
<FormComponents.FieldErrors
|
||||
key="form-nested-errors"
|
||||
errors={nestedArrayErrors}
|
||||
/>
|
||||
) : null
|
||||
]}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,53 +1,84 @@
|
|||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { Components, registerComponent } from 'meteor/vulcan:core';
|
||||
import mergeWithComponents from '../modules/mergeWithComponents';
|
||||
|
||||
const FormNestedItem = ({ nestedFields, name, path, removeItem, itemIndex, ...props }, { errors }) => {
|
||||
const FormComponents = props.formComponents;
|
||||
const FormNestedItemLayout = ({ content, removeButton }) => (
|
||||
<div className="form-nested-item">
|
||||
<div className="form-nested-item-inner">{content}</div>
|
||||
{removeButton && [
|
||||
<div key="remove-button" className="form-nested-item-remove">
|
||||
{removeButton}
|
||||
</div>,
|
||||
<div
|
||||
key="remove-button-overlay"
|
||||
className="form-nested-item-deleted-overlay"
|
||||
/>
|
||||
]}
|
||||
</div>
|
||||
);
|
||||
FormNestedItemLayout.propTypes = {
|
||||
content: PropTypes.node.isRequired,
|
||||
removeButton: PropTypes.node
|
||||
};
|
||||
registerComponent({
|
||||
name: 'FormNestedItemLayout',
|
||||
component: FormNestedItemLayout
|
||||
});
|
||||
|
||||
const FormNestedItem = (
|
||||
{ nestedFields, name, path, removeItem, itemIndex, formComponents, ...props },
|
||||
{ errors }
|
||||
) => {
|
||||
const FormComponents = mergeWithComponents(formComponents);
|
||||
const isArray = typeof itemIndex !== 'undefined';
|
||||
return (
|
||||
<div className="form-nested-item">
|
||||
<div className="form-nested-item-inner">
|
||||
{nestedFields.map((field, i) => {
|
||||
return (
|
||||
<FormComponents.FormComponent
|
||||
key={i}
|
||||
{...props}
|
||||
{...field}
|
||||
path={`${path}.${field.name}`}
|
||||
itemIndex={itemIndex}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
{isArray && [
|
||||
<div key="remove-button" className="form-nested-item-remove">
|
||||
<Components.Button
|
||||
className="form-nested-button"
|
||||
variant="danger"
|
||||
size="small"
|
||||
iconButton
|
||||
tabIndex="-1"
|
||||
onClick={() => {
|
||||
removeItem(name);
|
||||
}}
|
||||
>
|
||||
<Components.IconRemove height={12} width={12} />
|
||||
</Components.Button>
|
||||
</div>,
|
||||
<div key="remove-button-overlay" className="form-nested-item-deleted-overlay" />,
|
||||
]}
|
||||
</div>
|
||||
<FormComponents.FormNestedItemLayout
|
||||
content={nestedFields.map((field, i) => {
|
||||
return (
|
||||
<FormComponents.FormComponent
|
||||
key={i}
|
||||
{...props}
|
||||
{...field}
|
||||
path={`${path}.${field.name}`}
|
||||
itemIndex={itemIndex}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
removeButton={
|
||||
isArray && [
|
||||
<div key="remove-button" className="form-nested-item-remove">
|
||||
<Components.Button
|
||||
className="form-nested-button"
|
||||
variant="danger"
|
||||
size="small"
|
||||
iconButton
|
||||
tabIndex="-1"
|
||||
onClick={() => {
|
||||
removeItem(name);
|
||||
}}
|
||||
>
|
||||
<Components.IconRemove height={12} width={12} />
|
||||
</Components.Button>
|
||||
</div>,
|
||||
<div
|
||||
key="remove-button-overlay"
|
||||
className="form-nested-item-deleted-overlay"
|
||||
/>
|
||||
]
|
||||
}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
FormNestedItem.propTypes = {
|
||||
path: PropTypes.string.isRequired,
|
||||
itemIndex: PropTypes.number,
|
||||
formComponents: PropTypes.object
|
||||
};
|
||||
|
||||
FormNestedItem.contextTypes = {
|
||||
errors: PropTypes.array,
|
||||
errors: PropTypes.array
|
||||
};
|
||||
|
||||
registerComponent('FormNestedItem', FormNestedItem);
|
||||
|
|
|
@ -1,10 +1,30 @@
|
|||
import React, { PureComponent } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { Components, registerComponent } from 'meteor/vulcan:core';
|
||||
import { registerComponent } from 'meteor/vulcan:core';
|
||||
import mergeWithComponents from '../modules/mergeWithComponents';
|
||||
|
||||
// Replaceable layout
|
||||
const FormNestedObjectLayout = ({ hasErrors, label, content }) => (
|
||||
<div
|
||||
className={`form-group row form-nested ${hasErrors ? 'input-error' : ''}`}
|
||||
>
|
||||
<label className="control-label col-sm-3">{label}</label>
|
||||
<div className="col-sm-9">{content}</div>
|
||||
</div>
|
||||
);
|
||||
FormNestedObjectLayout.propTypes = {
|
||||
hasErrors: PropTypes.bool,
|
||||
label: PropTypes.node,
|
||||
content: PropTypes.node
|
||||
};
|
||||
registerComponent({
|
||||
name: 'FormNestedObjectLayout',
|
||||
component: FormNestedObjectLayout
|
||||
});
|
||||
|
||||
class FormNestedObject extends PureComponent {
|
||||
render() {
|
||||
const FormComponents = this.props.formComponents;
|
||||
const FormComponents = mergeWithComponents(this.props.formComponents);
|
||||
//const value = this.getCurrentValue()
|
||||
// do not pass FormNested's own value, input and inputProperties props down
|
||||
const properties = _.omit(
|
||||
|
@ -21,22 +41,23 @@ class FormNestedObject extends PureComponent {
|
|||
);
|
||||
const hasErrors = nestedObjectErrors && nestedObjectErrors.length;
|
||||
return (
|
||||
<div
|
||||
className={`form-group row form-nested ${
|
||||
hasErrors ? 'input-error' : ''
|
||||
}`}
|
||||
>
|
||||
<label className="control-label col-sm-3">{this.props.label}</label>
|
||||
<div className="col-sm-9">
|
||||
<FormComponents.FormNestedObjectLayout
|
||||
hasErros={hasErrors}
|
||||
label={this.props.label}
|
||||
content={[
|
||||
<FormComponents.FormNestedItem
|
||||
key="form-nested-item"
|
||||
{...properties}
|
||||
path={`${this.props.path}`}
|
||||
/>
|
||||
{hasErrors ? (
|
||||
<FormComponents.FieldErrors errors={nestedObjectErrors} />
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
/>,
|
||||
hasErrors ? (
|
||||
<FormComponents.FieldErrors
|
||||
key="form-nested-errors"
|
||||
errors={nestedObjectErrors}
|
||||
/>
|
||||
) : null
|
||||
]}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -45,7 +66,8 @@ FormNestedObject.propTypes = {
|
|||
currentValues: PropTypes.object,
|
||||
path: PropTypes.string,
|
||||
label: PropTypes.string,
|
||||
errors: PropTypes.array.isRequired
|
||||
errors: PropTypes.array.isRequired,
|
||||
formComponents: PropTypes.object
|
||||
};
|
||||
|
||||
module.exports = FormNestedObject;
|
||||
|
|
|
@ -27,7 +27,7 @@ component is also added to wait for withSingle'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 { withRouter } from 'react-router';
|
||||
import { withApollo, compose } from 'react-apollo';
|
||||
import {
|
||||
Components,
|
||||
|
@ -37,29 +37,32 @@ import {
|
|||
withNew,
|
||||
withUpdate,
|
||||
withDelete,
|
||||
getFragment,
|
||||
getCollection,
|
||||
getFragment
|
||||
} from 'meteor/vulcan:core';
|
||||
import gql from 'graphql-tag';
|
||||
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);
|
||||
}
|
||||
|
||||
getCollection() {
|
||||
return this.props.collection || getCollection(this.props.collectionName);
|
||||
}
|
||||
|
||||
// return the current schema based on either the schema or collection prop
|
||||
getSchema() {
|
||||
return this.props.schema ? this.props.schema : this.getCollection().simpleSchema()._schema;
|
||||
return this.props.schema
|
||||
? this.props.schema
|
||||
: this.props.collection.simpleSchema()._schema;
|
||||
}
|
||||
|
||||
// if a document is being passed, this is an edit form
|
||||
|
@ -67,71 +70,53 @@ class FormWrapper extends PureComponent {
|
|||
return this.props.documentId || this.props.slug ? 'edit' : 'new';
|
||||
}
|
||||
|
||||
// filter out fields with "." or "$"
|
||||
getValidFields() {
|
||||
return Object.keys(this.getSchema()).filter(fieldName => !fieldName.includes('$') && !fieldName.includes('.'));
|
||||
}
|
||||
|
||||
getReadableFields() {
|
||||
const schema = this.getSchema();
|
||||
// OpenCRUD backwards compatibility
|
||||
return this.getValidFields().filter(fieldName => schema[fieldName].canRead || schema[fieldName].viewableBy);
|
||||
}
|
||||
|
||||
getCreateableFields() {
|
||||
const schema = this.getSchema();
|
||||
// OpenCRUD backwards compatibility
|
||||
return this.getValidFields().filter(fieldName => schema[fieldName].canCreate || schema[fieldName].insertableBy);
|
||||
}
|
||||
|
||||
getUpdatetableFields() {
|
||||
const schema = this.getSchema();
|
||||
// OpenCRUD backwards compatibility
|
||||
return this.getValidFields().filter(fieldName => schema[fieldName].canUpdate || schema[fieldName].editableBy);
|
||||
}
|
||||
|
||||
// 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.getCollection()._name}${Utils.capitalize(this.getFormType())}`
|
||||
const prefix = `${this.props.collectionName}${Utils.capitalize(
|
||||
this.getFormType()
|
||||
)}`;
|
||||
const fragmentName = `${prefix}FormFragment`;
|
||||
|
||||
const fields = this.props.fields;
|
||||
const readableFields = this.getReadableFields();
|
||||
const createableFields = this.getCreateableFields();
|
||||
const updatetableFields = this.getUpdatetableFields();
|
||||
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)
|
||||
let queryFields = this.getFormType() === 'new' ? createableFields : updatetableFields;
|
||||
let queryFields =
|
||||
this.getFormType() === 'new' ? createableFields : updatetableFields;
|
||||
// for the mutations's return value, also get non-editable but viewable fields (such as createdAt, userId, etc.)
|
||||
let mutationFields = this.getFormType() === 'new' ? _.unique(createableFields.concat(readableFields)) : _.unique(createableFields.concat(updatetableFields));
|
||||
let mutationFields =
|
||||
this.getFormType() === 'new'
|
||||
? _.unique(createableFields.concat(readableFields))
|
||||
: _.unique(createableFields.concat(updatetableFields));
|
||||
|
||||
// if "fields" prop is specified, restrict list of fields to it
|
||||
if (typeof fields !== 'undefined' && fields.length > 0) {
|
||||
queryFields = _.intersection(queryFields, fields);
|
||||
mutationFields = _.intersection(mutationFields, fields);
|
||||
}
|
||||
|
||||
|
||||
const convertFields = field => {
|
||||
return field.slice(-5) === '_intl' ? `${field}{ locale value }`: field;
|
||||
}
|
||||
return field.slice(-5) === '_intl' ? `${field}{ locale value }` : field;
|
||||
};
|
||||
|
||||
// generate query fragment based on the fields that can be edited. Note: always add _id.
|
||||
const generatedQueryFragment = gql`
|
||||
fragment ${fragmentName} on ${this.getCollection().typeName} {
|
||||
fragment ${fragmentName} on ${this.props.typeName} {
|
||||
_id
|
||||
${queryFields.map(convertFields).join('\n')}
|
||||
}
|
||||
`
|
||||
`;
|
||||
|
||||
// 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.getCollection().typeName} {
|
||||
fragment ${fragmentName} on ${this.props.typeName} {
|
||||
_id
|
||||
${mutationFields.map(convertFields).join('\n')}
|
||||
}
|
||||
`
|
||||
`;
|
||||
|
||||
// default to generated fragments
|
||||
let queryFragment = generatedQueryFragment;
|
||||
|
@ -139,10 +124,20 @@ class FormWrapper extends PureComponent {
|
|||
|
||||
// if queryFragment or mutationFragment props are specified, accept either fragment object or fragment string
|
||||
if (this.props.queryFragment) {
|
||||
queryFragment = typeof this.props.queryFragment === 'string' ? gql`${this.props.queryFragment}` : this.props.queryFragment;
|
||||
queryFragment =
|
||||
typeof this.props.queryFragment === 'string'
|
||||
? gql`
|
||||
${this.props.queryFragment}
|
||||
`
|
||||
: this.props.queryFragment;
|
||||
}
|
||||
if (this.props.mutationFragment) {
|
||||
mutationFragment = typeof this.props.mutationFragment === 'string' ? gql`${this.props.mutationFragment}` : this.props.mutationFragment;
|
||||
mutationFragment =
|
||||
typeof this.props.mutationFragment === 'string'
|
||||
? gql`
|
||||
${this.props.mutationFragment}
|
||||
`
|
||||
: this.props.mutationFragment;
|
||||
}
|
||||
|
||||
// same with queryFragmentName and mutationFragmentName
|
||||
|
@ -154,108 +149,119 @@ class FormWrapper extends PureComponent {
|
|||
}
|
||||
|
||||
// if any field specifies extra queries, add them
|
||||
const extraQueries = _.compact(queryFields.map(fieldName => {
|
||||
const field = this.getSchema()[fieldName];
|
||||
return field.query
|
||||
}));
|
||||
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,
|
||||
extraQueries,
|
||||
extraQueries
|
||||
};
|
||||
}
|
||||
|
||||
getComponent() {
|
||||
|
||||
let WrappedComponent;
|
||||
|
||||
const prefix = `${this.getCollection()._name}${Utils.capitalize(this.getFormType())}`
|
||||
const prefix = `${this.props.collectionName}${Utils.capitalize(
|
||||
this.getFormType()
|
||||
)}`;
|
||||
|
||||
const { queryFragment, mutationFragment, extraQueries } = this.getFragments();
|
||||
const {
|
||||
queryFragment,
|
||||
mutationFragment,
|
||||
extraQueries
|
||||
} = this.getFragments();
|
||||
|
||||
// props to pass on to child component (i.e. <Form />)
|
||||
const childProps = {
|
||||
formType: this.getFormType(),
|
||||
schema: this.getSchema(),
|
||||
schema: this.getSchema()
|
||||
};
|
||||
|
||||
// options for withSingle HoC
|
||||
const queryOptions = {
|
||||
queryName: `${prefix}FormQuery`,
|
||||
collection: this.getCollection(),
|
||||
collection: this.props.collection,
|
||||
fragment: queryFragment,
|
||||
extraQueries,
|
||||
fetchPolicy: 'network-only', // we always want to load a fresh copy of the document
|
||||
enableCache: false,
|
||||
pollInterval: 0, // no polling, only load data once
|
||||
pollInterval: 0 // no polling, only load data once
|
||||
};
|
||||
|
||||
// options for withNew, withUpdate, and withDelete HoCs
|
||||
const mutationOptions = {
|
||||
collection: this.getCollection(),
|
||||
fragment: mutationFragment,
|
||||
collection: this.props.collection,
|
||||
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;
|
||||
return loading ?
|
||||
<Components.Loading /> :
|
||||
return loading ? (
|
||||
<Components.Loading />
|
||||
) : (
|
||||
<Components.Form
|
||||
document={document}
|
||||
loading={loading}
|
||||
{...childProps}
|
||||
{...props}
|
||||
/>;
|
||||
/>
|
||||
);
|
||||
};
|
||||
Loader.displayName = 'withLoader(Form)';
|
||||
|
||||
// if this is an edit from, load the necessary data using the withSingle HoC
|
||||
if (this.getFormType() === 'edit') {
|
||||
|
||||
WrappedComponent = compose(
|
||||
withSingle(queryOptions),
|
||||
withUpdate(mutationOptions),
|
||||
withDelete(mutationOptions)
|
||||
)(Loader);
|
||||
|
||||
return <WrappedComponent selector={{ documentId: this.props.documentId, slug: this.props.slug }}/>
|
||||
|
||||
return (
|
||||
<WrappedComponent
|
||||
selector={{
|
||||
documentId: this.props.documentId,
|
||||
slug: this.props.slug
|
||||
}}
|
||||
/>
|
||||
);
|
||||
} else {
|
||||
|
||||
if (extraQueries && extraQueries.length) {
|
||||
|
||||
const extraQueriesHoC = graphql(gql`
|
||||
const extraQueriesHoC = graphql(
|
||||
gql`
|
||||
query formNewExtraQuery {
|
||||
${extraQueries}
|
||||
}`, {
|
||||
}`,
|
||||
{
|
||||
alias: 'withExtraQueries',
|
||||
props: returnedProps => {
|
||||
const { /* ownProps, */ data } = returnedProps;
|
||||
const props = {
|
||||
loading: data.loading,
|
||||
data,
|
||||
data
|
||||
};
|
||||
return props;
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
WrappedComponent = compose(
|
||||
extraQueriesHoC,
|
||||
withNew(mutationOptions)
|
||||
)(Loader);
|
||||
|
||||
} else {
|
||||
WrappedComponent = compose(
|
||||
withNew(mutationOptions)
|
||||
)(Components.Form);
|
||||
WrappedComponent = compose(withNew(mutationOptions))(Components.Form);
|
||||
}
|
||||
|
||||
return <WrappedComponent {...childProps} />;
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -268,15 +274,10 @@ class FormWrapper extends PureComponent {
|
|||
|
||||
FormWrapper.propTypes = {
|
||||
// main options
|
||||
collection: PropTypes.object,
|
||||
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}`);
|
||||
}
|
||||
},
|
||||
collection: PropTypes.object.isRequired,
|
||||
collectionName: PropTypes.string.isRequired,
|
||||
typeName: PropTypes.string.isRequired,
|
||||
|
||||
documentId: PropTypes.string, // if a document is passed, this will be an edit form
|
||||
schema: PropTypes.object, // usually not needed
|
||||
queryFragment: PropTypes.object,
|
||||
|
@ -302,24 +303,23 @@ FormWrapper.propTypes = {
|
|||
warnUnsavedChanges: PropTypes.bool,
|
||||
|
||||
// callbacks
|
||||
submitCallback: PropTypes.func,
|
||||
successCallback: PropTypes.func,
|
||||
removeSuccessCallback: PropTypes.func,
|
||||
errorCallback: PropTypes.func,
|
||||
cancelCallback: PropTypes.func,
|
||||
revertCallback: PropTypes.func,
|
||||
...callbackProps,
|
||||
|
||||
currentUser: PropTypes.object,
|
||||
client: PropTypes.object,
|
||||
}
|
||||
client: PropTypes.object
|
||||
};
|
||||
|
||||
FormWrapper.defaultProps = {
|
||||
layout: 'horizontal',
|
||||
}
|
||||
layout: 'horizontal'
|
||||
};
|
||||
|
||||
FormWrapper.contextTypes = {
|
||||
closeCallback: PropTypes.func,
|
||||
intl: intlShape
|
||||
}
|
||||
};
|
||||
|
||||
registerComponent('SmartForm', FormWrapper, withCurrentUser, withApollo, withRouter);
|
||||
registerComponent({
|
||||
name: 'SmartForm',
|
||||
component: FormWrapper,
|
||||
hocs: [withCurrentUser, withApollo, withRouter, withCollectionProps]
|
||||
});
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
/** PropTypes for documentation purpose (not tested yet) */
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
const fieldProps = {
|
||||
//
|
||||
defaultValue: PropTypes.any,
|
||||
|
@ -20,12 +21,12 @@ const fieldProps = {
|
|||
// if it has an array field
|
||||
// e.g addresses.$ : { type: .... }
|
||||
arrayFieldSchema: PropTypes.object,
|
||||
arrayField: fieldProps,
|
||||
arrayField: PropTypes.object, //fieldProps,
|
||||
// if it is a nested object itself
|
||||
// eg address : { type : { ... }}
|
||||
nestedSchema: PropTypes.object,
|
||||
nestedInput: PropTypes.boolean, // flag
|
||||
nestedFields: PropTypes.arrayOf(fieldProps)
|
||||
nestedFields: PropTypes.array //arrayOf(fieldProps)
|
||||
};
|
||||
const groupProps = {
|
||||
name: PropTypes.string.isRequired,
|
||||
|
@ -33,3 +34,13 @@ const groupProps = {
|
|||
order: PropTypes.number,
|
||||
fields: PropTypes.arrayOf(PropTypes.shape(fieldProps))
|
||||
};
|
||||
|
||||
export const callbackProps = {
|
||||
changeCallback: PropTypes.func,
|
||||
submitCallback: PropTypes.func,
|
||||
successCallback: PropTypes.func,
|
||||
removeSuccessCallback: PropTypes.func,
|
||||
errorCallback: PropTypes.func,
|
||||
cancelCallback: PropTypes.func,
|
||||
revertCallback: PropTypes.func
|
||||
};
|
||||
|
|
31
packages/vulcan-forms/lib/components/withCollectionProps.js
Normal file
31
packages/vulcan-forms/lib/components/withCollectionProps.js
Normal file
|
@ -0,0 +1,31 @@
|
|||
import React from 'react';
|
||||
import { extractCollectionInfo } from 'meteor/vulcan:lib';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
/**
|
||||
* Handle the collection or collectionName and pass down other related
|
||||
* props (typeName, collectionName, etc.)
|
||||
*/
|
||||
const withCollectionProps = C => {
|
||||
const CollectionPropsWrapper = ({ collection: _collection, collectionName: _collectionName, ...otherProps }) => {
|
||||
const { collection, collectionName } = extractCollectionInfo({
|
||||
collection: _collection,
|
||||
collectionName: _collectionName
|
||||
});
|
||||
const typeName = collection.options.typeName;
|
||||
return <C {...otherProps} collection={collection} collectionName={collectionName} typeName={typeName} />;
|
||||
};
|
||||
CollectionPropsWrapper.propTypes = {
|
||||
collection: PropTypes.object,
|
||||
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}`);
|
||||
}
|
||||
}
|
||||
};
|
||||
return CollectionPropsWrapper;
|
||||
};
|
||||
export default withCollectionProps;
|
19
packages/vulcan-forms/lib/modules/mergeWithComponents.js
Normal file
19
packages/vulcan-forms/lib/modules/mergeWithComponents.js
Normal file
|
@ -0,0 +1,19 @@
|
|||
/**
|
||||
* Data structure to mix global Components and local FormComponents
|
||||
* without the need to merge
|
||||
*/
|
||||
import { Components } from 'meteor/vulcan:core';
|
||||
|
||||
// Example with Proxy (might be unstable/hard to reason about)
|
||||
//const mergeWithComponents = (myComponents = {}) => {
|
||||
// const handler = {
|
||||
// get: function(target, name) {
|
||||
// return name in target ? target[name] : Components[name];
|
||||
// }
|
||||
// };
|
||||
// const proxy = new Proxy(myComponents, handler);
|
||||
// return proxy;
|
||||
//};
|
||||
const mergeWithComponents = myComponents => (myComponents ? { ...Components, ...myComponents } : Components);
|
||||
|
||||
export default mergeWithComponents;
|
|
@ -1,3 +1,58 @@
|
|||
/*
|
||||
* Schema converter/getters
|
||||
*/
|
||||
import Users from 'meteor/vulcan:users';
|
||||
import _ from 'lodash';
|
||||
|
||||
/* getters */
|
||||
// filter out fields with "." or "$"
|
||||
export const getValidFields = schema => {
|
||||
return Object.keys(schema).filter(fieldName => !fieldName.includes('$') && !fieldName.includes('.'));
|
||||
};
|
||||
|
||||
export const getReadableFields = schema => {
|
||||
// OpenCRUD backwards compatibility
|
||||
return getValidFields(schema).filter(fieldName => schema[fieldName].canRead || schema[fieldName].viewableBy);
|
||||
};
|
||||
|
||||
export const getCreateableFields = schema => {
|
||||
// OpenCRUD backwards compatibility
|
||||
return getValidFields(schema).filter(fieldName => schema[fieldName].canCreate || schema[fieldName].insertableBy);
|
||||
};
|
||||
|
||||
export const getUpdateableFields = schema => {
|
||||
// OpenCRUD backwards compatibility
|
||||
return getValidFields(schema).filter(fieldName => schema[fieldName].canUpdate || schema[fieldName].editableBy);
|
||||
};
|
||||
|
||||
/* permissions */
|
||||
|
||||
/**
|
||||
* @method Mongo.Collection.getInsertableFields
|
||||
* Get an array of all fields editable by a specific user for a given collection
|
||||
* @param {Object} user – the user for which to check field permissions
|
||||
*/
|
||||
export const getInsertableFields = function(schema, user) {
|
||||
const fields = _.filter(_.keys(schema), function(fieldName) {
|
||||
var field = schema[fieldName];
|
||||
return Users.canCreateField(user, field);
|
||||
});
|
||||
return fields;
|
||||
};
|
||||
|
||||
/**
|
||||
* @method Mongo.Collection.getEditableFields
|
||||
* Get an array of all fields editable by a specific user for a given collection (and optionally document)
|
||||
* @param {Object} user – the user for which to check field permissions
|
||||
*/
|
||||
export const getEditableFields = function(schema, user, document) {
|
||||
const fields = _.filter(_.keys(schema), function(fieldName) {
|
||||
var field = schema[fieldName];
|
||||
return Users.canUpdateField(user, field, document);
|
||||
});
|
||||
return fields;
|
||||
};
|
||||
|
||||
/*
|
||||
|
||||
Convert a nested SimpleSchema schema into a JSON object
|
||||
|
@ -27,7 +82,7 @@ export const convertSchema = (schema, flatten = false) => {
|
|||
// or a schema on its own with subfields (convertedSchema will return smth)
|
||||
if (!convertedSubSchema) {
|
||||
// subSchema is a simple field in this case (eg array of numbers)
|
||||
jsonSchema[fieldName].field = getFieldSchema(`${fieldName}.$`, schema)
|
||||
jsonSchema[fieldName].field = getFieldSchema(`${fieldName}.$`, schema);
|
||||
} else {
|
||||
// subSchema is a full schema with multiple fields (eg array of objects)
|
||||
if (flatten) {
|
||||
|
@ -35,7 +90,6 @@ export const convertSchema = (schema, flatten = false) => {
|
|||
} else {
|
||||
jsonSchema[fieldName].schema = convertedSubSchema;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
});
|
||||
|
@ -61,44 +115,43 @@ export const getFieldSchema = (fieldName, schema) => {
|
|||
return fieldSchema;
|
||||
};
|
||||
|
||||
|
||||
// type is an array due to the possibility of using SimpleSchema.oneOf
|
||||
// right now we support only fields with one type
|
||||
export const getSchemaType = schema => schema.type.definitions[0].type
|
||||
export const getSchemaType = schema => schema.type.definitions[0].type;
|
||||
|
||||
const getArrayNestedSchema = (fieldName, schema) => {
|
||||
const arrayItemSchema = schema._schema[`${fieldName}.$`];
|
||||
const nestedSchema = arrayItemSchema && getSchemaType(arrayItemSchema)
|
||||
return nestedSchema
|
||||
}
|
||||
const nestedSchema = arrayItemSchema && getSchemaType(arrayItemSchema);
|
||||
return nestedSchema;
|
||||
};
|
||||
// nested object fields type is of the form "type: new SimpleSchema({...})"
|
||||
// so they should possess a "_schema" prop
|
||||
const isNestedSchemaField = (fieldSchema) => {
|
||||
const fieldType = getSchemaType(fieldSchema)
|
||||
const isNestedSchemaField = fieldSchema => {
|
||||
const fieldType = getSchemaType(fieldSchema);
|
||||
//console.log('fieldType', typeof fieldType, fieldType._schema)
|
||||
return fieldType && !!fieldType._schema
|
||||
}
|
||||
return fieldType && !!fieldType._schema;
|
||||
};
|
||||
const getObjectNestedSchema = (fieldName, schema) => {
|
||||
const fieldSchema = schema._schema[fieldName]
|
||||
if (!isNestedSchemaField(fieldSchema)) return null
|
||||
const nestedSchema = fieldSchema && getSchemaType(fieldSchema)
|
||||
return nestedSchema
|
||||
}
|
||||
const fieldSchema = schema._schema[fieldName];
|
||||
if (!isNestedSchemaField(fieldSchema)) return null;
|
||||
const nestedSchema = fieldSchema && getSchemaType(fieldSchema);
|
||||
return nestedSchema;
|
||||
};
|
||||
/*
|
||||
|
||||
Given an array field, get its nested schema
|
||||
If the field is not an object, this will return the subfield type instead
|
||||
*/
|
||||
export const getNestedFieldSchemaOrType = (fieldName, schema) => {
|
||||
const arrayItemSchema = getArrayNestedSchema(fieldName, schema)
|
||||
const arrayItemSchema = getArrayNestedSchema(fieldName, schema);
|
||||
if (!arrayItemSchema) {
|
||||
// look for an object schema
|
||||
const objectItemSchema = getObjectNestedSchema(fieldName, schema)
|
||||
const objectItemSchema = getObjectNestedSchema(fieldName, schema);
|
||||
// no schema was found
|
||||
if (!objectItemSchema) return null
|
||||
return objectItemSchema
|
||||
if (!objectItemSchema) return null;
|
||||
return objectItemSchema;
|
||||
}
|
||||
return arrayItemSchema
|
||||
return arrayItemSchema;
|
||||
};
|
||||
|
||||
export const schemaProperties = [
|
||||
|
@ -147,7 +200,7 @@ export const schemaProperties = [
|
|||
'options',
|
||||
'query',
|
||||
'fieldProperties',
|
||||
'intl',
|
||||
'intl'
|
||||
];
|
||||
|
||||
export const formProperties = [
|
||||
|
@ -179,5 +232,5 @@ export const formProperties = [
|
|||
'placeholder',
|
||||
'options',
|
||||
'query',
|
||||
'fieldProperties',
|
||||
'fieldProperties'
|
||||
];
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
import Users from 'meteor/vulcan:users';
|
||||
import merge from 'lodash/merge';
|
||||
import find from 'lodash/find';
|
||||
import isPlainObject from 'lodash/isPlainObject';
|
||||
|
@ -8,10 +7,10 @@ import size from 'lodash/size';
|
|||
import { removePrefix, filterPathsByPrefix } from './path_utils';
|
||||
|
||||
// add support for nested properties
|
||||
export const deepValue = function(obj, path){
|
||||
export const deepValue = function(obj, path) {
|
||||
const pathArray = path.split('.');
|
||||
|
||||
for (var i=0; i < pathArray.length; i++) {
|
||||
for (var i = 0; i < pathArray.length; i++) {
|
||||
obj = obj[pathArray[i]];
|
||||
}
|
||||
|
||||
|
@ -21,56 +20,27 @@ export const deepValue = function(obj, path){
|
|||
// see http://stackoverflow.com/questions/19098797/fastest-way-to-flatten-un-flatten-nested-json-objects
|
||||
export const flatten = function(data) {
|
||||
var result = {};
|
||||
function recurse (cur, prop) {
|
||||
|
||||
function recurse(cur, prop) {
|
||||
if (Object.prototype.toString.call(cur) !== '[object Object]') {
|
||||
result[prop] = cur;
|
||||
} else if (Array.isArray(cur)) {
|
||||
for(var i=0, l=cur.length; i<l; i++)
|
||||
recurse(cur[i], prop + '[' + i + ']');
|
||||
if (l == 0)
|
||||
result[prop] = [];
|
||||
for (var i = 0, l = cur.length; i < l; i++) recurse(cur[i], prop + '[' + i + ']');
|
||||
if (l == 0) result[prop] = [];
|
||||
} else {
|
||||
var isEmpty = true;
|
||||
for (var p in cur) {
|
||||
isEmpty = false;
|
||||
recurse(cur[p], prop ? prop+'.'+p : p);
|
||||
recurse(cur[p], prop ? prop + '.' + p : p);
|
||||
}
|
||||
if (isEmpty && prop)
|
||||
result[prop] = {};
|
||||
if (isEmpty && prop) result[prop] = {};
|
||||
}
|
||||
}
|
||||
recurse(data, '');
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* @method Mongo.Collection.getInsertableFields
|
||||
* Get an array of all fields editable by a specific user for a given collection
|
||||
* @param {Object} user – the user for which to check field permissions
|
||||
*/
|
||||
export const getInsertableFields = function (schema, user) {
|
||||
const fields = _.filter(_.keys(schema), function (fieldName) {
|
||||
var field = schema[fieldName];
|
||||
return Users.canCreateField(user, field);
|
||||
});
|
||||
return fields;
|
||||
};
|
||||
|
||||
/**
|
||||
* @method Mongo.Collection.getEditableFields
|
||||
* Get an array of all fields editable by a specific user for a given collection (and optionally document)
|
||||
* @param {Object} user – the user for which to check field permissions
|
||||
*/
|
||||
export const getEditableFields = function (schema, user, document) {
|
||||
const fields = _.filter(_.keys(schema), function (fieldName) {
|
||||
var field = schema[fieldName];
|
||||
return Users.canUpdateField(user, field, document);
|
||||
});
|
||||
return fields;
|
||||
};
|
||||
|
||||
export const isEmptyValue = value => (typeof value === 'undefined' || value === null || value === '' || Array.isArray(value) && value.length === 0);
|
||||
export const isEmptyValue = value =>
|
||||
typeof value === 'undefined' || value === null || value === '' || (Array.isArray(value) && value.length === 0);
|
||||
|
||||
/**
|
||||
* Merges values. It takes into account the current, original and deleted values,
|
||||
|
@ -85,14 +55,7 @@ export const isEmptyValue = value => (typeof value === 'undefined' || value ===
|
|||
* @return {*|undefined}
|
||||
* Merged value or undefined if no merge was performed
|
||||
*/
|
||||
export const mergeValue = ({
|
||||
currentValue,
|
||||
documentValue,
|
||||
deletedValues: deletedFields,
|
||||
path,
|
||||
locale,
|
||||
datatype,
|
||||
}) => {
|
||||
export const mergeValue = ({ currentValue, documentValue, deletedValues: deletedFields, path, locale, datatype }) => {
|
||||
if (locale) {
|
||||
// note: intl fields are of type Object but should be treated as Strings
|
||||
return currentValue || documentValue || '';
|
||||
|
@ -132,10 +95,7 @@ export const mergeValue = ({
|
|||
* // => { 'field': { 'subField': null, 'subFieldArray': [null] }, 'fieldArray': [null, undefined, { name: null } }
|
||||
*/
|
||||
export const getDeletedValues = (deletedFields, accumulator = {}) =>
|
||||
deletedFields.reduce(
|
||||
(deletedValues, path) => set(deletedValues, path, null),
|
||||
accumulator,
|
||||
);
|
||||
deletedFields.reduce((deletedValues, path) => set(deletedValues, path, null), accumulator);
|
||||
|
||||
/**
|
||||
* Filters the given field names by prefix, removes it from each one of them
|
||||
|
@ -164,16 +124,13 @@ export const getDeletedValues = (deletedFields, accumulator = {}) =>
|
|||
* // => [null, undefined, { 'name': null } ]
|
||||
*/
|
||||
export const getNestedDeletedValues = (prefix, deletedFields, accumulator = {}) =>
|
||||
getDeletedValues(
|
||||
removePrefix(prefix, filterPathsByPrefix(prefix, deletedFields)),
|
||||
accumulator,
|
||||
);
|
||||
getDeletedValues(removePrefix(prefix, filterPathsByPrefix(prefix, deletedFields)), accumulator);
|
||||
|
||||
export const getFieldType = datatype => datatype[0].type;
|
||||
/**
|
||||
* Get appropriate null value for various field types
|
||||
*
|
||||
* @param {Array} datatype
|
||||
*
|
||||
* @param {Array} datatype
|
||||
* Field's datatype property
|
||||
*/
|
||||
export const getNullValue = datatype => {
|
||||
|
@ -190,4 +147,4 @@ export const getNullValue = datatype => {
|
|||
// normalize to null
|
||||
return null;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
|
|
@ -172,24 +172,24 @@ describe('vulcan-forms/components', function() {
|
|||
context
|
||||
});
|
||||
|
||||
describe('Form (handle fields computation)', function() {
|
||||
describe('Form collectionName="" (handle fields computation)', function() {
|
||||
// getters
|
||||
const getArrayFormGroup = wrapper => wrapper.find('FormGroup').find({ name: 'addresses' });
|
||||
const getFields = arrayFormGroup => arrayFormGroup.prop('fields');
|
||||
|
||||
describe('basic collection - no nesting', function() {
|
||||
it('shallow render', function() {
|
||||
const wrapper = shallowWithContext(<Form collection={Addresses} />);
|
||||
const wrapper = shallowWithContext(<Form collectionName="" collection={Addresses} />);
|
||||
expect(wrapper).toBeDefined();
|
||||
});
|
||||
});
|
||||
describe('nested object (not in array)', function() {
|
||||
it('shallow render', () => {
|
||||
const wrapper = shallowWithContext(<Form collection={Objects} />);
|
||||
const wrapper = shallowWithContext(<Form collectionName="" collection={Objects} />);
|
||||
expect(wrapper).toBeDefined();
|
||||
});
|
||||
it('define one field', () => {
|
||||
const wrapper = shallowWithContext(<Form collection={Objects} />);
|
||||
const wrapper = shallowWithContext(<Form collectionName="" collection={Objects} />);
|
||||
const defaultGroup = wrapper.find('FormGroup').first();
|
||||
const fields = defaultGroup.prop('fields');
|
||||
expect(fields).toHaveLength(1); // addresses field
|
||||
|
@ -201,7 +201,7 @@ describe('vulcan-forms/components', function() {
|
|||
return fields;
|
||||
};
|
||||
const getFirstField = () => {
|
||||
const wrapper = shallowWithContext(<Form collection={Objects} />);
|
||||
const wrapper = shallowWithContext(<Form collectionName="" collection={Objects} />);
|
||||
const fields = getFormFields(wrapper);
|
||||
return fields[0];
|
||||
};
|
||||
|
@ -212,17 +212,17 @@ describe('vulcan-forms/components', function() {
|
|||
});
|
||||
describe('array of objects', function() {
|
||||
it('shallow render', () => {
|
||||
const wrapper = shallowWithContext(<Form collection={ArrayOfObjects} />);
|
||||
const wrapper = shallowWithContext(<Form collectionName="" collection={ArrayOfObjects} />);
|
||||
expect(wrapper).toBeDefined();
|
||||
});
|
||||
it('render a FormGroup for addresses', function() {
|
||||
const wrapper = shallowWithContext(<Form collection={ArrayOfObjects} />);
|
||||
const wrapper = shallowWithContext(<Form collectionName="" collection={ArrayOfObjects} />);
|
||||
const formGroup = wrapper.find('FormGroup').find({ name: 'addresses' });
|
||||
expect(formGroup).toBeDefined();
|
||||
expect(formGroup).toHaveLength(1);
|
||||
});
|
||||
it('passes down the array child fields', function() {
|
||||
const wrapper = shallowWithContext(<Form collection={ArrayOfObjects} />);
|
||||
const wrapper = shallowWithContext(<Form collectionName="" collection={ArrayOfObjects} />);
|
||||
const formGroup = getArrayFormGroup(wrapper);
|
||||
const fields = getFields(formGroup);
|
||||
const arrayField = fields[0];
|
||||
|
@ -232,11 +232,11 @@ describe('vulcan-forms/components', function() {
|
|||
});
|
||||
describe('array with custom children inputs (e.g array of url)', function() {
|
||||
it('shallow render', function() {
|
||||
const wrapper = shallowWithContext(<Form collection={ArrayOfUrls} />);
|
||||
const wrapper = shallowWithContext(<Form collectionName="" collection={ArrayOfUrls} />);
|
||||
expect(wrapper).toBeDefined();
|
||||
});
|
||||
it('passes down the array item custom input', () => {
|
||||
const wrapper = shallowWithContext(<Form collection={ArrayOfUrls} />);
|
||||
const wrapper = shallowWithContext(<Form collectionName="" collection={ArrayOfUrls} />);
|
||||
const formGroup = getArrayFormGroup(wrapper);
|
||||
const fields = getFields(formGroup);
|
||||
const arrayField = fields[0];
|
||||
|
@ -245,12 +245,12 @@ describe('vulcan-forms/components', function() {
|
|||
});
|
||||
describe('array of objects with custom children inputs', function() {
|
||||
it('shallow render', function() {
|
||||
const wrapper = shallowWithContext(<Form collection={ArrayOfCustomObjects} />);
|
||||
const wrapper = shallowWithContext(<Form collectionName="" collection={ArrayOfCustomObjects} />);
|
||||
expect(wrapper).toBeDefined();
|
||||
});
|
||||
// TODO: does not work, schema_utils needs an update
|
||||
it.skip('passes down the custom input', function() {
|
||||
const wrapper = shallowWithContext(<Form collection={ArrayOfCustomObjects} />);
|
||||
const wrapper = shallowWithContext(<Form collectionName="" collection={ArrayOfCustomObjects} />);
|
||||
const formGroup = getArrayFormGroup(wrapper);
|
||||
const fields = getFields(formGroup);
|
||||
const arrayField = fields[0];
|
||||
|
@ -259,11 +259,11 @@ describe('vulcan-forms/components', function() {
|
|||
});
|
||||
describe('array with a fully custom input (array itself and children)', function() {
|
||||
it('shallow render', function() {
|
||||
const wrapper = shallowWithContext(<Form collection={ArrayFullCustom} />);
|
||||
const wrapper = shallowWithContext(<Form collectionName="" collection={ArrayFullCustom} />);
|
||||
expect(wrapper).toBeDefined();
|
||||
});
|
||||
it('passes down the custom input', function() {
|
||||
const wrapper = shallowWithContext(<Form collection={ArrayFullCustom} />);
|
||||
const wrapper = shallowWithContext(<Form collectionName="" collection={ArrayFullCustom} />);
|
||||
const formGroup = getArrayFormGroup(wrapper);
|
||||
const fields = getFields(formGroup);
|
||||
const arrayField = fields[0];
|
||||
|
@ -358,18 +358,19 @@ describe('vulcan-forms/components', function() {
|
|||
const wrapper = shallow(<Components.FormNestedArray {...defaultProps} currentValues={{}} />);
|
||||
expect(wrapper).toBeDefined();
|
||||
});
|
||||
it('shows an add button when empty', function() {
|
||||
const wrapper = shallow(<Components.FormNestedArray {...defaultProps} currentValues={{}} />);
|
||||
// TODO: broken now we use a layout...
|
||||
it.skip('shows an add button when empty', function() {
|
||||
const wrapper = mount(<Components.FormNestedArray {...defaultProps} currentValues={{}} />);
|
||||
const addButton = wrapper.find('IconAdd');
|
||||
expect(addButton).toHaveLength(1);
|
||||
});
|
||||
it('shows 3 items', function() {
|
||||
const wrapper = shallow(<Components.FormNestedArray {...defaultProps} currentValues={{}} value={[1, 2, 3]} />);
|
||||
it.skip('shows 3 items', function() {
|
||||
const wrapper = mount(<Components.FormNestedArray {...defaultProps} currentValues={{}} value={[1, 2, 3]} />);
|
||||
const nestedItem = wrapper.find('FormNestedItem');
|
||||
expect(nestedItem).toHaveLength(3);
|
||||
});
|
||||
it('pass the correct path and itemIndex to each form', function() {
|
||||
const wrapper = shallow(<Components.FormNestedArray {...defaultProps} currentValues={{}} value={[1, 2]} />);
|
||||
it.skip('pass the correct path and itemIndex to each form', function() {
|
||||
const wrapper = mount(<Components.FormNestedArray {...defaultProps} currentValues={{}} value={[1, 2]} />);
|
||||
const nestedItem = wrapper.find('FormNestedItem');
|
||||
const item0 = nestedItem.at(0);
|
||||
const item1 = nestedItem.at(1);
|
||||
|
@ -389,7 +390,7 @@ describe('vulcan-forms/components', function() {
|
|||
const wrapper = shallow(<Components.FormNestedObject {...defaultProps} currentValues={{}} />);
|
||||
expect(wrapper).toBeDefined();
|
||||
});
|
||||
it.skip('render a form for the object', function() {
|
||||
it.skip('render a Form collectionName="" for the object', function() {
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
const wrapper = shallow(<Components.FormNestedObject {...defaultProps} currentValues={{}} />);
|
||||
expect(false).toBe(true);
|
||||
|
|
|
@ -1,2 +1,3 @@
|
|||
import './schema_utils.test.js'
|
||||
import './components.test.js'
|
||||
import './schema_utils.test.js';
|
||||
import './components.test.js';
|
||||
import './mergeWithComponents.test.js';
|
||||
|
|
26
packages/vulcan-forms/test/mergeWithComponents.test.js
Normal file
26
packages/vulcan-forms/test/mergeWithComponents.test.js
Normal file
|
@ -0,0 +1,26 @@
|
|||
import mergeWithComponents from '../lib/modules/mergeWithComponents';
|
||||
import { Components } from 'meteor/vulcan:core';
|
||||
import expect from 'expect';
|
||||
// we must import all the other components, so that "registerComponent" is called
|
||||
import '../lib/modules/components';
|
||||
// and then load them in the app so that <Component.Whatever /> is defined
|
||||
import { populateComponentsApp, initializeFragments } from 'meteor/vulcan:lib';
|
||||
// we need registered fragments to be initialized because populateComponentsApp will run
|
||||
// hocs, like withUpdate, that rely on fragments
|
||||
initializeFragments();
|
||||
// actually fills the Components object
|
||||
populateComponentsApp();
|
||||
|
||||
describe('vulcan-forms/mergeWithComponents', function() {
|
||||
const TestComponent = () => {};
|
||||
const OverrideTestComponent = () => {};
|
||||
Components.TestComponent = TestComponent;
|
||||
const MyComponents = { TestComponent: OverrideTestComponent };
|
||||
it('override existing components', function() {
|
||||
const MergedComponents = mergeWithComponents(MyComponents);
|
||||
expect(MergedComponents.TestComponent).toEqual(OverrideTestComponent);
|
||||
});
|
||||
it('return \'Components\' if no components are provided', function() {
|
||||
expect(mergeWithComponents()).toEqual(Components);
|
||||
});
|
||||
});
|
|
@ -1,4 +1,10 @@
|
|||
import { getNestedFieldSchemaOrType } from '../lib/modules/schema_utils.js';
|
||||
import {
|
||||
getNestedFieldSchemaOrType,
|
||||
getValidFields,
|
||||
getCreateableFields,
|
||||
getReadableFields,
|
||||
getUpdateableFields
|
||||
} from '../lib/modules/schema_utils.js';
|
||||
import SimpleSchema from 'simpl-schema';
|
||||
import expect from 'expect';
|
||||
|
||||
|
@ -48,4 +54,44 @@ describe('schema_utils', function() {
|
|||
expect(nestedSchema).toBeNull();
|
||||
});
|
||||
});
|
||||
describe('fields extraction', function() {
|
||||
describe('valid', function() {
|
||||
it('remove invalid fields', function() {
|
||||
const schema = {
|
||||
validField: {},
|
||||
arrayField: {},
|
||||
// array child
|
||||
'arrayField.$': {}
|
||||
};
|
||||
expect(getValidFields(schema)).toEqual(['validField', 'arrayField']);
|
||||
});
|
||||
});
|
||||
describe('readable', function() {
|
||||
it('get readable field', function() {
|
||||
const schema = {
|
||||
readable: { canRead: [] },
|
||||
notReadble: {}
|
||||
};
|
||||
expect(getReadableFields(schema)).toEqual(['readable']);
|
||||
});
|
||||
});
|
||||
describe('creatable', function() {
|
||||
it('get creatable field', function() {
|
||||
const schema = {
|
||||
creatable: { canCreate: [] },
|
||||
notCreatable: {}
|
||||
};
|
||||
expect(getCreateableFields(schema)).toEqual(['creatable']);
|
||||
});
|
||||
});
|
||||
describe('updatable', function() {
|
||||
it('get updatable field', function() {
|
||||
const schema = {
|
||||
updatable: { canUpdate: [] },
|
||||
notUpdatable: {}
|
||||
};
|
||||
expect(getUpdateableFields(schema)).toEqual(['updatable']);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -1,4 +1,10 @@
|
|||
import { getFragment, getCollection, getFragmentName } from 'meteor/vulcan:core';
|
||||
/** Helpers to get values depending on name
|
||||
* E.g. retrieving a collection and its name when only one value is provided
|
||||
*
|
||||
*/
|
||||
|
||||
import { getCollection } from './collections';
|
||||
import { getFragment, getFragmentName } from './fragments';
|
||||
/**
|
||||
* Extract collectionName from collection
|
||||
* or collection from collectionName
|
|
@ -30,4 +30,5 @@ export * from './intl.js';
|
|||
export * from './detect_locale.js';
|
||||
export * from './graphql_templates.js';
|
||||
export * from './validation.js';
|
||||
export * from './handleOptions';
|
||||
// export * from './resolvers.js';
|
||||
|
|
|
@ -2,7 +2,7 @@ Package.describe({
|
|||
name: 'vulcan:lib',
|
||||
summary: 'Vulcan libraries.',
|
||||
version: '1.12.8',
|
||||
git: 'https://github.com/VulcanJS/Vulcan.git',
|
||||
git: 'https://github.com/VulcanJS/Vulcan.git'
|
||||
});
|
||||
|
||||
Package.onUse(function(api) {
|
||||
|
@ -46,7 +46,7 @@ Package.onUse(function(api) {
|
|||
// 'aldeed:collection2-core@2.0.0',
|
||||
'meteorhacks:picker@1.0.3',
|
||||
'percolatestudio:synced-cron@1.1.0',
|
||||
'meteorhacks:inject-initial@1.0.4',
|
||||
'meteorhacks:inject-initial@1.0.4'
|
||||
];
|
||||
|
||||
api.use(packages);
|
||||
|
@ -58,3 +58,8 @@ Package.onUse(function(api) {
|
|||
api.mainModule('lib/server/main.js', 'server');
|
||||
api.mainModule('lib/client/main.js', 'client');
|
||||
});
|
||||
|
||||
Package.onTest(function(api) {
|
||||
api.use(['ecmascript', 'meteortesting:mocha']);
|
||||
api.mainModule('./test/index.js');
|
||||
});
|
||||
|
|
44
packages/vulcan-lib/test/handleOptions.test.js
Normal file
44
packages/vulcan-lib/test/handleOptions.test.js
Normal file
|
@ -0,0 +1,44 @@
|
|||
import { extractCollectionInfo, extractFragmentInfo } from '../lib/modules/containers/handleOptions';
|
||||
import expect from 'expect';
|
||||
|
||||
describe('vulcan:lib/handleOptions', function() {
|
||||
const expectedCollectionName = 'COLLECTION_NAME';
|
||||
const expectedCollection = { options: { collectionName: expectedCollectionName } };
|
||||
|
||||
it('get collectionName from collection', function() {
|
||||
const options = { collection: expectedCollection };
|
||||
const { collection, collectionName } = extractCollectionInfo(options);
|
||||
expect(collection).toEqual(expectedCollection);
|
||||
expect(collectionName).toEqual(expectedCollectionName);
|
||||
});
|
||||
it.skip('get collection from collectionName', function() {
|
||||
// TODO: mock getCollection
|
||||
const options = { collectionName: expectedCollectionName };
|
||||
const { collection, collectionName } = extractCollectionInfo(options);
|
||||
expect(collection).toEqual(expectedCollection);
|
||||
expect(collectionName).toEqual(expectedCollectionName);
|
||||
});
|
||||
const expectedFragmentName = 'FRAGMENT_NAME';
|
||||
const expectedFragment = { definitions: [{ name: { value: expectedFragmentName } }] };
|
||||
it.skip('get fragment from fragmentName', function() {
|
||||
// TODO: mock getCollection
|
||||
const options = { fragmentName: expectedFragmentName };
|
||||
const { fragment, fragmentName } = extractFragmentInfo(options);
|
||||
expect(fragment).toEqual(expectedFragment);
|
||||
expect(fragmentName).toEqual(expectedFragmentName);
|
||||
});
|
||||
it('get fragmentName from fragment', function() {
|
||||
const options = { fragment: expectedFragment };
|
||||
const { fragment, fragmentName } = extractFragmentInfo(options);
|
||||
expect(fragment).toEqual(expectedFragment);
|
||||
expect(fragmentName).toEqual(expectedFragmentName);
|
||||
});
|
||||
it.skip('get fragmentName and fragment from collectionName', function() {
|
||||
// TODO: mock getFragment
|
||||
// if options does not contain fragment, we get the collection default fragment based on its name
|
||||
const options = {};
|
||||
const { fragment, fragmentName } = extractFragmentInfo(options, expectedCollectionName);
|
||||
expect(fragment).toEqual(expectedFragment);
|
||||
expect(fragmentName).toEqual(expectedFragmentName);
|
||||
});
|
||||
});
|
1
packages/vulcan-lib/test/index.js
Normal file
1
packages/vulcan-lib/test/index.js
Normal file
|
@ -0,0 +1 @@
|
|||
import './handleOptions.test.js';
|
Loading…
Add table
Reference in a new issue