From fadea4a6aad60ab8739a17371fa0cd5e14e24fad Mon Sep 17 00:00:00 2001 From: Adam Spiers Date: Wed, 22 Mar 2017 01:22:53 +0000 Subject: [PATCH 1/6] ui: include thunk middleware using redux-devtools-extension To correctly support both thunk middleware and Redux Devtools, we use the convenient redux-devtools-extension library to compose thunk with the middleware enhancer, and then use that as the final argument to createStore(). --- src/js/frontend/package.json | 1 + src/js/frontend/src/Redux/store.js | 20 ++++++++++++-------- 2 files changed, 13 insertions(+), 8 deletions(-) diff --git a/src/js/frontend/package.json b/src/js/frontend/package.json index bfad919..876070b 100644 --- a/src/js/frontend/package.json +++ b/src/js/frontend/package.json @@ -38,6 +38,7 @@ "react-redux": "^4.4.5", "react-router": "^2.4.1", "redux": "^3.6.0", + "redux-devtools-extension": "^2.13.0", "redux-logger": "^2.6.1", "redux-storage": "^4.0.1", "redux-storage-decorator-debounce": "^1.0.1", diff --git a/src/js/frontend/src/Redux/store.js b/src/js/frontend/src/Redux/store.js index 23518d4..a63b48e 100644 --- a/src/js/frontend/src/Redux/store.js +++ b/src/js/frontend/src/Redux/store.js @@ -10,6 +10,7 @@ import merger from 'redux-storage-merger-immutablejs' //////////////////////////////////////////////////////////////////////////////// // redux stuff +import { composeWithDevTools } from 'redux-devtools-extension' // wrap our main reducer in a storage reducer - this intercepts LOAD actions and // calls the merger function to merge in the new state @@ -31,18 +32,21 @@ const loggerMiddleware = createLogger({ //predicate: (getState, action) => action.type !== CALCULATION_NEEDS_REFRESH }) -// now create our redux store, applying all our middleware -// const store = createStore(reducer, applyMiddleware( -// thunkMiddleware, // first, so function results get transformed -// loggerMiddleware, // now log everything at this state -// storageMiddleware // finally the storage middleware -// )) - +const composeEnhancers = composeWithDevTools({ + name: 'MUTI frontend', + // actionsBlacklist: ['REDUX_STORAGE_SAVE'] +}); const store = createStore( reducer, initialStoreState, - window.__REDUX_DEVTOOLS_EXTENSION__ && window.__REDUX_DEVTOOLS_EXTENSION__() + composeEnhancers( + applyMiddleware( + thunkMiddleware, // first, so function results get transformed +// loggerMiddleware, // now log everything at this state +// storageMiddleware, // finally the storage middleware + ) + ) ); window.store = store; From fc582c5cefe1797392a2adc175415b0cff5d6226 Mon Sep 17 00:00:00 2001 From: Adam Spiers Date: Fri, 10 Feb 2017 20:19:50 +0000 Subject: [PATCH 2/6] ui: add stub EntityListContainer This will allow us to see lists of people, organisations, and government offices, and eventually click on each one to examine it. --- src/js/frontend/src/components/Home.js | 17 +++++++++++ .../src/containers/EntityListContainer.js | 29 +++++++++++++++++++ src/js/frontend/src/main.js | 7 +++++ 3 files changed, 53 insertions(+) create mode 100644 src/js/frontend/src/containers/EntityListContainer.js diff --git a/src/js/frontend/src/components/Home.js b/src/js/frontend/src/components/Home.js index 040807a..7a34ce6 100644 --- a/src/js/frontend/src/components/Home.js +++ b/src/js/frontend/src/components/Home.js @@ -17,6 +17,23 @@ class OrgList extends React.Component { +

