From fadea4a6aad60ab8739a17371fa0cd5e14e24fad Mon Sep 17 00:00:00 2001 From: Adam Spiers Date: Wed, 22 Mar 2017 01:22:53 +0000 Subject: [PATCH 01/21] 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 02/21] 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 03/21] 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 04/21] 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 05/21] 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 06/21] 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; From cf4b2af6d000c4ce77076223e6786cad153a07d2 Mon Sep 17 00:00:00 2001 From: Adam Spiers Date: Sat, 25 Mar 2017 21:43:16 +0000 Subject: [PATCH 07/21] ui: don't try to display ChartTitle unless we have targetType This will fail if for some reason targetType is missing. --- src/js/frontend/src/components/ChartTitle.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/js/frontend/src/components/ChartTitle.js b/src/js/frontend/src/components/ChartTitle.js index c88ae75..e77fb8c 100644 --- a/src/js/frontend/src/components/ChartTitle.js +++ b/src/js/frontend/src/components/ChartTitle.js @@ -4,7 +4,7 @@ import Radium from "radium" import { Link } from 'react-router' function ChartTitle (props) { - let description = props.sourceName ? + let description = props.sourceName && props.targetType ?

Showing all meetings of {props.targetType} with {props.sourceName}

From 5dbc860e1a7f02b6a6e9bfd1fa4db451781efc34 Mon Sep 17 00:00:00 2001 From: Adam Spiers Date: Sat, 25 Mar 2017 21:45:44 +0000 Subject: [PATCH 08/21] ui: fix name of Home component's JS and CSS classes I forgot to rename them when renaming the component in 9ba199a73b264fb57ee07354e63f3a187bc50f2f. --- src/js/frontend/src/components/Home.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/js/frontend/src/components/Home.js b/src/js/frontend/src/components/Home.js index 7a34ce6..a652dbb 100644 --- a/src/js/frontend/src/components/Home.js +++ b/src/js/frontend/src/components/Home.js @@ -2,9 +2,9 @@ import React from "react" import Radium from "radium" import { Link } from 'react-router' -class OrgList extends React.Component { +class Home extends React.Component { render () { - return
+ return

