diff --git a/package-lock.json b/package-lock.json
index a162def2fc..b949bb345c 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,6 +1,6 @@
{
"name": "network-canvas",
- "version": "4.0.0-alpha.4",
+ "version": "4.0.0-alpha.5",
"lockfileVersion": 1,
"requires": true,
"dependencies": {
@@ -3195,7 +3195,7 @@
},
"ansi": {
"version": "0.3.1",
- "resolved": "https://registry.npmjs.org/ansi/-/ansi-0.3.1.tgz",
+ "resolved": "http://registry.npmjs.org/ansi/-/ansi-0.3.1.tgz",
"integrity": "sha1-DELU+xcWDVqa8eSEus4cZpIsGyE="
},
"balanced-match": {
@@ -3215,7 +3215,7 @@
},
"bplist-parser": {
"version": "0.1.1",
- "resolved": "https://registry.npmjs.org/bplist-parser/-/bplist-parser-0.1.1.tgz",
+ "resolved": "http://registry.npmjs.org/bplist-parser/-/bplist-parser-0.1.1.tgz",
"integrity": "sha1-1g1dzCDLptx+HymbNdPh+V2vuuY=",
"requires": {
"big-integer": "1.6.26"
@@ -3232,7 +3232,7 @@
},
"concat-map": {
"version": "0.0.1",
- "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
+ "resolved": "http://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
"integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s="
},
"cordova-common": {
@@ -3257,12 +3257,12 @@
},
"cordova-registry-mapper": {
"version": "1.1.15",
- "resolved": "https://registry.npmjs.org/cordova-registry-mapper/-/cordova-registry-mapper-1.1.15.tgz",
+ "resolved": "http://registry.npmjs.org/cordova-registry-mapper/-/cordova-registry-mapper-1.1.15.tgz",
"integrity": "sha1-4kS5GFuBdUc7/2B5MkkFEV+D3Hw="
},
"elementtree": {
"version": "0.1.6",
- "resolved": "https://registry.npmjs.org/elementtree/-/elementtree-0.1.6.tgz",
+ "resolved": "http://registry.npmjs.org/elementtree/-/elementtree-0.1.6.tgz",
"integrity": "sha1-KsTEbqMFFsjEy9teOsdBjlkt4gw=",
"requires": {
"sax": "0.3.5"
@@ -3270,7 +3270,7 @@
},
"glob": {
"version": "5.0.15",
- "resolved": "https://registry.npmjs.org/glob/-/glob-5.0.15.tgz",
+ "resolved": "http://registry.npmjs.org/glob/-/glob-5.0.15.tgz",
"integrity": "sha1-G8k2ueAvSmA/zCIuz3Yz0wuLk7E=",
"requires": {
"inflight": "1.0.6",
@@ -3282,7 +3282,7 @@
},
"inflight": {
"version": "1.0.6",
- "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz",
+ "resolved": "http://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz",
"integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=",
"requires": {
"once": "1.4.0",
@@ -3291,12 +3291,12 @@
},
"inherits": {
"version": "2.0.3",
- "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz",
+ "resolved": "http://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz",
"integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4="
},
"lodash": {
"version": "3.10.1",
- "resolved": "https://registry.npmjs.org/lodash/-/lodash-3.10.1.tgz",
+ "resolved": "http://registry.npmjs.org/lodash/-/lodash-3.10.1.tgz",
"integrity": "sha1-W/Rejkm6QYnhfUgnid/RW9FAt7Y="
},
"minimatch": {
@@ -3309,7 +3309,7 @@
},
"nopt": {
"version": "3.0.6",
- "resolved": "https://registry.npmjs.org/nopt/-/nopt-3.0.6.tgz",
+ "resolved": "http://registry.npmjs.org/nopt/-/nopt-3.0.6.tgz",
"integrity": "sha1-xkZdvwirzU2zWTF/eaxopkayj/k=",
"requires": {
"abbrev": "1.1.1"
@@ -3317,7 +3317,7 @@
},
"once": {
"version": "1.4.0",
- "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
+ "resolved": "http://registry.npmjs.org/once/-/once-1.4.0.tgz",
"integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=",
"requires": {
"wrappy": "1.0.2"
@@ -3325,12 +3325,12 @@
},
"os-homedir": {
"version": "1.0.2",
- "resolved": "https://registry.npmjs.org/os-homedir/-/os-homedir-1.0.2.tgz",
+ "resolved": "http://registry.npmjs.org/os-homedir/-/os-homedir-1.0.2.tgz",
"integrity": "sha1-/7xJiDNuDoM94MFox+8VISGqf7M="
},
"os-tmpdir": {
"version": "1.0.2",
- "resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz",
+ "resolved": "http://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz",
"integrity": "sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ="
},
"osenv": {
@@ -3344,12 +3344,12 @@
},
"path-is-absolute": {
"version": "1.0.1",
- "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz",
+ "resolved": "http://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz",
"integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18="
},
"plist": {
"version": "1.2.0",
- "resolved": "https://registry.npmjs.org/plist/-/plist-1.2.0.tgz",
+ "resolved": "http://registry.npmjs.org/plist/-/plist-1.2.0.tgz",
"integrity": "sha1-CEtQk93JJQbiWfh0uNmxr7jHlZM=",
"requires": {
"base64-js": "0.0.8",
@@ -3360,7 +3360,7 @@
},
"properties-parser": {
"version": "0.2.3",
- "resolved": "https://registry.npmjs.org/properties-parser/-/properties-parser-0.2.3.tgz",
+ "resolved": "http://registry.npmjs.org/properties-parser/-/properties-parser-0.2.3.tgz",
"integrity": "sha1-91kSVfcHq7/yJ8e1a2N9uwNzoQ8="
},
"q": {
@@ -3380,17 +3380,17 @@
},
"shelljs": {
"version": "0.5.3",
- "resolved": "https://registry.npmjs.org/shelljs/-/shelljs-0.5.3.tgz",
+ "resolved": "http://registry.npmjs.org/shelljs/-/shelljs-0.5.3.tgz",
"integrity": "sha1-xUmCuZbHbvDB5rWfvcWCX1txMRM="
},
"underscore": {
"version": "1.8.3",
- "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.8.3.tgz",
+ "resolved": "http://registry.npmjs.org/underscore/-/underscore-1.8.3.tgz",
"integrity": "sha1-Tz+1OxBuYJf8+ctBCfKl6b36UCI="
},
"unorm": {
"version": "1.4.1",
- "resolved": "https://registry.npmjs.org/unorm/-/unorm-1.4.1.tgz",
+ "resolved": "http://registry.npmjs.org/unorm/-/unorm-1.4.1.tgz",
"integrity": "sha1-NkIA1fE2RsqLzURJAnEzVhR5IwA="
},
"util-deprecate": {
@@ -3400,7 +3400,7 @@
},
"wrappy": {
"version": "1.0.2",
- "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
+ "resolved": "http://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
"integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8="
},
"xmlbuilder": {
@@ -3413,7 +3413,7 @@
},
"xmldom": {
"version": "0.1.27",
- "resolved": "https://registry.npmjs.org/xmldom/-/xmldom-0.1.27.tgz",
+ "resolved": "http://registry.npmjs.org/xmldom/-/xmldom-0.1.27.tgz",
"integrity": "sha1-1QH5ezvbQDr4757MIFcxh6rawOk="
}
}
@@ -12841,8 +12841,7 @@
"object-hash": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/object-hash/-/object-hash-1.3.0.tgz",
- "integrity": "sha512-05KzQ70lSeGSrZJQXE5wNDiTkBJDlUT/myi6RX9dVIvz7a7Qh4oH93BQdiPMn27nldYvVQCKMUaM83AfizZlsQ==",
- "dev": true
+ "integrity": "sha512-05KzQ70lSeGSrZJQXE5wNDiTkBJDlUT/myi6RX9dVIvz7a7Qh4oH93BQdiPMn27nldYvVQCKMUaM83AfizZlsQ=="
},
"object-inspect": {
"version": "1.5.0",
diff --git a/package.json b/package.json
index cfbe65a9d2..2cbfb06fc9 100644
--- a/package.json
+++ b/package.json
@@ -159,6 +159,7 @@
"cordova-plugin-wkwebview-engine": "^1.1.4",
"cordova-plugin-x-socialsharing": "^5.2.1",
"cordova-plugin-zeroconf": "^1.3.3",
+ "object-hash": "^1.3.0",
"react-id-swiper": "^1.6.4"
},
"homepage": ".",
diff --git a/public/protocols/development.netcanvas/protocol.json b/public/protocols/development.netcanvas/protocol.json
index ba89023333..755c29ca00 100644
--- a/public/protocols/development.netcanvas/protocol.json
+++ b/public/protocols/development.netcanvas/protocol.json
@@ -482,35 +482,35 @@
"previousInterview": {
"nodes": [
{
- "uid": "previous_1",
+ "uid": "person_1",
"type": "person",
"name": "Anita",
"nickname": "Annie",
"age": "23"
},
{
- "uid": "previous_2",
+ "uid": "person_2",
"type": "person",
"name": "Barry",
"nickname": "Baz",
"age": "23"
},
{
- "uid": "previous_3",
+ "uid": "person_3",
"type": "person",
"name": "Carlito",
"nickname": "Carl",
"age": "23"
},
{
- "uid": "previous_4",
+ "uid": "person_4",
"type": "person",
"name": "Dee",
"nickname": "Dee",
"age": "23"
},
{
- "uid": "previous_5",
+ "uid": "person_5",
"type": "person",
"name": "Eugine",
"nickname": "Eu",
diff --git a/src/components/CardList.js b/src/components/CardList.js
index e08204cb6b..eeb2beecaa 100644
--- a/src/components/CardList.js
+++ b/src/components/CardList.js
@@ -6,6 +6,7 @@ import cx from 'classnames';
import { scrollable, selectable } from '../behaviours';
import { Card } from '.';
import { Icon } from '../ui/components';
+import { NodePK } from '../ducks/modules/network';
const EnhancedCard = selectable(Card);
@@ -23,7 +24,7 @@ const CardList = (props) => {
onDeleteCard,
onToggleCard,
selected,
- uid,
+ getKey,
} = props;
const classNames = cx('card-list', className);
@@ -32,7 +33,7 @@ const CardList = (props) => {
{
nodes.map(node => (
-
+
{},
selected: () => false,
- uid: node => node.uid,
+ getKey: node => node[NodePK],
};
export default compose(
diff --git a/src/components/ListSelect.js b/src/components/ListSelect.js
index 200b5dbf98..48de483b36 100644
--- a/src/components/ListSelect.js
+++ b/src/components/ListSelect.js
@@ -3,6 +3,7 @@ import PropTypes from 'prop-types';
import { Button } from '../ui/components';
import { CardList } from '.';
+import { NodePK } from '../ducks/modules/network';
class ListSelect extends Component {
constructor(props) {
@@ -82,7 +83,7 @@ class ListSelect extends Component {
/**
* @param {object} node
*/
- selected = node => !!this.props.selectedNodes.find(current => current.uid === node.uid);
+ selected = node => !!this.props.selectedNodes.find(current => current[NodePK] === node[NodePK]);
/**
* @param property to sort by
@@ -129,11 +130,12 @@ class ListSelect extends Component {
* @param {object} node
*/
toggleCard = (node) => {
- const index = this.props.selectedNodes.findIndex(current => current.uid === node.uid);
+ const matchingPK = n => n[NodePK] === node[NodePK];
+ const index = this.props.selectedNodes.findIndex(matchingPK);
if (index !== -1) {
- this.props.onRemoveNode(this.props.nodes.find(current => current.uid === node.uid));
+ this.props.onRemoveNode(this.props.nodes.find(matchingPK));
} else {
- this.props.onSubmitNode(this.props.nodes.find(current => current.uid === node.uid));
+ this.props.onSubmitNode(this.props.nodes.find(matchingPK));
}
};
diff --git a/src/components/NodeBin.js b/src/components/NodeBin.js
index 23d5a80b32..ff0fc46b54 100644
--- a/src/components/NodeBin.js
+++ b/src/components/NodeBin.js
@@ -5,6 +5,7 @@ import { compose, withProps } from 'recompose';
import PropTypes from 'prop-types';
import cx from 'classnames';
import { actionCreators as sessionsActions } from '../ducks/modules/sessions';
+import { NodePK } from '../ducks/modules/network';
import { DropTarget, MonitorDropTarget } from '../behaviours/DragAndDrop';
/**
@@ -43,7 +44,7 @@ export default compose(
connect(null, mapDispatchToProps),
withProps(props => ({
accepts: ({ meta }) => meta.itemType === 'EXISTING_NODE',
- onDrop: ({ meta }) => props.removeNode(meta.uid),
+ onDrop: ({ meta }) => props.removeNode(meta[NodePK]),
})),
DropTarget,
MonitorDropTarget(['isOver', 'willAccept']),
diff --git a/src/components/NodeList.js b/src/components/NodeList.js
index 2695abbf85..6b4b86870d 100644
--- a/src/components/NodeList.js
+++ b/src/components/NodeList.js
@@ -15,6 +15,7 @@ import {
MonitorDragSource,
} from '../behaviours/DragAndDrop';
import sortOrder from '../utils/sortOrder';
+import { NodePK } from '../ducks/modules/network';
const EnhancedNode = DragSource(selectable(Node));
@@ -98,7 +99,7 @@ class NodeList extends Component {
exit,
} = this.state;
- const isSource = !!find(nodes, ['uid', get(meta, 'uid', null)]);
+ const isSource = !!find(nodes, [NodePK, get(meta, NodePK, null)]);
const isValidTarget = !isSource && willAccept;
const isHovering = isValidTarget && isOver;
@@ -118,7 +119,7 @@ class NodeList extends Component {
{
nodes.map((node, index) => (
diff --git a/src/components/OrdinalBinBucket.js b/src/components/OrdinalBinBucket.js
index 5a00a752fc..58631c2799 100644
--- a/src/components/OrdinalBinBucket.js
+++ b/src/components/OrdinalBinBucket.js
@@ -15,6 +15,7 @@ import {
MonitorDragSource,
} from '../behaviours/DragAndDrop';
import sortOrder from '../utils/sortOrder';
+import { NodePK } from '../ducks/modules/network';
const EnhancedNode = DragSource(selectable(Node));
@@ -92,7 +93,7 @@ class OrdinalBinBucket extends Component {
exit,
} = this.state;
- const isSource = !!find(nodes, ['uid', get(meta, 'uid', null)]);
+ const isSource = !!find(nodes, [NodePK, get(meta, NodePK, null)]);
const isValidTarget = !isSource && willAccept;
const isHovering = isValidTarget && isOver;
@@ -115,7 +116,7 @@ class OrdinalBinBucket extends Component {
nodes.map((node, index) => (
index < 3 && (
diff --git a/src/components/__tests__/__snapshots__/CardList-test.js.snap b/src/components/__tests__/__snapshots__/CardList-test.js.snap
index ed46f3d71c..f4cd71bcb9 100644
--- a/src/components/__tests__/__snapshots__/CardList-test.js.snap
+++ b/src/components/__tests__/__snapshots__/CardList-test.js.snap
@@ -45,6 +45,7 @@ ShallowWrapper {
className=""
compact={false}
details={[Function]}
+ getKey={[Function]}
label={[Function]}
multiselect={true}
nodes={
@@ -70,7 +71,6 @@ ShallowWrapper {
onToggleCard={[Function]}
scrollTop={[Function]}
selected={[Function]}
- uid={[Function]}
/>,
"className": "scrollable",
},
@@ -83,6 +83,7 @@ ShallowWrapper {
"className": "",
"compact": false,
"details": [Function],
+ "getKey": [Function],
"label": [Function],
"multiselect": true,
"nodes": Array [
@@ -106,7 +107,6 @@ ShallowWrapper {
"onToggleCard": [Function],
"scrollTop": [Function],
"selected": [Function],
- "uid": [Function],
},
"ref": null,
"rendered": null,
@@ -124,6 +124,7 @@ ShallowWrapper {
className=""
compact={false}
details={[Function]}
+ getKey={[Function]}
label={[Function]}
multiselect={true}
nodes={
@@ -149,7 +150,6 @@ ShallowWrapper {
onToggleCard={[Function]}
scrollTop={[Function]}
selected={[Function]}
- uid={[Function]}
/>,
"className": "scrollable",
},
@@ -162,6 +162,7 @@ ShallowWrapper {
"className": "",
"compact": false,
"details": [Function],
+ "getKey": [Function],
"label": [Function],
"multiselect": true,
"nodes": Array [
@@ -185,7 +186,6 @@ ShallowWrapper {
"onToggleCard": [Function],
"scrollTop": [Function],
"selected": [Function],
- "uid": [Function],
},
"ref": null,
"rendered": null,
diff --git a/src/containers/ConcentricCircles/ConcentricCircles.js b/src/containers/ConcentricCircles/ConcentricCircles.js
index 3c4525f439..5a776b334d 100644
--- a/src/containers/ConcentricCircles/ConcentricCircles.js
+++ b/src/containers/ConcentricCircles/ConcentricCircles.js
@@ -5,7 +5,7 @@ import NodeLayout from './NodeLayout';
import EdgeLayout from './EdgeLayout';
import Background from './Background';
-const Sociogram = ({ stage, prompt }) => (
+const ConcentricCircles = ({ stage, prompt }) => (
{
@@ -27,11 +27,11 @@ const Sociogram = ({ stage, prompt }) => (
);
-Sociogram.propTypes = {
+ConcentricCircles.propTypes = {
stage: PropTypes.object.isRequired,
prompt: PropTypes.object.isRequired,
};
-export { Sociogram };
+export { ConcentricCircles };
-export default Sociogram;
+export default ConcentricCircles;
diff --git a/src/containers/ConcentricCircles/NodeLayout.js b/src/containers/ConcentricCircles/NodeLayout.js
index 5a420b75be..8bbdb14a83 100644
--- a/src/containers/ConcentricCircles/NodeLayout.js
+++ b/src/containers/ConcentricCircles/NodeLayout.js
@@ -8,6 +8,7 @@ import LayoutNode from './LayoutNode';
import { withBounds } from '../../behaviours';
import { makeGetSociogramOptions, makeGetPlacedNodes } from '../../selectors/sociogram';
import { actionCreators as sessionsActions } from '../../ducks/modules/sessions';
+import { NodePK } from '../../ducks/modules/network';
import { DropTarget } from '../../behaviours/DragAndDrop';
import sociogramOptionsProps from './propTypes';
@@ -30,7 +31,7 @@ const dropHandlers = compose(
accepts: () => ({ meta }) => meta.itemType === 'POSITIONED_NODE',
onDrop: props => (item) => {
props.updateNode({
- uid: item.meta.uid,
+ [NodePK]: item.meta[NodePK],
[props.layoutVariable]: relativeCoords(props, item),
});
@@ -41,7 +42,7 @@ const dropHandlers = compose(
if (!has(item.meta, props.layoutVariable)) { return; }
props.updateNode({
- uid: item.meta.uid,
+ [NodePK]: item.meta[NodePK],
[props.layoutVariable]: relativeCoords(props, item),
});
},
@@ -84,9 +85,9 @@ class NodeLayout extends Component {
if (!allowSelect) { return; }
- this.connectNode(node.id);
+ this.connectNode(node[NodePK]);
- this.toggleHighlightAttributes(node.uid);
+ this.toggleHighlightAttributes(node[NodePK]);
this.forceUpdate();
}
@@ -130,7 +131,7 @@ class NodeLayout extends Component {
isLinking(node) {
return this.props.allowSelect &&
this.props.canCreateEdge &&
- node.id === this.state.connectFrom;
+ node[NodePK] === this.state.connectFrom;
}
render() {
@@ -148,7 +149,7 @@ class NodeLayout extends Component {
return (
this.onSelected(node)}
diff --git a/src/containers/ConcentricCircles/__tests__/NodeLayout.test.js b/src/containers/ConcentricCircles/__tests__/NodeLayout.test.js
index 23e990878e..82d0503927 100644
--- a/src/containers/ConcentricCircles/__tests__/NodeLayout.test.js
+++ b/src/containers/ConcentricCircles/__tests__/NodeLayout.test.js
@@ -3,12 +3,13 @@
import React from 'react';
import { shallow } from 'enzyme';
import { NodeLayout } from '../NodeLayout';
+import { NodePK } from '../../../ducks/modules/network';
const layout = 'foo';
const mockProps = {
nodes: [
- { bar: 'buzz', uid: 123, [layout]: { x: 0, y: 0 } },
+ { bar: 'buzz', [NodePK]: 123, [layout]: { x: 0, y: 0 } },
],
updateNode: () => {},
toggleEdge: () => {},
@@ -47,12 +48,12 @@ describe('', () => {
component.setProps({
...mockProps,
- nodes: [{ bar: 'buzz', uid: 123 }],
+ nodes: [{ bar: 'buzz', [NodePK]: 123 }],
});
component.setProps({
...mockProps,
- nodes: [{ bar: 'buzz', uid: 123 }, { bar: 'bing', uid: 456 }],
+ nodes: [{ bar: 'buzz', [NodePK]: 123 }, { bar: 'bing', [NodePK]: 456 }],
});
expect(componentDidUpdate.mock.calls.length).toEqual(1);
@@ -69,7 +70,7 @@ describe('', () => {
component.setProps({
...mockProps,
- nodes: [{ bar: 'buzz', uid: 123 }],
+ nodes: [{ bar: 'buzz', [NodePK]: 123 }],
});
component.setProps({
diff --git a/src/containers/ConcentricCircles/__tests__/__snapshots__/ConcentricCircles.test.js.snap b/src/containers/ConcentricCircles/__tests__/__snapshots__/ConcentricCircles.test.js.snap
index f0350c3fe6..847b31ac97 100644
--- a/src/containers/ConcentricCircles/__tests__/__snapshots__/ConcentricCircles.test.js.snap
+++ b/src/containers/ConcentricCircles/__tests__/__snapshots__/ConcentricCircles.test.js.snap
@@ -4,7 +4,7 @@ exports[` renders ok 1`] = `
ShallowWrapper {
"length": 1,
Symbol(enzyme.__root__): [Circular],
- Symbol(enzyme.__unrendered__): {
- this.props.removeNode(item.uid);
+ this.props.removeNode(item[NodePK]);
}
label = node => `${node[this.props.labelKey]}`;
@@ -114,7 +115,10 @@ function makeMapStateToProps() {
return function mapStateToProps(state, props) {
let nodesForList = getDataByPrompt(state, props);
if (!props.stage.showExistingNodes) {
- nodesForList = differenceBy(getDataByPrompt(state, props), networkNodesForOtherPrompts(state, props), 'uid');
+ nodesForList = differenceBy(
+ getDataByPrompt(state, props),
+ networkNodesForOtherPrompts(state, props),
+ NodePK);
}
return {
diff --git a/src/containers/NodePanels.js b/src/containers/NodePanels.js
index 60cd143bf8..c7c6418517 100644
--- a/src/containers/NodePanels.js
+++ b/src/containers/NodePanels.js
@@ -4,8 +4,9 @@ import { connect } from 'react-redux';
import PropTypes from 'prop-types';
import { includes, map, differenceBy } from 'lodash';
import { getNodeLabelFunction, networkNodes, makeNetworkNodesForOtherPrompts } from '../selectors/interface';
-import { getExternalData } from '../selectors/protocol';
+import { getExternalData } from '../selectors/externalData';
import { actionCreators as sessionsActions } from '../ducks/modules/sessions';
+import { NodePK } from '../ducks/modules/network';
import { makeGetPromptNodeAttributes, makeGetPanelConfiguration } from '../selectors/name-generator';
import { Panel, Panels, NodeList } from '../components/';
import { getCSSVariableAsString } from '../utils/CSSVariables';
@@ -52,9 +53,9 @@ class NodePanels extends PureComponent {
onDrop = ({ meta }, dataSource) => {
if (dataSource === 'existing') {
- this.props.toggleNodeAttributes(meta.uid, { ...this.props.activePromptAttributes });
+ this.props.toggleNodeAttributes(meta[NodePK], { ...this.props.activePromptAttributes });
} else {
- this.props.removeNode(meta.uid);
+ this.props.removeNode(meta[NodePK]);
}
}
@@ -112,13 +113,13 @@ class NodePanels extends PureComponent {
const getNodesForDataSource = ({ nodes, existingNodes, externalData, dataSource }) => (
dataSource === 'existing' ?
existingNodes :
- differenceBy(externalData[dataSource].nodes, nodes, 'uid')
+ differenceBy(externalData[dataSource].nodes, nodes, NodePK)
);
const getOriginNodeIds = ({ existingNodes, externalData, dataSource }) => (
dataSource === 'existing' ?
- map(existingNodes, 'uid') :
- map(externalData[dataSource].nodes, 'uid')
+ map(existingNodes, NodePK) :
+ map(externalData[dataSource].nodes, NodePK)
);
function makeMapStateToProps() {
@@ -155,7 +156,7 @@ function makeMapStateToProps() {
) :
({ meta }) => (
meta.itemType === 'EXISTING_NODE' &&
- includes(originNodeIds, meta.uid)
+ includes(originNodeIds, meta[NodePK])
);
return {
diff --git a/src/containers/OrdinalBins.js b/src/containers/OrdinalBins.js
index ebed68f494..d35f7c40bc 100644
--- a/src/containers/OrdinalBins.js
+++ b/src/containers/OrdinalBins.js
@@ -5,6 +5,7 @@ import PropTypes from 'prop-types';
import color from 'color';
import { getNodeLabelFunction, makeNetworkNodesForSubject, makeGetOrdinalValues, makeGetPromptVariable } from '../selectors/interface';
import { actionCreators as sessionsActions } from '../ducks/modules/sessions';
+import { NodePK } from '../ducks/modules/network';
import { NodeList } from '../components/';
import { MonitorDragSource } from '../behaviours/DragAndDrop';
import { getCSSVariableAsString } from '../utils/CSSVariables';
@@ -63,7 +64,7 @@ class OrdinalBins extends PureComponent {
const newValue = {};
newValue[this.props.activePromptVariable] = bin.value;
- this.props.toggleNodeAttributes(meta.uid, newValue);
+ this.props.toggleNodeAttributes(meta[NodePK], newValue);
};
const accentColor = this.calculateAccentColor(index, missingValue);
diff --git a/src/containers/Search/Search.js b/src/containers/Search/Search.js
index 41e9dd4eb1..dea31326b1 100644
--- a/src/containers/Search/Search.js
+++ b/src/containers/Search/Search.js
@@ -9,6 +9,7 @@ import SearchTransition from '../../components/Transition/Search';
import SearchResults from './SearchResults';
import AddCountButton from '../../components/AddCountButton';
import { actionCreators as searchActions } from '../../ducks/modules/search';
+import { NodePK } from '../../ducks/modules/network';
import { makeGetFuse } from '../../selectors/search';
/**
@@ -110,35 +111,13 @@ class Search extends Component {
});
}
- /**
- * A result is considered unique only if it presents some disambiguating
- * information to the user (i.e., its combination of display attributes is unique).
- *
- * The `uid` attribute cannot be used to determine uniqueness: once an item
- * from external data (having no `uid`) is added to the network, it will have
- * a `uid`.
- *
- * @param {object} result A search result
- * @return {string} a unique identifier for the result
- */
- uniqueKeyForResult(result) {
- const displayFields = [
- result[this.props.primaryDisplayField],
- ...this.props.additionalAttributes.map(prop => prop.variable),
- ];
- return displayFields.map(field => result[field]).join('.');
- }
-
- // See uniqueness discussion at uniqueKeyForResult.
// If false, suppress candidate from appearing in search results —
// for example, if the node has already been selected.
// Assumption:
// `excludedNodes` size is small, but search set may be large,
// and so preferable to filter found results dynamically.
isAllowedResult(candidate) {
- const uid = this.uniqueKeyForResult.bind(this);
- const candidateUid = uid(candidate);
- return this.props.excludedNodes.every(excluded => uid(excluded) !== candidateUid);
+ return this.props.excludedNodes.every(excluded => excluded[NodePK] !== candidate[NodePK]);
}
render() {
@@ -181,7 +160,6 @@ class Search extends Component {
const getLabel = result => result[primaryDisplayField];
const getSelected = result => this.state.selectedResults.indexOf(result) > -1;
const getDetails = result => additionalAttributes.map(attr => toDetail(result, attr));
- const getUid = result => this.uniqueKeyForResult(result);
return (
jest.fn(() => (
const mockReduxState = {
protocol: {
variableRegistry: {},
- externalData: {},
},
+ externalData: {},
search: {
collapsed: false,
},
diff --git a/src/containers/Setup/SessionList.js b/src/containers/Setup/SessionList.js
index c7b030089b..14e0c79bce 100644
--- a/src/containers/Setup/SessionList.js
+++ b/src/containers/Setup/SessionList.js
@@ -12,7 +12,7 @@ import { protocolsByPath } from '../../selectors/protocols';
import { CardList } from '../../components';
import { matchSessionPath } from '../../utils/matchSessionPath';
-const shortUid = uid => (uid || '').replace(/-.*/, '');
+const shortId = uuid => (uuid || '').replace(/-.*/, '');
const displayDate = timestamp => timestamp && new Date(timestamp).toLocaleString(undefined, { year: 'numeric', month: 'numeric', day: 'numeric' });
@@ -46,8 +46,8 @@ const emptyView = (
*/
class SessionList extends Component {
onClickLoadSession = (session) => {
- const pathname = this.props.getSessionPath(session.uid);
- this.props.setSession(session.uid);
+ const pathname = this.props.getSessionPath(session.uuid);
+ this.props.setSession(session.uuid);
this.props.loadSession(pathname);
}
@@ -56,7 +56,7 @@ class SessionList extends Component {
// Display most recent first, and filter out any session that doesn't have a protocol
const sessionList = Object.keys(sessions)
- .map(key => ({ uid: key, value: sessions[key] }))
+ .map(key => ({ uuid: key, value: sessions[key] }))
.filter(s => s.value.protocolPath);
sessionList.sort((a, b) => b.value.updatedAt - a.value.updatedAt);
@@ -72,12 +72,13 @@ class SessionList extends Component {
onDeleteCard={(data) => {
// eslint-disable-next-line no-alert
if (confirm('Delete this interview?')) {
- removeSession(data.uid);
+ removeSession(data.uuid);
}
}}
- label={sessionInfo => shortUid(sessionInfo.uid)}
+ label={sessionInfo => shortId(sessionInfo.uuid)}
nodes={sessionList}
onToggleCard={this.onClickLoadSession}
+ getKey={sessionInfo => sessionInfo.uuid}
details={(sessionInfo) => {
const session = sessionInfo.value;
const info = pathInfo(session.path);
diff --git a/src/containers/__tests__/Field.test.js b/src/containers/__tests__/Field.test.js
index e09e552f68..9ca7cb1f96 100644
--- a/src/containers/__tests__/Field.test.js
+++ b/src/containers/__tests__/Field.test.js
@@ -15,19 +15,13 @@ const validation = {
minLength: 2,
};
-jest.mock('uuid');
-
const reduxFormFieldProperties = { input: { name: 'foo', value: '' }, meta: { invalid: false } };
describe('getInputComponent()', () => {
- it('should return renderable component', () => {
- const Input = getInputComponent('Alphanumeric');
-
- const subject = shallow((
-
- ));
-
- expect(subject).toMatchSnapshot();
+ it('should return a dom input', () => {
+ const Input = getInputComponent();
+ const subject = shallow();
+ expect(subject.find('input')).toHaveLength(1);
});
});
diff --git a/src/containers/__tests__/__snapshots__/Field.test.js.snap b/src/containers/__tests__/__snapshots__/Field.test.js.snap
index d70a5d9eab..2bbcedfc8c 100644
--- a/src/containers/__tests__/__snapshots__/Field.test.js.snap
+++ b/src/containers/__tests__/__snapshots__/Field.test.js.snap
@@ -56,214 +56,3 @@ ShallowWrapper {
},
}
`;
-
-exports[`getInputComponent() should return renderable component 1`] = `
-ShallowWrapper {
- "length": 1,
- Symbol(enzyme.__root__): [Circular],
- Symbol(enzyme.__unrendered__): ,
- Symbol(enzyme.__renderer__): Object {
- "batchedUpdates": [Function],
- "getNode": [Function],
- "render": [Function],
- "simulateEvent": [Function],
- "unmount": [Function],
- },
- Symbol(enzyme.__node__): Object {
- "instance": null,
- "key": undefined,
- "nodeType": "host",
- "props": Object {
- "children": Array [
-
-
-
,
-
-
-
,
- ],
- "className": "form-field-container",
- "hidden": false,
- },
- "ref": null,
- "rendered": Array [
- Object {
- "instance": null,
- "key": undefined,
- "nodeType": "host",
- "props": Object {
- "children": "",
- },
- "ref": null,
- "rendered": "",
- "type": "h4",
- },
- Object {
- "instance": null,
- "key": undefined,
- "nodeType": "host",
- "props": Object {
- "children": Array [
- ,
- false,
- ],
- "className": "form-field form-field-text",
- },
- "ref": null,
- "rendered": Array [
- Object {
- "instance": null,
- "key": undefined,
- "nodeType": "host",
- "props": Object {
- "autoFocus": false,
- "className": "form-field-text__input",
- "id": undefined,
- "name": "foo",
- "placeholder": null,
- "type": "text",
- "value": "",
- },
- "ref": null,
- "rendered": null,
- "type": "input",
- },
- false,
- ],
- "type": "div",
- },
- ],
- "type": "div",
- },
- Symbol(enzyme.__nodes__): Array [
- Object {
- "instance": null,
- "key": undefined,
- "nodeType": "host",
- "props": Object {
- "children": Array [
-
-
-
,
-
-
-
,
- ],
- "className": "form-field-container",
- "hidden": false,
- },
- "ref": null,
- "rendered": Array [
- Object {
- "instance": null,
- "key": undefined,
- "nodeType": "host",
- "props": Object {
- "children": "",
- },
- "ref": null,
- "rendered": "",
- "type": "h4",
- },
- Object {
- "instance": null,
- "key": undefined,
- "nodeType": "host",
- "props": Object {
- "children": Array [
- ,
- false,
- ],
- "className": "form-field form-field-text",
- },
- "ref": null,
- "rendered": Array [
- Object {
- "instance": null,
- "key": undefined,
- "nodeType": "host",
- "props": Object {
- "autoFocus": false,
- "className": "form-field-text__input",
- "id": undefined,
- "name": "foo",
- "placeholder": null,
- "type": "text",
- "value": "",
- },
- "ref": null,
- "rendered": null,
- "type": "input",
- },
- false,
- ],
- "type": "div",
- },
- ],
- "type": "div",
- },
- ],
- Symbol(enzyme.__options__): Object {
- "adapter": ReactSixteenAdapter {
- "options": Object {
- "enableComponentDidUpdateOnSetState": true,
- },
- },
- },
-}
-`;
diff --git a/src/ducks/modules/__tests__/externalData.test.js b/src/ducks/modules/__tests__/externalData.test.js
new file mode 100644
index 0000000000..f47697371a
--- /dev/null
+++ b/src/ducks/modules/__tests__/externalData.test.js
@@ -0,0 +1,37 @@
+/* eslint-env jest */
+import reducer from '../externalData';
+import { NodePK } from '../network';
+
+const initialState = null;
+
+const actionWithData = externalData => ({
+ type: 'SET_PROTOCOL',
+ protocol: {
+ externalData,
+ },
+});
+
+describe('the externalData reducer', () => {
+ it('returns the initial state', () => {
+ expect(reducer(undefined, {})).toEqual(initialState);
+ });
+
+ it('sets the state after protocol import', () => {
+ const data = { students: { nodes: [] } };
+ const newState = reducer(initialState, actionWithData(data));
+ expect(newState).toEqual(data);
+ });
+
+ it('assigns a UID to new data', () => {
+ const data = { students: { nodes: [{ name: 'a' }] } };
+ const newState = reducer(initialState, actionWithData(data));
+ expect(newState.students.nodes[0]).toMatchObject({ [NodePK]: expect.any(String) });
+ });
+
+ it('uses consistent hashing for IDs', () => {
+ const data = { students: { nodes: [{ name: 'a' }, { name: 'a' }] } };
+ const newState = reducer(initialState, actionWithData(data));
+ expect(newState.students.nodes[0][NodePK]).toBeDefined();
+ expect(newState.students.nodes[0]).toEqual(newState.students.nodes[1]);
+ });
+});
diff --git a/src/ducks/modules/__tests__/network.test.js b/src/ducks/modules/__tests__/network.test.js
index 5a407cd73c..235104890b 100644
--- a/src/ducks/modules/__tests__/network.test.js
+++ b/src/ducks/modules/__tests__/network.test.js
@@ -1,6 +1,6 @@
/* eslint-env jest */
-import reducer, { actionTypes } from '../network';
+import reducer, { actionTypes, NodePK as PK } from '../network';
const mockState = {
ego: {},
@@ -8,7 +8,7 @@ const mockState = {
edges: [],
};
-const UIDPattern = /[0-9]+_[0-9]+/;
+const UIDPattern = /[a-f\d]+-/;
describe('network reducer', () => {
it('should return the initial state', () => {
@@ -31,16 +31,15 @@ describe('network reducer', () => {
expect(newState.nodes[0]).toEqual({ id: 1, name: 'baz' });
const newNode = newState.nodes[1];
- expect(newNode.id).toEqual(2);
expect(newNode.name).toEqual('foo');
- expect(newNode.uid).toMatch(UIDPattern);
+ expect(newNode[PK]).toMatch(UIDPattern);
});
it('should handle ADD_NODES', () => {
const newState = reducer(
{
...mockState,
- nodes: [{ id: 1, name: 'baz' }],
+ nodes: [{ [PK]: 1, name: 'baz' }],
},
{
type: actionTypes.ADD_NODES,
@@ -49,14 +48,20 @@ describe('network reducer', () => {
);
expect(newState.nodes.length).toBe(3);
- expect(newState.nodes[0]).toEqual({ id: 1, name: 'baz' });
+ expect(newState.nodes[0]).toEqual({ [PK]: 1, name: 'baz' });
+ expect(newState.nodes[1]).toMatchObject({ name: 'foo', [PK]: expect.stringMatching(UIDPattern) });
+ expect(newState.nodes[2]).toMatchObject({ name: 'bar', [PK]: expect.stringMatching(UIDPattern) });
+ });
- const node1 = newState.nodes[1];
- const node2 = newState.nodes[2];
- expect(node1).toMatchObject({ name: 'foo', id: 2 });
- expect(node1.uid).toMatch(UIDPattern);
- expect(node2).toMatchObject({ name: 'bar', id: 3 });
- expect(node2.uid).toMatch(UIDPattern);
+ it('preserves UID when adding a node', () => {
+ const newState = reducer(
+ mockState,
+ {
+ type: actionTypes.ADD_NODES,
+ nodes: [{ name: 'foo', [PK]: '22' }],
+ },
+ );
+ expect(newState.nodes[0][PK]).toEqual('22');
});
it('should support additionalAttributes for ADD_NODES', () => {
@@ -81,16 +86,16 @@ describe('network reducer', () => {
reducer(
{
...mockState,
- nodes: [{ uid: 1, name: 'foo' }, { uid: 2, name: 'bar' }, { uid: 3, name: 'baz' }],
+ nodes: [{ [PK]: 1, name: 'foo' }, { [PK]: 2, name: 'bar' }, { [PK]: 3, name: 'baz' }],
},
{
type: actionTypes.REMOVE_NODE,
- uid: 2,
+ [PK]: 2,
},
),
).toEqual({
...mockState,
- nodes: [{ uid: 1, name: 'foo' }, { uid: 3, name: 'baz' }],
+ nodes: [{ [PK]: 1, name: 'foo' }, { [PK]: 3, name: 'baz' }],
});
});
@@ -98,25 +103,25 @@ describe('network reducer', () => {
const newState = reducer(
{
...mockState,
- nodes: [{ uid: 1, id: 1, name: 'baz' }],
+ nodes: [{ [PK]: 1, id: 1, name: 'baz' }],
},
{
type: actionTypes.UPDATE_NODE,
- node: { uid: 1, name: 'foo' },
+ node: { [PK]: 1, name: 'foo' },
},
);
- expect(newState.nodes[0]).toEqual({ uid: 1, id: 1, name: 'foo' });
+ expect(newState.nodes[0]).toEqual({ [PK]: 1, id: 1, name: 'foo' });
});
it('should handle TOGGLE_NODE_ATTRIBUTES', () => {
const newState = reducer(
{
...mockState,
- nodes: [{ uid: 1, name: 'foo' }, { uid: 2, name: 'bar' }],
+ nodes: [{ [PK]: 1, name: 'foo' }, { [PK]: 2, name: 'bar' }],
},
{
type: actionTypes.TOGGLE_NODE_ATTRIBUTES,
- uid: 1,
+ [PK]: 1,
attributes: { stage: 1 },
},
);
@@ -127,11 +132,11 @@ describe('network reducer', () => {
const secondState = reducer(
{
...mockState,
- nodes: [{ uid: 1, stage: 1, name: 'foo' }, { uid: 2, stage: 1, name: 'bar' }],
+ nodes: [{ [PK]: 1, stage: 1, name: 'foo' }, { [PK]: 2, stage: 1, name: 'bar' }],
},
{
type: actionTypes.TOGGLE_NODE_ATTRIBUTES,
- uid: 2,
+ [PK]: 2,
attributes: { stage: 1 },
},
);
diff --git a/src/ducks/modules/__tests__/sessions.test.js b/src/ducks/modules/__tests__/sessions.test.js
index f6243e051f..60e5f4c469 100644
--- a/src/ducks/modules/__tests__/sessions.test.js
+++ b/src/ducks/modules/__tests__/sessions.test.js
@@ -3,6 +3,7 @@ import configureMockStore from 'redux-mock-store';
import thunk from 'redux-thunk';
import reducer, { actionCreators, actionTypes } from '../sessions';
+import { NodePK } from '../network';
import uuidv4 from '../../../utils/uuid';
const middlewares = [thunk];
@@ -161,7 +162,7 @@ describe('sessions actions', () => {
const expectedAction = {
type: actionTypes.TOGGLE_NODE_ATTRIBUTES,
sessionId: 'a',
- uid: 2,
+ [NodePK]: 2,
attributes: {},
};
@@ -175,7 +176,7 @@ describe('sessions actions', () => {
const expectedAction = {
type: actionTypes.REMOVE_NODE,
sessionId: 'a',
- uid: 2,
+ [NodePK]: 2,
};
store.dispatch(actionCreators.removeNode(2));
diff --git a/src/ducks/modules/externalData.js b/src/ducks/modules/externalData.js
new file mode 100644
index 0000000000..2452bc2809
--- /dev/null
+++ b/src/ducks/modules/externalData.js
@@ -0,0 +1,44 @@
+import objectHash from 'object-hash';
+
+import { actionTypes } from './protocol';
+import { NodePK } from './network';
+
+const initialState = null;
+
+/**
+ * @private
+ * All external data nodes must be identified in the app with a primary key (== NodePK).
+ *
+ * For each object in an external data set's collection of nodes:
+ * - If the object is missing a PK, we assign it one (to the [NodePK] prop)
+ * + The assigned PK is equal to the hash of the object contents, so is consistent across imports
+ * + A PK is missing if it is falsy, and not equal to 0. (`0` is allowed as an identifier.)
+ */
+const dataWithNodePKs = (externalData) => {
+ const cleanedData = {};
+ if (!externalData) {
+ return externalData;
+ }
+ Object.entries(externalData).forEach(([key, val]) => {
+ if (val.nodes) {
+ cleanedData[key] = {
+ ...val,
+ nodes: val.nodes.map(node => ({ ...node, [NodePK]: objectHash(node) })),
+ };
+ } else {
+ cleanedData[key] = val;
+ }
+ });
+ return cleanedData;
+};
+
+export default function reducer(state = initialState, action = {}) {
+ switch (action.type) {
+ case actionTypes.SET_PROTOCOL:
+ return {
+ ...dataWithNodePKs(action.protocol.externalData) || {},
+ };
+ default:
+ return state;
+ }
+}
diff --git a/src/ducks/modules/network.js b/src/ducks/modules/network.js
index af26570611..06ecb194cb 100644
--- a/src/ducks/modules/network.js
+++ b/src/ducks/modules/network.js
@@ -1,4 +1,9 @@
-import { maxBy, reject, findIndex, isMatch, omit } from 'lodash';
+import { reject, findIndex, isMatch, omit } from 'lodash';
+
+import uuidv4 from '../../utils/uuid';
+
+// Primary key used on node data
+export const NodePK = '_uid';
export const ADD_NODES = 'ADD_NODES';
export const REMOVE_NODE = 'REMOVE_NODE';
@@ -16,18 +21,6 @@ const initialState = {
edges: [],
};
-// We use these internally to uniquely identify nodes accross previous data / network data
-export function nextUid(nodes, index = 1) {
- return `${Date.now()}_${nodes.length + index}`;
-}
-
-// We use these internally to uniquely identify nodes accross network data only
-// (previous data is immutable)
-function nextId(nodes) {
- if (nodes.length === 0) { return 1; }
- return maxBy(nodes, 'id').id + 1;
-}
-
function flipEdge(edge) {
return { from: edge.to, to: edge.from, type: edge.type };
}
@@ -40,33 +33,25 @@ function edgeExists(edges, edge) {
}
function getNodesWithBatchAdd(oldNodes, newNodes, additionalAttributes) {
- let nodes = oldNodes;
- newNodes.forEach((newNode) => {
- const id = nextId(nodes);
- const uid = nextUid(nodes);
- // Provided uid can override generated one, but not id
- nodes = [...nodes, { uid, ...newNode, ...additionalAttributes, id }];
- });
- return nodes;
+ const withAttrs = newNode => ({ ...additionalAttributes, [NodePK]: uuidv4(), ...newNode });
+ return oldNodes.concat(newNodes.map(withAttrs));
}
function getUpdatedNodes(nodes, updatedNode, full) {
const updatedNodes = nodes.map((node) => {
- if (node.uid !== updatedNode.uid) { return node; }
+ if (node[NodePK] !== updatedNode[NodePK]) { return node; }
if (full) {
return {
...updatedNode,
- id: node.id,
- uid: node.uid,
+ [NodePK]: node[NodePK],
};
}
return {
...node,
...updatedNode,
- id: node.id,
- uid: node.uid,
+ [NodePK]: node[NodePK],
};
});
return updatedNodes;
@@ -81,10 +66,10 @@ export default function reducer(state = initialState, action = {}) {
};
}
case TOGGLE_NODE_ATTRIBUTES: {
- const attributes = omit(action.attributes, ['uid', 'id']);
+ const attributes = omit(action.attributes, [NodePK]);
const updatedNodes = state.nodes.map((node) => {
- if (node.uid !== action.uid) { return node; }
+ if (node[NodePK] !== action[NodePK]) { return node; }
if (isMatch(node, attributes)) {
return omit(node, Object.getOwnPropertyNames(attributes));
@@ -108,10 +93,9 @@ export default function reducer(state = initialState, action = {}) {
};
}
case REMOVE_NODE:
- // TODO: Shouldn't this use node.id?
return {
...state,
- nodes: reject(state.nodes, node => node.uid === action.uid),
+ nodes: reject(state.nodes, node => node[NodePK] === action[NodePK]),
};
case ADD_EDGE:
if (edgeExists(state.edges, action.edge)) { return state; }
diff --git a/src/ducks/modules/protocol.js b/src/ducks/modules/protocol.js
index ce4e7143f5..b063ed348d 100644
--- a/src/ducks/modules/protocol.js
+++ b/src/ducks/modules/protocol.js
@@ -1,5 +1,6 @@
import { combineEpics } from 'redux-observable';
import { Observable } from 'rxjs';
+import { omit } from 'lodash';
import { loadProtocol, importProtocol, downloadProtocol, loadFactoryProtocol } from '../../utils/protocol';
import { actionTypes as SessionActionTypes } from './session';
@@ -47,7 +48,7 @@ export default function reducer(state = initialState, action = {}) {
case SET_PROTOCOL:
return {
...state,
- ...action.protocol,
+ ...omit(action.protocol, 'externalData'),
path: action.path,
isLoaded: true,
isLoading: false,
diff --git a/src/ducks/modules/rootReducer.js b/src/ducks/modules/rootReducer.js
index 4fa2933574..e85bfbc1c7 100644
--- a/src/ducks/modules/rootReducer.js
+++ b/src/ducks/modules/rootReducer.js
@@ -2,6 +2,7 @@ import { combineReducers } from 'redux';
import { routerReducer } from 'react-router-redux';
import { reducer as formReducer } from 'redux-form';
+import externalData from './externalData';
import sessions from './sessions';
import session from './session';
import device from './device';
@@ -19,6 +20,7 @@ const appReducer = combineReducers({
session,
sessions,
device,
+ externalData,
protocol,
protocols,
modals,
diff --git a/src/ducks/modules/sessions.js b/src/ducks/modules/sessions.js
index c93288e654..0aacdc4521 100644
--- a/src/ducks/modules/sessions.js
+++ b/src/ducks/modules/sessions.js
@@ -3,11 +3,12 @@ import { Observable } from 'rxjs';
import { combineEpics } from 'redux-observable';
import uuidv4 from '../../utils/uuid';
-import network, { ADD_NODES, REMOVE_NODE, UPDATE_NODE, TOGGLE_NODE_ATTRIBUTES, ADD_EDGE, TOGGLE_EDGE, REMOVE_EDGE, SET_EGO, UNSET_EGO } from './network';
+import network, { NodePK, ADD_NODES, REMOVE_NODE, 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';
import { getPairedServerFactory } from '../../selectors/servers';
+
const ADD_SESSION = 'ADD_SESSION';
const UPDATE_SESSION = 'UPDATE_SESSION';
const UPDATE_PROMPT = 'UPDATE_PROMPT';
@@ -124,7 +125,7 @@ const toggleNodeAttributes = (uid, attributes) => (dispatch, getState) => {
dispatch({
type: TOGGLE_NODE_ATTRIBUTES,
sessionId: session,
- uid,
+ [NodePK]: uid,
attributes,
});
};
@@ -135,7 +136,7 @@ const removeNode = uid => (dispatch, getState) => {
dispatch({
type: REMOVE_NODE,
sessionId: session,
- uid,
+ [NodePK]: uid,
});
};
diff --git a/src/selectors/__tests__/externalData.test.js b/src/selectors/__tests__/externalData.test.js
new file mode 100644
index 0000000000..f2bd7c6deb
--- /dev/null
+++ b/src/selectors/__tests__/externalData.test.js
@@ -0,0 +1,23 @@
+/* eslint-env jest */
+import { getExternalData } from '../externalData';
+
+const externalData = {
+ baz: 'bar',
+};
+
+const mockState = {
+ externalData,
+};
+
+const emptyState = {
+ externalData: null,
+};
+
+describe('protocol selector', () => {
+ describe('memoed selectors', () => {
+ it('should get external data', () => {
+ expect(getExternalData(mockState)).toEqual(externalData);
+ expect(getExternalData(emptyState)).toEqual(null);
+ });
+ });
+});
diff --git a/src/selectors/__tests__/name-generator.test.js b/src/selectors/__tests__/name-generator.test.js
index 2d47f1ec25..15424e3177 100644
--- a/src/selectors/__tests__/name-generator.test.js
+++ b/src/selectors/__tests__/name-generator.test.js
@@ -45,11 +45,6 @@ const externalNode = {
};
const mockProtocol = {
- externalData: {
- schoolPupils: {
- nodes: [externalNode],
- },
- },
variableRegistry: {
node: {
person: {
@@ -82,6 +77,11 @@ const edges = [{ to: 'bar', from: 'foo' }, { to: 'asdf', from: 'qwerty' }];
const mockState = {
network: { nodes, edges },
protocol: mockProtocol,
+ externalData: {
+ schoolPupils: {
+ nodes: [externalNode],
+ },
+ },
};
describe('name generator selector', () => {
diff --git a/src/selectors/__tests__/protocol.test.js b/src/selectors/__tests__/protocol.test.js
index d666963d3e..8f54e0dd10 100644
--- a/src/selectors/__tests__/protocol.test.js
+++ b/src/selectors/__tests__/protocol.test.js
@@ -49,11 +49,6 @@ describe('protocol selector', () => {
expect(Protocol.protocolForms(emptyState)).toEqual(undefined);
});
- it('should get external data', () => {
- expect(Protocol.getExternalData(mockState)).toEqual(externalData);
- expect(Protocol.getExternalData(emptyState)).toEqual(undefined);
- });
-
it('should get node color', () => {
const selected = Protocol.makeGetNodeColor();
expect(selected(mockState, mockProps)).toEqual('node-color-seq-2');
diff --git a/src/selectors/__tests__/search.test.js b/src/selectors/__tests__/search.test.js
index 128040096c..f90b62aa53 100644
--- a/src/selectors/__tests__/search.test.js
+++ b/src/selectors/__tests__/search.test.js
@@ -16,7 +16,12 @@ const externalNode = {
age: 23,
};
-const mockProtocol = {
+const mockProps = {
+ options: {},
+ dataSource: 'schoolPupils',
+};
+
+const mockState = {
externalData: {
schoolPupils: {
nodes: [externalNode, {
@@ -26,15 +31,6 @@ const mockProtocol = {
},
};
-const mockProps = {
- options: {},
- dataSource: 'schoolPupils',
-};
-
-const mockState = {
- protocol: mockProtocol,
-};
-
describe('search', () => {
describe('memoed selectors', () => {
it('should makeGetFuse', () => {
diff --git a/src/selectors/externalData.js b/src/selectors/externalData.js
new file mode 100644
index 0000000000..7e2c0b929f
--- /dev/null
+++ b/src/selectors/externalData.js
@@ -0,0 +1,7 @@
+/* eslint-disable import/prefer-default-export */
+import { createDeepEqualSelector } from './utils';
+
+export const getExternalData = createDeepEqualSelector(
+ state => state.externalData,
+ protocolData => protocolData,
+);
diff --git a/src/selectors/name-generator.js b/src/selectors/name-generator.js
index d39746033d..a2d0a86e63 100644
--- a/src/selectors/name-generator.js
+++ b/src/selectors/name-generator.js
@@ -3,9 +3,8 @@
import { createSelector } from 'reselect';
import { has, get } from 'lodash';
import { makeGetSubject, makeGetIds, makeGetNodeType, makeGetAdditionalAttributes } from './interface';
-import { getExternalData, protocolRegistry } from './protocol';
-import { nextUid } from '../ducks/modules/network';
-
+import { protocolRegistry } from './protocol';
+import { getExternalData } from './externalData';
// Selectors that are specific to the name generator
@@ -78,8 +77,7 @@ export const getSortDirectionDefault = createSelector(
export const getDataByPrompt = createSelector(
getExternalData,
getDatasourceKey,
- (externalData, key) => externalData[key].nodes.map(
- (node, index) => ({ uid: nextUid(externalData[key].nodes, index), ...node })),
+ (externalData, key) => externalData[key].nodes,
);
export const makeGetNodeIconName = () => createSelector(
diff --git a/src/selectors/node-provider.js b/src/selectors/node-provider.js
index f49cfaa8d6..17cdfec120 100644
--- a/src/selectors/node-provider.js
+++ b/src/selectors/node-provider.js
@@ -3,7 +3,8 @@
import { createSelector } from 'reselect';
import { differenceBy } from 'lodash';
import { networkNodes, makeNetworkNodesForOtherPrompts } from './interface';
-import { getExternalData } from './protocol';
+import { getExternalData } from './externalData';
+import { NodePK } from '../ducks/modules/network';
const propDataSource = (_, props) => props.dataSource;
@@ -23,6 +24,6 @@ export const makeGetProviderNodes = () => {
networkNodesForOtherPrompts,
getExternalData,
(dataSource, nodes, existingNodes, externalData) =>
- (dataSource === 'existing' ? existingNodes : differenceBy(externalData[dataSource].nodes, nodes, 'uid')),
+ (dataSource === 'existing' ? existingNodes : differenceBy(externalData[dataSource].nodes, nodes, NodePK)),
);
};
diff --git a/src/selectors/protocol.js b/src/selectors/protocol.js
index 13e87bcb9b..4c6e2784a0 100644
--- a/src/selectors/protocol.js
+++ b/src/selectors/protocol.js
@@ -18,11 +18,6 @@ export const protocolForms = createDeepEqualSelector(
forms => forms,
);
-export const getExternalData = createDeepEqualSelector(
- state => state.protocol.externalData,
- protocolData => protocolData,
-);
-
export const getRemoteProtocolId = createDeepEqualSelector(
state => state.protocol && state.protocol.type !== 'factory' && state.protocol.name,
remoteName => nameDigest(remoteName) || null,
diff --git a/src/selectors/search.js b/src/selectors/search.js
index 116ed8b811..e201ce6bcc 100644
--- a/src/selectors/search.js
+++ b/src/selectors/search.js
@@ -2,7 +2,7 @@
import Fuse from 'fuse.js';
import { createSelector } from 'reselect';
-import { getExternalData } from './protocol';
+import { getExternalData } from './externalData';
// The value of this key should point to an attribute in the protocol's externalData.
const getDatasourceKey = (_, props) => props.dataSourceKey;
diff --git a/src/selectors/sociogram.js b/src/selectors/sociogram.js
index 465af135b9..2d13b30aac 100644
--- a/src/selectors/sociogram.js
+++ b/src/selectors/sociogram.js
@@ -2,7 +2,6 @@
import { createSelector } from 'reselect';
import {
- find,
filter,
has,
get,
@@ -18,6 +17,7 @@ import {
import { networkEdges, makeGetDisplayVariable, makeNetworkNodesForSubject } from './interface';
import { createDeepEqualSelector } from './utils';
import sortOrder from '../utils/sortOrder';
+import { NodePK } from '../ducks/modules/network';
// Selectors that are specific to the name generator
@@ -121,8 +121,8 @@ export const makeGetNextUnplacedNode = () => {
};
const edgeCoords = (edge, { nodes, layoutVariable }) => {
- const from = find(nodes, ['id', edge.from]);
- const to = find(nodes, ['id', edge.to]);
+ const from = nodes.find(n => n[NodePK] === edge.from);
+ const to = nodes.find(n => n[NodePK] === edge.to);
if (!from || !to) { return { from: null, to: null }; }
diff --git a/src/utils/ExportData.js b/src/utils/ExportData.js
index 22f122eafb..bd81e7cece 100644
--- a/src/utils/ExportData.js
+++ b/src/utils/ExportData.js
@@ -1,6 +1,7 @@
import { findKey, forInRight, isNil, join } from 'lodash';
import saveFile from './SaveFile';
+import { NodePK } from '../ducks/modules/network';
const setUpXml = () => {
const xmlDoc = '\n' +
@@ -146,8 +147,8 @@ const addElements = (graph, uri, dataList, type, excludeList, variableRegistry,
extra: false) => {
dataList.forEach((dataElement, index) => {
const domElement = document.createElementNS(uri, type);
- if (dataElement.id) {
- domElement.setAttribute('id', dataElement.id);
+ if (dataElement[NodePK]) {
+ domElement.setAttribute('id', dataElement[NodePK]);
} else {
domElement.setAttribute('id', index);
}
@@ -199,8 +200,8 @@ const createGraphML = (networkData, variableRegistry, openErrorDialog) => {
});
// generate keys for attributes
- let missingVariables = generateKeys(graph, graphML, networkData.nodes, 'node', ['id'], variableRegistry, layoutVariable);
- missingVariables = missingVariables.concat(generateKeys(graph, graphML, networkData.edges, 'edge', ['from', 'to', 'id'], variableRegistry));
+ let missingVariables = generateKeys(graph, graphML, networkData.nodes, 'node', [NodePK], variableRegistry, layoutVariable);
+ missingVariables = missingVariables.concat(generateKeys(graph, graphML, networkData.edges, 'edge', ['from', 'to'], variableRegistry));
if (missingVariables.length > 0) {
// hard fail if checking the registry fails
// remove this to fall back to using "text" for unknowns
@@ -209,8 +210,8 @@ const createGraphML = (networkData, variableRegistry, openErrorDialog) => {
}
// add nodes and edges to graph
- addElements(graph, graphML.namespaceURI, networkData.nodes, 'node', ['id'], variableRegistry, layoutVariable);
- addElements(graph, graphML.namespaceURI, networkData.edges, 'edge', ['from', 'to', 'id'], variableRegistry, null, true);
+ addElements(graph, graphML.namespaceURI, networkData.nodes, 'node', [NodePK], variableRegistry, layoutVariable);
+ addElements(graph, graphML.namespaceURI, networkData.edges, 'edge', ['from', 'to'], variableRegistry, null, true);
return saveFile(xmlToString(xml), openErrorDialog, 'graphml', ['graphml'], 'networkcanvas.graphml', 'text/xml',
{ message: 'Your network canvas graphml file.', subject: 'network canvas export' });
diff --git a/src/utils/Network.js b/src/utils/Network.js
index f9c11ce336..120134209e 100644
--- a/src/utils/Network.js
+++ b/src/utils/Network.js
@@ -1,4 +1,5 @@
import { filter, differenceBy } from 'lodash';
+import { NodePK } from '../ducks/modules/network';
const nodeIncludesAttributes = (network, attributes) => {
const nodes = filter(network.nodes, attributes);
@@ -10,7 +11,7 @@ const nodeIncludesAttributes = (network, attributes) => {
};
const difference = (source, target) => {
- const nodes = differenceBy(source.nodes, target.nodes, 'uid');
+ const nodes = differenceBy(source.nodes, target.nodes, NodePK);
return {
...source, // TODO: filter edge etc.
diff --git a/src/utils/__mocks__/uuid.js b/src/utils/__mocks__/uuid.js
new file mode 100644
index 0000000000..659413d084
--- /dev/null
+++ b/src/utils/__mocks__/uuid.js
@@ -0,0 +1,9 @@
+/* eslint-env jest */
+const uuidv4 = jest.fn(() => {
+ // Fake uuids
+ const bytes = Array.from(Array(16), () => Math.floor(Math.random() * 256));
+ const s = bytes.map(b => b.toString(16).padStart(2, '0')).join('');
+ return `${s.slice(0, 8)}-${s.slice(8, 12)}-${s.slice(12, 16)}-${s.slice(16, 20)}-${s.slice(20)}`;
+});
+
+export default uuidv4;
diff --git a/src/utils/__tests__/ExportData.test.js b/src/utils/__tests__/ExportData.test.js
index cff63432df..477d1c5ac6 100644
--- a/src/utils/__tests__/ExportData.test.js
+++ b/src/utils/__tests__/ExportData.test.js
@@ -1,6 +1,7 @@
/* eslint-env jest */
import saveFile from '../SaveFile';
import ExportData from '../ExportData';
+import { NodePK } from '../../ducks/modules/network';
function mockSerializeToString() {
return { serializeToString: xmlData => xmlData.documentElement.outerHTML };
@@ -42,11 +43,11 @@ const variableRegistry = {
const sessionA = {
network: {
edges: [
- { id: 1, type: 'friend', to: 1, from: 2, connected: true },
+ { type: 'friend', to: 1, from: 2, connected: true },
],
edgo: {},
nodes: [
- { id: 1,
+ { [NodePK]: 1,
type: 'person',
name: 'soAndSo',
aString: 'content',
@@ -60,7 +61,7 @@ const sessionA = {
aLayout: { x: 0.4134, y: 0.2356 },
aLocation: { latitude: 41.799756, longitude: -87.66443 },
},
- { id: 2,
+ { [NodePK]: 2,
type: 'person',
name: 'whoDunnit',
aString: 'Another Content',
@@ -74,7 +75,7 @@ const sessionA = {
aLayout: { x: 0.3434, y: 0.3156 },
aLocation: { latitude: 44.9756, longitude: 18.443 },
},
- { id: 3,
+ { [NodePK]: 3,
type: 'person',
name: 'whatsErName',
aString: 'More Content',
@@ -97,4 +98,12 @@ describe('export data function', () => {
const xmlResult = ExportData(sessionA.network, variableRegistry, () => {});
expect(xmlResult).toMatchSnapshot();
});
+
+ it('translates node primary key to "id" attribute', () => {
+ const xml = ExportData(sessionA.network, variableRegistry, () => {});
+ const doc = new DOMParser().parseFromString(xml, 'application/xml');
+ const node = doc.querySelector('graph node:first-child');
+ expect(node.id).toEqual(`${sessionA.network.nodes[0][NodePK]}`);
+ expect(node.querySelector(`[key="${NodePK}"]`)).toBe(null);
+ });
});
diff --git a/src/utils/__tests__/__snapshots__/ExportData.test.js.snap b/src/utils/__tests__/__snapshots__/ExportData.test.js.snap
index 641761d96c..b1ce450ec5 100644
--- a/src/utils/__tests__/__snapshots__/ExportData.test.js.snap
+++ b/src/utils/__tests__/__snapshots__/ExportData.test.js.snap
@@ -4,6 +4,6 @@ exports[`export data function should create valid xml for network data 1`] = `
"
- personsoAndSocontent1230.333true15293494518472410.41340.2356{\\"latitude\\":41.799756,\\"longitude\\":-87.66443}423.3216587.0591999999999personwhoDunnitAnother Content4561.833false15293434818477720.34340.3156{\\"latitude\\":44.9756,\\"longitude\\":18.443}351.6416525.6192personwhatsErNameMore Content7890.430.1true157934948184734-10.11340.7356{\\"latitude\\":13.9756,\\"longitude\\":-27.443}116.1216203.05919999999998friendtrue
+ personsoAndSocontent1230.333true15293494518472410.41340.2356{\\"latitude\\":41.799756,\\"longitude\\":-87.66443}423.3216587.0591999999999personwhoDunnitAnother Content4561.833false15293434818477720.34340.3156{\\"latitude\\":44.9756,\\"longitude\\":18.443}351.6416525.6192personwhatsErNameMore Content7890.430.1true157934948184734-10.11340.7356{\\"latitude\\":13.9756,\\"longitude\\":-27.443}116.1216203.05919999999998friendtrue
"
`;
diff --git a/src/utils/uidGenerator.js b/src/utils/uidGenerator.js
deleted file mode 100644
index 5235da80f2..0000000000
--- a/src/utils/uidGenerator.js
+++ /dev/null
@@ -1,9 +0,0 @@
-function* uidGenerator() {
- let i = 1;
- for (;;) {
- yield i;
- i += 1;
- }
-}
-
-export default uidGenerator;