diff --git a/src/components/effectful/request-map-style.js b/src/components/effectful/request-map-style.js
new file mode 100644
index 0000000000..8c0f78b9bb
--- /dev/null
+++ b/src/components/effectful/request-map-style.js
@@ -0,0 +1,102 @@
+import {default as Console} from 'global/console';
+import {useEffect} from 'react';
+import {json as requestJson} from 'd3-request';
+import {generateHashId} from 'utils/utils';
+
+// Utils
+import {isValidStyleUrl, getStyleDownloadUrl} from 'utils/map-style-utils/mapbox-gl-style-editor';
+
+// This is exported to aid testing.
+export const TASKS = {
+ loadMapStyleTask,
+ requestMapStyles
+};
+
+export default function RequestMapStyle({
+ defaultMapStyles,
+ mapStyles,
+ loadMapStyles,
+ mapboxApiAccessToken,
+ mapboxApiUrl
+}) {
+ useEffect(
+ () => {
+ const defaultStyles = Object.values(defaultMapStyles);
+ // add id to custom map styles if not given
+ const customStyles = (mapStyles || []).map(ms => ({
+ ...ms,
+ id: ms.id || generateHashId()
+ }));
+
+ const allStyles = [...customStyles, ...defaultStyles].reduce(
+ (accu, style) => {
+ const hasStyleObject = style.style && typeof style.style === 'object';
+ accu[hasStyleObject ? 'toLoad' : 'toRequest'][style.id] = style;
+
+ return accu;
+ },
+ {toLoad: {}, toRequest: {}}
+ );
+ loadMapStyles(allStyles.toLoad);
+ // TODO: Make the side effect here and then call loadMapStyles
+ TASKS.requestMapStyles(allStyles.toRequest, {
+ mapboxApiAccessToken,
+ mapboxApiUrl,
+ loadMapStyles
+ });
+ },
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ [
+ // This is intentional. This effect is only called once, on mount.
+ ]
+ );
+ return null;
+}
+
+function requestMapStyles(mapStyles, {mapboxApiAccessToken, mapboxApiUrl, loadMapStyles}) {
+ return Promise.all(
+ Object.values(mapStyles)
+ .map(({id, url, accessToken}) => ({
+ id,
+ url: isValidStyleUrl(url)
+ ? getStyleDownloadUrl(url, accessToken || mapboxApiAccessToken, mapboxApiUrl)
+ : url
+ }))
+ .map(TASKS.loadMapStyleTask)
+ ).then(
+ // success
+ results =>
+ loadMapStyles(
+ results.reduce(
+ (accu, {id, style}) => ({
+ ...accu,
+ [id]: {
+ ...mapStyles[id],
+ style
+ }
+ }),
+ {}
+ )
+ ),
+ // error
+ // loadMapStyleErr
+ e => {
+ Console.warn('loadMapStyleErr', e);
+ }
+ );
+}
+
+function loadMapStyleTask({url, id}) {
+ return new Promise((success, error) =>
+ requestJson(url, (err, result) => {
+ if (err) {
+ error(err);
+ } else {
+ if (!result) {
+ error(new Error('Map style response is empty'));
+ }
+ success({id, style: result});
+ }
+ })
+ );
+}
diff --git a/src/components/kepler-gl.js b/src/components/kepler-gl.js
index d3fe8102d7..e36377ee97 100644
--- a/src/components/kepler-gl.js
+++ b/src/components/kepler-gl.js
@@ -50,8 +50,8 @@ import ModalContainerFactory from './modal-container';
import PlotContainerFactory from './plot-container';
import NotificationPanelFactory from './notification-panel';
import GeoCoderPanelFactory from './geocoder-panel';
+import RequestMapStyle from './effectful/request-map-style';
-import {generateHashId} from 'utils/utils';
import {validateToken} from 'utils/mapbox-utils';
import {mergeMessages} from 'utils/locale-utils';
@@ -259,7 +259,6 @@ function KeplerGlFactory(
componentDidMount() {
this._validateMapboxToken();
- this._loadMapStyle();
this._handleResize(this.props);
if (typeof this.props.onKeplerGlInitialized === 'function') {
this.props.onKeplerGlInitialized();
@@ -332,28 +331,6 @@ function KeplerGlFactory(
});
}
- _loadMapStyle = () => {
- const defaultStyles = Object.values(this.props.mapStyle.mapStyles);
- // add id to custom map styles if not given
- const customStyles = (this.props.mapStyles || []).map(ms => ({
- ...ms,
- id: ms.id || generateHashId()
- }));
-
- const allStyles = [...customStyles, ...defaultStyles].reduce(
- (accu, style) => {
- const hasStyleObject = style.style && typeof style.style === 'object';
- accu[hasStyleObject ? 'toLoad' : 'toRequest'][style.id] = style;
-
- return accu;
- },
- {toLoad: {}, toRequest: {}}
- );
-
- this.props.mapStyleActions.loadMapStyles(allStyles.toLoad);
- this.props.mapStyleActions.requestMapStyles(allStyles.toRequest);
- };
-
render() {
const {
id,
@@ -409,6 +386,13 @@ function KeplerGlFactory(
}}
ref={this.root}
>
+
{!uiState.readOnly && !readOnly && }
diff --git a/test/browser/components/effectful/index.js b/test/browser/components/effectful/index.js
new file mode 100644
index 0000000000..2ba3b13a36
--- /dev/null
+++ b/test/browser/components/effectful/index.js
@@ -0,0 +1 @@
+import './request-map-style-test';
diff --git a/test/browser/components/kepler-gl-test.js b/test/browser/components/kepler-gl-test.js
index ffcfb4c179..b336666881 100644
--- a/test/browser/components/kepler-gl-test.js
+++ b/test/browser/components/kepler-gl-test.js
@@ -21,9 +21,11 @@
import React from 'react';
import test from 'tape';
import {mount} from 'enzyme';
-import {drainTasksForTesting, succeedTaskWithValues} from 'react-palm/tasks';
import configureStore from 'redux-mock-store';
import {Provider} from 'react-redux';
+import {render} from 'react-dom';
+import {act} from 'react-dom/test-utils';
+import sinon from 'sinon';
import coreReducer from 'reducers/core';
import {keplerGlInit} from 'actions/actions';
@@ -38,6 +40,7 @@ import {
GeocoderPanelFactory
} from 'components';
import NotificationPanelFactory from 'components/notification-panel';
+import {TASKS as RequestMapStyleTasks} from 'components/effectful/request-map-style';
import {ActionTypes} from 'actions';
import {DEFAULT_MAP_STYLES, EXPORT_IMAGE_ID} from 'constants';
@@ -222,41 +225,60 @@ test('Components -> KeplerGl -> Mount -> Split Maps', t => {
t.end();
});
-test('Components -> KeplerGl -> Mount -> Load default map style task', t => {
+test('Components -> KeplerGl -> Mount -> Load default map style task', async t => {
+ // setup
+ const container = document.createElement('div');
+ document.body.appendChild(container);
+ const requestMapStylesStub = sinon.stub(RequestMapStyleTasks, 'requestMapStyles').callThrough();
+ const loadMapStylesStub = sinon.stub(RequestMapStyleTasks, 'loadMapStyleTask');
+ t.plan(5);
+
+ [
+ {id: 'dark', style: {layers: [], name: 'dark'}},
+ {id: 'light', style: {layers: [], name: 'light'}},
+ {id: 'muted', style: {hello: 'world'}},
+ {id: 'muted_night', style: {world: 'hello'}},
+ {id: 'satellite', style: {satellite: 'yes'}}
+ ].forEach((ret, i) => loadMapStylesStub.onCall(i).resolves(ret));
+
+ const stateSelector = state => state.keplerGl.map;
+
// mount with empty store
const store = mockStore(initialState);
- t.doesNotThrow(() => {
- mount(
+ // t.doesNotThrow(async () => {
+ await act(async () =>
+ render(
state.keplerGl.map}
+ selector={stateSelector}
dispatch={store.dispatch}
/>
-
- );
- }, 'Should not throw error when mount KeplerGl');
+ ,
+ container
+ )
+ );
+ // }, 'Should not throw error when mount KeplerGl');
const actions = store.getActions();
const expectedActions = [
- {type: ActionTypes.LOAD_MAP_STYLES, payload: {}},
- {
- type: ActionTypes.REQUEST_MAP_STYLES,
- payload: DEFAULT_MAP_STYLES.reduce((accu, curr) => ({...accu, [curr.id]: curr}), {})
- },
- {type: ActionTypes.UPDATE_MAP, payload: {width: 800, height: 800}}
+ {type: ActionTypes.UPDATE_MAP, payload: {width: 800, height: 800}},
+ {type: ActionTypes.LOAD_MAP_STYLES, payload: {}}
];
t.deepEqual(
- actions,
+ requestMapStylesStub.firstCall.args[0],
+ DEFAULT_MAP_STYLES.reduce((accu, curr) => ({...accu, [curr.id]: curr}), {}),
+ 'Should call request map styles'
+ );
+ t.deepEqual(
+ actions.slice(0, 2),
expectedActions,
'Should mount kepler.gl and dispatch 2 actions to load map styles'
);
const resultState1 = coreReducer(initialCoreState, actions[1]);
- const [task1, ...rest] = drainTasksForTesting();
- t.equal(rest.length, 0, 'should dispatch 1 tasks');
const expectedTask = {
payload: [
@@ -288,19 +310,14 @@ test('Components -> KeplerGl -> Mount -> Load default map style task', t => {
]
};
- t.deepEqual(task1.payload, expectedTask.payload, 'should create task to load map styles');
+ t.deepEqual(
+ loadMapStylesStub.getCalls().map(call => call.args[0]),
+ expectedTask.payload,
+ 'should create task to load map styles'
+ );
t.deepEqual(resultState1, initialCoreState, 'state should be the same');
- const resultState2 = coreReducer(
- resultState1,
- succeedTaskWithValues(task1, [
- {id: 'dark', style: {layers: [], name: 'dark'}},
- {id: 'light', style: {layers: [], name: 'light'}},
- {id: 'muted', style: {hello: 'world'}},
- {id: 'muted_night', style: {world: 'hello'}},
- {id: 'satellite', style: {satellite: 'yes'}}
- ])
- );
+ const resultState2 = coreReducer(resultState1, store.getActions()[2]);
const expectedStateMapStyles = {
dark: {
@@ -332,10 +349,29 @@ test('Components -> KeplerGl -> Mount -> Load default map style task', t => {
'should update state with loaded map styles'
);
- t.end();
+ // teardown
+ document.body.removeChild(container);
+ requestMapStylesStub.restore();
+ loadMapStylesStub.restore();
});
-test('Components -> KeplerGl -> Mount -> Load custom map style task', t => {
+test('Components -> KeplerGl -> Mount -> Load custom map style task', async t => {
+ // setup
+ t.plan(4);
+ const container = document.createElement('div');
+ document.body.appendChild(container);
+ const requestMapStylesStub = sinon.spy(RequestMapStyleTasks, 'requestMapStyles');
+ // Intentionally use a promise that never resolves, since we don't care about the task
+ // TODO: This isn't great since there are calls still waiting for this promise after
+ // this test is torn down.
+ let tearDownReject;
+ const neverResolvedPromise = new Promise((_, reject) => {
+ tearDownReject = reject;
+ });
+ const loadMapStylesStub = sinon
+ .stub(RequestMapStyleTasks, 'loadMapStyleTask')
+ .returns(neverResolvedPromise);
+
// mount with empty store
const store = mockStore(initialState);
// mount without id or a kepler.gl state
@@ -363,8 +399,10 @@ test('Components -> KeplerGl -> Mount -> Load custom map style task', t => {
}
]
};
- t.doesNotThrow(() => {
- mount(
+ // t.doesNotThrow(() => {
+ await act(async () =>
+ render(
+ // mount(
KeplerGl -> Mount -> Load custom map style task', t => {
dispatch={store.dispatch}
mapStyles={[customStyle1, customStyle2, customStyle3]}
/>
-
- );
- }, 'Should not throw error when mount KeplerGl');
+ ,
+ container
+ )
+ );
+ // );
+ // }, 'Should not throw error when mount KeplerGl');
const actions = store.getActions();
const expectedActions = [
+ {type: ActionTypes.UPDATE_MAP, payload: {width: 800, height: 800}},
{
type: ActionTypes.LOAD_MAP_STYLES,
payload: {
milkshake: customStyle2,
chai: customStyle3
}
- },
- {
- type: ActionTypes.REQUEST_MAP_STYLES,
- payload: DEFAULT_MAP_STYLES.reduce(
- (accu, curr) => ({...accu, [curr.id]: curr, smoothie: customStyle1}),
- {}
- )
- },
- {type: ActionTypes.UPDATE_MAP, payload: {width: 800, height: 800}}
+ }
];
+ t.deepEqual(
+ requestMapStylesStub.firstCall.args[0],
+ DEFAULT_MAP_STYLES.reduce(
+ (accu, curr) => ({...accu, [curr.id]: curr, smoothie: customStyle1}),
+ {}
+ ),
+ 'Should call request map styles'
+ );
t.deepEqual(
actions,
expectedActions,
- 'Should mount kepler.gl and dispatch 2 actions to load map styles'
+ 'Should mount kepler.gl and dispatch 1 action to load map styles'
);
- const resultState1 = coreReducer(initialCoreState, actions[0]);
+ const resultState1 = coreReducer(initialCoreState, actions[1]);
const expectedMapStyleState1 = {
...initialCoreState.mapStyle,
@@ -425,12 +467,6 @@ test('Components -> KeplerGl -> Mount -> Load custom map style task', t => {
'Should load map style into reducer and create layer groups'
);
- // Do not remove this. Necessary for testing flow
- // eslint-disable-next-line no-unused-vars
- const resultState2 = coreReducer(resultState1, actions[1]);
- const [task1, ...rest] = drainTasksForTesting();
- t.equal(rest.length, 0, 'should dispatch 1 tasks');
-
const expectedTask = {
payload: [
{
@@ -466,7 +502,15 @@ test('Components -> KeplerGl -> Mount -> Load custom map style task', t => {
]
};
- t.deepEqual(task1.payload, expectedTask.payload, 'should create task to load map styles');
+ t.deepEqual(
+ loadMapStylesStub.getCalls().map(call => call.args[0]),
+ expectedTask.payload,
+ 'should create task to load map styles'
+ );
- t.end();
+ // teardown
+ document.body.removeChild(container);
+ requestMapStylesStub.restore();
+ loadMapStylesStub.restore();
+ tearDownReject();
});