Eventually this home page will allow you to choose from dynamic lists of organisations, people, government offices etc. @@ -69,4 +69,4 @@ class OrgList extends React.Component { } } -export default Radium(OrgList); +export default Radium(Home); From 4ddb923d4bc2c4e59053c416957c4322485b05fc Mon Sep 17 00:00:00 2001 From: Adam Spiers Date: Thu, 23 Mar 2017 21:47:56 +0000 Subject: [PATCH 09/21] ui: add reducers for entity list API actions Also make the previously existing reducer for the ADD_ENTITY action use the same new tree structure in the store, and fix bugs where parts of the state tree were not immutable. --- .../src/Redux/reducers/dataReducer.js | 74 +++++++++++++------ .../frontend/src/containers/ChartContainer.js | 16 ++-- 2 files changed, 62 insertions(+), 28 deletions(-) diff --git a/src/js/frontend/src/Redux/reducers/dataReducer.js b/src/js/frontend/src/Redux/reducers/dataReducer.js index 97fe310..bccea2d 100644 --- a/src/js/frontend/src/Redux/reducers/dataReducer.js +++ b/src/js/frontend/src/Redux/reducers/dataReducer.js @@ -1,30 +1,62 @@ -import {fromJS} from "immutable"; -import {ADD_ENTITY} from "../action-types"; +import {fromJS, Map} from "immutable"; -const initialState = fromJS({ - entities: { - people: {}, - organisations: {}, - "government-offices": {}, +import * as actionTypes from "../action-types"; +import entityData from "../../lib/entityData" + +const initialEntityState = { + fetching: false, // We can show a spinner if this is true + fetched: false, // We can show an entity list if this is true + byId: {}, +}; + +const initialEntitiesState = entityData.validTypes.reduce( + (obj, type) => { + obj[type] = initialEntityState; + return obj; }, + {} +); + +const initialState = fromJS({ + entities: initialEntitiesState }); export const dataReducer = (state = initialState, action) => { - let {type, entityType, id, data} = action; - - switch (type) { - case ADD_ENTITY: - if (!state.get("entities").has(entityType)) { - console.error(`Received ADD_ENTITY action with invalid entity type ${entityType}`); - return state; - } - return state.mergeDeep({ - entities: { - [entityType]: { - [id]: data + switch (action.type) { + case actionTypes.ADD_ENTITY: + return state.setIn( + ["entities", action.entityType, "byId", action.id], + fromJS(action.data) + ); + + case actionTypes.FETCH_ENTITIES_PENDING: + return state.setIn( + ["entities", action.meta.entityType, "fetching"], + true, + ); + + case actionTypes.FETCH_ENTITIES_FULFILLED: + let newState = state.setIn( + ["entities", action.meta.entityType], + Map({fetching: false, fetched: true}) + ); + + return newState.mergeIn( + ['entities', action.meta.entityType, 'byId'], + action.payload.data.data.reduce( + (obj, entity) => { + obj[entity.id] = entity.attributes; + return obj }, - }, - }); + {} + ) + ); + + case actionTypes.FETCH_ENTITIES_REJECTED: + return state.setIn( + ["entities", action.meta.entityType, "fetching"], + false, + ); default: return state; diff --git a/src/js/frontend/src/containers/ChartContainer.js b/src/js/frontend/src/containers/ChartContainer.js index 3347413..a643578 100644 --- a/src/js/frontend/src/containers/ChartContainer.js +++ b/src/js/frontend/src/containers/ChartContainer.js @@ -18,13 +18,9 @@ class ChartContainer extends React.Component { let sourceType = this.props.route.sourceType; let targetType = this.props.params.targetType; let id = this.props.params.id; - let entities = this.props.entities; // comes in via mapStateToProps - let sourceData = entities && entities.get(sourceType); - let entityData = sourceData && sourceData.get(id); - let entityName = entityData && entityData.get("name"); return

- { +const mapStateToProps = (state, ownProps) => { + if (ownProps.route.sourceType === "demo") { + return { entityName: "demo" }; + } + + let entities = state.data.getIn(["entities", ownProps.route.sourceType]); + let entity = entities.getIn(["byId", ownProps.params.id]); return { - entities: data.get('entities'), + entityName: entity && entity.get("name"), }; }; From 0713580c63ea141b120f64ac14c2ff2b998d31c8 Mon Sep 17 00:00:00 2001 From: Adam Spiers Date: Thu, 23 Mar 2017 22:06:46 +0000 Subject: [PATCH 10/21] ui: connect EntityListContainer to store This provides the entity data to the container. It isn't used yet - that will be handled in the next commit. --- .../src/containers/EntityListContainer.js | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/src/js/frontend/src/containers/EntityListContainer.js b/src/js/frontend/src/containers/EntityListContainer.js index 1b577bd..46302ac 100644 --- a/src/js/frontend/src/containers/EntityListContainer.js +++ b/src/js/frontend/src/containers/EntityListContainer.js @@ -26,11 +26,11 @@ class EntityListContainer extends React.Component { } } -// const mapStateToProps = (state) => { -// return { -// entities: state.get("entities"), -// }; -// }; +const mapStateToProps = (state, ownProps) => { + return { + entities: state.getIn("data", "entities", ownProps.route.entityType), + }; +}; const mapDispatchToProps = (dispatch) => { return { @@ -40,5 +40,8 @@ const mapDispatchToProps = (dispatch) => { } } -export default connect(null, mapDispatchToProps)(Radium(EntityListContainer)); +export default connect( + mapStateToProps, + mapDispatchToProps +)(Radium(EntityListContainer)); From ef3d6b5b1862fc5ca44432f4832ce7c1f5cc93e4 Mon Sep 17 00:00:00 2001 From: Adam Spiers Date: Sat, 25 Mar 2017 16:57:48 +0000 Subject: [PATCH 11/21] ui: display list of entities Display the entity data provided by the store, as a simple list of entities, sorted lexically case-insensitive. --- src/js/frontend/src/components/EntityList.js | 66 +++++++++++++++++++ src/js/frontend/src/components/Home.js | 35 ---------- .../src/containers/EntityListContainer.js | 16 ++++- src/js/frontend/src/lib/sorters.js | 9 +++ src/js/frontend/src/main.js | 6 +- 5 files changed, 91 insertions(+), 41 deletions(-) create mode 100644 src/js/frontend/src/components/EntityList.js create mode 100644 src/js/frontend/src/lib/sorters.js diff --git a/src/js/frontend/src/components/EntityList.js b/src/js/frontend/src/components/EntityList.js new file mode 100644 index 0000000..3aa1105 --- /dev/null +++ b/src/js/frontend/src/components/EntityList.js @@ -0,0 +1,66 @@ +import React from "react" +import Radium from "radium" +import { Link } from "react-router" + +import * as sorters from "../lib/sorters" + +// This is a truly horrible UI design, but good enough for testing. +const TEMPLATES = { + organisations: (id) => + + government {" "} + offices + {" "} and {" "} + people + , + people: (id) => + + government offices + {" "} and {" "} + their people, + {" "} and {" "} + organisations + {" "} and {" "} + their people + , + "government-offices": (id) => + + organisations + {" "} and {" "} + their people + +}; + +function EntityList (props) { + if (props.fetching) { + return

Still fetching list of {props.entityType} ...

; + } + + if (!props.fetched) { + return

Didn't fetch list of {props.entityType} yet

; // ' + } + + let sortedEntities = props.entitiesById.sortBy(data => data.get("name")); + + let template = TEMPLATES[props.entityType]; + let listItems = sortedEntities.map((data, id) => +
  • + {data.get("name")} - meetings with {template(id)} +
  • + ).toArray(); + + return ( +
      + {listItems} +
    + ) +} + +EntityList.propTypes = { + entityType: React.PropTypes.string.isRequired, + fetching: React.PropTypes.bool.isRequired, + fetched: React.PropTypes.bool.isRequired, + entitiesById: React.PropTypes.object.isRequired, +}; + +module.exports = Radium(EntityList); diff --git a/src/js/frontend/src/components/Home.js b/src/js/frontend/src/components/Home.js index a652dbb..bcc78bb 100644 --- a/src/js/frontend/src/components/Home.js +++ b/src/js/frontend/src/components/Home.js @@ -5,10 +5,6 @@ import { Link } from 'react-router' class Home extends React.Component { render () { return
    -

    - Eventually this home page will allow you to choose from dynamic lists - of organisations, people, government offices etc. -

    • @@ -34,37 +30,6 @@ class Home 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 - the organisation BAE Systems has id 369 in the database, that - David Cameron has id 755, and that the FCO has id 437, so - currently you will have to manually adjust the URLs to point - to the right ids, but we'll hopefully fix that soon. -

    -
      -
    • - - See which ministers met with BAE Systems the most - -
    • -
    • - - See which government offices met with BAE Systems the most - -
    • -
    • - - See which organisations met with David Cameron the most - -
    • -
    • - - See which organisations met with the Foreign & Commonwealth - Office the most - -
    • -
    } } diff --git a/src/js/frontend/src/containers/EntityListContainer.js b/src/js/frontend/src/containers/EntityListContainer.js index 46302ac..ea126b9 100644 --- a/src/js/frontend/src/containers/EntityListContainer.js +++ b/src/js/frontend/src/containers/EntityListContainer.js @@ -4,10 +4,13 @@ import { connect } from 'react-redux'; import * as actions from "../Redux/actions" import entityData from "../lib/entityData" +import EntityList from "../components/EntityList" class EntityListContainer extends React.Component { componentWillMount () { - this.props.fetchEntities(this.props.route.entityType); + if (!this.props.fetched) { + this.props.fetchEntities(this.props.route.entityType); + } } render () { @@ -21,14 +24,21 @@ class EntityListContainer extends React.Component { } return
    - List of {entityType} entities +
    } } const mapStateToProps = (state, ownProps) => { + let entities = state.data.getIn(["entities", ownProps.route.entityType]); return { - entities: state.getIn("data", "entities", ownProps.route.entityType), + fetching: entities.get("fetching"), + fetched: entities.get("fetched"), + entitiesById: entities.get("byId"), }; }; diff --git a/src/js/frontend/src/lib/sorters.js b/src/js/frontend/src/lib/sorters.js new file mode 100644 index 0000000..299029b --- /dev/null +++ b/src/js/frontend/src/lib/sorters.js @@ -0,0 +1,9 @@ +let sorters = { + lexically: (a, b) => { + var x = a.toLowerCase(); + var y = b.toLowerCase(); + return x < y ? -1 : x > y ? 1 : 0; + }, +} + +module.exports = sorters; diff --git a/src/js/frontend/src/main.js b/src/js/frontend/src/main.js index 32f7c91..6dd12a6 100644 --- a/src/js/frontend/src/main.js +++ b/src/js/frontend/src/main.js @@ -26,19 +26,19 @@ loaded.then((newState) => { + + - - From 1ba81422a4d90a9bc805a8df0485f96c7355bcb9 Mon Sep 17 00:00:00 2001 From: Adam Spiers Date: Sun, 26 Mar 2017 11:47:34 +0100 Subject: [PATCH 12/21] ui: check entityType is valid for all actions In the old ADD_ENTITY action used by ChartTitle, we had some nice validation for the action's entity type. Extend this to all actions which specify an entity type (which is currently all 4 in dataReducer), by unifying the actions' structures so that entityType is always under action.meta. --- src/js/frontend/src/Redux/actions.js | 2 +- .../frontend/src/Redux/reducers/dataReducer.js | 16 +++++++++++++++- 2 files changed, 16 insertions(+), 2 deletions(-) diff --git a/src/js/frontend/src/Redux/actions.js b/src/js/frontend/src/Redux/actions.js index 4d03560..ab8ee2f 100644 --- a/src/js/frontend/src/Redux/actions.js +++ b/src/js/frontend/src/Redux/actions.js @@ -13,7 +13,7 @@ export const toggleAbout = () => { export const addEntity = (entityType, id, data) => { return { type: actionTypes.ADD_ENTITY, - entityType, + meta: { entityType }, id, data, } diff --git a/src/js/frontend/src/Redux/reducers/dataReducer.js b/src/js/frontend/src/Redux/reducers/dataReducer.js index bccea2d..b6f81c0 100644 --- a/src/js/frontend/src/Redux/reducers/dataReducer.js +++ b/src/js/frontend/src/Redux/reducers/dataReducer.js @@ -21,11 +21,25 @@ const initialState = fromJS({ entities: initialEntitiesState }); +const checkActionValidEntityType = (action, state) => { + let entityType = action.meta && action.meta.entityType; + if (!entityType) return true; + let valid = state.get("entities").has(entityType); + if (!valid) { + console.error(`Received ${action.type} action with invalid entity type ${entityType}`); + } + return valid; +} + export const dataReducer = (state = initialState, action) => { + if (!checkActionValidEntityType(action, state)) { + return state; + } + switch (action.type) { case actionTypes.ADD_ENTITY: return state.setIn( - ["entities", action.entityType, "byId", action.id], + ["entities", action.meta.entityType, "byId", action.id], fromJS(action.data) ); From 35d1547319079b6ef24eb86bd522f9887abbd855 Mon Sep 17 00:00:00 2001 From: Adam Spiers Date: Sun, 26 Mar 2017 11:50:28 +0100 Subject: [PATCH 13/21] ui: add comment explaining how ChartContainer gets sourceType --- src/js/frontend/src/containers/ChartContainer.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/js/frontend/src/containers/ChartContainer.js b/src/js/frontend/src/containers/ChartContainer.js index a643578..9b23ac4 100644 --- a/src/js/frontend/src/containers/ChartContainer.js +++ b/src/js/frontend/src/containers/ChartContainer.js @@ -15,7 +15,9 @@ class ChartContainer extends React.Component { } render () { + // This comes from the element in main.js let sourceType = this.props.route.sourceType; + let targetType = this.props.params.targetType; let id = this.props.params.id; From 4f590d7a46eb12f33a445fc93b4154360c6639b7 Mon Sep 17 00:00:00 2001 From: Adam Spiers Date: Sun, 26 Mar 2017 18:39:10 +0100 Subject: [PATCH 14/21] ui: only dispatch ADD_ENTITY if we don't already have it --- src/js/frontend/src/containers/ChartContainer.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/js/frontend/src/containers/ChartContainer.js b/src/js/frontend/src/containers/ChartContainer.js index 9b23ac4..57a6c08 100644 --- a/src/js/frontend/src/containers/ChartContainer.js +++ b/src/js/frontend/src/containers/ChartContainer.js @@ -9,7 +9,7 @@ import * as actions from "../Redux/actions" class ChartContainer extends React.Component { componentWillMount () { - if (this.props.route.sourceType !== "demo") { + if (this.props.route.sourceType !== "demo" && !this.props.entityName) { this.getEntityName() } } From 20f8c316a6e53600ef417a053365b49f75a60023 Mon Sep 17 00:00:00 2001 From: Adam Spiers Date: Sun, 26 Mar 2017 19:35:08 +0100 Subject: [PATCH 15/21] ui: make entity list scrollable --- src/js/frontend/src/components/Header.js | 2 +- .../src/containers/EntityListContainer.js | 9 ++++++++- src/js/frontend/src/containers/PageLayout.js | 15 +++++++++++---- 3 files changed, 20 insertions(+), 6 deletions(-) diff --git a/src/js/frontend/src/components/Header.js b/src/js/frontend/src/components/Header.js index 63fa0db..5c11c62 100644 --- a/src/js/frontend/src/components/Header.js +++ b/src/js/frontend/src/components/Header.js @@ -13,6 +13,7 @@ class Header extends React.Component {
    @@ -26,7 +27,6 @@ let shadow = 'inset #F2E9E9 0px -2px 0px 0px'; let styles = { base: { // backgroundColor: '#F5F5ED', - height: "12vh", borderBottom: '5px solid white', WebkitBoxShadow: shadow, MozBoxShadow: shadow, diff --git a/src/js/frontend/src/containers/EntityListContainer.js b/src/js/frontend/src/containers/EntityListContainer.js index ea126b9..533bfce 100644 --- a/src/js/frontend/src/containers/EntityListContainer.js +++ b/src/js/frontend/src/containers/EntityListContainer.js @@ -23,7 +23,7 @@ class EntityListContainer extends React.Component {
    } - return
    + return
    { } } +let styles = { + "entity-list": { + overflowY: "auto", + height: "100%", + } +}; + export default connect( mapStateToProps, mapDispatchToProps diff --git a/src/js/frontend/src/containers/PageLayout.js b/src/js/frontend/src/containers/PageLayout.js index cade820..52ad543 100644 --- a/src/js/frontend/src/containers/PageLayout.js +++ b/src/js/frontend/src/containers/PageLayout.js @@ -5,6 +5,8 @@ import Header from "../components/Header"; import About from "../components/About"; import { toggleAbout } from '../Redux/actions'; +const HEADER_HEIGHT = 12; + class PageLayout extends React.Component { constructor(props) { super(props); @@ -23,9 +25,11 @@ class PageLayout extends React.Component { styles.base, ]} className="app-container"> -
    +
    - {this.props.children} +
    + {this.props.children} +
    ) } @@ -33,9 +37,12 @@ class PageLayout extends React.Component { let styles = { base: { - height:"100vh", + height: "100vh", overflow: 'hidden', - } + }, + main: { + height: (100 - HEADER_HEIGHT) + "vh", + }, } const mapStateToProps = (state) => { From 69ab996575e5cf051a0819ef7d974ae7c72a763a Mon Sep 17 00:00:00 2001 From: Adam Spiers Date: Sun, 26 Mar 2017 19:36:27 +0100 Subject: [PATCH 16/21] ui: catch occasional errors when store is empty This seems to be some strange race condition where state.data is not always initialized by the time mapStateToProps is invoked, causing an exception when attempting to call getIn() on it. Maybe it only happens when auto-reloading the app whilst a git rebase is running, but I'm not really sure. Either way, we should watch out for this by logging a warning. --- src/js/frontend/src/containers/EntityListContainer.js | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/js/frontend/src/containers/EntityListContainer.js b/src/js/frontend/src/containers/EntityListContainer.js index 533bfce..cce6477 100644 --- a/src/js/frontend/src/containers/EntityListContainer.js +++ b/src/js/frontend/src/containers/EntityListContainer.js @@ -34,6 +34,11 @@ class EntityListContainer extends React.Component { } const mapStateToProps = (state, ownProps) => { + if (!state.data) { + console.warn("No data in store yet?!"); + return {}; + } + let entities = state.data.getIn(["entities", ownProps.route.entityType]); return { fetching: entities.get("fetching"), From 2f4728fb6e6b7a39623248eefe5a21686482d525 Mon Sep 17 00:00:00 2001 From: Adam Spiers Date: Sun, 26 Mar 2017 20:42:41 +0100 Subject: [PATCH 17/21] ui: extract BackToHome component This can be reused by EntityList. --- src/js/frontend/src/components/BackToHome.js | 13 +++++++++++++ src/js/frontend/src/components/ChartTitle.js | 4 +++- 2 files changed, 16 insertions(+), 1 deletion(-) create mode 100644 src/js/frontend/src/components/BackToHome.js diff --git a/src/js/frontend/src/components/BackToHome.js b/src/js/frontend/src/components/BackToHome.js new file mode 100644 index 0000000..b0e9c37 --- /dev/null +++ b/src/js/frontend/src/components/BackToHome.js @@ -0,0 +1,13 @@ +import React from "react" +import ReactDOM from "react-dom" +import Radium from "radium" +import { Link } from 'react-router' + +const BackToHome = (props) => + Back to home; + +BackToHome.propTypes = { + sourceName: React.PropTypes.string, +}; + +module.exports = Radium(BackToHome); diff --git a/src/js/frontend/src/components/ChartTitle.js b/src/js/frontend/src/components/ChartTitle.js index e77fb8c..99b3b9e 100644 --- a/src/js/frontend/src/components/ChartTitle.js +++ b/src/js/frontend/src/components/ChartTitle.js @@ -3,6 +3,8 @@ import ReactDOM from "react-dom" import Radium from "radium" import { Link } from 'react-router' +import BackToHome from './BackToHome' + function ChartTitle (props) { let description = props.sourceName && props.targetType ?

    @@ -13,7 +15,7 @@ function ChartTitle (props) { return (

    {description} - Back to home +
    ) } From 8f7ee825657e208cc69954cc843b8042f2e1fadc Mon Sep 17 00:00:00 2001 From: Adam Spiers Date: Sun, 26 Mar 2017 21:17:34 +0100 Subject: [PATCH 18/21] ui: remove unused import of ReactDOM --- src/js/frontend/src/components/ChartTitle.js | 1 - 1 file changed, 1 deletion(-) diff --git a/src/js/frontend/src/components/ChartTitle.js b/src/js/frontend/src/components/ChartTitle.js index 99b3b9e..fb0cd0a 100644 --- a/src/js/frontend/src/components/ChartTitle.js +++ b/src/js/frontend/src/components/ChartTitle.js @@ -1,5 +1,4 @@ import React from "react" -import ReactDOM from "react-dom" import Radium from "radium" import { Link } from 'react-router' From 9d0764b91d3846be913dd482b2cb0f943b6f5101 Mon Sep 17 00:00:00 2001 From: Adam Spiers Date: Sun, 26 Mar 2017 20:43:30 +0100 Subject: [PATCH 19/21] ui: de-hyphenate targetType in ChartTitle --- src/js/frontend/src/components/ChartTitle.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/js/frontend/src/components/ChartTitle.js b/src/js/frontend/src/components/ChartTitle.js index fb0cd0a..2d1d643 100644 --- a/src/js/frontend/src/components/ChartTitle.js +++ b/src/js/frontend/src/components/ChartTitle.js @@ -7,7 +7,9 @@ import BackToHome from './BackToHome' function ChartTitle (props) { let description = props.sourceName && props.targetType ?

    - Showing all meetings of {props.targetType} with {props.sourceName} + Showing all meetings of { + props.targetType.replace('-', ' ') + } with {props.sourceName}

    : ""; From 746473cea1ba437bf61852780d0e6b758acbc9df Mon Sep 17 00:00:00 2001 From: Adam Spiers Date: Sun, 26 Mar 2017 21:25:31 +0100 Subject: [PATCH 20/21] ui: fix rendering of About screen This was broken by the restructuring of the state tree when introducing combineReducers via 22cdf4461a9cd19e88e6d68a9a03b41b437e7910 in #159. --- src/js/frontend/src/containers/PageLayout.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/js/frontend/src/containers/PageLayout.js b/src/js/frontend/src/containers/PageLayout.js index 52ad543..f1a4711 100644 --- a/src/js/frontend/src/containers/PageLayout.js +++ b/src/js/frontend/src/containers/PageLayout.js @@ -46,9 +46,9 @@ let styles = { } const mapStateToProps = (state) => { - return { - showAboutScreen: state.showAboutScreen, - }; + return { + showAboutScreen: state.ui.get("showAboutScreen"), + }; }; const mapDispatchToProps = (dispatch) => { From 653799efd393e2747b1c5b23a83e6e37880be31f Mon Sep 17 00:00:00 2001 From: Adam Spiers Date: Sun, 26 Mar 2017 21:31:44 +0100 Subject: [PATCH 21/21] ui: fix indentation in uiReducer --- src/js/frontend/src/Redux/reducers/uiReducer.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/js/frontend/src/Redux/reducers/uiReducer.js b/src/js/frontend/src/Redux/reducers/uiReducer.js index a0d3b0d..bd7af28 100644 --- a/src/js/frontend/src/Redux/reducers/uiReducer.js +++ b/src/js/frontend/src/Redux/reducers/uiReducer.js @@ -2,7 +2,7 @@ import {fromJS} from "immutable"; import {TOGGLE_ABOUT} from "../action-types"; const initialState = fromJS({ - showAboutScreen: false + showAboutScreen: false }); export const uiReducer = (state = initialState, {type}) => {