diff --git a/src/js/frontend/package.json b/src/js/frontend/package.json index bfad919..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", @@ -38,7 +39,9 @@ "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-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/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..ab8ee2f 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 @@ -12,8 +13,22 @@ export const toggleAbout = () => { export const addEntity = (entityType, id, data) => { return { type: actionTypes.ADD_ENTITY, - entityType, + meta: { entityType }, id, 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/Redux/reducers/dataReducer.js b/src/js/frontend/src/Redux/reducers/dataReducer.js index 97fe310..b6f81c0 100644 --- a/src/js/frontend/src/Redux/reducers/dataReducer.js +++ b/src/js/frontend/src/Redux/reducers/dataReducer.js @@ -1,30 +1,76 @@ -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 }); +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) => { - 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 + if (!checkActionValidEntityType(action, state)) { + return state; + } + + switch (action.type) { + case actionTypes.ADD_ENTITY: + return state.setIn( + ["entities", action.meta.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/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}) => { diff --git a/src/js/frontend/src/Redux/store.js b/src/js/frontend/src/Redux/store.js index 23518d4..7c9e70b 100644 --- a/src/js/frontend/src/Redux/store.js +++ b/src/js/frontend/src/Redux/store.js @@ -5,11 +5,13 @@ 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' //////////////////////////////////////////////////////////////////////////////// // 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 +33,22 @@ 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 + promiseMiddleware(), +// loggerMiddleware, // now log everything at this state +// storageMiddleware, // finally the storage middleware + ) + ) ); window.store = store; 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 c88ae75..2d1d643 100644 --- a/src/js/frontend/src/components/ChartTitle.js +++ b/src/js/frontend/src/components/ChartTitle.js @@ -1,19 +1,22 @@ import React from "react" -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 ? + 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}
: ""; return (Still fetching list of {props.entityType} ...
; + } + + if (!props.fetched) { + returnDidn'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) => +- Eventually this home page will allow you to choose from dynamic lists - of organisations, people, government offices etc. -
+ return- 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. -