From 7b72cae015f411f53d29223fd65f62b02a0d2549 Mon Sep 17 00:00:00 2001 From: Steve Mckellar Date: Tue, 8 Jan 2019 15:25:34 +0000 Subject: [PATCH 1/3] Updated mock protocol and created mock external data file --- src/utils/webShims/mockExternalData.json | 44 ++++++++++++++ src/utils/webShims/mockProtocol.json | 74 ++++++++---------------- 2 files changed, 69 insertions(+), 49 deletions(-) create mode 100644 src/utils/webShims/mockExternalData.json diff --git a/src/utils/webShims/mockExternalData.json b/src/utils/webShims/mockExternalData.json new file mode 100644 index 000000000..3af6b2124 --- /dev/null +++ b/src/utils/webShims/mockExternalData.json @@ -0,0 +1,44 @@ +{ + "nodes": [ + { + "type": "d39a47507bbe27c2a7948861847f3607eda8e1be", + "attributes": { + "6ae999552a0d2dca14d62e2bc8b764d377b1dd6c": "Anita", + "7428c4145a49b9d07e4f88033b7fc97aa98b6b99": "Annie", + "5dc56b9aab61867257a3c1bd7c786c9410d38cd2": "21" + } + }, + { + "type": "d39a47507bbe27c2a7948861847f3607eda8e1be", + "attributes": { + "6ae999552a0d2dca14d62e2bc8b764d377b1dd6c": "Barry", + "7428c4145a49b9d07e4f88033b7fc97aa98b6b99": "Baz", + "5dc56b9aab61867257a3c1bd7c786c9410d38cd2": "28" + } + }, + { + "type": "d39a47507bbe27c2a7948861847f3607eda8e1be", + "attributes": { + "6ae999552a0d2dca14d62e2bc8b764d377b1dd6c": "Carlito", + "7428c4145a49b9d07e4f88033b7fc97aa98b6b99": "Carl", + "5dc56b9aab61867257a3c1bd7c786c9410d38cd2": "23" + } + }, + { + "type": "d39a47507bbe27c2a7948861847f3607eda8e1be", + "attributes": { + "6ae999552a0d2dca14d62e2bc8b764d377b1dd6c": "Dee", + "7428c4145a49b9d07e4f88033b7fc97aa98b6b99": "Dee", + "5dc56b9aab61867257a3c1bd7c786c9410d38cd2": "40" + } + }, + { + "type": "d39a47507bbe27c2a7948861847f3607eda8e1be", + "attributes": { + "6ae999552a0d2dca14d62e2bc8b764d377b1dd6c": "Eugine", + "7428c4145a49b9d07e4f88033b7fc97aa98b6b99": "Eu", + "5dc56b9aab61867257a3c1bd7c786c9410d38cd2": "18" + } + } + ] +} diff --git a/src/utils/webShims/mockProtocol.json b/src/utils/webShims/mockProtocol.json index a3f43125e..66c1eb969 100644 --- a/src/utils/webShims/mockProtocol.json +++ b/src/utils/webShims/mockProtocol.json @@ -396,50 +396,26 @@ } } }, - "externalData": { - "previousInterview": { - "nodes": [ - { - "type": "d39a47507bbe27c2a7948861847f3607eda8e1be", - "attributes": { - "6ae999552a0d2dca14d62e2bc8b764d377b1dd6c": "Anita", - "7428c4145a49b9d07e4f88033b7fc97aa98b6b99": "Annie", - "5dc56b9aab61867257a3c1bd7c786c9410d38cd2": "21" - } - }, - { - "type": "d39a47507bbe27c2a7948861847f3607eda8e1be", - "attributes": { - "6ae999552a0d2dca14d62e2bc8b764d377b1dd6c": "Barry", - "7428c4145a49b9d07e4f88033b7fc97aa98b6b99": "Baz", - "5dc56b9aab61867257a3c1bd7c786c9410d38cd2": "28" - } - }, - { - "type": "d39a47507bbe27c2a7948861847f3607eda8e1be", - "attributes": { - "6ae999552a0d2dca14d62e2bc8b764d377b1dd6c": "Carlito", - "7428c4145a49b9d07e4f88033b7fc97aa98b6b99": "Carl", - "5dc56b9aab61867257a3c1bd7c786c9410d38cd2": "23" - } - }, - { - "type": "d39a47507bbe27c2a7948861847f3607eda8e1be", - "attributes": { - "6ae999552a0d2dca14d62e2bc8b764d377b1dd6c": "Dee", - "7428c4145a49b9d07e4f88033b7fc97aa98b6b99": "Dee", - "5dc56b9aab61867257a3c1bd7c786c9410d38cd2": "40" - } - }, - { - "type": "d39a47507bbe27c2a7948861847f3607eda8e1be", - "attributes": { - "6ae999552a0d2dca14d62e2bc8b764d377b1dd6c": "Eugine", - "7428c4145a49b9d07e4f88033b7fc97aa98b6b99": "Eu", - "5dc56b9aab61867257a3c1bd7c786c9410d38cd2": "18" - } - } - ] + "assetManifest": { + "475fd0720355e5fb2bf96964820e6d82159c2dd2": { + "type": "network", + "name": "Previous Interview", + "source": "previous_interview.json" + }, + "c9b7477e17321509df4a4b88731b2ab3674a21f8": { + "type": "image", + "name": "Rubber Duck", + "source": "rubberduck.jpg" + }, + "f65e5c7753fd9db39e7b9bdf6352ec139d49f03d": { + "type": "video", + "name": "Video", + "source": "video.mov" + }, + "79879b313425e99ea0b1b8cede607374eab0b1f3": { + "type": "audio", + "name": "Click the thing", + "source": "click_the_thing.mp3" } }, "forms": { @@ -483,9 +459,9 @@ "type": "Information", "title": "Title for the welcome stage", "items": [ - { "id": "2", "size": "SMALL", "type": "audio", "content": "click_the_thing.mp3" }, - { "id": "3", "size": "MEDIUM", "type": "video", "content": "video.mov" }, - { "id": "4", "size": "SMALL", "type": "image", "content": "rubberduck.jpg" } + { "id": "2", "size": "SMALL", "type": "asset", "content": "79879b313425e99ea0b1b8cede607374eab0b1f3" }, + { "id": "3", "size": "MEDIUM", "type": "asset", "content": "f65e5c7753fd9db39e7b9bdf6352ec139d49f03d" }, + { "id": "4", "size": "SMALL", "type": "asset", "content": "c9b7477e17321509df4a4b88731b2ab3674a21f8" } ] }, { @@ -588,7 +564,7 @@ { "id": "6su", "text": "Within the past 6 months, who has been supportive?", - "dataSource": "previousInterview", + "dataSource": "475fd0720355e5fb2bf96964820e6d82159c2dd2", "showExistingNodes": true, "additionalAttributes": { "5d69e2e1f8a9ea63d425602943831500c3d5c01d": true @@ -625,7 +601,7 @@ { "id": "6su", "text": "Within the past 6 months, who has been supportive?", - "dataSource": "previousInterview", + "dataSource": "475fd0720355e5fb2bf96964820e6d82159c2dd2", "cardOptions": { "displayLabel": "6ae999552a0d2dca14d62e2bc8b764d377b1dd6c", "additionalProperties": [ From d5e2e9821de337549b5391b474161704920b8f99 Mon Sep 17 00:00:00 2001 From: Steve Mckellar Date: Tue, 8 Jan 2019 17:57:23 +0000 Subject: [PATCH 2/3] Get asset item meta from assetManifest --- .../sections/ContentGrid/ContentGrid.js | 4 +-- .../StageEditor/sections/ContentGrid/Item.js | 14 ++++---- .../ContentGrid/withFilteredFieldErrors.js | 11 ++++++ .../sections/ContentGrid/withItemMeta.js | 34 +++++++++++++++++++ 4 files changed, 54 insertions(+), 9 deletions(-) create mode 100644 src/components/StageEditor/sections/ContentGrid/withFilteredFieldErrors.js create mode 100644 src/components/StageEditor/sections/ContentGrid/withItemMeta.js diff --git a/src/components/StageEditor/sections/ContentGrid/ContentGrid.js b/src/components/StageEditor/sections/ContentGrid/ContentGrid.js index 23a2100c1..0a1aa7c60 100644 --- a/src/components/StageEditor/sections/ContentGrid/ContentGrid.js +++ b/src/components/StageEditor/sections/ContentGrid/ContentGrid.js @@ -57,9 +57,9 @@ class ContentGrid extends Component { return (
-

Content Boxes

+

Content Items

- Use this section to configure up to three content boxes, containing images, video, + Use this section to configure up to three content items, containing images, video, audio, or text.

diff --git a/src/components/StageEditor/sections/ContentGrid/Item.js b/src/components/StageEditor/sections/ContentGrid/Item.js index 0d780f30e..8d67290e6 100644 --- a/src/components/StageEditor/sections/ContentGrid/Item.js +++ b/src/components/StageEditor/sections/ContentGrid/Item.js @@ -1,12 +1,14 @@ import React, { Component } from 'react'; -import { connect } from 'react-redux'; import PropTypes from 'prop-types'; import cx from 'classnames'; +import { compose } from 'recompose'; import { get } from 'lodash'; import ItemPreview from './ItemPreview'; import ItemChooser from './ItemChooser'; import ItemEditor from './ItemEditor'; import { sizes } from './sizes'; +import withItemMeta from './withItemMeta'; +import withFilteredFieldErrors from './withFilteredFieldErrors'; class Item extends Component { static propTypes = { @@ -108,11 +110,9 @@ class Item extends Component { } } -const mapStateToProps = (state, { fieldId, form, errors }) => ({ - item: form.getValues(state, `${fieldId}`), - error: get(errors, fieldId), -}); - export { Item }; -export default connect(mapStateToProps)(Item); +export default compose( + withFilteredFieldErrors, + withItemMeta, +)(Item); diff --git a/src/components/StageEditor/sections/ContentGrid/withFilteredFieldErrors.js b/src/components/StageEditor/sections/ContentGrid/withFilteredFieldErrors.js new file mode 100644 index 000000000..becd3b74f --- /dev/null +++ b/src/components/StageEditor/sections/ContentGrid/withFilteredFieldErrors.js @@ -0,0 +1,11 @@ +import { withPropsOnChange } from 'recompose'; +import { get } from 'lodash'; + +const withFilteredFieldErrors = withPropsOnChange( + ['errors', 'fieldId'], + ({ errors, fieldId }) => ({ + errors: get(errors, fieldId), + }), +); + +export default withFilteredFieldErrors; diff --git a/src/components/StageEditor/sections/ContentGrid/withItemMeta.js b/src/components/StageEditor/sections/ContentGrid/withItemMeta.js new file mode 100644 index 000000000..0e8069328 --- /dev/null +++ b/src/components/StageEditor/sections/ContentGrid/withItemMeta.js @@ -0,0 +1,34 @@ +import { connect } from 'react-redux'; +import { get } from 'lodash'; + +// TODO: move to selectors +const getAssetManifest = state => + get(state, 'protocol.present.assetManifest', {}); + +const mapStateToProps = (state, { fieldId, form }) => { + const field = form.getValues(state, `${fieldId}`); + + if (!field) { return {}; } + + if (field.type !== 'asset') { + return { item: field }; + } + + const assetManifest = getAssetManifest(state); + const itemMeta = get(assetManifest, field.content, {}); + const item = { + ...field, + ...itemMeta, + content: itemMeta.source, + }; + + return { + item, + }; +}; + +const withItemMeta = connect( + mapStateToProps, +); + +export default withItemMeta; From 7560151dec99ff026e96e4698482ea40a906a5e6 Mon Sep 17 00:00:00 2001 From: Steve Mckellar Date: Wed, 9 Jan 2019 19:00:44 +0000 Subject: [PATCH 3/3] Retrofit assetManifest where externalData was used previously --- .../__tests__/selectors.test.js | 40 +++++--------- .../NameGeneratorListPrompts/selectors.js | 54 ++++++------------- .../withDataSourceOptions.js | 5 +- .../sections/NodePanels/NodePanel.js | 14 ++--- .../sections/NodePanels/NodePanels.js | 10 ++-- src/selectors/protocol.js | 24 +++++---- src/utils/__tests__/getAssetData.test.js | 48 +++++++++++++++++ src/utils/getAssetData.js | 19 +++++++ 8 files changed, 119 insertions(+), 95 deletions(-) create mode 100644 src/utils/__tests__/getAssetData.test.js create mode 100644 src/utils/getAssetData.js diff --git a/src/components/StageEditor/sections/NameGeneratorListPrompts/__tests__/selectors.test.js b/src/components/StageEditor/sections/NameGeneratorListPrompts/__tests__/selectors.test.js index 86d41a2ac..1052275b0 100644 --- a/src/components/StageEditor/sections/NameGeneratorListPrompts/__tests__/selectors.test.js +++ b/src/components/StageEditor/sections/NameGeneratorListPrompts/__tests__/selectors.test.js @@ -1,6 +1,6 @@ /* eslint-env jest */ import { - makeGetDataSourcesWithNodeTypeOptions, + getNetworkOptions, makeGetExternalDataPropertyOptions, } from '../selectors'; @@ -8,18 +8,16 @@ import { const mockState = { protocol: { present: { - externalData: { + assetManifest: { foo: { - nodes: [ - { type: 'bar', attributes: { alpha: 1, bravo: 2 } }, - { type: 'bar', attributes: { charlie: 3, bravo: 2 } }, - ], + type: 'network', + name: 'My Network', + source: 'myNetwork.json', }, - sourceWithMixedNodes: { - nodes: [ - { type: 'something', attributes: { alpha: 1, bravo: 2 } }, - { type: 'else', attributes: { charlie: 3, bravo: 2 } }, - ], + bar: { + type: 'image', + name: 'An Image', + source: 'anImage.jpg', }, }, variableRegistry: { @@ -38,22 +36,10 @@ const mockState = { }; describe('NameGeneratorListPrompts selectors', () => { - describe('getDataSourcesWithNodeTypeOptions()', () => { - let getDataSourcesWithNodeTypeOptions; - - beforeEach(() => { - getDataSourcesWithNodeTypeOptions = makeGetDataSourcesWithNodeTypeOptions(); - }); - - it('extracts dataSource properties into options list', () => { - const nodeType = 'something'; - - const mockProps = { - nodeType, - }; - - expect(getDataSourcesWithNodeTypeOptions(mockState, mockProps)).toEqual([ - { value: 'sourceWithMixedNodes', label: 'sourceWithMixedNodes' }, + describe('getNetworkOptions()', () => { + it('extracts assetManifest networks into options list', () => { + expect(getNetworkOptions(mockState)).toEqual([ + { value: 'foo', label: 'My Network' }, ]); }); }); diff --git a/src/components/StageEditor/sections/NameGeneratorListPrompts/selectors.js b/src/components/StageEditor/sections/NameGeneratorListPrompts/selectors.js index 9b774267f..4a78f1849 100644 --- a/src/components/StageEditor/sections/NameGeneratorListPrompts/selectors.js +++ b/src/components/StageEditor/sections/NameGeneratorListPrompts/selectors.js @@ -1,52 +1,28 @@ import { createSelector } from 'reselect'; -import { get, uniq, map, mapValues, reduce } from 'lodash'; -import { getExternalData, getVariableRegistry } from '../../../../selectors/protocol'; +import { get, map, reduce } from 'lodash'; +import { getNetworkAssets, getVariableRegistry } from '../../../../selectors/protocol'; import { LABEL_VARIABLE_TYPES } from '../../../../config'; -const getUniqueTypes = data => - uniq(map(data, 'type')); - const getNodeType = (_, props) => props.nodeType; -/** - * Create an index of types available in each data source - */ -const getTypesBySource = createSelector( - getExternalData, - getVariableRegistry, - (externalData) => { - const typesBySource = mapValues( - externalData, - data => new Set(getUniqueTypes(data.nodes)), - ); - - return typesBySource; - }, -); - /** * Return a list of options for the current props.nodeType */ -const makeGetDataSourcesWithNodeTypeOptions = () => - createSelector( - getTypesBySource, - getNodeType, - (typesBySource, nodeType) => reduce( - typesBySource, - (acc, source, name) => { - if (!source.has(nodeType)) { return acc; } - return [ - ...acc, - { label: name, value: name }, - ]; - }, - [], - ), +const getNetworkOptions = (state) => { + const networkAssets = getNetworkAssets(state); + + return map( + networkAssets, + (asset, name) => ({ + label: asset.name, + value: name, + }), ); +}; /** - * Extracts unique variables used in `dataSource`, and combines them with the registry to - * create list of options in the format: `[ { value, label }, ...]` + * Create list of options for attributes from the variable registry in + * the format: `[ { value, label }, ...]` */ const makeGetExternalDataPropertyOptions = () => createSelector( @@ -80,6 +56,6 @@ const makeGetExternalDataPropertyOptions = () => ); export { - makeGetDataSourcesWithNodeTypeOptions, + getNetworkOptions, makeGetExternalDataPropertyOptions, }; diff --git a/src/components/StageEditor/sections/NameGeneratorListPrompts/withDataSourceOptions.js b/src/components/StageEditor/sections/NameGeneratorListPrompts/withDataSourceOptions.js index 87b23cf69..735fc5764 100644 --- a/src/components/StageEditor/sections/NameGeneratorListPrompts/withDataSourceOptions.js +++ b/src/components/StageEditor/sections/NameGeneratorListPrompts/withDataSourceOptions.js @@ -1,7 +1,7 @@ import { connect } from 'react-redux'; import { + getNetworkOptions, makeGetExternalDataPropertyOptions, - makeGetDataSourcesWithNodeTypeOptions, } from './selectors'; /** @@ -9,10 +9,9 @@ import { */ const makeMapStateToProps = () => { const getExternalDataPropertyOptions = makeGetExternalDataPropertyOptions(); - const getDataSourcesWithNodeTypeOptions = makeGetDataSourcesWithNodeTypeOptions(); const mapStateToProps = (state, props) => ({ - dataSources: getDataSourcesWithNodeTypeOptions(state, props), + dataSources: getNetworkOptions(state), externalDataPropertyOptions: getExternalDataPropertyOptions(state, props), }); diff --git a/src/components/StageEditor/sections/NodePanels/NodePanel.js b/src/components/StageEditor/sections/NodePanels/NodePanel.js index ad3a76e55..d1c3fa633 100644 --- a/src/components/StageEditor/sections/NodePanels/NodePanel.js +++ b/src/components/StageEditor/sections/NodePanels/NodePanel.js @@ -8,16 +8,10 @@ import ValidatedField from '../../../Form/ValidatedField'; import { Item } from '../../../OrderedList'; import { getFieldId } from '../../../../utils/issues'; -const getDataSourceOptions = (dataSources) => { - const externalData = dataSources.map(dataSource => ( - { value: dataSource, label: dataSource } - )); - - return ([ - { value: 'existing', label: 'Current network' }, - ...externalData, - ]); -}; +const getDataSourceOptions = dataSources => ([ + { value: 'existing', label: 'Current network' }, + ...dataSources, +]); const NodePanel = ({ fieldId, dataSources, ...rest }) => ( diff --git a/src/components/StageEditor/sections/NodePanels/NodePanels.js b/src/components/StageEditor/sections/NodePanels/NodePanels.js index 010211948..e5124cc0e 100644 --- a/src/components/StageEditor/sections/NodePanels/NodePanels.js +++ b/src/components/StageEditor/sections/NodePanels/NodePanels.js @@ -5,9 +5,10 @@ import { connect } from 'react-redux'; import { FieldArray, arrayPush } from 'redux-form'; import uuid from 'uuid'; import cx from 'classnames'; -import { keys, has, get } from 'lodash'; +import { has } from 'lodash'; import Guidance from '../../../Guidance'; import OrderedList, { NewButton } from '../../../OrderedList'; +import { getNetworkOptions } from '../NameGeneratorListPrompts/selectors'; import NodePanel from './NodePanel'; const NodePanels = ({ form, createNewPanel, dataSources, disabled, panels }) => { @@ -54,14 +55,9 @@ NodePanels.defaultProps = { panels: [], }; -const getDataSources = (state) => { - const externalData = get(state, 'protocol.present.externalData', {}); - return keys(externalData); -}; - const mapStateToProps = (state, props) => ({ disabled: !has(props.form.getValues(state, 'subject'), 'type'), - dataSources: getDataSources(state), + dataSources: getNetworkOptions(state), panels: props.form.getValues(state, 'panels'), }); diff --git a/src/selectors/protocol.js b/src/selectors/protocol.js index e25f499c5..28062a448 100644 --- a/src/selectors/protocol.js +++ b/src/selectors/protocol.js @@ -8,7 +8,7 @@ const activeProtocolId = state => state.session.activeProtocol; const protocolsMeta = state => state.protocols; export const getProtocol = state => state.protocol.present; -export const getExternalData = state => state.protocol.present.externalData; +export const getAssetManifest = state => state.protocol.present.assetManifest; export const getVariableRegistry = state => state.protocol.present.variableRegistry; export const getActiveProtocolMeta = createSelector( @@ -24,17 +24,23 @@ export const makeGetStage = () => (protocol, stageId) => find(protocol.stages, ['id', stageId]), ); -export const getExternalDataSources = createSelector( - getExternalData, - externalData => +const networkTypes = new Set([ + 'network', + 'async:network', +]); + +// TODO: Does this method make sense here? +export const getNetworkAssets = createSelector( + getAssetManifest, + assetManifest => reduce( - externalData, - (memo, dataSource, name) => { - if (!Object.prototype.hasOwnProperty.call(dataSource, 'nodes')) { return memo; } + assetManifest, + (memo, asset, name) => { + if (!networkTypes.has(asset.type)) { return memo; } - return [...memo, name]; + return { ...memo, [name]: asset }; }, - [], + {}, ), ); diff --git a/src/utils/__tests__/getAssetData.test.js b/src/utils/__tests__/getAssetData.test.js new file mode 100644 index 000000000..1368920e6 --- /dev/null +++ b/src/utils/__tests__/getAssetData.test.js @@ -0,0 +1,48 @@ +/* eslint-env jest */ +import fs from 'fs'; +import getAssetData from '../getAssetData'; + +const mockData = { + nodes: [], + edges: [], +}; + +fs.readFile = jest.fn( + (path, format, resolve) => + resolve(null, JSON.stringify(mockData)), +); + +describe('getAssetData', () => { + it('can load a json network', (done) => { + const source = '/dev/null/myMockSource.json'; + const type = 'network'; + + getAssetData(source, type).then( + (data) => { + expect(data).toEqual(mockData); + + done(); + }, + ); + }); + + it('it caches responses', (done) => { + const source = '/dev/null/myMockSource.json'; + const type = 'network'; + + Promise.all([ + getAssetData(source, type), + getAssetData(source, type), + ]).then( + (results) => { + const isSameObject = results.every( + (result, index, all) => result === all[0], + ); + + expect(isSameObject).toBe(true); + + done(); + }, + ); + }); +}); diff --git a/src/utils/getAssetData.js b/src/utils/getAssetData.js new file mode 100644 index 000000000..b01648d7d --- /dev/null +++ b/src/utils/getAssetData.js @@ -0,0 +1,19 @@ +import fs from 'fs'; +import { memoize } from 'lodash'; + +const resolver = sourcePath => sourcePath; + +const getAssetData = (sourcePath, type) => { + switch (type) { + default: + return new Promise((resolve, reject) => { + fs.readFile(sourcePath, 'utf8', (error, data) => { + if (error) { reject(error); return; } + + resolve(JSON.parse(data)); + }); + }); + } +}; + +export default memoize(getAssetData, resolver);