From d21dd5fcfad645327abd9977eb996b55bdfc01b1 Mon Sep 17 00:00:00 2001 From: Eric Burel Date: Wed, 25 Jul 2018 17:16:38 +0200 Subject: [PATCH 1/4] wrote passing tests and a failing test for nested objects --- .../lib/components/FormComponent.jsx | 10 +- .../vulcan-forms/lib/components/FormGroup.jsx | 4 +- packages/vulcan-forms/test/components.test.js | 226 +++++++++++++----- 3 files changed, 178 insertions(+), 62 deletions(-) diff --git a/packages/vulcan-forms/lib/components/FormComponent.jsx b/packages/vulcan-forms/lib/components/FormComponent.jsx index 3a902a3a4..ff18fdfa3 100644 --- a/packages/vulcan-forms/lib/components/FormComponent.jsx +++ b/packages/vulcan-forms/lib/components/FormComponent.jsx @@ -277,9 +277,9 @@ class FormComponent extends Component { case 'time': return Components.FormComponentTime; - case 'statictext': + case 'statictext': return Components.FormComponentStaticText; - + default: const CustomComponent = Components[this.props.input]; return CustomComponent ? CustomComponent : Components.FormComponentDefault; @@ -290,9 +290,9 @@ class FormComponent extends Component { render() { if (this.props.intlInput) { return ; - } else if (this.props.nestedInput){ + } else if (this.props.nestedInput) { return ; - } + } return (

{this.props.label}

- {this.state.collapsed ? : } + {this.state.collapsed ? : } ); @@ -80,6 +80,8 @@ FormGroup.propTypes = { currentUser: PropTypes.object, }; +module.exports = FormGroup + registerComponent('FormGroup', FormGroup); const IconRight = ({ width = 24, height = 24 }) => ( diff --git a/packages/vulcan-forms/test/components.test.js b/packages/vulcan-forms/test/components.test.js index f3557bc54..f01a220d5 100644 --- a/packages/vulcan-forms/test/components.test.js +++ b/packages/vulcan-forms/test/components.test.js @@ -1,13 +1,16 @@ // setup JSDOM server side for testing (necessary for Enzyme to mount) import 'jsdom-global/register' import React from 'react' +// we must import all the other components, so that "registerComponent" is called +import "../lib/modules/components" +// TODO: should be loaded from Components instead? import Form from '../lib/components/Form' +import FormGroup from "../lib/components/FormGroup" +import FormComponent from "../lib/components/FormComponent" import FormNested from '../lib/components/FormNested' import expect from 'expect' import Enzyme, { mount, shallow } from 'enzyme' import Adapter from 'enzyme-adapter-react-16'; -// we must import all the other components, so that "registerComponent" is called -import "../lib/modules/components" @@ -22,61 +25,55 @@ const addressGroup = { label: "Addresses", order: 10 }; -const addressSchema = new SimpleSchema({ +const addressSchema = { street: { type: String, optional: true, viewableBy: ["guests"], - editableBy: ["members"], - insertableBy: ["members"], + editableBy: ["quests"], + insertableBy: ["quests"], max: 100 // limit street address to 100 characters }, -}); -const schema = { - _id: { - type: String, - optional: true, - viewableBy: ["guests"] - }, - createdAt: { - type: Date, - optional: true, - onInsert: (document, currentUser) => { - return new Date(); - } - }, - userId: { - type: String, - optional: true - }, - name: { - type: String, - optional: false, - viewableBy: ["guests"], - editableBy: ["members"], - insertableBy: ["members"], - searchable: true // make field searchable - }, +}; +const arraySchema = { addresses: { type: Array, viewableBy: ["guests"], - editableBy: ["members"], - insertableBy: ["members"], + editableBy: ["quests"], + insertableBy: ["quests"], group: addressGroup }, "addresses.$": { - type: addressSchema + type: new SimpleSchema(addressSchema) + } +}; +const objectSchema = { + addresses: { + type: Object, + viewableBy: ["guests"], + editableBy: ["quests"], + insertableBy: ["quests"], + }, + "addresses.$": { + type: new SimpleSchema(addressSchema) } }; // stub collection import { createCollection, getDefaultResolvers, getDefaultMutations } from 'meteor/vulcan:core' -const Customers = createCollection({ - collectionName: 'Customers', - typeName: 'Customer', - schema, - resolvers: getDefaultResolvers('Customers'), - mutations: getDefaultMutations('Customers'), +const WithArrays = createCollection({ + collectionName: 'WithArrays', + typeName: 'WithArray', + schema: arraySchema, + resolvers: getDefaultResolvers('WithArrays'), + mutations: getDefaultMutations('WithArrays'), +}); +const WithObjects = createCollection({ + collectionName: 'WithObjects', + typeName: 'WithObject', + schema: objectSchema, + resolvers: getDefaultResolvers('WithObjects'), + mutations: getDefaultMutations('WithObjects'), }); const Addresses = createCollection({ @@ -89,22 +86,22 @@ const Addresses = createCollection({ describe('vulcan-forms/components', function () { describe('Form', function () { - const mountWithContext = C => mount(C, { - context: { - intl: { - formatMessage: () => "", - formatDate: () => "" - } + const context = { + intl: { + formatMessage: () => "", + formatDate: () => "", + formatTime: () => "", + formatRelative: () => "", + formatNumber: () => "", + formatPlural: () => "", } + + } + const mountWithContext = C => mount(C, { + context }) const shallowWithContext = C => shallow(C, { - context: { - intl: { - formatMessage: () => "", - formatDate: () => "", - formatTime: () => "" - } - } + context }) describe('basic', function () { it('shallow render', function () { @@ -113,18 +110,133 @@ describe('vulcan-forms/components', function () { }) }) describe('nested forms', function () { - it('shallow render', () => { - const wrapper = shallowWithContext(
) + describe('array', function () { + it('shallow render', () => { + const wrapper = shallowWithContext() + expect(wrapper).toBeDefined() + }) + it('render a FormGroup for addresses', function () { + const wrapper = shallowWithContext() + const formGroup = wrapper.find('FormGroup').find({ name: 'addresses' }) + expect(formGroup).toBeDefined() + expect(formGroup).toHaveLength(1) + }) + }) + describe('object', function () { + it('shallow render', () => { + const wrapper = shallowWithContext() + expect(wrapper).toBeDefined() + }) + it('define one field', () => { + const wrapper = shallowWithContext() + const defaultGroup = wrapper.find('FormGroup').first() + const fields = defaultGroup.prop('fields') + expect(fields).toHaveLength(1) // addresses field + }) + + const getFormFields = (wrapper) => { + const defaultGroup = wrapper.find('FormGroup').first() + const fields = defaultGroup.prop('fields') + return fields + } + const getFirstField = () => { + const wrapper = shallowWithContext() + const fields = getFormFields(wrapper) + return fields[0] + } + it('define the nestedSchema', () => { + const addressField = getFirstField() + expect(addressField.nestedSchema.street).toBeDefined() + }) + }) + }) + }) + + + describe('FormComponent', function () { + const shallowWithContext = C => shallow(C, { + context: { + getDocument: () => { } + } + }) + const defaultProps = { + "disabled": false, + "optional": true, + "document": {}, + "name": "meetingPlace", + "path": "meetingPlace", + "datatype": [{ type: Object }], + "layout": "horizontal", + "label": "Meeting place", + "currentValues": {}, + "formType": "new", + deletedValues: [], + throwError: () => { }, + updateCurrentValues: () => { }, + errors: [], + clearFieldErrors: () => { }, + } + it('shallow render', function () { + const wrapper = shallowWithContext() + expect(wrapper).toBeDefined() + }) + describe('nested object', function () { + const props = { + ...defaultProps, + "datatype": [{ type: Object }], + "nestedSchema": { + "street": {}, + "country": {}, + "zipCode": {} + }, + "nestedInput": true, + "nestedFields": [ + {}, + {}, + {} + ], + "currentValues": {}, + } + it('shallow render', function () { + const wrapper = shallowWithContext() expect(wrapper).toBeDefined() }) + it('render a FormNested', function () { + const wrapper = shallowWithContext() + const formNested = wrapper.find('FormNested') + expect(formNested).toHaveLength(1) + }) }) }) describe('FormNested', function () { - it('mount', function () { + it('shallow render', function () { const wrapper = shallow() expect(wrapper).toBeDefined() - + }) + describe('nested object', function () { + it.skip('render a form for the object', function () { + const wrapper = shallow() + expect(false).toBe(true) + }) + it.skip('does not show any button', function () { + const wrapper = shallow() + const button = wrapper.find('Components.Button') + // TODO: COMPONENTS values are undefined, because in the test env + // meteor/vulan:core Components is not set correctly + console.log(wrapper.debug()) + expect(button).toHaveLength(0) + }) + it('does not show add button', function () { + const wrapper = shallow() + const addButton = wrapper.find('IconAdd') + expect(addButton).toHaveLength(0) + }) + it('does not show remove button', function () { + const wrapper = shallow() + const removeButton = wrapper.find('IconRemove') + expect(removeButton).toHaveLength(0) + }) }) }) }) \ No newline at end of file From 25db5c04b87f3981928276571440534f4b05470e Mon Sep 17 00:00:00 2001 From: Eric Burel Date: Thu, 26 Jul 2018 15:43:09 +0200 Subject: [PATCH 2/4] load Components correctly in tests Now `vulcan:ui-bootstrap` is explicitely loaded in the `vulcan:forms` package. This avoid undefined `Components.Button` if the use did not load a specific styling package --- packages/vulcan-forms/package.js | 2 +- packages/vulcan-forms/test/components.test.js | 37 +++++++++++++++---- 2 files changed, 30 insertions(+), 9 deletions(-) diff --git a/packages/vulcan-forms/package.js b/packages/vulcan-forms/package.js index 9e196b3dd..c9e702c78 100644 --- a/packages/vulcan-forms/package.js +++ b/packages/vulcan-forms/package.js @@ -8,7 +8,7 @@ Package.describe({ Package.onUse(function (api) { api.versionsFrom("1.6.1"); - api.use(["vulcan:core@1.11.2", "fourseven:scss@4.5.0"]); + api.use(["vulcan:core@1.11.2", "vulcan:ui-bootstrap@1.11.2", "fourseven:scss@4.5.0"]); api.addFiles(["lib/stylesheets/style.scss", "lib/stylesheets/datetime.scss"], "client"); diff --git a/packages/vulcan-forms/test/components.test.js b/packages/vulcan-forms/test/components.test.js index f01a220d5..ea240ec81 100644 --- a/packages/vulcan-forms/test/components.test.js +++ b/packages/vulcan-forms/test/components.test.js @@ -1,8 +1,6 @@ // setup JSDOM server side for testing (necessary for Enzyme to mount) import 'jsdom-global/register' import React from 'react' -// we must import all the other components, so that "registerComponent" is called -import "../lib/modules/components" // TODO: should be loaded from Components instead? import Form from '../lib/components/Form' import FormGroup from "../lib/components/FormGroup" @@ -11,13 +9,24 @@ import FormNested from '../lib/components/FormNested' import expect from 'expect' import Enzyme, { mount, shallow } from 'enzyme' import Adapter from 'enzyme-adapter-react-16'; - - +import 'meteor/vulcan:users' // setup enzyme // TODO: write a reusable helper and move this to the tests setup Enzyme.configure({ adapter: new Adapter() }) +// 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 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() + + + // fixtures import SimpleSchema from "simpl-schema"; const addressGroup = { @@ -84,6 +93,7 @@ const Addresses = createCollection({ mutations: getDefaultMutations('Addresses'), }) +// tests describe('vulcan-forms/components', function () { describe('Form', function () { const context = { @@ -94,6 +104,7 @@ describe('vulcan-forms/components', function () { formatRelative: () => "", formatNumber: () => "", formatPlural: () => "", + formatHTMLMessage: () => "" } } @@ -152,7 +163,6 @@ describe('vulcan-forms/components', function () { }) }) - describe('FormComponent', function () { const shallowWithContext = C => shallow(C, { context: { @@ -214,17 +224,28 @@ describe('vulcan-forms/components', function () { const wrapper = shallow() expect(wrapper).toBeDefined() }) + describe('array', function () { + it('shows a button', function () { + const wrapper = shallow() + const button = wrapper.find('BootstrapButton') + expect(button).toHaveLength(1) + }) + it('shows an add button', function () { + const wrapper = shallow() + const addButton = wrapper.find('IconAdd') + expect(addButton).toHaveLength(1) + }) + }) describe('nested object', function () { it.skip('render a form for the object', function () { const wrapper = shallow() expect(false).toBe(true) }) - it.skip('does not show any button', function () { + it('does not show any button', function () { const wrapper = shallow() - const button = wrapper.find('Components.Button') + const button = wrapper.find('BootstrapButton') // TODO: COMPONENTS values are undefined, because in the test env // meteor/vulan:core Components is not set correctly - console.log(wrapper.debug()) expect(button).toHaveLength(0) }) it('does not show add button', function () { From b5e54ead1733e361a2640e6a921579833d44603f Mon Sep 17 00:00:00 2001 From: Eric Burel Date: Thu, 26 Jul 2018 17:07:48 +0200 Subject: [PATCH 3/4] Splitted FormNested between objects an arrays Nested form is much simpler for objects than for arrays. --- packages/vulcan-forms/lib/components/Form.jsx | 1 + .../lib/components/FormComponent.jsx | 6 +- .../{FormNested.jsx => FormNestedArray.jsx} | 52 +----- .../lib/components/FormNestedItem.jsx | 53 ++++++ .../lib/components/FormNestedObject.jsx | 37 ++++ .../vulcan-forms/lib/modules/components.js | 3 +- packages/vulcan-forms/test/components.test.js | 173 ++++++++++-------- 7 files changed, 201 insertions(+), 124 deletions(-) rename packages/vulcan-forms/lib/components/{FormNested.jsx => FormNestedArray.jsx} (69%) create mode 100644 packages/vulcan-forms/lib/components/FormNestedItem.jsx create mode 100644 packages/vulcan-forms/lib/components/FormNestedObject.jsx diff --git a/packages/vulcan-forms/lib/components/Form.jsx b/packages/vulcan-forms/lib/components/Form.jsx index 696011ab9..be293ac16 100644 --- a/packages/vulcan-forms/lib/components/Form.jsx +++ b/packages/vulcan-forms/lib/components/Form.jsx @@ -393,6 +393,7 @@ class SmartForm extends Component { if (fieldSchema.schema) { field.nestedSchema = fieldSchema.schema; field.nestedInput = true; + // get nested schema // for each nested field, get field object by calling createField recursively field.nestedFields = this.getFieldNames({ schema: field.nestedSchema }).map(subFieldName => { diff --git a/packages/vulcan-forms/lib/components/FormComponent.jsx b/packages/vulcan-forms/lib/components/FormComponent.jsx index ff18fdfa3..c46046c3d 100644 --- a/packages/vulcan-forms/lib/components/FormComponent.jsx +++ b/packages/vulcan-forms/lib/components/FormComponent.jsx @@ -291,7 +291,11 @@ class FormComponent extends Component { if (this.props.intlInput) { return ; } else if (this.props.nestedInput) { - return ; + if (this.props.datatype[0].type === Array) { + return ; + } else if (this.props.datatype[0].type === Object) { + return ; + } } return ( { - return ( -
-
- {nestedFields.map((field, i) => { - return ( - - ); - })} -
-
- { - removeItem(name); - }} - > - - -
-
-
- ); -}; - - -FormNestedItem.contextTypes = { - errors: PropTypes.array, -}; - - -registerComponent('FormNestedItem', FormNestedItem); - -class FormNested extends PureComponent { +class FormNestedArray extends PureComponent { getCurrentValue() { return this.props.currentValues[this.props.path] || [] } @@ -78,7 +38,7 @@ class FormNested extends PureComponent { {value.map( (subDocument, i) => !this.isDeleted(i) && ( - ( diff --git a/packages/vulcan-forms/lib/components/FormNestedItem.jsx b/packages/vulcan-forms/lib/components/FormNestedItem.jsx new file mode 100644 index 000000000..58aee0281 --- /dev/null +++ b/packages/vulcan-forms/lib/components/FormNestedItem.jsx @@ -0,0 +1,53 @@ +import React from "react" +import PropTypes from 'prop-types'; +import { Components, registerComponent } from 'meteor/vulcan:core'; + +const FormNestedItem = ({ nestedFields, name, path, removeItem, itemIndex, ...props }, { errors }) => { + const isArray = typeof itemIndex !== 'undefined' + return ( +
+
+ {nestedFields.map((field, i) => { + return ( + + ); + })} +
+ { + isArray && [ +
+ { + removeItem(name); + }} + > + + +
, +
+ ] + } +
+ ); +}; + +FormNestedItem.propTypes = { + path: PropTypes.string.isRequired, + itemIndex: PropTypes.number +} + + +FormNestedItem.contextTypes = { + errors: PropTypes.array, +}; + +registerComponent('FormNestedItem', FormNestedItem); \ No newline at end of file diff --git a/packages/vulcan-forms/lib/components/FormNestedObject.jsx b/packages/vulcan-forms/lib/components/FormNestedObject.jsx new file mode 100644 index 000000000..48a69d44d --- /dev/null +++ b/packages/vulcan-forms/lib/components/FormNestedObject.jsx @@ -0,0 +1,37 @@ +import React, { PureComponent } from 'react'; +import PropTypes from 'prop-types'; +import { Components, registerComponent } from 'meteor/vulcan:core'; +import "./FormNestedItem" + +class FormNestedObject extends PureComponent { + /*getCurrentValue() { + return this.props.currentValues[this.props.path] || {} + }*/ + render() { + //const value = this.getCurrentValue() + // do not pass FormNested's own value, input and inputProperties props down + const properties = _.omit(this.props, 'value', 'input', 'inputProperties', 'nestedInput'); + return ( +
+ +
+ +
+
+ ); + } +} + +FormNestedObject.propTypes = { + currentValues: PropTypes.object, + path: PropTypes.string, + label: PropTypes.string +}; + +module.exports = FormNestedObject + +registerComponent('FormNestedObject', FormNestedObject); + diff --git a/packages/vulcan-forms/lib/modules/components.js b/packages/vulcan-forms/lib/modules/components.js index 3b1886396..ca0c8fa1a 100644 --- a/packages/vulcan-forms/lib/modules/components.js +++ b/packages/vulcan-forms/lib/modules/components.js @@ -2,7 +2,8 @@ import '../components/FieldErrors.jsx'; import '../components/FormErrors.jsx'; import '../components/FormError.jsx'; import '../components/FormComponent.jsx'; -import '../components/FormNested.jsx'; +import '../components/FormNestedArray.jsx'; +import '../components/FormNestedObject.jsx'; import '../components/FormIntl.jsx'; import '../components/FormGroup.jsx'; import '../components/FormSubmit.jsx'; diff --git a/packages/vulcan-forms/test/components.test.js b/packages/vulcan-forms/test/components.test.js index ea240ec81..61e402b0e 100644 --- a/packages/vulcan-forms/test/components.test.js +++ b/packages/vulcan-forms/test/components.test.js @@ -5,11 +5,11 @@ import React from 'react' import Form from '../lib/components/Form' import FormGroup from "../lib/components/FormGroup" import FormComponent from "../lib/components/FormComponent" -import FormNested from '../lib/components/FormNested' +import '../lib/components/FormNestedArray' import expect from 'expect' import Enzyme, { mount, shallow } from 'enzyme' import Adapter from 'enzyme-adapter-react-16'; -import 'meteor/vulcan:users' +import { Components } from "meteor/vulcan:core" // setup enzyme // TODO: write a reusable helper and move this to the tests setup @@ -120,45 +120,43 @@ describe('vulcan-forms/components', function () { expect(wrapper).toBeDefined() }) }) - describe('nested forms', function () { - describe('array', function () { - it('shallow render', () => { - const wrapper = shallowWithContext() - expect(wrapper).toBeDefined() - }) - it('render a FormGroup for addresses', function () { - const wrapper = shallowWithContext() - const formGroup = wrapper.find('FormGroup').find({ name: 'addresses' }) - expect(formGroup).toBeDefined() - expect(formGroup).toHaveLength(1) - }) + describe('nested array', function () { + it('shallow render', () => { + const wrapper = shallowWithContext() + expect(wrapper).toBeDefined() + }) + it('render a FormGroup for addresses', function () { + const wrapper = shallowWithContext() + const formGroup = wrapper.find('FormGroup').find({ name: 'addresses' }) + expect(formGroup).toBeDefined() + expect(formGroup).toHaveLength(1) + }) + }) + describe('nested object', function () { + it('shallow render', () => { + const wrapper = shallowWithContext() + expect(wrapper).toBeDefined() + }) + it('define one field', () => { + const wrapper = shallowWithContext() + const defaultGroup = wrapper.find('FormGroup').first() + const fields = defaultGroup.prop('fields') + expect(fields).toHaveLength(1) // addresses field }) - describe('object', function () { - it('shallow render', () => { - const wrapper = shallowWithContext() - expect(wrapper).toBeDefined() - }) - it('define one field', () => { - const wrapper = shallowWithContext() - const defaultGroup = wrapper.find('FormGroup').first() - const fields = defaultGroup.prop('fields') - expect(fields).toHaveLength(1) // addresses field - }) - const getFormFields = (wrapper) => { - const defaultGroup = wrapper.find('FormGroup').first() - const fields = defaultGroup.prop('fields') - return fields - } - const getFirstField = () => { - const wrapper = shallowWithContext() - const fields = getFormFields(wrapper) - return fields[0] - } - it('define the nestedSchema', () => { - const addressField = getFirstField() - expect(addressField.nestedSchema.street).toBeDefined() - }) + const getFormFields = (wrapper) => { + const defaultGroup = wrapper.find('FormGroup').first() + const fields = defaultGroup.prop('fields') + return fields + } + const getFirstField = () => { + const wrapper = shallowWithContext() + const fields = getFormFields(wrapper) + return fields[0] + } + it('define the nestedSchema', () => { + const addressField = getFirstField() + expect(addressField.nestedSchema.street).toBeDefined() }) }) }) @@ -190,6 +188,29 @@ describe('vulcan-forms/components', function () { const wrapper = shallowWithContext() expect(wrapper).toBeDefined() }) + describe('nested array', function () { + const props = { + ...defaultProps, + "datatype": [{ type: Array }], + "nestedSchema": { + "street": {}, + "country": {}, + "zipCode": {} + }, + "nestedInput": true, + "nestedFields": [ + {}, + {}, + {} + ], + "currentValues": {}, + } + it('render a FormNestedArray', function () { + const wrapper = shallowWithContext() + const formNested = wrapper.find('FormNestedArray') + expect(formNested).toHaveLength(1) + }) + }) describe('nested object', function () { const props = { ...defaultProps, @@ -211,53 +232,53 @@ describe('vulcan-forms/components', function () { const wrapper = shallowWithContext() expect(wrapper).toBeDefined() }) - it('render a FormNested', function () { + it('render a FormNestedObject', function () { const wrapper = shallowWithContext() - const formNested = wrapper.find('FormNested') + const formNested = wrapper.find('FormNestedObject') expect(formNested).toHaveLength(1) }) }) }) - describe('FormNested', function () { + describe('FormNestedArray', function () { it('shallow render', function () { - const wrapper = shallow() + const wrapper = shallow() expect(wrapper).toBeDefined() }) - describe('array', function () { - it('shows a button', function () { - const wrapper = shallow() - const button = wrapper.find('BootstrapButton') - expect(button).toHaveLength(1) - }) - it('shows an add button', function () { - const wrapper = shallow() - const addButton = wrapper.find('IconAdd') - expect(addButton).toHaveLength(1) - }) + it('shows a button', function () { + const wrapper = shallow() + const button = wrapper.find('BootstrapButton') + expect(button).toHaveLength(1) }) - describe('nested object', function () { - it.skip('render a form for the object', function () { - const wrapper = shallow() - expect(false).toBe(true) - }) - it('does not show any button', function () { - const wrapper = shallow() - const button = wrapper.find('BootstrapButton') - // TODO: COMPONENTS values are undefined, because in the test env - // meteor/vulan:core Components is not set correctly - expect(button).toHaveLength(0) - }) - it('does not show add button', function () { - const wrapper = shallow() - const addButton = wrapper.find('IconAdd') - expect(addButton).toHaveLength(0) - }) - it('does not show remove button', function () { - const wrapper = shallow() - const removeButton = wrapper.find('IconRemove') - expect(removeButton).toHaveLength(0) - }) + it('shows an add button', function () { + const wrapper = shallow() + const addButton = wrapper.find('IconAdd') + expect(addButton).toHaveLength(1) + }) + }) + describe('FormNestedObject', function () { + it('shallow render', function () { + const wrapper = shallow() + expect(wrapper).toBeDefined() + }) + it.skip('render a form for the object', function () { + const wrapper = shallow() + expect(false).toBe(true) + }) + it('does not show any button', function () { + const wrapper = shallow() + const button = wrapper.find('BootstrapButton') + expect(button).toHaveLength(0) + }) + it('does not show add button', function () { + const wrapper = shallow() + const addButton = wrapper.find('IconAdd') + expect(addButton).toHaveLength(0) + }) + it('does not show remove button', function () { + const wrapper = shallow() + const removeButton = wrapper.find('IconRemove') + expect(removeButton).toHaveLength(0) }) }) }) \ No newline at end of file From e79b8a524f355e2096b1fe7e20c06c20656df53f Mon Sep 17 00:00:00 2001 From: Eric Burel Date: Fri, 27 Jul 2018 15:51:04 +0200 Subject: [PATCH 4/4] updated schema_utils and FormComponent to correctly detect nested objects --- .../lib/components/FormComponent.jsx | 14 +++++- .../vulcan-forms/lib/modules/schema_utils.js | 38 ++++++++++++-- packages/vulcan-forms/test/components.test.js | 7 +-- packages/vulcan-forms/test/index.js | 1 - .../vulcan-forms/test/schema_utils.test.js | 49 +++++++++++++++++-- 5 files changed, 92 insertions(+), 17 deletions(-) diff --git a/packages/vulcan-forms/lib/components/FormComponent.jsx b/packages/vulcan-forms/lib/components/FormComponent.jsx index c46046c3d..e073be0aa 100644 --- a/packages/vulcan-forms/lib/components/FormComponent.jsx +++ b/packages/vulcan-forms/lib/components/FormComponent.jsx @@ -5,6 +5,7 @@ import { registerComponent } from 'meteor/vulcan:core'; import get from 'lodash/get'; import isEqual from 'lodash/isEqual'; import { isEmptyValue, mergeValue } from '../modules/utils.js'; +import SimpleSchema from 'simpl-schema' class FormComponent extends Component { constructor(props) { @@ -287,13 +288,22 @@ class FormComponent extends Component { } }; + getFieldType = () => { + return this.props.datatype[0].type + } + isArrayField = () => { + return this.getFieldType() === Array + } + isObjectField = () => { + return this.getFieldType() instanceof SimpleSchema + } render() { if (this.props.intlInput) { return ; } else if (this.props.nestedInput) { - if (this.props.datatype[0].type === Array) { + if (this.isArrayField()) { return ; - } else if (this.props.datatype[0].type === Object) { + } else if (this.isObjectField()) { return ; } } diff --git a/packages/vulcan-forms/lib/modules/schema_utils.js b/packages/vulcan-forms/lib/modules/schema_utils.js index 62e5882c6..30f924d61 100644 --- a/packages/vulcan-forms/lib/modules/schema_utils.js +++ b/packages/vulcan-forms/lib/modules/schema_utils.js @@ -7,7 +7,7 @@ If flatten = true, will create a flat object instead of nested tree export const convertSchema = (schema, flatten = false) => { if (schema._schema) { let jsonSchema = {}; - + Object.keys(schema._schema).forEach(fieldName => { // exclude array fields if (fieldName.includes('$')) { @@ -17,7 +17,7 @@ export const convertSchema = (schema, flatten = false) => { // extract schema jsonSchema[fieldName] = getFieldSchema(fieldName, schema); - // check for existence of nested schema on corresponding array field + // check for existence of nested schema const subSchema = getNestedSchema(fieldName, schema); // if nested schema exists, call convertSchema recursively if (subSchema) { @@ -51,15 +51,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 +const getArrayNestedSchema = (fieldName, schema) => { + const arrayItemSchema = schema._schema[`${fieldName}.$`]; + 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) + //console.log('fieldType', typeof 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 +} /* Given an array field, get its nested schema */ export const getNestedSchema = (fieldName, schema) => { - const arrayItemSchema = schema._schema[`${fieldName}.$`]; - const nestedSchema = arrayItemSchema && arrayItemSchema.type.definitions[0].type; - return nestedSchema; + const arrayItemSchema = getArrayNestedSchema(fieldName, schema) + if (!arrayItemSchema) { + // look for an object schema + const objectItemSchema = getObjectNestedSchema(fieldName, schema) + // no schema was found + if (!objectItemSchema) return null + return objectItemSchema + } + return arrayItemSchema }; export const schemaProperties = [ diff --git a/packages/vulcan-forms/test/components.test.js b/packages/vulcan-forms/test/components.test.js index 61e402b0e..487dbdbb5 100644 --- a/packages/vulcan-forms/test/components.test.js +++ b/packages/vulcan-forms/test/components.test.js @@ -58,14 +58,11 @@ const arraySchema = { }; const objectSchema = { addresses: { - type: Object, + type: new SimpleSchema(addressSchema), viewableBy: ["guests"], editableBy: ["quests"], insertableBy: ["quests"], }, - "addresses.$": { - type: new SimpleSchema(addressSchema) - } }; // stub collection @@ -214,7 +211,7 @@ describe('vulcan-forms/components', function () { describe('nested object', function () { const props = { ...defaultProps, - "datatype": [{ type: Object }], + "datatype": [{ type: new SimpleSchema({}) }], "nestedSchema": { "street": {}, "country": {}, diff --git a/packages/vulcan-forms/test/index.js b/packages/vulcan-forms/test/index.js index c38edd528..6fc1b82ed 100644 --- a/packages/vulcan-forms/test/index.js +++ b/packages/vulcan-forms/test/index.js @@ -1,3 +1,2 @@ import './schema_utils.test.js' -import './package.test.js' import './components.test.js' \ No newline at end of file diff --git a/packages/vulcan-forms/test/schema_utils.test.js b/packages/vulcan-forms/test/schema_utils.test.js index c050a29c6..f3c2bca03 100644 --- a/packages/vulcan-forms/test/schema_utils.test.js +++ b/packages/vulcan-forms/test/schema_utils.test.js @@ -1,10 +1,51 @@ -//import { convertSchema } from '../lib/modules/schema_utils.js' +import { convertSchema, getSchemaType, getNestedSchema } from '../lib/modules/schema_utils.js' +import SimpleSchema from 'simpl-schema' import expect from 'expect' +const addressSchema = { + street: { + type: String, + }, + country: { + type: String, + }, +} +const addressSimpleSchema = new SimpleSchema(addressSchema) + describe('schema_utils', function () { - describe('convertSchema', function () { - it('run a test', function () { - expect(true).toBe(true) + describe('getNestedSchema', function () { + it('get nested schema of an array', function () { + const simpleSchema = new SimpleSchema({ + addresses: { + type: Array + }, + "addresses.$": { + // this is due to SimpleSchema objects structure + type: addressSimpleSchema + } + }) + const nestedSchema = getNestedSchema('addresses', simpleSchema) + // nestedSchema is a complex SimpleSchema object, so we can only + // test its type instead (might not be the simplest way though) + expect(Object.keys(nestedSchema._schema)).toEqual(Object.keys(addressSchema)) + }) + it('get nested schema of an object', function () { + const simpleSchema = new SimpleSchema({ + meetingPlace: { + type: addressSimpleSchema + } + }) + const nestedSchema = getNestedSchema('meetingPlace', simpleSchema) + expect(Object.keys(nestedSchema._schema)).toEqual(Object.keys(addressSchema)) + }) + it('return null for other types', function () { + const simpleSchema = new SimpleSchema({ + createdAt: { + type: Date + } + }) + const nestedSchema = getNestedSchema('createdAt', simpleSchema) + expect(nestedSchema).toBeNull() }) }) }) \ No newline at end of file