Here are sample URLs you can use to visualize parts of the data set relating to a given organisation. They assume that diff --git a/src/js/frontend/src/containers/EntityListContainer.js b/src/js/frontend/src/containers/EntityListContainer.js new file mode 100644 index 0000000..50b9f40 --- /dev/null +++ b/src/js/frontend/src/containers/EntityListContainer.js @@ -0,0 +1,29 @@ +import React from "react" +import Radium from "radium" +import { connect } from 'react-redux'; + +import * as actions from "../Redux/actions" + +class EntityListContainer extends React.Component { + componentWillMount () { + } + + render () { + let entityType = this.props.route.entityType; + + return

+ List of {entityType} entities +
+ } +} + +// const mapStateToProps = (state) => { +// return { +// entities: state.get("entities"), +// }; +// }; +// +// export default connect(mapStateToProps)(Radium(EntityListContainer)); + +export default Radium(EntityListContainer); + diff --git a/src/js/frontend/src/main.js b/src/js/frontend/src/main.js index b0dca48..32f7c91 100644 --- a/src/js/frontend/src/main.js +++ b/src/js/frontend/src/main.js @@ -9,6 +9,7 @@ import App from "./app" import PageLayout from "./containers/PageLayout" import Home from "./components/Home" import ChartContainer from "./containers/ChartContainer" +import EntityListContainer from "./containers/EntityListContainer" // now everything is set up, create a loader and use it to load the store const load = storage.createLoader(engine) @@ -25,6 +26,12 @@ loaded.then((newState) => { + + + Date: Thu, 23 Mar 2017 21:47:32 +0000 Subject: [PATCH 3/6] ui: make EntityListContainer check entity type is valid --- src/js/frontend/src/containers/EntityListContainer.js | 7 +++++++ src/js/frontend/src/lib/entityData.js | 8 ++++++++ 2 files changed, 15 insertions(+) diff --git a/src/js/frontend/src/containers/EntityListContainer.js b/src/js/frontend/src/containers/EntityListContainer.js index 50b9f40..f30a387 100644 --- a/src/js/frontend/src/containers/EntityListContainer.js +++ b/src/js/frontend/src/containers/EntityListContainer.js @@ -11,6 +11,13 @@ class EntityListContainer extends React.Component { render () { let entityType = this.props.route.entityType; + if (! entityData.isValidType(entityType)) { + return
+ Invalid entity type {entityType}. + Must be one of: {entityData.validTypes.join(", ")} +
+ } + return
List of {entityType} entities
diff --git a/src/js/frontend/src/lib/entityData.js b/src/js/frontend/src/lib/entityData.js index 66c4625..0e281ef 100644 --- a/src/js/frontend/src/lib/entityData.js +++ b/src/js/frontend/src/lib/entityData.js @@ -2,6 +2,14 @@ let d3 = require('d3'); let api = require('./api'); let entityData = { + validTypes: [ + "organisations", "people", "government-offices" + ], + + isValidType: function(entityType) { + return this.validTypes.indexOf(entityType) >= 0; + }, + fetch: function(entityType, entityId, onSuccess) { let url = this.apiURL(entityType, entityId); console.debug("Fetching from " + url); From 35bdd5f4b0a5be200d07a289267d2afdd4ed6700 Mon Sep 17 00:00:00 2001 From: Adam Spiers Date: Tue, 21 Mar 2017 22:56:55 +0000 Subject: [PATCH 4/6] ui: add redux-promise-middleware We'll use this for asynchronous fetching of data from the Rails API into the Redux store. --- src/js/frontend/package.json | 1 + src/js/frontend/src/Redux/store.js | 2 ++ 2 files changed, 3 insertions(+) diff --git a/src/js/frontend/package.json b/src/js/frontend/package.json index 876070b..9d987f1 100644 --- a/src/js/frontend/package.json +++ b/src/js/frontend/package.json @@ -40,6 +40,7 @@ "redux": "^3.6.0", "redux-devtools-extension": "^2.13.0", "redux-logger": "^2.6.1", + "redux-promise-middleware": "^4.2.0", "redux-storage": "^4.0.1", "redux-storage-decorator-debounce": "^1.0.1", "redux-storage-engine-localstorage": "^1.1.1", diff --git a/src/js/frontend/src/Redux/store.js b/src/js/frontend/src/Redux/store.js index a63b48e..7c9e70b 100644 --- a/src/js/frontend/src/Redux/store.js +++ b/src/js/frontend/src/Redux/store.js @@ -5,6 +5,7 @@ import {rootReducer, storeStructure, initialStoreState} from "./reducer" import * as storage from "redux-storage" import createEngine from "redux-storage-engine-localstorage" import storageDebounce from 'redux-storage-decorator-debounce' +import promiseMiddleware from 'redux-promise-middleware' import immutablejs from 'redux-storage-decorator-immutablejs' import merger from 'redux-storage-merger-immutablejs' @@ -43,6 +44,7 @@ const store = createStore( composeEnhancers( applyMiddleware( thunkMiddleware, // first, so function results get transformed + promiseMiddleware(), // loggerMiddleware, // now log everything at this state // storageMiddleware, // finally the storage middleware ) From 12c0ca146118810417589270cb908c27dc81d8ba Mon Sep 17 00:00:00 2001 From: Adam Spiers Date: Thu, 23 Mar 2017 21:45:15 +0000 Subject: [PATCH 5/6] ui: move apiURL earlier This lets us group request-related functions together. --- src/js/frontend/src/lib/entityData.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/js/frontend/src/lib/entityData.js b/src/js/frontend/src/lib/entityData.js index 0e281ef..d665149 100644 --- a/src/js/frontend/src/lib/entityData.js +++ b/src/js/frontend/src/lib/entityData.js @@ -2,6 +2,10 @@ let d3 = require('d3'); let api = require('./api'); let entityData = { + apiURL: function(entityType, entityId) { + return api.URL(entityType + "/" + entityId); + }, + validTypes: [ "organisations", "people", "government-offices" ], @@ -20,10 +24,6 @@ let entityData = { .get(this.dataLoadedHandler(onSuccess, url, entityType, entityId)); }, - apiURL: function(entityType, entityId) { - return api.URL(entityType + "/" + entityId); - }, - dataLoadedHandler: function(onSuccess, url, entityType, entityId) { let self = this; From 4cea67d673ca435d91a507535ede6ea8349712a2 Mon Sep 17 00:00:00 2001 From: Adam Spiers Date: Thu, 23 Mar 2017 22:06:19 +0000 Subject: [PATCH 6/6] ui: use redux-promise-middleware to request lists of entities This dispatches the FETCH_ENTITIES action whose payload is an axios request object. redux-promise-middleware will handle this by executing the request's promise, and then magically dispatching FETCH_ENTITIES_{PENDING,FULFILLED,REJECTED} actions according to the 3 possible stages of the conversation with the API endpoint. Currently these 3 actions are not handled by any reducer; that will come in subsequent commits. --- src/js/frontend/package.json | 1 + src/js/frontend/src/Redux/action-types.js | 6 ++++++ src/js/frontend/src/Redux/actions.js | 15 +++++++++++++++ .../src/containers/EntityListContainer.js | 14 +++++++++++--- src/js/frontend/src/lib/api.js | 4 ++++ src/js/frontend/src/lib/entityData.js | 11 +++++++++++ 6 files changed, 48 insertions(+), 3 deletions(-) diff --git a/src/js/frontend/package.json b/src/js/frontend/package.json index 9d987f1..30c01f6 100644 --- a/src/js/frontend/package.json +++ b/src/js/frontend/package.json @@ -20,6 +20,7 @@ "dependencies": { "@n3dst4/browser-bundle": "^1.1.0", "@n3dst4/build-stylesheets": "^1.0.1", + "axios": "^0.15.3", "babel-polyfill": "6.9.1", "browser-sync": "^2.12.3", "d3": "^4.3.0", diff --git a/src/js/frontend/src/Redux/action-types.js b/src/js/frontend/src/Redux/action-types.js index e0a0eb6..c8a84ab 100644 --- a/src/js/frontend/src/Redux/action-types.js +++ b/src/js/frontend/src/Redux/action-types.js @@ -1,2 +1,8 @@ export const TOGGLE_ABOUT = "TOGGLE_ABOUT" + export const ADD_ENTITY = "ADD_ENTITY" + +export const FETCH_ENTITIES = "FETCH_ENTITIES" +export const FETCH_ENTITIES_PENDING = "FETCH_ENTITIES_PENDING" +export const FETCH_ENTITIES_FULFILLED = "FETCH_ENTITIES_FULFILLED" +export const FETCH_ENTITIES_REJECTED = "FETCH_ENTITIES_REJECTED" diff --git a/src/js/frontend/src/Redux/actions.js b/src/js/frontend/src/Redux/actions.js index 3c0b300..4d03560 100644 --- a/src/js/frontend/src/Redux/actions.js +++ b/src/js/frontend/src/Redux/actions.js @@ -1,4 +1,5 @@ import * as actionTypes from "./action-types" +import entityData from "../lib/entityData" // These are action creators // http://redux.js.org/docs/basics/Actions.html#action-creators @@ -17,3 +18,17 @@ export const addEntity = (entityType, id, data) => { data, } } + +export const fetchEntities = (entityType) => { + const request = entityData.fetchEntitiesRequest(entityType); + + // When dispatched, redux-promise-middleware will handle this and + // magically dispatch FETCH_ENTITIES_{PENDING,FULFILLED,REJECTED} + // actions according to the 3 possible stages of the conversation + // with the API endpoint. + return { + type: actionTypes.FETCH_ENTITIES, + meta: { entityType }, + payload: request + } +} diff --git a/src/js/frontend/src/containers/EntityListContainer.js b/src/js/frontend/src/containers/EntityListContainer.js index f30a387..1b577bd 100644 --- a/src/js/frontend/src/containers/EntityListContainer.js +++ b/src/js/frontend/src/containers/EntityListContainer.js @@ -3,9 +3,11 @@ import Radium from "radium" import { connect } from 'react-redux'; import * as actions from "../Redux/actions" +import entityData from "../lib/entityData" class EntityListContainer extends React.Component { componentWillMount () { + this.props.fetchEntities(this.props.route.entityType); } render () { @@ -29,8 +31,14 @@ class EntityListContainer extends React.Component { // entities: state.get("entities"), // }; // }; -// -// export default connect(mapStateToProps)(Radium(EntityListContainer)); -export default Radium(EntityListContainer); +const mapDispatchToProps = (dispatch) => { + return { + fetchEntities: (entityType) => { + dispatch(actions.fetchEntities(entityType)); + } + } +} + +export default connect(null, mapDispatchToProps)(Radium(EntityListContainer)); diff --git a/src/js/frontend/src/lib/api.js b/src/js/frontend/src/lib/api.js index 89e3afc..b47cadc 100644 --- a/src/js/frontend/src/lib/api.js +++ b/src/js/frontend/src/lib/api.js @@ -8,6 +8,10 @@ let api = { URL: function (path) { return this.protocol + "://" + this.host + ":" + this.port + this.apiPathPrefix + path; + }, + + fetchEntitiesURL: function (entityType) { + return this.URL(entityType); } } diff --git a/src/js/frontend/src/lib/entityData.js b/src/js/frontend/src/lib/entityData.js index d665149..b929f50 100644 --- a/src/js/frontend/src/lib/entityData.js +++ b/src/js/frontend/src/lib/entityData.js @@ -1,5 +1,6 @@ let d3 = require('d3'); let api = require('./api'); +import axios from "axios" let entityData = { apiURL: function(entityType, entityId) { @@ -14,6 +15,7 @@ let entityData = { return this.validTypes.indexOf(entityType) >= 0; }, + // FIXME: retire this in favour of fetchEntitiesRequest() below fetch: function(entityType, entityId, onSuccess) { let url = this.apiURL(entityType, entityId); console.debug("Fetching from " + url); @@ -47,6 +49,15 @@ let entityData = { onSuccess(json); }; }, + + fetchEntitiesRequest: function (entityType) { + return axios.get(api.fetchEntitiesURL(entityType), { + headers: { + "Content-Type": "application/json", + "Accept": "application/vnd.api+json" + }, + }); + } } module.exports = entityData;