From 1d420b835c69132812176a08c2610a9ed84f5646 Mon Sep 17 00:00:00 2001 From: Stuart Taylor Date: Tue, 20 Feb 2018 13:07:32 +0000 Subject: [PATCH] feat(mapUi): Create mapUi specific service --- common/app/Map/redux/fetch-map-ui-epic.js | 46 ++++++++++++++ common/app/Map/redux/index.js | 19 ++++-- common/app/entities/index.js | 7 +-- common/app/redux/fetch-challenges-epic.js | 3 +- server/boot/a-services.js | 3 + server/services/mapUi.js | 56 +++++++++++++++++ server/utils/map.js | 74 +++++++++++------------ 7 files changed, 158 insertions(+), 50 deletions(-) create mode 100644 common/app/Map/redux/fetch-map-ui-epic.js create mode 100644 server/services/mapUi.js diff --git a/common/app/Map/redux/fetch-map-ui-epic.js b/common/app/Map/redux/fetch-map-ui-epic.js new file mode 100644 index 0000000000..875f617683 --- /dev/null +++ b/common/app/Map/redux/fetch-map-ui-epic.js @@ -0,0 +1,46 @@ +import { ofType } from 'redux-epic'; +import debug from 'debug'; + +import { + types as appTypes, + createErrorObservable +} from '../../redux'; +import { types, fetchMapUiComplete } from './'; +import { langSelector } from '../../Router/redux'; +import { shapeChallenges } from '../../redux/utils'; + +const isDev = debug.enabled('fcc:*'); + +export default function fetchMapUiEpic( + actions, + { getState }, + { services } +) { + return actions::ofType( + appTypes.appMounted + ) + .flatMapLatest(() => { + const lang = langSelector(getState()); + const options = { + params: { lang }, + service: 'map-ui' + }; + return services.readService$(options) + .retry(3) + .map(({ entities, ...res }) => ({ + entities: shapeChallenges( + entities, + isDev + ), + ...res + })) + .map(({ entities, result } = {}) => { + return fetchMapUiComplete( + entities, + result + ); + }) + .startWith({ type: types.fetchMapUi.start }) + .catch(createErrorObservable); + }); + } diff --git a/common/app/Map/redux/index.js b/common/app/Map/redux/index.js index 62b9d15b95..adff2aa4b1 100644 --- a/common/app/Map/redux/index.js +++ b/common/app/Map/redux/index.js @@ -1,25 +1,26 @@ import { createAction, + createAsyncTypes, createTypes, handleActions } from 'berkeleys-redux-utils'; import { createSelector } from 'reselect'; -import noop from 'lodash/noop'; -import capitalize from 'lodash/capitalize'; +import { capitalize, noop} from 'lodash'; import * as utils from './utils.js'; import ns from '../ns.json'; import { - types as app, createEventMetaCreator } from '../../redux'; -export const epics = []; +import fewtchMapUiEpic from './fetch-map-ui-epic'; + +export const epics = [ fewtchMapUiEpic ]; export const types = createTypes([ 'onRouteMap', 'initMap', - + createAsyncTypes('fetchMapUi'), 'toggleThisPanel', 'isAllCollapsed', @@ -31,6 +32,12 @@ export const types = createTypes([ export const initMap = createAction(types.initMap); +export const fetchMapUiComplete = createAction( + types.fetchMapUi.complete, + (entities, result) => ({ entities, result }), + entities => ({ entities }) +); + export const toggleThisPanel = createAction(types.toggleThisPanel); export const collapseAll = createAction(types.collapseAll); @@ -100,7 +107,7 @@ export default handleActions( mapUi }; }, - [app.fetchChallenges.complete]: (state, { payload }) => { + [types.fetchMapUi.complete]: (state, { payload }) => { const { entities, result } = payload; return { ...state, diff --git a/common/app/entities/index.js b/common/app/entities/index.js index d8dae89a97..b58d8eeb89 100644 --- a/common/app/entities/index.js +++ b/common/app/entities/index.js @@ -1,4 +1,4 @@ -import { findIndex, invert, pick, property } from 'lodash'; +import { findIndex, invert, pick, property, merge } from 'lodash'; import uuid from 'uuid/v4'; import { composeReducers, @@ -162,10 +162,7 @@ export default composeReducers( } }; } - return { - ...state, - ...action.meta.entities - }; + return merge(state, action.meta.entities); } return state; }, diff --git a/common/app/redux/fetch-challenges-epic.js b/common/app/redux/fetch-challenges-epic.js index 6873507cc6..733223b4ee 100644 --- a/common/app/redux/fetch-challenges-epic.js +++ b/common/app/redux/fetch-challenges-epic.js @@ -19,7 +19,7 @@ import { langSelector } from '../Router/redux'; const isDev = debug.enabled('fcc:*'); -export function fetchChallengeEpic(actions, { getState }, { services }) { +export default function fetchChallengeEpic(actions, { getState }, { services }) { return actions::ofType(challenge.onRouteChallenges) .filter(({ payload }) => !isChallengeLoaded(getState(), payload)) .flatMapLatest(({ payload: params }) => { @@ -84,4 +84,3 @@ export function fetchChallengesEpic( }); } -export default combineEpics(fetchChallengeEpic, fetchChallengesEpic); diff --git a/server/boot/a-services.js b/server/boot/a-services.js index f9bdeb60c1..777b367fce 100644 --- a/server/boot/a-services.js +++ b/server/boot/a-services.js @@ -1,12 +1,15 @@ import Fetchr from 'fetchr'; import getUserServices from '../services/user'; import getMapServices from '../services/map'; +import getMapUiServices from '../services/mapUi'; export default function bootServices(app) { const userServices = getUserServices(app); const mapServices = getMapServices(app); + const mapUiServices = getMapUiServices(app); Fetchr.registerFetcher(userServices); Fetchr.registerFetcher(mapServices); + Fetchr.registerFetcher(mapUiServices); app.use('/services', Fetchr.middleware()); } diff --git a/server/services/mapUi.js b/server/services/mapUi.js new file mode 100644 index 0000000000..b4cf0d46ac --- /dev/null +++ b/server/services/mapUi.js @@ -0,0 +1,56 @@ +import debug from 'debug'; +import { Observable } from 'rx'; + +import { cachedMap, getMapForLang } from '../utils/map'; + +const log = debug('fcc:services:mapUi'); + +export default function mapUiService(app) { + const challengeMap = cachedMap(app.models); + return { + name: 'map-ui', + read: function readMapUi(req, resource, { lang = 'en' } = {}, config, cb) { + log(`generating mapUi for ${lang}`); + return challengeMap.map(getMapForLang(lang)) + .flatMap(({ + result: { superBlocks }, + entities: { + superBlock: fullSuperBlockMap, + block: fullBlockMap, + challenge: fullChallengeMap + } + }) => { + const superBlockMap = superBlocks + .map(superBlock => fullSuperBlockMap[superBlock]) + .reduce((map, { dashedName, blocks, title }) => { + map[dashedName] = { blocks, title, dashedName}; + return map; + }, {}); + const blockMap = Object.keys(fullBlockMap) + .map(block => fullBlockMap[block]) + .reduce((map, { dashedName, title, time, challenges }) => { + map[dashedName] = { dashedName, title, time, challenges }; + return map; + }, {}); + const challengeMap = Object.keys(fullChallengeMap) + .map(challenge => fullChallengeMap[challenge]) + .reduce((map, { dashedName, name, id}) => { + map[dashedName] = {name, dashedName, id}; + return map; + }, {}); + + return Observable.of({ + result: { superBlocks }, + entities: { + superBlock: superBlockMap, + block: blockMap, + challenge: challengeMap + } + }); + }).subscribe( + mapUi => cb(null, mapUi ), + err => { log(err); return cb(err); } + ); + } + }; +} diff --git a/server/utils/map.js b/server/utils/map.js index 99b67d159a..b7483ee961 100644 --- a/server/utils/map.js +++ b/server/utils/map.js @@ -74,43 +74,43 @@ export function _cachedMap({ Block, Challenge }) { }, blocksMap); }); const superBlockMap = blocks.map(blocks => blocks.reduce((map, block) => { - if ( - map[block.superBlock] && - map[block.superBlock].blocks - ) { - map[block.superBlock].blocks.push(block.dashedName); - } else { - map[block.superBlock] = { - title: _.startCase(block.superBlock), - order: block.superOrder, - name: nameify(_.startCase(block.superBlock)), - dashedName: block.superBlock, - blocks: [block.dashedName], - message: block.superBlockMessage - }; - } - return map; - }, {})); - const superBlocks = superBlockMap.map(superBlockMap => { - return Object.keys(superBlockMap) - .map(key => superBlockMap[key]) - .map(({ dashedName }) => dashedName); - }); - return Observable.combineLatest( - superBlockMap, - blockMap, - challengeMap, - superBlocks, - (superBlock, block, challenge, superBlocks) => ({ - entities: { - superBlock, - block, - challenge - }, - result: { - superBlocks - } - }) + if ( + map[block.superBlock] && + map[block.superBlock].blocks + ) { + map[block.superBlock].blocks.push(block.dashedName); + } else { + map[block.superBlock] = { + title: _.startCase(block.superBlock), + order: block.superOrder, + name: nameify(_.startCase(block.superBlock)), + dashedName: block.superBlock, + blocks: [block.dashedName], + message: block.superBlockMessage + }; + } + return map; + }, {})); + const superBlocks = superBlockMap.map(superBlockMap => { + return Object.keys(superBlockMap) + .map(key => superBlockMap[key]) + .map(({ dashedName }) => dashedName); + }); + return Observable.combineLatest( + superBlockMap, + blockMap, + challengeMap, + superBlocks, + (superBlock, block, challenge, superBlocks) => ({ + entities: { + superBlock, + block, + challenge + }, + result: { + superBlocks + } + }) ) .do(checkMapData) .shareReplay();