diff --git a/packages/vulcan-forms/lib/components/FormComponent.jsx b/packages/vulcan-forms/lib/components/FormComponent.jsx index 3303dd3f5..51572e4ee 100644 --- a/packages/vulcan-forms/lib/components/FormComponent.jsx +++ b/packages/vulcan-forms/lib/components/FormComponent.jsx @@ -196,10 +196,12 @@ class FormComponent extends Component { Get errors from Form state through context + Note: we use `includes` to get all errors from nested components, which have longer paths + */ getErrors = errors => { errors = errors || this.props.errors; - const fieldErrors = errors.filter(error => error.path === this.props.path); + const fieldErrors = errors.filter(error => error.path.includes(this.props.path)); return fieldErrors; }; diff --git a/packages/vulcan-forms/lib/components/FormIntl.jsx b/packages/vulcan-forms/lib/components/FormIntl.jsx index a3a3f9cc6..5db706322 100644 --- a/packages/vulcan-forms/lib/components/FormIntl.jsx +++ b/packages/vulcan-forms/lib/components/FormIntl.jsx @@ -1,6 +1,7 @@ import React, { PureComponent } from 'react'; import PropTypes from 'prop-types'; import { Components, registerComponent, Locales } from 'meteor/vulcan:core'; +import omit from 'lodash/omit'; class FormIntl extends PureComponent { @@ -11,20 +12,20 @@ class FormIntl extends PureComponent { so we just use the order of the Locales array. */ - getLocalePath = (locale, defaultIndex) => { + getLocalePath = (defaultIndex) => { return `${this.props.path}_intl.${defaultIndex}`; } - + render() { // do not pass FormIntl's own value, inputProperties, and intlInput props down - const properties = _.omit(this.props, 'value', 'inputProperties', 'intlInput'); + const properties = omit(this.props, 'value', 'inputProperties', 'intlInput'); return (
{Locales.map((locale, i) => (
- +
))}
diff --git a/packages/vulcan-lib/lib/modules/collections.js b/packages/vulcan-lib/lib/modules/collections.js index 28581afdf..965793542 100644 --- a/packages/vulcan-lib/lib/modules/collections.js +++ b/packages/vulcan-lib/lib/modules/collections.js @@ -1,12 +1,12 @@ import { Mongo } from 'meteor/mongo'; import SimpleSchema from 'simpl-schema'; -import { addGraphQLCollection, addGraphQLQuery, addGraphQLMutation, addGraphQLResolvers, addToGraphQLContext } from './graphql.js'; +import { addGraphQLCollection, addToGraphQLContext } from './graphql.js'; import { Utils } from './utils.js'; import { runCallbacks } from './callbacks.js'; import { getSetting, registerSetting } from './settings.js'; import { registerFragment, getDefaultFragmentText } from './fragments.js'; import escapeStringRegexp from 'escape-string-regexp'; -import { multiQueryTemplate, singleQueryTemplate, createMutationTemplate, updateMutationTemplate, upsertMutationTemplate, deleteMutationTemplate } from './graphql_templates'; +import { Locales, getIntlString } from './intl'; const wrapAsync = (Meteor.wrapAsync)? Meteor.wrapAsync : Meteor._wrapAsync; // import { debug } from './debug.js'; @@ -119,6 +119,33 @@ Mongo.Collection.prototype.helpers = function(helpers) { }); }; +/* + +Custom validation function to check for required locales + +See https://github.com/aldeed/simple-schema-js#custom-field-validation + +*/ +const validateIntlField = function () { + let errors = []; + + // if field is required, go through locales to check which one are required + if (!this.definition.optional) { + const requiredLocales = Locales.filter(locale => locale.required); + + requiredLocales.forEach((locale, index) => { + const strings = this.value; + const hasString = strings.some(s => s.locale === locale.id && s.value); + if (!hasString) { + errors.push({ path: this.key, locale: locale.id, index, name: `${this.key.replace('_intl', '')} (${locale.id})` }); + } + }); + + } + // hack to work around the fact that custom validation function can only return a single string + return `intlError|${JSON.stringify(errors)}`; +} + export const createCollection = options => { const { typeName, collectionName = getCollectionName(typeName), schema, generateGraphQLSchema = true, dbCollectionName } = options; @@ -138,23 +165,23 @@ export const createCollection = options => { // generate foo_intl fields Object.keys(schema).forEach(fieldName => { const fieldSchema = schema[fieldName]; - if (fieldSchema.type && fieldSchema.type.name === 'IntlString') { + if (fieldSchema.intl || (fieldSchema.type && fieldSchema.type.name === 'IntlString')) { // we have at least one intl field hasIntlFields = true; - // make non-intl field optional - schema[fieldName].optional = true; - schema[`${fieldName}_intl`] = { ...schema[fieldName], // copy properties from regular field hidden: true, type: Array, + custom: validateIntlField, } schema[`${fieldName}_intl.$`] = { - type: Object, - blackbox: true, + type: getIntlString(), } + + // make non-intl field optional + schema[fieldName].optional = true; } }); diff --git a/packages/vulcan-lib/lib/modules/config.js b/packages/vulcan-lib/lib/modules/config.js index f7dd70f2f..e8808dab6 100644 --- a/packages/vulcan-lib/lib/modules/config.js +++ b/packages/vulcan-lib/lib/modules/config.js @@ -51,6 +51,8 @@ SimpleSchema.extendOptions([ 'query', // field-specific data loading query 'selectable', // field can be used as part of a selector when querying for data 'orderable', // field can be used to order results when querying for data + + 'intl', // set to `true` to make a field international ]); // eslint-disable-next-line no-undef diff --git a/packages/vulcan-lib/lib/modules/intl.js b/packages/vulcan-lib/lib/modules/intl.js index 9f5f587a6..c1d5df8c7 100644 --- a/packages/vulcan-lib/lib/modules/intl.js +++ b/packages/vulcan-lib/lib/modules/intl.js @@ -1,5 +1,4 @@ import SimpleSchema from 'simpl-schema'; -import { Strings } from './strings'; export const Locales = []; @@ -31,14 +30,16 @@ Generate custom IntlString SimpleSchema type */ export const getIntlString = () => { - const schema = {}; - - Object.keys(Strings).forEach(locale => { - schema[locale] = { + const schema = { + locale: { type: String, - optional: true, - }; - }); + optional: false, + }, + value: { + type: String, + optional: false, + } + }; const IntlString = new SimpleSchema(schema); IntlString.name = 'IntlString'; diff --git a/packages/vulcan-lib/lib/modules/validation.js b/packages/vulcan-lib/lib/modules/validation.js index 08b677ab2..55768fa6c 100644 --- a/packages/vulcan-lib/lib/modules/validation.js +++ b/packages/vulcan-lib/lib/modules/validation.js @@ -47,11 +47,22 @@ export const validateDocument = (document, collection, context) => { errors.forEach(error => { // eslint-disable-next-line no-console // console.log(error); - validationErrors.push({ - id: `errors.${error.type}`, - path: error.name, - properties: error, - }); + if (error.type.includes('intlError')) { + const intlErrors = JSON.parse(error.type.replace('intlError|', '')); + intlErrors.forEach(intlError => { + validationErrors.push({ + id: `errors.required`, + path: `${intlError.path}.${intlError.index}`, + properties: intlError, + }); + }); + } else { + validationErrors.push({ + id: `errors.${error.type}`, + path: error.name, + properties: error, + }); + } }); } @@ -96,11 +107,22 @@ export const validateModifier = (modifier, document, collection, context) => { errors.forEach(error => { // eslint-disable-next-line no-console // console.log(error); - validationErrors.push({ - id: `errors.${error.type}`, - path: error.name, - properties: error, - }); + if (error.type.includes('intlError')) { + const intlErrors = JSON.parse(error.type.replace('intlError|', '')); + intlErrors.forEach(intlError => { + validationErrors.push({ + id: `errors.required`, + path: `${intlError.path}.${intlError.index}`, + properties: intlError, + }); + }); + } else { + validationErrors.push({ + id: `errors.${error.type}`, + path: error.name, + properties: error, + }); + } }); }