diff --git a/config.xml b/config.xml index fefd838f72..95a268f8c2 100644 --- a/config.xml +++ b/config.xml @@ -10,7 +10,7 @@ - + @@ -29,6 +29,7 @@ + diff --git a/package.json b/package.json index 77222324ba..6d4d9d7d1d 100644 --- a/package.json +++ b/package.json @@ -26,7 +26,7 @@ "build-docs": "cross-env NODE_ENV=production jsdoc src -r -d docs-build -c ./jsdoc.conf.json --verbose", "electron": "electron ./www", "electron:dev": "cross-env NODE_ENV=development NC_DEVSERVER_FILE=\".devserver\" electron public/", - "ios:dev": "node scripts/check-dev-server.js && cross-env LIVE_RELOAD=1 cordova run ios --buildFlag='-UseModernBuildSystem=0' --developmentTeam=85EZ69PQHJ --device", + "ios:dev": "node scripts/check-dev-server.js && cross-env LIVE_RELOAD=1 cordova run ios --buildFlag='-UseModernBuildSystem=0' --developmentTeam=85EZ69PQHJ", "android:dev": "node scripts/check-dev-server.js && cross-env LIVE_RELOAD=1 cordova run android", "generate-icons": "node scripts/generate-app-icons.js", "dist:android": "npm run build:android && cordova build android --release", @@ -243,4 +243,4 @@ "cordova-plugin-network-canvas-client": {} } } -} +} \ No newline at end of file diff --git a/public/protocols/development.netcanvas/protocol.json b/public/protocols/development.netcanvas/protocol.json index 2f89444684..9b719854b2 100644 --- a/public/protocols/development.netcanvas/protocol.json +++ b/public/protocols/development.netcanvas/protocol.json @@ -1,6 +1,6 @@ { "name": "Development Protocol", - "description": "", + "description": "The Network Canvas development protocol is designed for our team to test new features. It is not intended for general use.", "lastModified": "2018-10-01T00:00:00.000Z", "networkCanvasVersion": "~4.0.0", "variableRegistry": { @@ -705,10 +705,8 @@ "type": "eda5e3bb-8e1c-4216-9e06-adc0ff6b7f73", "attributes": { "8a35cd77-7bc4-4c7e-b98a-673b6a21321f": "About My Health", - "8ee3a187-d4be-458e-8abb-71efcc071949": "", "1e9ce62c-44c5-484d-9c26-0d20cc7d7238": "About My Health", "66646d68-cd33-407b-a349-56707021df72": "About My Health", - "18fbc928-d027-42de-bc96-ff5c09bf4944": "", "67132d2b-c371-4c57-a5eb-6520083f9d22": "55 E Washington St, Chicago, Illinois, 60602", "bdc60147-fe7a-4c3c-a164-e5b370f6a281": 41.882945, "931a7b23-e433-4e7e-8e13-48b72e5f0549": -87.62572, @@ -722,7 +720,6 @@ "8ee3a187-d4be-458e-8abb-71efcc071949": "Access Anixter Center", "1e9ce62c-44c5-484d-9c26-0d20cc7d7238": "Access - Anixter", "66646d68-cd33-407b-a349-56707021df72": "Access - Anixter", - "18fbc928-d027-42de-bc96-ff5c09bf4944": "", "67132d2b-c371-4c57-a5eb-6520083f9d22": "2020 N Clybourn Ave, Chicago, Illinois, 60614", "bdc60147-fe7a-4c3c-a164-e5b370f6a281": 41.91837, "931a7b23-e433-4e7e-8e13-48b72e5f0549": -87.660281, @@ -736,7 +733,6 @@ "8ee3a187-d4be-458e-8abb-71efcc071949": "ACCESS Ashland Family Health Center", "1e9ce62c-44c5-484d-9c26-0d20cc7d7238": "Access - Ashland Family Health Center", "66646d68-cd33-407b-a349-56707021df72": "Access - Ashland Family Health Center", - "18fbc928-d027-42de-bc96-ff5c09bf4944": "", "67132d2b-c371-4c57-a5eb-6520083f9d22": "5159 S Ashland Ave, Chicago, IL 60609", "bdc60147-fe7a-4c3c-a164-e5b370f6a281": 41.799756, "931a7b23-e433-4e7e-8e13-48b72e5f0549": -87.66443, @@ -750,7 +746,6 @@ "8ee3a187-d4be-458e-8abb-71efcc071949": "ACCESS Auburn-Gresham Family Health Center", "1e9ce62c-44c5-484d-9c26-0d20cc7d7238": "Access - Auburn-Gresham Family Health Center", "66646d68-cd33-407b-a349-56707021df72": "Access - Auburn-Gresham Family Health Center", - "18fbc928-d027-42de-bc96-ff5c09bf4944": "", "67132d2b-c371-4c57-a5eb-6520083f9d22": "8234 S Ashland Ave, Chicago, IL 60620", "bdc60147-fe7a-4c3c-a164-e5b370f6a281": 41.743832, "931a7b23-e433-4e7e-8e13-48b72e5f0549": -87.663648, @@ -764,7 +759,6 @@ "8ee3a187-d4be-458e-8abb-71efcc071949": "ACCESS Booker Family Health Center", "1e9ce62c-44c5-484d-9c26-0d20cc7d7238": "Access - Booker Family Health Center", "66646d68-cd33-407b-a349-56707021df72": "Access - Booker Family Health Center", - "18fbc928-d027-42de-bc96-ff5c09bf4944": "", "67132d2b-c371-4c57-a5eb-6520083f9d22": "654 E 47th St, Chicago, IL 60653", "bdc60147-fe7a-4c3c-a164-e5b370f6a281": 41.809683, "931a7b23-e433-4e7e-8e13-48b72e5f0549": -87.609344, @@ -778,7 +772,6 @@ "8ee3a187-d4be-458e-8abb-71efcc071949": "ACCESS Brandon Family Health Center", "1e9ce62c-44c5-484d-9c26-0d20cc7d7238": "Access - Brandon Family Health Center", "66646d68-cd33-407b-a349-56707021df72": "Access - Brandon Family Health Center", - "18fbc928-d027-42de-bc96-ff5c09bf4944": "", "67132d2b-c371-4c57-a5eb-6520083f9d22": "8300 S Brandon Ave, Chicago, IL 60617", "bdc60147-fe7a-4c3c-a164-e5b370f6a281": 40.74453, "931a7b23-e433-4e7e-8e13-48b72e5f0549": -87.547014, @@ -792,7 +785,6 @@ "8ee3a187-d4be-458e-8abb-71efcc071949": "ACCESS Cabrini Family Health Center", "1e9ce62c-44c5-484d-9c26-0d20cc7d7238": "Access - Cabrini Family Health Center", "66646d68-cd33-407b-a349-56707021df72": "Access - Cabrini Family Health Center", - "18fbc928-d027-42de-bc96-ff5c09bf4944": "", "67132d2b-c371-4c57-a5eb-6520083f9d22": "3450 S Archer Ave, Chicago, IL 60608", "bdc60147-fe7a-4c3c-a164-e5b370f6a281": 41.831331, "931a7b23-e433-4e7e-8e13-48b72e5f0549": -87.676624, @@ -806,7 +798,6 @@ "8ee3a187-d4be-458e-8abb-71efcc071949": "ACCESS Centro Medico", "1e9ce62c-44c5-484d-9c26-0d20cc7d7238": "Access - Centro Medico", "66646d68-cd33-407b-a349-56707021df72": "Access - Centro Medico", - "18fbc928-d027-42de-bc96-ff5c09bf4944": "", "67132d2b-c371-4c57-a5eb-6520083f9d22": "3700 W 26th St, Chicago, IL 60623", "bdc60147-fe7a-4c3c-a164-e5b370f6a281": 41.844509, "931a7b23-e433-4e7e-8e13-48b72e5f0549": -87.717409, @@ -820,7 +811,6 @@ "8ee3a187-d4be-458e-8abb-71efcc071949": "ACCESS Centro Medico San Rafael", "1e9ce62c-44c5-484d-9c26-0d20cc7d7238": "Access - Centro Medico San Rafael", "66646d68-cd33-407b-a349-56707021df72": "Access - Centro Medico San Rafael", - "18fbc928-d027-42de-bc96-ff5c09bf4944": "", "67132d2b-c371-4c57-a5eb-6520083f9d22": "3204 W 26th St, Chicago, IL 60623", "bdc60147-fe7a-4c3c-a164-e5b370f6a281": 41.844713, "931a7b23-e433-4e7e-8e13-48b72e5f0549": -87.705448, @@ -834,7 +824,6 @@ "8ee3a187-d4be-458e-8abb-71efcc071949": "ACCESS Doctors Medical Center", "1e9ce62c-44c5-484d-9c26-0d20cc7d7238": "Access - Doctors Medical Center", "66646d68-cd33-407b-a349-56707021df72": "Access - Doctors Medical Center", - "18fbc928-d027-42de-bc96-ff5c09bf4944": "", "67132d2b-c371-4c57-a5eb-6520083f9d22": "6240 W 55th St, Chicago, IL 60638", "bdc60147-fe7a-4c3c-a164-e5b370f6a281": 41.792657, "931a7b23-e433-4e7e-8e13-48b72e5f0549": -87.778601, @@ -848,7 +837,6 @@ "8ee3a187-d4be-458e-8abb-71efcc071949": "ACCESS Evanston-Rogers Park Family Health Center", "1e9ce62c-44c5-484d-9c26-0d20cc7d7238": "Access - Evanston-Rogers Park Family Health Center", "66646d68-cd33-407b-a349-56707021df72": "Access - Evanston-Rogers Park Family Health Center", - "18fbc928-d027-42de-bc96-ff5c09bf4944": "", "67132d2b-c371-4c57-a5eb-6520083f9d22": "1555 Howard St, Chicago, IL 60626", "bdc60147-fe7a-4c3c-a164-e5b370f6a281": 42.019113, "931a7b23-e433-4e7e-8e13-48b72e5f0549": -87.670241, @@ -862,7 +850,6 @@ "8ee3a187-d4be-458e-8abb-71efcc071949": "ACCESS at Gary Comer Youth Center", "1e9ce62c-44c5-484d-9c26-0d20cc7d7238": "Access - Gary Comer Youth Center", "66646d68-cd33-407b-a349-56707021df72": "Access - Gary Comer Youth Center", - "18fbc928-d027-42de-bc96-ff5c09bf4944": "", "67132d2b-c371-4c57-a5eb-6520083f9d22": "7200 S Ingleside Ave, Chicago, IL 60619", "bdc60147-fe7a-4c3c-a164-e5b370f6a281": 41.764069, "931a7b23-e433-4e7e-8e13-48b72e5f0549": -87.602436, @@ -890,7 +877,6 @@ "8ee3a187-d4be-458e-8abb-71efcc071949": "ACCESS Humboldt Park Family Health Center", "1e9ce62c-44c5-484d-9c26-0d20cc7d7238": "Access - Humboldt Park Family Health Center", "66646d68-cd33-407b-a349-56707021df72": "Access - Humboldt Park Family Health Center", - "18fbc928-d027-42de-bc96-ff5c09bf4944": "", "67132d2b-c371-4c57-a5eb-6520083f9d22": "3202 W North Ave, Chicago, IL 60647", "bdc60147-fe7a-4c3c-a164-e5b370f6a281": 41.910444, "931a7b23-e433-4e7e-8e13-48b72e5f0549": -87.707085, @@ -904,7 +890,6 @@ "8ee3a187-d4be-458e-8abb-71efcc071949": "ACCESS at the Illinois Eye Institute", "1e9ce62c-44c5-484d-9c26-0d20cc7d7238": "Access - Illinois Eye Institute", "66646d68-cd33-407b-a349-56707021df72": "Access - Illinois Eye Institute", - "18fbc928-d027-42de-bc96-ff5c09bf4944": "", "67132d2b-c371-4c57-a5eb-6520083f9d22": "3241 S Michigan Ave, Chicago, IL 60616", "bdc60147-fe7a-4c3c-a164-e5b370f6a281": 41.835265, "931a7b23-e433-4e7e-8e13-48b72e5f0549": -87.622279, @@ -918,7 +903,6 @@ "8ee3a187-d4be-458e-8abb-71efcc071949": "ACCESS Kedzie Family Health Center", "1e9ce62c-44c5-484d-9c26-0d20cc7d7238": "Access - Kedzie Family Health Center", "66646d68-cd33-407b-a349-56707021df72": "Access - Kedzie Family Health Center", - "18fbc928-d027-42de-bc96-ff5c09bf4944": "", "67132d2b-c371-4c57-a5eb-6520083f9d22": "3213 W 47th Pl, Chicago, IL 60632", "bdc60147-fe7a-4c3c-a164-e5b370f6a281": 41.806249, "931a7b23-e433-4e7e-8e13-48b72e5f0549": -87.704915, @@ -932,7 +916,6 @@ "8ee3a187-d4be-458e-8abb-71efcc071949": "Servicios Medicos La Villita", "1e9ce62c-44c5-484d-9c26-0d20cc7d7238": "Access - La Villita", "66646d68-cd33-407b-a349-56707021df72": "Access - La Villita", - "18fbc928-d027-42de-bc96-ff5c09bf4944": "", "67132d2b-c371-4c57-a5eb-6520083f9d22": "3303 W 26th St, Chicago, Illinois, 60623", "bdc60147-fe7a-4c3c-a164-e5b370f6a281": 41.843835, "931a7b23-e433-4e7e-8e13-48b72e5f0549": -87.707797, @@ -946,7 +929,6 @@ "8ee3a187-d4be-458e-8abb-71efcc071949": "ACCESS Madison Family Health Center", "1e9ce62c-44c5-484d-9c26-0d20cc7d7238": "Access - Madison Family Health Center", "66646d68-cd33-407b-a349-56707021df72": "Access - Madison Family Health Center", - "18fbc928-d027-42de-bc96-ff5c09bf4944": "", "67132d2b-c371-4c57-a5eb-6520083f9d22": "3800 W Madison St, Chicago, IL 60624", "bdc60147-fe7a-4c3c-a164-e5b370f6a281": 41.881126, "931a7b23-e433-4e7e-8e13-48b72e5f0549": -87.721142, @@ -1134,10 +1116,11 @@ }, { "id": "2we", - "text": "Within the past 2 weeks, who has provided advice?", - "additionalAttributes": { - "03b03617-46ae-41cb-9462-9acd8a17edd6": true - } + "text": "Prompt with no additional attributes" + }, + { + "id": "2wj", + "text": "Second prompt with no additional attributes" } ] }, diff --git a/src/behaviours/DragAndDrop/reducer.js b/src/behaviours/DragAndDrop/reducer.js index a60c5d03c5..d2dff6588e 100644 --- a/src/behaviours/DragAndDrop/reducer.js +++ b/src/behaviours/DragAndDrop/reducer.js @@ -27,7 +27,7 @@ const willAccept = (accepts, source) => { ...source, }); } catch (e) { - console.log('Error in accept() function', e, source); // eslint-disable-line no-console + console.warn('Error in accept() function', e, source); // eslint-disable-line no-console return false; } }; diff --git a/src/components/Canvas/EdgeLayout.js b/src/components/Canvas/EdgeLayout.js index 26fea49db0..71be8c8558 100644 --- a/src/components/Canvas/EdgeLayout.js +++ b/src/components/Canvas/EdgeLayout.js @@ -13,9 +13,17 @@ export class EdgeLayout extends PureComponent { edges: [], }; - renderEdge = ({ key, from, to, type }) => ( - - ); + renderEdge = (edge) => { + if (!['key', 'from', 'to', 'type'].every(prop => prop in edge)) { + return null; + } + + const { key, from, to, type } = edge; + + return ( + + ); + }; render() { const { edges } = this.props; diff --git a/src/components/Canvas/NodeLayout.js b/src/components/Canvas/NodeLayout.js index 3c2dbff12f..93ae6953a2 100644 --- a/src/components/Canvas/NodeLayout.js +++ b/src/components/Canvas/NodeLayout.js @@ -1,6 +1,6 @@ import React, { Component } from 'react'; import PropTypes from 'prop-types'; -import { isEmpty, isEqual, pick, has } from 'lodash'; +import { isEmpty, isEqual, pick, has, isNil } from 'lodash'; import LayoutNode from '../../containers/Canvas/LayoutNode'; import { nodePrimaryKeyProperty, getNodeAttributes, nodeAttributesProperty } from '../../ducks/modules/network'; @@ -73,7 +73,9 @@ class NodeLayout extends Component {
{ nodes.map((node) => { const nodeAttributes = getNodeAttributes(node); - if (!has(nodeAttributes, layoutVariable)) { return null; } + if (!has(nodeAttributes, layoutVariable) || isNil(nodeAttributes[layoutVariable])) { + return null; + } return ( , @@ -79,7 +80,7 @@ ShallowWrapper { "y": 100, } } - type={undefined} + type="type" viewBoxScale={100} />, ], @@ -102,7 +103,7 @@ ShallowWrapper { "x": 100, "y": 100, }, - "type": undefined, + "type": "type", "viewBoxScale": 100, }, "ref": null, @@ -138,7 +139,7 @@ ShallowWrapper { "y": 100, } } - type={undefined} + type="type" viewBoxScale={100} /> , @@ -164,7 +165,7 @@ ShallowWrapper { "y": 100, } } - type={undefined} + type="type" viewBoxScale={100} />, ], @@ -187,7 +188,7 @@ ShallowWrapper { "x": 100, "y": 100, }, - "type": undefined, + "type": "type", "viewBoxScale": 100, }, "ref": null, diff --git a/src/containers/Canvas/LayoutNode.js b/src/containers/Canvas/LayoutNode.js index 3ce73557ba..b42d17640d 100644 --- a/src/containers/Canvas/LayoutNode.js +++ b/src/containers/Canvas/LayoutNode.js @@ -24,7 +24,6 @@ class LayoutNode extends PureComponent { selected, selectedColor, } = this.props; - const nodeAttributes = getNodeAttributes(node); const { x, y } = nodeAttributes[layoutVariable]; diff --git a/src/containers/Canvas/NodeLayout.js b/src/containers/Canvas/NodeLayout.js index a34c40581c..f8d6a1ecfa 100644 --- a/src/containers/Canvas/NodeLayout.js +++ b/src/containers/Canvas/NodeLayout.js @@ -1,7 +1,7 @@ import { bindActionCreators } from 'redux'; +import { isNil } from 'lodash'; import { connect } from 'react-redux'; import { compose, withHandlers, withState } from 'recompose'; -import { has } from 'lodash'; import { withBounds } from '../../behaviours'; import { actionCreators as sessionsActions } from '../../ducks/modules/sessions'; import { nodePrimaryKeyProperty, nodeAttributesProperty } from '../../ducks/modules/network'; @@ -21,7 +21,8 @@ const withDropHandlers = withHandlers({ onDrop: ({ updateNode, layoutVariable, setRerenderCount, rerenderCount, width, height, x, y }) => (item) => { updateNode( - item.meta, + item.meta[nodePrimaryKeyProperty], + {}, { [layoutVariable]: relativeCoords({ width, height, x, y }, item), }, @@ -30,19 +31,18 @@ const withDropHandlers = withHandlers({ // Horrible hack for performance (only re-render nodes on drop, not on drag) setRerenderCount(rerenderCount + 1); }, - onDrag: ({ layoutVariable, updateNode, width, height, x, y }) => (item) => { - if (!has(item.meta[nodeAttributesProperty], layoutVariable)) { return; } - - updateNode( - item.meta, - { - [layoutVariable]: relativeCoords({ width, height, x, y }, item), - }, - ); - }, - onDragEnd: ({ layoutVariable, setRerenderCount, rerenderCount }) => (item) => { - if (!has(item.meta[nodeAttributesProperty], layoutVariable)) { return; } - + onDrag: ({ layoutVariable, updateNode, width, height, x, y }) => + (item) => { + if (isNil(item.meta[nodeAttributesProperty][layoutVariable])) { return; } + updateNode( + item.meta[nodePrimaryKeyProperty], + {}, + { + [layoutVariable]: relativeCoords({ width, height, x, y }, item), + }, + ); + }, + onDragEnd: ({ setRerenderCount, rerenderCount }) => () => { // make sure to also re-render nodes that were updated on drag end setRerenderCount(rerenderCount + 1); }, diff --git a/src/containers/CategoricalList.js b/src/containers/CategoricalList.js index b87cd200c5..c6c2105854 100644 --- a/src/containers/CategoricalList.js +++ b/src/containers/CategoricalList.js @@ -139,8 +139,11 @@ class CategoricalList extends Component { return; } - this.props.toggleNodeAttributes(meta[nodePrimaryKeyProperty], - { [this.props.activePromptVariable]: [binValue] }); + this.props.updateNode( + meta[nodePrimaryKeyProperty], + {}, + { [this.props.activePromptVariable]: [binValue] }, + ); }; renderCategoricalBin = (bin, index, sizes) => { @@ -219,7 +222,7 @@ CategoricalList.propTypes = { displayVariable: PropTypes.string.isRequired, prompt: PropTypes.object.isRequired, stage: PropTypes.object.isRequired, - toggleNodeAttributes: PropTypes.func.isRequired, + updateNode: PropTypes.func.isRequired, }; CategoricalList.defaultProps = { @@ -259,7 +262,7 @@ function makeMapStateToProps() { function mapDispatchToProps(dispatch) { return { - toggleNodeAttributes: bindActionCreators(sessionsActions.toggleNodeAttributes, dispatch), + updateNode: bindActionCreators(sessionsActions.updateNode, dispatch), }; } diff --git a/src/containers/Interfaces/NameGenerator.js b/src/containers/Interfaces/NameGenerator.js index b5a8606b82..bb5d2c15c2 100644 --- a/src/containers/Interfaces/NameGenerator.js +++ b/src/containers/Interfaces/NameGenerator.js @@ -2,14 +2,15 @@ import React, { Component } from 'react'; import { bindActionCreators, compose } from 'redux'; import { connect } from 'react-redux'; import PropTypes from 'prop-types'; -import { get, has } from 'lodash'; +import { get, has, omit } from 'lodash'; import withPrompt from '../../behaviours/withPrompt'; import { actionCreators as sessionsActions } from '../../ducks/modules/sessions'; -import { makeNetworkNodesForPrompt } from '../../selectors/interface'; -import { makeGetPromptNodeAttributes, makeGetNodeIconName } from '../../selectors/name-generator'; +import { makeNetworkNodesForPrompt, makeGetAdditionalAttributes } from '../../selectors/interface'; +import { makeGetPromptNodeModelData, makeGetNodeIconName } from '../../selectors/name-generator'; import { PromptSwiper, NodePanels, NodeForm } from '../'; import { NodeList, NodeBin } from '../../components/'; import { Icon } from '../../ui/components'; +import { nodeAttributesProperty, nodePrimaryKeyProperty } from '../../ducks/modules/network'; /** * Name Generator Interface @@ -31,9 +32,19 @@ class NameGenerator extends Component { handleSubmitForm = ({ form, addAnotherNode } = { addAnotherNode: false }) => { if (form) { if (!this.state.selectedNode) { - this.props.addNodes({ attributes: { ...form } }, this.props.newNodeAttributes); + /** + * addNode(modelData, attributeData); + */ + this.props.addNode( + this.props.newNodeModelData, + { ...this.props.newNodeAttributes, ...form }, + ); } else { - this.props.updateNode({ ...this.state.selectedNode }, form); + /** + * updateNode(nodeId, newModelData, newAttributeData) + */ + const selectedUID = this.state.selectedNode[nodePrimaryKeyProperty]; + this.props.updateNode(selectedUID, {}, form); } } @@ -43,15 +54,25 @@ class NameGenerator extends Component { /** * Drop node handler * Adds prompt attributes to existing nodes, or adds new nodes to the network. - * @param {object} node - key/value object containing node object from the network store + * @param {object} item - key/value object containing node object from the network store */ handleDropNode = (item) => { const node = { ...item.meta }; // Test if we are updating an existing network node, or adding it to the network - if (has(node, 'promptId') || has(node, 'stageId')) { - this.props.updateNode(node, { ...this.props.activePromptAttributes }); + if (has(node, 'promptIDs')) { + this.props.updateNode( + node[nodePrimaryKeyProperty], + { ...this.props.newNodeModelData }, + { ...this.props.activePromptAttributes }, + ); } else { - this.props.addNodes(node, this.props.newNodeAttributes); + const droppedAttributeData = node[nodeAttributesProperty]; + const droppedModelData = omit(node, nodeAttributesProperty); + + this.props.addNode( + { ...this.props.newNodeModelData, ...droppedModelData }, + { ...droppedAttributeData, ...this.props.newNodeAttributes }, + ); } } @@ -155,9 +176,10 @@ NameGenerator.defaultProps = { NameGenerator.propTypes = { activePromptAttributes: PropTypes.object, - addNodes: PropTypes.func.isRequired, + addNode: PropTypes.func.isRequired, form: PropTypes.object, newNodeAttributes: PropTypes.object.isRequired, + newNodeModelData: PropTypes.object.isRequired, nodesForPrompt: PropTypes.array.isRequired, nodeIconName: PropTypes.string.isRequired, prompt: PropTypes.object.isRequired, @@ -169,13 +191,15 @@ NameGenerator.propTypes = { function makeMapStateToProps() { const networkNodesForPrompt = makeNetworkNodesForPrompt(); - const getPromptNodeAttributes = makeGetPromptNodeAttributes(); + const getPromptNodeAttributes = makeGetAdditionalAttributes(); + const getPromptNodeModelData = makeGetPromptNodeModelData(); const getNodeIconName = makeGetNodeIconName(); return function mapStateToProps(state, props) { return { activePromptAttributes: props.prompt.additionalAttributes, newNodeAttributes: getPromptNodeAttributes(state, props), + newNodeModelData: getPromptNodeModelData(state, props), nodesForPrompt: networkNodesForPrompt(state, props), nodeIconName: getNodeIconName(state, props), }; @@ -184,7 +208,7 @@ function makeMapStateToProps() { function mapDispatchToProps(dispatch) { return { - addNodes: bindActionCreators(sessionsActions.addNodes, dispatch), + addNode: bindActionCreators(sessionsActions.addNode, dispatch), updateNode: bindActionCreators(sessionsActions.updateNode, dispatch), }; } diff --git a/src/containers/Interfaces/NameGeneratorAutoComplete.js b/src/containers/Interfaces/NameGeneratorAutoComplete.js index ffcdf23e6f..fe71967f59 100644 --- a/src/containers/Interfaces/NameGeneratorAutoComplete.js +++ b/src/containers/Interfaces/NameGeneratorAutoComplete.js @@ -1,4 +1,5 @@ import React, { Component } from 'react'; +import { map } from 'lodash'; import { bindActionCreators, compose } from 'redux'; import { connect } from 'react-redux'; import PropTypes from 'prop-types'; @@ -10,23 +11,23 @@ import Search from '../../containers/Search'; import { actionCreators as sessionsActions } from '../../ducks/modules/sessions'; import { actionCreators as searchActions } from '../../ducks/modules/search'; import { nodeAttributesProperty } from '../../ducks/modules/network'; -import { getNodeLabelFunction, makeGetSubjectType, makeNetworkNodesForPrompt, networkNodes } from '../../selectors/interface'; -import { getCardDisplayLabel, getCardAdditionalProperties, makeGetNodeIconName, makeGetPromptNodeAttributes } from '../../selectors/name-generator'; +import { getNodeLabelFunction, makeGetSubjectType, makeNetworkNodesForPrompt, networkNodes, makeGetAdditionalAttributes } from '../../selectors/interface'; +import { getCardDisplayLabel, getCardAdditionalProperties, makeGetNodeIconName, makeGetPromptNodeModelData } from '../../selectors/name-generator'; import { PromptSwiper } from '../'; import { NodeBin, NodeList } from '../../components/'; -const networkNodesForPrompt = makeNetworkNodesForPrompt(); -const getPromptNodeAttributes = makeGetPromptNodeAttributes(); -const getNodeType = makeGetSubjectType(); -const getNodeIconName = makeGetNodeIconName(); - /** * NameGeneratorAutoComplete Interface * @extends Component */ class NameGeneratorAutoComplete extends Component { onSearchComplete(selectedResults) { - this.props.addNodes(selectedResults, this.props.newNodeAttributes); + const withNewModelData = map(selectedResults, result => ({ + ...this.props.newNodeModelData, + ...result, + })); + + this.props.batchAddNodes(withNewModelData, this.props.newNodeAttributes); this.props.closeSearch(); } @@ -113,12 +114,13 @@ class NameGeneratorAutoComplete extends Component { } NameGeneratorAutoComplete.propTypes = { - addNodes: PropTypes.func.isRequired, + batchAddNodes: PropTypes.func.isRequired, closeSearch: PropTypes.func.isRequired, excludedNodes: PropTypes.array.isRequired, getLabel: PropTypes.func.isRequired, labelKey: PropTypes.string.isRequired, newNodeAttributes: PropTypes.object.isRequired, + newNodeModelData: PropTypes.object.isRequired, nodesForPrompt: PropTypes.array.isRequired, nodeIconName: PropTypes.string.isRequired, nodeType: PropTypes.string.isRequired, @@ -133,19 +135,26 @@ NameGeneratorAutoComplete.propTypes = { function mapDispatchToProps(dispatch) { return { - addNodes: bindActionCreators(sessionsActions.addNodes, dispatch), + batchAddNodes: bindActionCreators(sessionsActions.batchAddNodes, dispatch), closeSearch: bindActionCreators(searchActions.closeSearch, dispatch), toggleSearch: bindActionCreators(searchActions.toggleSearch, dispatch), }; } function makeMapStateToProps() { + const networkNodesForPrompt = makeNetworkNodesForPrompt(); + const getPromptNodeAttributes = makeGetAdditionalAttributes(); + const getPromptNodeModelData = makeGetPromptNodeModelData(); + const getNodeType = makeGetSubjectType(); + const getNodeIconName = makeGetNodeIconName(); + return function mapStateToProps(state, props) { return { excludedNodes: networkNodes(state, props), getLabel: getNodeLabelFunction(state), labelKey: getCardDisplayLabel(state, props), newNodeAttributes: getPromptNodeAttributes(state, props), + newNodeModelData: getPromptNodeModelData(state, props), nodeIconName: getNodeIconName(state, props), nodesForPrompt: networkNodesForPrompt(state, props), nodeType: getNodeType(state, props), diff --git a/src/containers/Interfaces/NameGeneratorList.js b/src/containers/Interfaces/NameGeneratorList.js index 2c8ea50251..d4a1702cca 100644 --- a/src/containers/Interfaces/NameGeneratorList.js +++ b/src/containers/Interfaces/NameGeneratorList.js @@ -2,19 +2,19 @@ import React, { Component } from 'react'; import { bindActionCreators, compose } from 'redux'; import { connect } from 'react-redux'; import PropTypes from 'prop-types'; -import { differenceBy } from 'lodash'; +import { differenceBy, omit } from 'lodash'; import withPrompt from '../../behaviours/withPrompt'; import { actionCreators as sessionsActions } from '../../ducks/modules/sessions'; -import { nodePrimaryKeyProperty, getNodeAttributes } from '../../ducks/modules/network'; -import { makeNetworkNodesForOtherPrompts, networkNodes } from '../../selectors/interface'; +import { nodePrimaryKeyProperty, getNodeAttributes, nodeAttributesProperty } from '../../ducks/modules/network'; +import { makeNetworkNodesForOtherPrompts, networkNodes, makeGetAdditionalAttributes } from '../../selectors/interface'; import { getDataByPrompt, getCardDisplayLabel, getCardAdditionalProperties, getSortableFields, - makeGetPromptNodeAttributes, getInitialSortOrder, + makeGetPromptNodeModelData, } from '../../selectors/name-generator'; import { PromptSwiper } from '../../containers'; import { ListSelect } from '../../components'; @@ -37,7 +37,15 @@ class NameGeneratorList extends Component { * Select node submit handler */ onSubmitNewNode = (node) => { - this.props.addNode({ ...node }, { ...this.props.newNodeAttributes }); + const attributeData = { + ...this.props.newNodeAttributes, + ...node[nodeAttributesProperty], + }; + const modelData = { + ...this.props.newNodeModelData, + ...omit(node, nodeAttributesProperty), + }; + this.props.addNode(modelData, attributeData); } onRemoveNode = (node) => { @@ -101,6 +109,7 @@ NameGeneratorList.propTypes = { initialSortOrder: PropTypes.array.isRequired, labelKey: PropTypes.string.isRequired, newNodeAttributes: PropTypes.object.isRequired, + newNodeModelData: PropTypes.object.isRequired, nodesForList: PropTypes.array.isRequired, prompt: PropTypes.object.isRequired, promptForward: PropTypes.func.isRequired, @@ -112,15 +121,9 @@ NameGeneratorList.propTypes = { visibleSupplementaryFields: PropTypes.array.isRequired, }; -// NameGeneratorList.defaultProps = { -// initialSortOrder: [{ -// property: '', -// direction: 'asc', -// }], -// }; - function makeMapStateToProps() { - const getPromptNodeAttributes = makeGetPromptNodeAttributes(); + const getPromptNodeAttributes = makeGetAdditionalAttributes(); + const getPromptNodeModelData = makeGetPromptNodeModelData(); const networkNodesForOtherPrompts = makeNetworkNodesForOtherPrompts(); return function mapStateToProps(state, props) { @@ -135,6 +138,7 @@ function makeMapStateToProps() { return { labelKey: getCardDisplayLabel(state, props), newNodeAttributes: getPromptNodeAttributes(state, props), + newNodeModelData: getPromptNodeModelData(state, props), nodesForList, initialSortOrder: getInitialSortOrder(state, props), selectedNodes: networkNodes(state), @@ -146,7 +150,7 @@ function makeMapStateToProps() { function mapDispatchToProps(dispatch) { return { - addNode: bindActionCreators(sessionsActions.addNodes, dispatch), + addNode: bindActionCreators(sessionsActions.addNode, dispatch), removeNode: bindActionCreators(sessionsActions.removeNode, dispatch), }; } diff --git a/src/containers/Interfaces/OrdinalBin.js b/src/containers/Interfaces/OrdinalBin.js index 6c50ab2c6e..f136d6a0f7 100644 --- a/src/containers/Interfaces/OrdinalBin.js +++ b/src/containers/Interfaces/OrdinalBin.js @@ -1,12 +1,11 @@ import React from 'react'; -import { bindActionCreators, compose } from 'redux'; +import { compose } from 'redux'; import { connect } from 'react-redux'; import PropTypes from 'prop-types'; import withPrompt from '../../behaviours/withPrompt'; import { PromptSwiper, OrdinalBins } from '../'; import { makeGetPromptVariable, makeNetworkNodesForType } from '../../selectors/interface'; import { MultiNodeBucket } from '../../components'; -import { actionCreators as sessionsActions } from '../../ducks/modules/sessions'; import { nodeAttributesProperty } from '../../ducks/modules/network'; /** @@ -72,13 +71,7 @@ function makeMapStateToProps() { }; } -function mapDispatchToProps(dispatch) { - return { - updateNode: bindActionCreators(sessionsActions.updateNode, dispatch), - }; -} - export default compose( withPrompt, - connect(makeMapStateToProps, mapDispatchToProps), + connect(makeMapStateToProps), )(OrdinalBin); diff --git a/src/containers/Interfaces/__tests__/NameGenerator.test.js b/src/containers/Interfaces/__tests__/NameGenerator.test.js index 3b5cdf477e..01075b8ee0 100644 --- a/src/containers/Interfaces/__tests__/NameGenerator.test.js +++ b/src/containers/Interfaces/__tests__/NameGenerator.test.js @@ -5,7 +5,7 @@ import { shallow } from 'enzyme'; import { UnconnectedNameGenerator as NameGenerator } from '../NameGenerator'; const requiredProps = { - addNodes: jest.fn(), + addNode: jest.fn(), getLabel: jest.fn(), newNodeAttributes: {}, nodesForPrompt: [], diff --git a/src/containers/MainMenu/__tests__/MainMenu.test.js b/src/containers/MainMenu/__tests__/MainMenu.test.js index 584dbebdb9..5ca65f765e 100644 --- a/src/containers/MainMenu/__tests__/MainMenu.test.js +++ b/src/containers/MainMenu/__tests__/MainMenu.test.js @@ -130,8 +130,7 @@ describe('', () => { it('Mock data button', () => { subject.find('Button[children="Add mock nodes"]').at(0).simulate('click'); - expect(actions.filter(({ type }) => type === 'ADD_NODES')).toHaveLength(1); - expect(actions.filter(({ type }) => type === 'ADD_NODES')[0].nodes).toHaveLength(20); + expect(actions.filter(({ type }) => type === 'ADD_NODE')).toHaveLength(20); expect(isMenuOpen(subject)).toBe(false); }); }); diff --git a/src/containers/NodePanels.js b/src/containers/NodePanels.js index c3492b2090..52f9ea1a12 100644 --- a/src/containers/NodePanels.js +++ b/src/containers/NodePanels.js @@ -3,11 +3,11 @@ import { compose, bindActionCreators } from 'redux'; import { connect } from 'react-redux'; import PropTypes from 'prop-types'; import { includes, map, differenceBy } from 'lodash'; -import { networkNodes, makeNetworkNodesForOtherPrompts } from '../selectors/interface'; +import { networkNodes, makeNetworkNodesForOtherPrompts, makeGetAdditionalAttributes } from '../selectors/interface'; import { getExternalData } from '../selectors/externalData'; import { actionCreators as sessionsActions } from '../ducks/modules/sessions'; import { nodePrimaryKeyProperty } from '../ducks/modules/network'; -import { makeGetPromptNodeAttributes, makeGetPanelConfiguration } from '../selectors/name-generator'; +import { makeGetPanelConfiguration } from '../selectors/name-generator'; import { Panel, Panels, NodeList } from '../components/'; import { getCSSVariableAsString } from '../ui/utils/CSSVariables'; import { MonitorDragSource } from '../behaviours/DragAndDrop'; @@ -17,7 +17,6 @@ import { MonitorDragSource } from '../behaviours/DragAndDrop'; */ class NodePanels extends PureComponent { static propTypes = { - activePromptAttributes: PropTypes.object, isDragging: PropTypes.bool, meta: PropTypes.object, panels: PropTypes.array, @@ -25,11 +24,10 @@ class NodePanels extends PureComponent { newNodeAttributes: PropTypes.object.isRequired, removeNode: PropTypes.func.isRequired, stage: PropTypes.object, - toggleNodeAttributes: PropTypes.func.isRequired, + removeNodeFromPrompt: PropTypes.func.isRequired, }; static defaultProps = { - activePromptAttributes: {}, isDragging: false, meta: {}, panels: [], @@ -40,13 +38,14 @@ class NodePanels extends PureComponent { onDrop = ({ meta }, dataSource) => { /** * Handle a node being dropped into a panel - * - * If + * If this panel is showing the interview network, remove the node from the current prompt. + * If it is an external data panel, remove the node form the interview network. */ if (dataSource === 'existing') { - this.props.toggleNodeAttributes( + this.props.removeNodeFromPrompt( meta[nodePrimaryKeyProperty], - { ...this.props.activePromptAttributes }, + this.props.prompt.id, + this.props.newNodeAttributes, ); } else { this.props.removeNode(meta[nodePrimaryKeyProperty]); @@ -116,12 +115,17 @@ class NodePanels extends PureComponent { } } -const getNodesForDataSource = ({ nodes, existingNodes, externalData, dataSource }) => ( +/** + * + * @param {array} nodes - all network nodes + * + */ +const getNodesForDataSource = ({ sessionNodes, otherPromptNodes, externalData, dataSource }) => ( dataSource === 'existing' ? - existingNodes : + otherPromptNodes : differenceBy( - externalData[dataSource] && externalData[dataSource].nodes, - nodes, + externalData[dataSource].nodes, + sessionNodes, nodePrimaryKeyProperty, ) ); @@ -133,13 +137,13 @@ const getOriginNodeIds = ({ existingNodes, externalData, dataSource }) => ( ); function makeMapStateToProps() { - const getPromptNodeAttributes = makeGetPromptNodeAttributes(); - const networkNodesForOtherPrompts = makeNetworkNodesForOtherPrompts(); + const getPromptNodeAttributes = makeGetAdditionalAttributes(); const getPanelConfiguration = makeGetPanelConfiguration(); + const getNetworkNodesForOtherPrompts = makeNetworkNodesForOtherPrompts(); return function mapStateToProps(state, props) { const allNodes = networkNodes(state); - const existingNodes = networkNodesForOtherPrompts(state, props); + const existingNodes = getNetworkNodesForOtherPrompts(state, props); const externalData = getExternalData(state); const newNodeAttributes = getPromptNodeAttributes(state, props); @@ -152,18 +156,18 @@ function makeMapStateToProps() { }); const nodes = getNodesForDataSource({ - nodes: allNodes, - existingNodes, + sessionNodes: allNodes, + otherPromptNodes: existingNodes, externalData, dataSource: panel.dataSource, }); const accepts = (panel.dataSource === 'existing') ? ({ meta }) => ( - meta.itemType === 'EXISTING_NODE' && - (meta.stageId !== newNodeAttributes.stageId || - meta.promptId !== newNodeAttributes.promptId) + // existing network node + meta.itemType === 'EXISTING_NODE' ) : ({ meta }) => ( + // external data meta.itemType === 'EXISTING_NODE' && includes(originNodeIds, meta[nodePrimaryKeyProperty]) ); @@ -177,7 +181,7 @@ function makeMapStateToProps() { }); return { - activePromptAttributes: props.prompt.additionalAttributes, + activePromptId: props.prompt.id, newNodeAttributes, panels, }; @@ -186,7 +190,7 @@ function makeMapStateToProps() { function mapDispatchToProps(dispatch) { return { - toggleNodeAttributes: bindActionCreators(sessionsActions.toggleNodeAttributes, dispatch), + removeNodeFromPrompt: bindActionCreators(sessionsActions.removeNodeFromPrompt, dispatch), removeNode: bindActionCreators(sessionsActions.removeNode, dispatch), }; } diff --git a/src/containers/OrdinalBins.js b/src/containers/OrdinalBins.js index 9167688c49..067b4a4df4 100644 --- a/src/containers/OrdinalBins.js +++ b/src/containers/OrdinalBins.js @@ -16,7 +16,7 @@ class OrdinalBins extends PureComponent { bins: PropTypes.array.isRequired, prompt: PropTypes.object.isRequired, stage: PropTypes.object.isRequired, - toggleNodeAttributes: PropTypes.func.isRequired, + updateNode: PropTypes.func.isRequired, }; static defaultProps = { @@ -61,9 +61,11 @@ class OrdinalBins extends PureComponent { return; } - const newValue = {}; - newValue[this.props.activePromptVariable] = bin.value; - this.props.toggleNodeAttributes(meta[nodePrimaryKeyProperty], newValue); + this.props.updateNode( + meta[nodePrimaryKeyProperty], + {}, + { [this.props.activePromptVariable]: bin.value }, + ); }; const accentColor = this.calculateAccentColor(index, missingValue); @@ -128,7 +130,7 @@ function makeMapStateToProps() { function mapDispatchToProps(dispatch) { return { - toggleNodeAttributes: bindActionCreators(sessionsActions.toggleNodeAttributes, dispatch), + updateNode: bindActionCreators(sessionsActions.updateNode, dispatch), }; } diff --git a/src/containers/Search/Search.js b/src/containers/Search/Search.js index 6a4dd71033..b07a7d5470 100644 --- a/src/containers/Search/Search.js +++ b/src/containers/Search/Search.js @@ -170,7 +170,7 @@ class Search extends Component { className={searchClasses} in={!collapsed} > -
+ { e.preventDefault(); }}> this.onClose(evt)} /> {Headers} diff --git a/src/containers/__tests__/NodePanels.test.js b/src/containers/__tests__/NodePanels.test.js index 390a32c5dd..dfcccb0cda 100644 --- a/src/containers/__tests__/NodePanels.test.js +++ b/src/containers/__tests__/NodePanels.test.js @@ -7,7 +7,6 @@ import { NodePanels } from '../NodePanels'; jest.mock('../../ui/utils/CSSVariables'); const mockProps = { - toggleNodeAttributes: () => {}, removeNode: () => {}, activePromptAttributes: {}, newNodeAttributes: {}, diff --git a/src/containers/__tests__/__snapshots__/NodePanels.test.js.snap b/src/containers/__tests__/__snapshots__/NodePanels.test.js.snap index e71c5b28fc..5a980ab0e5 100644 --- a/src/containers/__tests__/__snapshots__/NodePanels.test.js.snap +++ b/src/containers/__tests__/__snapshots__/NodePanels.test.js.snap @@ -21,7 +21,6 @@ ShallowWrapper { "id": null, } } - toggleNodeAttributes={[Function]} />, Symbol(enzyme.__renderer__): Object { "batchedUpdates": [Function], diff --git a/src/ducks/modules/__tests__/network.test.js b/src/ducks/modules/__tests__/network.test.js index 82c6e22417..c360283133 100644 --- a/src/ducks/modules/__tests__/network.test.js +++ b/src/ducks/modules/__tests__/network.test.js @@ -3,6 +3,7 @@ import reducer, { actionTypes, nodePrimaryKeyProperty as PK, + nodeAttributesProperty, } from '../network'; const mockState = { @@ -11,95 +12,55 @@ const mockState = { edges: [], }; -const UIDPattern = /[a-f\d]+-/; - describe('network reducer', () => { it('should return the initial state', () => { expect(reducer(undefined, {})).toEqual(mockState); }); - it('should handle ADD_NODES with a single node', () => { + it('should handle ADD_NODE', () => { const newState = reducer( { ...mockState, - nodes: [{ id: 1, attributes: { name: 'baz' } }], }, { - type: actionTypes.ADD_NODES, - nodes: [{ attributes: { name: 'foo' } }], + type: actionTypes.ADD_NODE, + modelData: { [PK]: '383a6119e94aa2a1b2e1a5e84b2936b753437a11' }, + attributeData: { name: 'foo' }, }, ); + expect(newState.nodes.length).toBe(1); + expect(newState.nodes[0]).toEqual({ [PK]: '383a6119e94aa2a1b2e1a5e84b2936b753437a11', [nodeAttributesProperty]: { name: 'foo' }, itemType: undefined, promptIDs: [undefined], stageId: undefined, type: undefined }); - expect(newState.nodes.length).toBe(2); - expect(newState.nodes[0]).toEqual({ id: 1, attributes: { name: 'baz' } }); - - const newNode = newState.nodes[1]; + const newNode = newState.nodes[0]; expect(newNode.attributes.name).toEqual('foo'); - expect(newNode[PK]).toMatch(UIDPattern); - }); - - it('should handle ADD_NODES', () => { - const newState = reducer( - { - ...mockState, - nodes: [{ [PK]: 1, attributes: { name: 'baz' } }], - }, - { - type: actionTypes.ADD_NODES, - nodes: [{ attributes: { name: 'foo' } }, { attributes: { name: 'bar' } }], - }, - ); - - expect(newState.nodes.length).toBe(3); - expect(newState.nodes[0]).toEqual({ [PK]: 1, attributes: { name: 'baz' } }); - expect(newState.nodes[1]).toMatchObject({ attributes: { name: 'foo' }, [PK]: expect.stringMatching(UIDPattern) }); - expect(newState.nodes[2]).toMatchObject({ attributes: { name: 'bar' }, [PK]: expect.stringMatching(UIDPattern) }); }); it('preserves UID when adding a node', () => { const newState = reducer( mockState, { - type: actionTypes.ADD_NODES, - nodes: [{ attributes: { name: 'foo' }, [PK]: '22' }], + type: actionTypes.ADD_NODE, + modelData: { [PK]: '22' }, + attributeData: { name: 'foo' }, }, ); expect(newState.nodes[0][PK]).toEqual('22'); }); - it('should support additionalProperties for ADD_NODES', () => { + it('should support additionalProperties for ADD_NODE', () => { const newState = reducer( { ...mockState, nodes: [], }, { - type: actionTypes.ADD_NODES, - nodes: [{ attributes: { name: 'foo' } }, { attributes: { name: 'bar' } }], - additionalProperties: { stageId: '2', attributes: { isFriend: true } }, + type: actionTypes.ADD_NODE, + modelData: {}, + attributeData: { name: 'foo', isFriend: true }, }, ); - expect(newState.nodes[0].stageId).toBe('2'); - expect(newState.nodes[1].stageId).toBe('2'); expect(newState.nodes[0].attributes.isFriend).toBe(true); - expect(newState.nodes[1].attributes.isFriend).toBe(true); - expect(newState.nodes[0].attributes.name).toEqual('foo'); - expect(newState.nodes[1].attributes.name).toEqual('bar'); - }); - - it('should prefer node.attributes to additionalAttributes.attributes ', () => { - const newState = reducer( - { - ...mockState, - nodes: [], - }, - { - type: actionTypes.ADD_NODES, - nodes: [{ attributes: { name: 'foo' } }], - additionalAttributes: { attributes: { name: 'defaultName' } }, - }, - ); expect(newState.nodes[0].attributes.name).toEqual('foo'); }); @@ -136,14 +97,16 @@ describe('network reducer', () => { const newState = reducer( { ...mockState, - nodes: [{ [PK]: 1, id: 1, name: 'baz' }], + nodes: [{ [PK]: 1, id: 1, [nodeAttributesProperty]: { name: 'baz' } }], }, { type: actionTypes.UPDATE_NODE, - node: { [PK]: 1, name: 'foo' }, + nodeId: 1, + newModelData: {}, + newAttributeData: { name: 'foo' }, }, ); - expect(newState.nodes[0]).toEqual({ [PK]: 1, id: 1, name: 'foo' }); + expect(newState.nodes[0]).toEqual({ [PK]: 1, id: 1, [nodeAttributesProperty]: { name: 'foo' } }); }); it('toggles node attributes on', () => { diff --git a/src/ducks/modules/__tests__/sessions.test.js b/src/ducks/modules/__tests__/sessions.test.js index 56436ef27c..811d036f88 100644 --- a/src/ducks/modules/__tests__/sessions.test.js +++ b/src/ducks/modules/__tests__/sessions.test.js @@ -92,12 +92,13 @@ describe('sessions reducer', () => { expect(newState[mockSessionId]).toEqual(undefined); }); - it('should handle ADD_NODES', () => { + it('should handle ADD_NODE', () => { const newState = reducer(mockStateWithSession, { - type: actionTypes.ADD_NODES, + type: actionTypes.ADD_NODE, sessionId: mockSessionId, - nodes: [{}], + modelData: {}, + attributeData: {}, }, ); expect(newState[mockSessionId].network.nodes).toHaveLength(1); @@ -106,7 +107,7 @@ describe('sessions reducer', () => { it('should throw if ADD_NODES called without an active session', () => { expect(() => reducer(mockState, { - type: actionTypes.ADD_NODES, + type: actionTypes.ADD_NODE, sessionId: 'a', nodes: [{}], }, @@ -115,30 +116,30 @@ describe('sessions reducer', () => { }); describe('sessions actions', () => { - it('should create an ADD_NODES action with a single node', () => { - const store = mockStore({ sessions: { a: {} }, session: 'a' }); - - const expectedAction = { - type: actionTypes.ADD_NODES, - sessionId: 'a', - nodes: [{ name: 'foo' }], - }; - - store.dispatch(actionCreators.addNodes({ name: 'foo' })); - - expect(store.getActions()).toEqual([expectedAction]); - }); - - it('should create an ADD_NODES action for batch adding', () => { - const store = mockStore({ sessions: { a: {} }, session: 'a' }); + it('should create an BATCH_ADD_NODES action for batch adding', () => { + const store = mockStore({ + sessions: { a: {} }, + session: 'a', + protocol: { + variableRegistry: { + node: { + nodeType: { + variables: {}, + }, + }, + }, + }, + }); const expectedAction = { - type: actionTypes.ADD_NODES, + type: actionTypes.BATCH_ADD_NODES, sessionId: 'a', - nodes: [{ name: 'foo' }, { name: 'bar' }], + nodeList: [], + attributeData: {}, + registryForTypes: {}, }; - store.dispatch(actionCreators.addNodes([{ name: 'foo' }, { name: 'bar' }])); + store.dispatch(actionCreators.batchAddNodes([], {})); expect(store.getActions()).toEqual([expectedAction]); }); @@ -148,11 +149,12 @@ describe('sessions actions', () => { const expectedAction = { type: actionTypes.UPDATE_NODE, sessionId: 'a', - node: {}, - additionalProperties: null, + nodeId: {}, + newAttributeData: null, + newModelData: {}, }; - store.dispatch(actionCreators.updateNode({})); + store.dispatch(actionCreators.updateNode({}, {})); expect(store.getActions()).toEqual([expectedAction]); }); diff --git a/src/ducks/modules/mock.js b/src/ducks/modules/mock.js index 2b31ed721f..6ffe164611 100644 --- a/src/ducks/modules/mock.js +++ b/src/ducks/modules/mock.js @@ -3,7 +3,6 @@ import faker from 'faker'; import { has, times } from 'lodash'; import { actionCreators as sessionsActions } from './sessions'; -import { nodeAttributesProperty } from './network'; const MOCK_GENERATE_NODES = 'MOCK/GENERATE_NODES'; @@ -29,29 +28,25 @@ const mockValue = (nodeVariable) => { }; const generateNodes = (variableDefs, typeKey, howMany = 0, additionalAttributes = {}) => - (dispatch) => { - const mockNodes = times(howMany, () => { - const mockAttrs = Object.entries(variableDefs).reduce((acc, [variableId, variable]) => { - if (!has(additionalAttributes, variableId)) { - acc[variableId] = mockValue(variable); - } - return acc; - }, {}); - - return { - [nodeAttributesProperty]: mockAttrs, + dispatch => + times(howMany, () => { + const mockAttributes = Object.entries(variableDefs).reduce( + (acc, [variableId, variable]) => { + if (!has(additionalAttributes, variableId)) { + acc[variableId] = mockValue(variable); + } + return acc; + }, {}, + ); + + const modelData = { + promptIDs: ['mock'], + stageId: 'mock', + type: typeKey, }; - }); - const additionalProperties = { - promptId: 'mock', - stageId: 'mock', - type: typeKey, - [nodeAttributesProperty]: additionalAttributes, - }; - - return dispatch(sessionsActions.addNodes(mockNodes, { ...additionalProperties })); - }; + dispatch(sessionsActions.addNode(modelData, mockAttributes)); + }); const actionCreators = { generateNodes, diff --git a/src/ducks/modules/network.js b/src/ducks/modules/network.js index afb219152d..5c6792bb76 100644 --- a/src/ducks/modules/network.js +++ b/src/ducks/modules/network.js @@ -1,4 +1,4 @@ -import { reject, findIndex, isMatch, omit } from 'lodash'; +import { reject, findIndex, isMatch, omit, keys } from 'lodash'; import uuidv4 from '../../utils/uuid'; @@ -12,8 +12,10 @@ export const nodeAttributesProperty = 'attributes'; export const primaryKeyPropertyForWorker = 'networkCanvasId'; export const nodeTypePropertyForWorker = 'networkCanvasType'; -export const ADD_NODES = 'ADD_NODES'; +export const ADD_NODE = 'ADD_NODE'; +export const BATCH_ADD_NODES = 'BATCH_ADD_NODES'; export const REMOVE_NODE = 'REMOVE_NODE'; +export const REMOVE_NODE_FROM_PROMPT = 'REMOVE_NODE_FROM_PROMPT'; export const UPDATE_NODE = 'UPDATE_NODE'; export const TOGGLE_NODE_ATTRIBUTES = 'TOGGLE_NODE_ATTRIBUTES'; export const ADD_EDGE = 'ADD_EDGE'; @@ -40,105 +42,94 @@ function edgeExists(edges, edge) { ); } -/** - * All generated data is stored inside an 'attributes' property on the node - */ export const getNodeAttributes = node => node[nodeAttributesProperty] || {}; -/** - * existingNodes - Existing network.nodes - * netNodes - nodes to be added to the network - * additionalProperties - static props shared to add to each member of newNodes -*/ -function getNodesWithBatchAdd(existingNodes, newNodes, additionalProperties = {}) { - // Create a function to create a UUID and merge node attributes - const withModelandAttributeData = newNode => ({ - ...additionalProperties, - [nodePrimaryKeyProperty]: uuidv4(), - ...newNode, // second to prevent overwriting existing node UUID (e.g., assigned to externalData) - [nodeAttributesProperty]: { - ...additionalProperties[nodeAttributesProperty], - ...newNode[nodeAttributesProperty], - }, - }); - - return existingNodes.concat(newNodes.map(withModelandAttributeData)); -} - -/** - * @param {Array} nodes - the current state.nodes - * @param {Object} updatingNode - the node to be updated. Will match on _uid. - * @param {Object} nodeAttributeData - additional attributes to update the node with. - * If null, then the updatingNode's `attributes` property - * will overwrite the original node's. Use this to perform - * a 'full' update, but ensure the entire updated node is - * passed as `updatingNode`. - */ -function getUpdatedNodes(nodes, updatingNode, nodeAttributeData = null) { - return nodes.map((node) => { - if (node[nodePrimaryKeyProperty] !== updatingNode[nodePrimaryKeyProperty]) { return node; } - - const updatedNode = { - ...node, - ...updatingNode, - [nodePrimaryKeyProperty]: node[nodePrimaryKeyProperty], - }; - - if (nodeAttributeData) { - updatedNode[nodeAttributesProperty] = { - ...node[nodeAttributesProperty], - ...updatingNode[nodeAttributesProperty], - ...nodeAttributeData, - }; - } - - return updatedNode; - }); -} +const nodeWithModelandAttributeData = (modelData, attributeData) => ({ + ...omit(modelData, 'promptId'), + [nodePrimaryKeyProperty]: + modelData[nodePrimaryKeyProperty] ? modelData[nodePrimaryKeyProperty] : uuidv4(), + [nodeAttributesProperty]: { + ...modelData[nodeAttributesProperty], + ...attributeData, + }, + promptIDs: [modelData.promptId], + stageId: modelData.stageId, + type: modelData.type, + itemType: modelData.itemType, +}); export default function reducer(state = initialState, action = {}) { switch (action.type) { - case ADD_NODES: { + case ADD_NODE: { return { ...state, - nodes: getNodesWithBatchAdd(state.nodes, action.nodes, action.additionalProperties), + nodes: ( + () => state.nodes.concat( + nodeWithModelandAttributeData( + action.modelData, + action.attributeData, + ), + ) + )(), + }; + } + case BATCH_ADD_NODES: { + return { + ...state, + nodes: (() => + state.nodes.concat(action.nodeList.map(node => nodeWithModelandAttributeData( + node, + action.attributeData, + action.registryForTypes[node.type], + ))) + )(), }; } - /** - * TOGGLE_NODE_ATTRIBUTES - */ case TOGGLE_NODE_ATTRIBUTES: { - const updatedNodes = state.nodes.map((node) => { - if (node[nodePrimaryKeyProperty] !== action[nodePrimaryKeyProperty]) { - return node; - } - - // If the node's attrs contain the same key/vals, remove them - if (isMatch(node[nodeAttributesProperty], action.attributes)) { - const omittedKeys = Object.keys(action.attributes); - const nestedProps = omittedKeys.map(key => `${nodeAttributesProperty}.${key}`); - return omit(node, nestedProps); - } - - // Otherwise, add/update - return { - ...node, - [nodeAttributesProperty]: { - ...node[nodeAttributesProperty], - ...action.attributes, - }, - }; - }); - return { ...state, - nodes: updatedNodes, + nodes: ( + () => state.nodes.map( + (node) => { + if (node[nodePrimaryKeyProperty] !== action[nodePrimaryKeyProperty]) { return node; } + + // If the node's attrs contain the same key/vals, remove them + if (isMatch(node[nodeAttributesProperty], action.attributes)) { + const omittedKeys = Object.keys(action.attributes); + const nestedProps = omittedKeys.map(key => `${nodeAttributesProperty}.${key}`); + return omit(node, nestedProps); + } + + // Otherwise, add/update + return { + ...node, + [nodeAttributesProperty]: { + ...node[nodeAttributesProperty], + ...action.attributes, + }, + }; + }, // end node map function + ) + )(), }; } case UPDATE_NODE: { return { ...state, - nodes: getUpdatedNodes(state.nodes, action.node, action.additionalProperties), + nodes: (() => state.nodes.map((node) => { + if (node[nodePrimaryKeyProperty] !== action.nodeId) { return node; } + return { + ...node, + ...omit(action.newModelData, 'promptId'), + promptIDs: action.newModelData.promptId ? + [...node.promptIDs, action.newModelData.promptId] : node.promptIDs, + [nodeAttributesProperty]: { + ...node[nodeAttributesProperty], + ...action.newAttributeData, + }, + }; + }) + )(), }; } case REMOVE_NODE: { @@ -151,6 +142,22 @@ export default function reducer(state = initialState, action = {}) { edge.from === removenodePrimaryKeyProperty || edge.to === removenodePrimaryKeyProperty), }; } + case REMOVE_NODE_FROM_PROMPT: { + return { + ...state, + nodes: (() => state.nodes.map( + (node) => { + if (node[nodePrimaryKeyProperty] !== action.nodeId) { return node; } + return { + ...node, + [nodeAttributesProperty]: + omit(node[nodeAttributesProperty], keys(action.promptAttributes)), + promptIDs: node.promptIDs.filter(id => id !== action.promptId), + }; + }) + )(), + }; + } case ADD_EDGE: if (edgeExists(state.edges, action.edge)) { return state; } return { @@ -188,7 +195,8 @@ export default function reducer(state = initialState, action = {}) { const actionCreators = {}; const actionTypes = { - ADD_NODES, + ADD_NODE, + BATCH_ADD_NODES, UPDATE_NODE, TOGGLE_NODE_ATTRIBUTES, REMOVE_NODE, diff --git a/src/ducks/modules/reset.js b/src/ducks/modules/reset.js index 3adb13d7da..724ba37008 100644 --- a/src/ducks/modules/reset.js +++ b/src/ducks/modules/reset.js @@ -1,6 +1,5 @@ -import { omit } from 'lodash'; import { actionCreators as sessionsActions } from './sessions'; -import { nodeAttributesProperty } from './network'; +import { nodePrimaryKeyProperty } from './network'; import { actionCreators as deviceActions } from './deviceSettings'; const RESET_STATE = 'RESET_STATE'; @@ -9,11 +8,33 @@ const RESET_PROPERTY_FOR_ALL_NODES = 'RESET/PROPERTY_FOR_ALL_NODES'; const resetPropertyForAllNodes = property => (dispatch, getState) => { - const { session } = getState(); - const { sessions: { [session]: { network: { nodes } } } } = getState(); + const { session: sessionId } = getState(); + const { + sessions: { + [sessionId]: { + network: { nodes }, + }, + }, + protocol: { + variableRegistry: { + node: nodeRegistry, + }, + }, + } = getState(); - nodes.forEach(node => dispatch( - sessionsActions.updateNode(omit(node, `${nodeAttributesProperty}.${property}`)))); + nodes.forEach((node) => { + const registryForType = nodeRegistry[node.type].variables; + const variableType = registryForType[property].type; + dispatch( + sessionsActions.updateNode( + node[nodePrimaryKeyProperty], + {}, + { + [property]: variableType === 'boolean' ? false : null, + }, + ), + ); + }); }; const resetEdgesOfType = edgeType => diff --git a/src/ducks/modules/sessions.js b/src/ducks/modules/sessions.js index 6011ba01ca..ea25a6509b 100644 --- a/src/ducks/modules/sessions.js +++ b/src/ducks/modules/sessions.js @@ -1,9 +1,9 @@ -import { isArray, omit } from 'lodash'; +import { omit, each, map } from 'lodash'; import { Observable } from 'rxjs'; import { combineEpics } from 'redux-observable'; import uuidv4 from '../../utils/uuid'; -import network, { nodePrimaryKeyProperty, ADD_NODES, REMOVE_NODE, UPDATE_NODE, TOGGLE_NODE_ATTRIBUTES, ADD_EDGE, TOGGLE_EDGE, REMOVE_EDGE, SET_EGO, UNSET_EGO } from './network'; +import network, { nodePrimaryKeyProperty, ADD_NODE, BATCH_ADD_NODES, REMOVE_NODE, REMOVE_NODE_FROM_PROMPT, UPDATE_NODE, TOGGLE_NODE_ATTRIBUTES, ADD_EDGE, TOGGLE_EDGE, REMOVE_EDGE, SET_EGO, UNSET_EGO } from './network'; import ApiClient from '../../utils/ApiClient'; import { protocolIdFromSessionPath } from '../../utils/matchSessionPath'; @@ -24,8 +24,10 @@ const withTimestamp = session => ({ export default function reducer(state = initialState, action = {}) { switch (action.type) { - case ADD_NODES: + case ADD_NODE: + case BATCH_ADD_NODES: case REMOVE_NODE: + case REMOVE_NODE_FROM_PROMPT: case UPDATE_NODE: case TOGGLE_NODE_ATTRIBUTES: case ADD_EDGE: @@ -83,39 +85,74 @@ export default function reducer(state = initialState, action = {}) { } /** - * Add a node or nodes to the state. + * Add a batch of nodes to the state. * - * @param {Array|Object} nodes - one or more nodes to add - * @param {Object} [additionalProperties] shared properties to apply to every new node. Note that - * user data (e.g., the "additionalAttributes" defined in - * a protocol) should exist under a child property named - * 'attributes'. + * @param {Collection} [nodeList] An array of objects representing nodes to add. + * @param {Object} [attributeData] Attribute data that will be merged with each node * * @memberof! NetworkActionCreators */ -const addNodes = (nodes, additionalProperties) => (dispatch, getState) => { - const { session } = getState(); +const batchAddNodes = (nodeList, attributeData) => (dispatch, getState) => { + const { session: sessionId, protocol: { variableRegistry: { node: nodeRegistry } } } = getState(); + const nodeTypes = map(nodeList, 'type'); + + const registryForTypes = {}; + each(nodeTypes, (nodeType) => { + registryForTypes[nodeType] = nodeRegistry[nodeType].variables; + }); - let nodeOrNodes = nodes; - if (!isArray(nodeOrNodes)) { - nodeOrNodes = [nodeOrNodes]; - } dispatch({ - type: ADD_NODES, - sessionId: session, - nodes: nodeOrNodes, - additionalProperties, + type: BATCH_ADD_NODES, + sessionId, + nodeList, + attributeData, + registryForTypes, + }); +}; + +/** + * This function generates default values for all variables in the variable registry for this node + * type. + * + * @param {object} registryForType - An object containing the variable registry entry for this + * node type. + */ + +const getDefaultAttributesForNodeType = (registryForType = {}) => { + const defaultAttributesObject = {}; + + // Boolean variables are initialised as `false`, and everything else as `null` + Object.keys(registryForType).forEach((key) => { + defaultAttributesObject[key] = registryForType[key].type === 'boolean' ? false : null; }); + + return defaultAttributesObject; }; -const updateNode = (node, additionalProperties = null) => (dispatch, getState) => { +const addNode = (modelData, attributeData) => (dispatch, getState) => { + const { session: sessionId, protocol: { variableRegistry: { node: nodeRegistry } } } = getState(); + const registryForType = nodeRegistry[modelData.type].variables; + + dispatch({ + type: ADD_NODE, + sessionId, + modelData, + attributeData: { + ...getDefaultAttributesForNodeType(registryForType), + ...attributeData, + }, + }); +}; + +const updateNode = (nodeId, newModelData, newAttributeData = null) => (dispatch, getState) => { const { session } = getState(); dispatch({ type: UPDATE_NODE, sessionId: session, - node, - additionalProperties, + nodeId, + newModelData, + newAttributeData, }); }; @@ -140,6 +177,18 @@ const removeNode = uid => (dispatch, getState) => { }); }; +const removeNodeFromPrompt = (nodeId, promptId, promptAttributes) => (dispatch, getState) => { + const { session } = getState(); + + dispatch({ + type: REMOVE_NODE_FROM_PROMPT, + sessionId: session, + nodeId, + promptId, + promptAttributes, + }); +}; + const addEdge = edge => (dispatch, getState) => { const { session } = getState(); @@ -245,9 +294,11 @@ const exportSessionEpic = (action$, store) => ( ); const actionCreators = { - addNodes, + addNode, + batchAddNodes, updateNode, removeNode, + removeNodeFromPrompt, addEdge, toggleEdge, removeEdge, @@ -261,8 +312,10 @@ const actionCreators = { }; const actionTypes = { - ADD_NODES, + ADD_NODE, + BATCH_ADD_NODES, REMOVE_NODE, + REMOVE_NODE_FROM_PROMPT, UPDATE_NODE, TOGGLE_NODE_ATTRIBUTES, ADD_EDGE, diff --git a/src/selectors/__tests__/canvas.test.js b/src/selectors/__tests__/canvas.test.js index 3cf3a27b31..f4c0692d1a 100644 --- a/src/selectors/__tests__/canvas.test.js +++ b/src/selectors/__tests__/canvas.test.js @@ -6,12 +6,14 @@ import { makeGetPlacedNodes, makeGetDisplayEdges, } from '../canvas'; +import { nodeAttributesProperty } from '../../ducks/modules/network'; + +const node1 = { _uid: 1, type: 'person', [nodeAttributesProperty]: { role: ['a'], name: 'alpha', closeness: [1, 1] } }; +const node2 = { _uid: 2, type: 'person', [nodeAttributesProperty]: { role: ['a'], name: 'foxtrot', closeness: null } }; +const node3 = { _uid: 3, type: 'person', [nodeAttributesProperty]: { role: ['a'], name: 'bravo', closeness: null } }; +const node4 = { _uid: 4, type: 'person', [nodeAttributesProperty]: { role: ['a'], name: 'echo', closeness: [1, 1] } }; +const node5 = { _uid: 5, type: 'person', [nodeAttributesProperty]: { role: ['b'], name: 'charlie', closeness: [1, 1] } }; -const node1 = { _uid: 1, type: 'person', attributes: { role: ['a'], name: 'alpha', closeness: [1, 1] } }; -const node2 = { _uid: 2, type: 'person', attributes: { role: ['a'], name: 'foxtrot' } }; -const node3 = { _uid: 3, type: 'person', attributes: { role: ['a'], name: 'bravo' } }; -const node4 = { _uid: 4, type: 'person', attributes: { role: ['a'], name: 'echo', closeness: [1, 1] } }; -const node5 = { _uid: 5, type: 'person', attributes: { role: ['b'], name: 'charlie', closeness: [1, 1] } }; const mockState = { session: 'testSession', diff --git a/src/selectors/__tests__/interface.test.js b/src/selectors/__tests__/interface.test.js index c3ff9a6f87..21f67ac9f5 100644 --- a/src/selectors/__tests__/interface.test.js +++ b/src/selectors/__tests__/interface.test.js @@ -24,6 +24,7 @@ const mockStage = { const externalNode1 = { uid: 'person_1', type: 'person', + promptIDs: ['promptId123'], attributes: { name: 'F. Anita', nickname: 'Annie', @@ -35,6 +36,7 @@ const externalNode1 = { const externalNode2 = { uid: 'person_2', type: 'person', + promptIDs: ['promptId123'], attributes: { name: 'H. Barry', nickname: 'Baz', @@ -45,6 +47,7 @@ const externalNode2 = { const externalNode3 = { uid: 'person_3', type: 'person', + promptIDs: ['promptId123'], attributes: { nickname: 'Carl', age: 25, @@ -54,6 +57,7 @@ const externalNode3 = { const externalNode4 = { id: 4, uid: 'person_3', + promptIDs: ['promptId123'], type: 'person', attributes: { age: 25, @@ -93,12 +97,12 @@ const emptyProps = { stage: {}, }; -const personNode = { uid: 1, type: 'person', attributes: { name: 'foo' } }; -const closeFriendNode = { uid: 2, type: 'person', attributes: { name: 'bar', close_friend: true } }; +const personNode = { uid: 1, promptIDs: ['promptIdxxx'], type: 'person', attributes: { name: 'foo' } }; +const closeFriendNode = { uid: 2, promptIDs: ['promptId123'], type: 'person', attributes: { name: 'bar', close_friend: true } }; const nodes = [ personNode, closeFriendNode, - { uid: 3, attributes: { name: 'baz' }, type: 'venue' }, + { uid: 3, promptIDs: ['promptId456'], attributes: { name: 'baz' }, type: 'venue' }, ]; const edges = [{ to: 'bar', from: 'foo' }, { to: 'asdf', from: 'qwerty' }]; diff --git a/src/selectors/__tests__/name-generator.test.js b/src/selectors/__tests__/name-generator.test.js index 8e661d5ad3..85317acea5 100644 --- a/src/selectors/__tests__/name-generator.test.js +++ b/src/selectors/__tests__/name-generator.test.js @@ -115,16 +115,6 @@ describe('name generator selector', () => { }); }); describe('memoed selectors', () => { - it('should get node attributes for the prompt', () => { - const selected = NameGen.makeGetPromptNodeAttributes(); - expect(selected(mockState, mockProps)).toEqual({ - attributes: { close_friend: true }, - type: 'person', - promptId: 'promptId123', - stageId: 'stageId123', - }); - }); - it('should get card display label', () => { expect(NameGen.getCardDisplayLabel(mockState, mockProps)).toEqual('card label'); expect(NameGen.getCardDisplayLabel(null, emptyProps)).toEqual(undefined); diff --git a/src/selectors/canvas.js b/src/selectors/canvas.js index 69f51af634..d74d6d48a9 100644 --- a/src/selectors/canvas.js +++ b/src/selectors/canvas.js @@ -1,4 +1,4 @@ -import { first, has } from 'lodash'; +import { first, has, isNil } from 'lodash'; import { networkNodes, networkEdges } from './interface'; import { createDeepEqualSelector } from './utils'; import sortOrder from '../utils/sortOrder'; @@ -26,14 +26,14 @@ export const makeGetNextUnplacedNode = () => getSubject, getLayout, getSortOptions, - (nodes, subject, layout, sortOptions) => { + (nodes, subject, layoutVariable, sortOptions) => { const type = subject && subject.type; const unplacedNodes = nodes.filter((node) => { const attributes = getNodeAttributes(node); return ( node.type === type && - !has(attributes, layout) + (has(attributes, layoutVariable) && isNil(attributes[layoutVariable])) ); }); @@ -54,13 +54,13 @@ export const makeGetPlacedNodes = () => networkNodes, getSubject, getLayout, - (nodes, subject, layout) => { + (nodes, subject, layoutVariable) => { const type = subject && subject.type; return nodes.filter((node) => { const attributes = getNodeAttributes(node); return ( node.type === type && - has(attributes, layout) + (has(attributes, layoutVariable) && !isNil(attributes[layoutVariable])) ); }); }, diff --git a/src/selectors/interface.js b/src/selectors/interface.js index 4d0dfee18d..1651091c8b 100644 --- a/src/selectors/interface.js +++ b/src/selectors/interface.js @@ -1,14 +1,13 @@ /* eslint-disable import/prefer-default-export */ import { createSelector } from 'reselect'; -import { findKey, filter, isMatch, reject } from 'lodash'; +import { findKey, filter, includes } from 'lodash'; import { assert, createDeepEqualSelector } from './utils'; import { protocolRegistry } from './protocol'; import { getAdditionalAttributes, getSubject } from '../utils/protocol/accessors'; import { getCurrentSession } from './session'; import { getNodeAttributes, - nodeAttributesProperty, } from '../ducks/modules/network'; import { asExportableNetwork, @@ -60,6 +59,7 @@ export const getWorkerNetwork = createDeepEqualSelector( (network, registry) => asWorkerAgentNetwork(network, registry), ); +// Returns current stage and prompt ID export const makeGetIds = () => createSelector( propStageId, propPromptId, @@ -79,8 +79,7 @@ export const makeGetSubject = () => ); const nodeTypeIsDefined = (variableRegistry, nodeType) => - variableRegistry.node && - !!variableRegistry.node[nodeType]; + variableRegistry.node && !!variableRegistry.node[nodeType]; // TODO: Once schema validation is in place, we don't need these asserts. export const makeGetSubjectType = () => (createSelector( @@ -166,34 +165,30 @@ export const makeNetworkNodesForType = () => /** * makeNetworkNodesForPrompt - * Take the "additional attributes" specified by the current prompt, and filter nodes of the current - * prompt type + * + * Return a filtered node list containing only nodes where node IDs contains the current promptId. */ export const makeNetworkNodesForPrompt = () => { - const getAttributes = makeGetAdditionalAttributes(); const networkNodesForSubject = makeNetworkNodesForType(); - return createSelector( - networkNodesForSubject, getAttributes, - (nodes, attributes) => - filter(nodes, { [nodeAttributesProperty]: attributes }), + networkNodesForSubject, propPromptId, + (nodes, promptId) => filter(nodes, node => includes(node.promptIDs, promptId)), ); }; /** * makeNetworkNodesForOtherPrompts() - * Same as above, except returns a filtered node list that **excludes** nodes that match. + * + * Same as above, except returns a filtered node list that **excludes** nodes that match the current + * prompt's promptId. */ export const makeNetworkNodesForOtherPrompts = () => { - // used to check prompt ids - const getAttributes = makeGetAdditionalAttributes(); const networkNodesForSubject = makeNetworkNodesForType(); return createSelector( - networkNodesForSubject, getAttributes, - (nodes, attributes) => - reject(nodes, node => isMatch(getNodeAttributes(node), attributes)), + networkNodesForSubject, propPromptId, + (nodes, promptId) => filter(nodes, node => !includes(node.promptIDs, promptId)), ); }; diff --git a/src/selectors/name-generator.js b/src/selectors/name-generator.js index 2e0bebabfa..baff51fa29 100644 --- a/src/selectors/name-generator.js +++ b/src/selectors/name-generator.js @@ -2,8 +2,7 @@ import { createSelector } from 'reselect'; import { has } from 'lodash'; -import { makeGetSubject, makeGetIds, makeGetSubjectType, makeGetAdditionalAttributes } from './interface'; -import { nodeAttributesProperty } from '../ducks/modules/network'; +import { makeGetSubject, makeGetIds, makeGetSubjectType } from './interface'; import { protocolRegistry } from './protocol'; import { getExternalData } from './externalData'; @@ -28,24 +27,17 @@ const propCardOptions = (_, props) => props.prompt.cardOptions; const propSortOptions = (_, props) => props.prompt.sortOptions; const propPanels = (_, props) => props.stage.panels; -// Static props that will be added to any created/edited node on the prompt -// Any protocol-specific props will exist in the [nodeAttributesProperty] object -export const makeGetPromptNodeAttributes = () => { +// blah! +export const makeGetPromptNodeModelData = () => { const getSubject = makeGetSubject(); const getIds = makeGetIds(); - const getAdditionalAttributes = makeGetAdditionalAttributes(); return createSelector( getSubject, getIds, - getAdditionalAttributes - , - ({ type }, ids, additionalAttributes) => ({ + ({ type }, ids) => ({ type, ...ids, - [nodeAttributesProperty]: { - ...additionalAttributes, - }, }), ); }; diff --git a/src/ui b/src/ui index 23ed556e65..ca8243cd6f 160000 --- a/src/ui +++ b/src/ui @@ -1 +1 @@ -Subproject commit 23ed556e65b2ef0d5f29c949dfd4c689641fbce2 +Subproject commit ca8243cd6f612ed479ed82f988925722f1c363eb