mirror of
https://github.com/vale981/Vulcan
synced 2025-03-05 17:41:43 -05:00
start splitting code, reuse options handler
This commit is contained in:
parent
6114ee9da6
commit
284cdbae18
23 changed files with 278 additions and 229 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 +1 @@
|
|||
import './containers.test.js';
|
||||
//import './containers.test.js';
|
||||
|
|
|
@ -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';
|
||||
|
@ -59,6 +58,11 @@ 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/fieldsUtils.js';
|
||||
import withCollectionProps from './withCollectionProps';
|
||||
|
||||
const compactParent = (object, path) => {
|
||||
const parentPath = getParentPath(path);
|
||||
|
@ -1055,19 +1059,9 @@ 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
|
||||
|
||||
|
@ -1137,4 +1131,8 @@ SmartForm.childContextTypes = {
|
|||
|
||||
module.exports = SmartForm;
|
||||
|
||||
registerComponent('Form', SmartForm);
|
||||
registerComponent({
|
||||
name: 'Form',
|
||||
component: SmartForm,
|
||||
hocs: [withCollectionProps]
|
||||
});
|
||||
|
|
|
@ -37,12 +37,18 @@ 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';
|
||||
|
||||
class FormWrapper extends PureComponent {
|
||||
constructor(props) {
|
||||
|
@ -51,16 +57,11 @@ class FormWrapper extends PureComponent {
|
|||
// 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;
|
||||
: this.props.collection.simpleSchema()._schema;
|
||||
}
|
||||
|
||||
// if a document is being passed, this is an edit form
|
||||
|
@ -68,49 +69,18 @@ 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(
|
||||
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 =
|
||||
|
@ -133,7 +103,7 @@ class FormWrapper extends PureComponent {
|
|||
|
||||
// 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')}
|
||||
}
|
||||
|
@ -141,7 +111,7 @@ class FormWrapper extends PureComponent {
|
|||
|
||||
// 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')}
|
||||
}
|
||||
|
@ -196,7 +166,7 @@ class FormWrapper extends PureComponent {
|
|||
getComponent() {
|
||||
let WrappedComponent;
|
||||
|
||||
const prefix = `${this.getCollection()._name}${Utils.capitalize(
|
||||
const prefix = `${this.props.collectionName}${Utils.capitalize(
|
||||
this.getFormType()
|
||||
)}`;
|
||||
|
||||
|
@ -215,7 +185,7 @@ class FormWrapper extends PureComponent {
|
|||
// 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
|
||||
|
@ -225,7 +195,7 @@ class FormWrapper extends PureComponent {
|
|||
|
||||
// options for withNew, withUpdate, and withDelete HoCs
|
||||
const mutationOptions = {
|
||||
collection: this.getCollection(),
|
||||
collection: this.props.collection,
|
||||
fragment: mutationFragment
|
||||
};
|
||||
|
||||
|
@ -303,19 +273,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,
|
||||
|
@ -362,10 +323,8 @@ FormWrapper.contextTypes = {
|
|||
intl: intlShape
|
||||
};
|
||||
|
||||
registerComponent(
|
||||
'SmartForm',
|
||||
FormWrapper,
|
||||
withCurrentUser,
|
||||
withApollo,
|
||||
withRouter
|
||||
);
|
||||
registerComponent({
|
||||
name: 'SmartForm',
|
||||
component: FormWrapper,
|
||||
hocs: [withCurrentUser, withApollo, withRouter, withCollectionProps]
|
||||
});
|
||||
|
|
|
@ -33,3 +33,5 @@ const groupProps = {
|
|||
order: PropTypes.number,
|
||||
fields: PropTypes.arrayOf(PropTypes.shape(fieldProps))
|
||||
};
|
||||
|
||||
const callbacksProps = {};
|
||||
|
|
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;
|
26
packages/vulcan-forms/lib/modules/fieldsUtils.js
Normal file
26
packages/vulcan-forms/lib/modules/fieldsUtils.js
Normal file
|
@ -0,0 +1,26 @@
|
|||
import Users from 'meteor/vulcan:users';
|
||||
/**
|
||||
* @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;
|
||||
};
|
|
@ -1,3 +1,28 @@
|
|||
/**
|
||||
* Schema converter/getters
|
||||
* @param {*} schema
|
||||
*/
|
||||
|
||||
// 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);
|
||||
};
|
||||
|
||||
/*
|
||||
|
||||
Convert a nested SimpleSchema schema into a JSON object
|
||||
|
@ -27,7 +52,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 +60,6 @@ export const convertSchema = (schema, flatten = false) => {
|
|||
} else {
|
||||
jsonSchema[fieldName].schema = convertedSubSchema;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
});
|
||||
|
@ -61,44 +85,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 +170,7 @@ export const schemaProperties = [
|
|||
'options',
|
||||
'query',
|
||||
'fieldProperties',
|
||||
'intl',
|
||||
'intl'
|
||||
];
|
||||
|
||||
export const formProperties = [
|
||||
|
@ -179,5 +202,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;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
|
0
packages/vulcan-forms/test/fieldUtils.test.js
Normal file
0
packages/vulcan-forms/test/fieldUtils.test.js
Normal file
|
@ -1,3 +1,5 @@
|
|||
import './schema_utils.test.js';
|
||||
import './components.test.js';
|
||||
import './mergeWithComponents.test.js';
|
||||
|
||||
import './fieldUtils.test.js';
|
||||
|
|
|
@ -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