diff --git a/src/modules/Map/Map/Map.js b/src/modules/Map/Map/Map.js
index a4d60154..35988382 100644
--- a/src/modules/Map/Map/Map.js
+++ b/src/modules/Map/Map/Map.js
@@ -2,6 +2,7 @@ import React from 'react';
import mapBoxGl from 'mapbox-gl';
import PropTypes from 'prop-types';
import 'mapbox-gl/dist/mapbox-gl.css';
+import { connectState } from '../../State/context';
import { updateCluster } from '../services/cluster';
@@ -11,6 +12,7 @@ import CaptureControl from './components/CaptureControl';
import DrawControl from './components/DrawControl';
import PrintControl from './components/PrintControl';
import HomeControl from './components/HomeControl';
+import PermalinkControl from './components/PermalinkControl';
import './Map.scss';
@@ -27,6 +29,7 @@ export const CONTROL_CAPTURE = 'CaptureControl';
export const CONTROL_DRAW = 'DrawControl';
export const CONTROL_PRINT = 'PrintControl';
export const CONTROL_HOME = 'HomeControl';
+export const CONTROL_PERMALINK = 'PermalinkControl';
export const DEFAULT_CONTROLS = [{
control: CONTROL_ATTRIBUTION,
@@ -79,6 +82,7 @@ export class MapComponent extends React.Component {
CONTROL_DRAW,
CONTROL_PRINT,
CONTROL_HOME,
+ CONTROL_PERMALINK,
]),
PropTypes.shape({
onAdd: PropTypes.func,
@@ -173,7 +177,7 @@ export class MapComponent extends React.Component {
const { map: { flyTo } } = this.props;
flyTo(flyToConfig);
}
- }
+ };
updateMapProperties = prevProps => {
const {
@@ -218,7 +222,7 @@ export class MapComponent extends React.Component {
if (JSON.stringify(customStyle) !== JSON.stringify(prevProps.customStyle)) {
this.replaceLayers(prevProps.customStyle);
}
- }
+ };
focusOnSearchResult = ({ center, bounds }) => {
const { map } = this.props;
@@ -231,7 +235,7 @@ export class MapComponent extends React.Component {
if (center) {
map.setCenter(center);
}
- }
+ };
onSearchResultClick = onResultClick => ({ result, ...rest }) => {
const { map } = this.props;
@@ -245,7 +249,7 @@ export class MapComponent extends React.Component {
} else {
this.focusOnSearchResult(result);
}
- }
+ };
async initMapProperties () {
const {
@@ -386,6 +390,15 @@ export class MapComponent extends React.Component {
map.addControl(controlInstance, position);
break;
}
+ case CONTROL_PERMALINK: {
+ const controlInstance = new PermalinkControl({
+ ...this.props,
+ ...params,
+ });
+ this.controls.push(controlInstance);
+ map.addControl(controlInstance, position);
+ break;
+ }
default: {
const controlInstance = typeof control === 'string'
? new mapBoxGl[control]({ ...params })
@@ -403,4 +416,4 @@ export class MapComponent extends React.Component {
}
}
-export default MapComponent;
+export default connectState('initialState')(MapComponent);
diff --git a/src/modules/Map/Map/Map.scss b/src/modules/Map/Map/Map.scss
index 9e38255c..793c313c 100644
--- a/src/modules/Map/Map/Map.scss
+++ b/src/modules/Map/Map/Map.scss
@@ -43,7 +43,8 @@
}
// Taken from .mapboxgl-ctrl-group > button
-.print-button .mapboxgl-ctrl-icon {
+.mapboxgl-ctrl-permalink .mapboxgl-ctrl-icon,
+.mapboxgl-ctrl-print .mapboxgl-ctrl-icon {
width: 30px;
height: 30px;
display: block;
diff --git a/src/modules/Map/Map/Map.test.js b/src/modules/Map/Map/Map.test.js
index fe7a46f4..af3961f7 100644
--- a/src/modules/Map/Map/Map.test.js
+++ b/src/modules/Map/Map/Map.test.js
@@ -599,6 +599,19 @@ describe('controls', () => {
expect(map.addControl).toHaveBeenCalledWith(instance.controls[0], 'top-right');
});
+ it('should update permalink control state', () => {
+ const instance = new Map({});
+ instance.props = {
+ map,
+ controls: [{
+ control: 'PermalinkControl',
+ position: 'top-right',
+ }],
+ };
+ instance.resetControls();
+ expect(map.addControl).toHaveBeenCalledWith(instance.controls[0], 'top-right');
+ });
+
it('should focus on search result', () => {
const instance = new Map({ map });
diff --git a/src/modules/Map/Map/components/PermalinkControl/PermalinkControl.js b/src/modules/Map/Map/components/PermalinkControl/PermalinkControl.js
new file mode 100644
index 00000000..2422d723
--- /dev/null
+++ b/src/modules/Map/Map/components/PermalinkControl/PermalinkControl.js
@@ -0,0 +1,90 @@
+import {
+ Button,
+ ControlGroup,
+ Icon,
+ Popover,
+ PopoverPosition,
+} from '@blueprintjs/core';
+import PropTypes from 'prop-types';
+import { stringify } from 'query-string';
+import React from 'react';
+import translateMock from '../../../../../utils/translate';
+import { DEFAULT_OPTIONS } from '../../../../State/Hash/withHashState';
+
+import AbstractMapControl from '../../../helpers/AbstractMapControl';
+
+
+export class PermalinkControl extends AbstractMapControl {
+ static containerClassName = 'mapboxgl-ctrl mapboxgl-ctrl-group mapboxgl-ctrl-permalink';
+
+ static propTypes = {
+ /** Function used to translate wording. Takes key and object of options as parameters */
+ translate: PropTypes.func,
+ };
+
+ static defaultProps = {
+ translate: translateMock({
+ 'terralego.map.permalink_control.button_label': 'Get permalink',
+ }),
+ };
+
+ state = {};
+
+ inputRef = React.createRef();
+
+ options = DEFAULT_OPTIONS;
+
+ generateHashString = () => {
+ const { initialState } = this.props;
+ const [currentUrl] = window.location.href.split('#');
+ const url = `${currentUrl}#${stringify(initialState, this.options)}`;
+ this.setState({ url });
+ };
+
+ copyToCliboard = () => {
+ const { current: textInput } = this.inputRef;
+ textInput.setSelectionRange(0, textInput.value.length);
+ textInput.focus();
+ document.execCommand('copy');
+ this.setState({ copySuccess: true });
+ setTimeout(() => this.setState({ copySuccess: false }), 2000);
+ };
+
+ render () {
+ const { translate } = this.props;
+ const { url, copySuccess } = this.state;
+ return (
+
+
+
+ target.setSelectionRange(0, value.length)}
+ value={url}
+ readOnly
+ size={80}
+ />
+
+
+ {copySuccess && 'Copied !'}
+
+
+
+ );
+ }
+}
+
+export default PermalinkControl;
diff --git a/src/modules/Map/Map/components/PermalinkControl/PermalinkControl.test.js b/src/modules/Map/Map/components/PermalinkControl/PermalinkControl.test.js
new file mode 100644
index 00000000..3ad91681
--- /dev/null
+++ b/src/modules/Map/Map/components/PermalinkControl/PermalinkControl.test.js
@@ -0,0 +1,57 @@
+import { Popover } from '@blueprintjs/core';
+import { mount } from 'enzyme';
+import React from 'react';
+import renderer from 'react-test-renderer';
+
+import PermalinkControl from './PermalinkControl';
+
+it('should render', () => {
+ const tree = renderer.create();
+ expect(tree.toJSON()).toMatchSnapshot();
+});
+
+it('should display correct url', () => {
+ const instance = new PermalinkControl({
+ initialState: {
+ foo: 'bar',
+ baz: false,
+ },
+ });
+ instance.setState = jest.fn();
+ instance.generateHashString();
+ expect(instance.setState).toHaveBeenCalledWith({ url: 'http://localhost/#foo=bar&baz=false' });
+});
+
+it('should permit copy to clipboard', () => {
+ jest.useFakeTimers();
+ document.execCommand = jest.fn();
+
+ const wrapper = mount(
+ ,
+ );
+ const selectionRange = jest.fn();
+
+ wrapper.find(Popover).setState({ isOpen: true });
+ wrapper.find('.bp3-input').props().onClick({
+ target: {
+ setSelectionRange: selectionRange,
+ value: 'coucou',
+ },
+ });
+
+ wrapper.instance().inputRef.current = {
+ setSelectionRange: jest.fn(),
+ focus: jest.fn(),
+ value: 'coucou',
+ };
+ wrapper.instance().copyToCliboard();
+ expect(document.execCommand).toHaveBeenCalledWith('copy');
+ expect(wrapper.instance().state).toEqual({ copySuccess: true });
+ jest.runAllTimers();
+ expect(wrapper.instance().state).toEqual({ copySuccess: false });
+});
diff --git a/src/modules/Map/Map/components/PermalinkControl/__snapshots__/PermalinkControl.test.js.snap b/src/modules/Map/Map/components/PermalinkControl/__snapshots__/PermalinkControl.test.js.snap
new file mode 100644
index 00000000..afbbaf6c
--- /dev/null
+++ b/src/modules/Map/Map/components/PermalinkControl/__snapshots__/PermalinkControl.test.js.snap
@@ -0,0 +1,40 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`should render 1`] = `
+
+
+
+
+
+`;
diff --git a/src/modules/Map/Map/components/PermalinkControl/index.js b/src/modules/Map/Map/components/PermalinkControl/index.js
new file mode 100644
index 00000000..e08716f3
--- /dev/null
+++ b/src/modules/Map/Map/components/PermalinkControl/index.js
@@ -0,0 +1,3 @@
+import PermalinkControl from './PermalinkControl';
+
+export default PermalinkControl;
diff --git a/src/modules/Map/Map/components/PrintControl/PrintControl.js b/src/modules/Map/Map/components/PrintControl/PrintControl.js
index 344fe717..e589f268 100644
--- a/src/modules/Map/Map/components/PrintControl/PrintControl.js
+++ b/src/modules/Map/Map/components/PrintControl/PrintControl.js
@@ -21,7 +21,7 @@ const ORIENTATION_PORTRAIT = 'portrait';
const ORIENTATION_LANDSCAPE = 'landscape';
export class PrintControl extends AbstractMapControl {
- static containerClassName = 'mapboxgl-ctrl mapboxgl-ctrl-group print-button';
+ static containerClassName = 'mapboxgl-ctrl mapboxgl-ctrl-group mapboxgl-ctrl-print';
static propTypes = {
translate: PropTypes.func,
diff --git a/src/modules/Map/Map/hash.js b/src/modules/Map/Map/hash.js
index a9f05db2..4677f4e6 100644
--- a/src/modules/Map/Map/hash.js
+++ b/src/modules/Map/Map/hash.js
@@ -9,7 +9,6 @@ import debounce from 'lodash.debounce';
*/
class Hash {
constructor (hashName) {
- this._onHashChange.bind(this);
this._hashName = hashName;
// Mobile Safari doesn't allow updating the hash more than 100 times per 30 seconds.
@@ -68,7 +67,7 @@ class Hash {
return hash;
}
- _onHashChange () {
+ _onHashChange = () => {
let loc = '';
if (this._hashName) {
const params = new URLSearchParams(window.location.hash.slice(1));
diff --git a/src/modules/Map/Map/withMap.js b/src/modules/Map/Map/withMap.js
index 3305189b..a60339c1 100644
--- a/src/modules/Map/Map/withMap.js
+++ b/src/modules/Map/Map/withMap.js
@@ -20,7 +20,6 @@ export const withMap = WrappedComponent =>
fitBounds: PropTypes.shape({
coordinates: PropTypes.arrayOf(
PropTypes.array,
- PropTypes.array,
),
padding: PropTypes.shape({
top: PropTypes.number,
@@ -30,7 +29,6 @@ export const withMap = WrappedComponent =>
}),
offset: PropTypes.arrayOf(
PropTypes.number,
- PropTypes.number,
),
}),
onMapInit: PropTypes.func,
diff --git a/src/modules/State/Hash/index.js b/src/modules/State/Hash/index.js
new file mode 100644
index 00000000..fd583701
--- /dev/null
+++ b/src/modules/State/Hash/index.js
@@ -0,0 +1,4 @@
+import StateProvider from '../StateProvider';
+import { withHashState } from './withHashState';
+
+export default withHashState()(StateProvider);
diff --git a/src/modules/State/Hash/withHashState.js b/src/modules/State/Hash/withHashState.js
new file mode 100644
index 00000000..c3e3b23e
--- /dev/null
+++ b/src/modules/State/Hash/withHashState.js
@@ -0,0 +1,72 @@
+import PropTypes from 'prop-types';
+import { stringify, parse } from 'query-string';
+import React from 'react';
+
+
+export const DEFAULT_OPTIONS = {
+ encode: false,
+ arrayFormat: 'comma',
+ sort: false,
+ parseNumbers: true,
+ parseBooleans: true,
+};
+
+/**
+ * Decorator for getting an initialState from hash.
+ */
+export const withHashState = () => WrappedComponent =>
+ class WithHashState extends React.Component {
+ static propTypes = {
+ listenHash: PropTypes.bool,
+ updateHash: PropTypes.bool,
+ };
+
+ static defaultProps = {
+ listenHash: true,
+ updateHash: true,
+ };
+
+ options = DEFAULT_OPTIONS;
+
+ componentDidMount () {
+ const { listenHash } = this.props;
+ if (listenHash) {
+ window.addEventListener('hashchange', this.onHashChange, false);
+ }
+ }
+
+ componentWillUnmount () {
+ this.isUnmount = true;
+ window.removeEventListener('hashchange', this.onHashChange, false);
+ }
+
+ onHashChange = () => {
+ if (!this.isUnmount) {
+ this.forceUpdate();
+ }
+ };
+
+ getCurrentHashString = state => `#${stringify(state, this.options)}`;
+
+ updateHashString = state => {
+ const { updateHash } = this.props;
+ if (updateHash) {
+ window.history.replaceState(window.history.state, '', `#${stringify(state, this.options)}`);
+ }
+ };
+
+ render () {
+ const { listenHash, updateHash, ...props } = this.props;
+ const initialState = parse(window.location.hash, this.options);
+ return (
+
+ );
+ }
+ };
+
+export default withHashState;
diff --git a/src/modules/State/Hash/withHashState.test.js b/src/modules/State/Hash/withHashState.test.js
new file mode 100644
index 00000000..030facb9
--- /dev/null
+++ b/src/modules/State/Hash/withHashState.test.js
@@ -0,0 +1,61 @@
+import React from 'react';
+import { shallow } from 'enzyme';
+import withHashState from './withHashState';
+
+const Component = jest.fn(() => null);
+const ComponentWithHash = withHashState()(Component);
+
+it('should not set listeners', () => {
+ jest.spyOn(window, 'addEventListener');
+ jest.spyOn(window.history, 'replaceState');
+
+ const wrapper = shallow(
+ ,
+ );
+ const instance = wrapper.instance();
+ expect(window.addEventListener).not.toHaveBeenCalled();
+
+ instance.updateHashString({ foo: 'bar' });
+ expect(window.history.replaceState).not.toHaveBeenCalled();
+});
+
+it('should get correct parameters', () => {
+ jest.spyOn(window, 'addEventListener');
+ jest.spyOn(window, 'removeEventListener');
+
+ window.location.hash = '#myparam=1&foo=bar&baz=false';
+
+ const wrapper = shallow(
+ ,
+ );
+ const listener = wrapper.instance().onHashChange;
+ expect(window.addEventListener).toHaveBeenCalledWith('hashchange', listener, false);
+ listener();
+
+ const { initialState } = wrapper.props();
+ expect(initialState).toEqual({
+ myparam: 1,
+ foo: 'bar',
+ baz: false,
+ });
+ expect(wrapper.instance().getCurrentHashString(initialState)).toEqual(window.location.hash);
+
+ wrapper.unmount();
+ expect(window.removeEventListener).toHaveBeenCalledWith('hashchange', listener, false);
+ listener();
+});
+
+it('should set correct parameters', () => {
+ const instance = shallow(
+ ,
+ ).instance();
+
+ instance.updateHashString({ myparam: 1 });
+ expect(window.location.hash).toEqual('#myparam=1');
+
+ instance.updateHashString({ myparam: 1, foo: 'bar' });
+ expect(window.location.hash).toEqual('#myparam=1&foo=bar');
+
+ instance.updateHashString({ myparam: 0, baz: false });
+ expect(window.location.hash).toEqual('#myparam=0&baz=false');
+});
diff --git a/src/modules/State/StateProvider.js b/src/modules/State/StateProvider.js
new file mode 100644
index 00000000..05d210cc
--- /dev/null
+++ b/src/modules/State/StateProvider.js
@@ -0,0 +1,40 @@
+import PropTypes from 'prop-types';
+import React from 'react';
+import context from './context';
+
+const { Provider } = context;
+
+export class StateProvider extends React.Component {
+ static propTypes = {
+ onStateChange: PropTypes.func,
+ };
+
+ static defaultProps = {
+ onStateChange () {},
+ };
+
+ componentWillMount () {
+ const { initialState } = this.props;
+ this.setState(initialState);
+ }
+
+ setCurrentState = state => {
+ const { onStateChange } = this.props;
+ this.setState(state, () => onStateChange(this.state));
+ };
+
+ render () {
+ const { children } = this.props;
+ return (
+
+ {children}
+
+ );
+ }
+}
+
+export default StateProvider;
diff --git a/src/modules/State/StateProvider.test.js b/src/modules/State/StateProvider.test.js
new file mode 100644
index 00000000..fb67c20c
--- /dev/null
+++ b/src/modules/State/StateProvider.test.js
@@ -0,0 +1,20 @@
+import React from 'react';
+import { shallow } from 'enzyme';
+import StateProvider from './StateProvider';
+
+
+it('should set state', () => {
+ const onStateChange = jest.fn();
+ const wrapper = shallow(
+ ,
+ );
+
+ expect(wrapper.state()).toEqual({ foo: 'bar' });
+
+ wrapper.instance().setCurrentState({ foo: 'baz' });
+ expect(wrapper.state()).toEqual({ foo: 'baz' });
+ expect(onStateChange).toHaveBeenCalled();
+});
diff --git a/src/modules/State/context.js b/src/modules/State/context.js
new file mode 100644
index 00000000..ab96143e
--- /dev/null
+++ b/src/modules/State/context.js
@@ -0,0 +1,7 @@
+import React from 'react';
+import connect from 'react-ctx-connect';
+
+export const context = React.createContext();
+export const connectState = connect(context);
+
+export default context;
diff --git a/src/modules/Visualizer/LayersTree/LayersTreeProvider/LayersTreeProvider.js b/src/modules/Visualizer/LayersTree/LayersTreeProvider/LayersTreeProvider.js
index 0652af98..d1843700 100644
--- a/src/modules/Visualizer/LayersTree/LayersTreeProvider/LayersTreeProvider.js
+++ b/src/modules/Visualizer/LayersTree/LayersTreeProvider/LayersTreeProvider.js
@@ -1,5 +1,6 @@
import React from 'react';
import PropTypes from 'prop-types';
+import { connectState } from '../../../State/context';
import context from './context';
import {
@@ -14,8 +15,13 @@ export class LayersTreeProvider extends React.Component {
static propTypes = {
/** Callback executed everytime layersTreeState change. Takes layersTreeState as parameter */
onChange: PropTypes.func,
+ /** Initial state */
+ initialState: PropTypes.shape({
+ layers: PropTypes.oneOfType([PropTypes.string, PropTypes.array]), // Active layer id(s)
+ table: PropTypes.string, // Layer if table is active
+ }),
/** Initial layer tree state */
- initialState: PropTypes.instanceOf(Map),
+ initialLayersTreeState: PropTypes.instanceOf(Map),
/**
* Function called when a filter property of single or many type need to fetch values
* Takes `layer` and `property` as parameters
@@ -25,25 +31,28 @@ export class LayersTreeProvider extends React.Component {
/**
* Function called when a filter property of range type need to fetch min and max
* Takes `layer` and `property` as parameters
- * @return {min: Number, max: Number}
+ * @return {{}} Object of form {min: Number, max: Number}
* */
fetchPropertyRange: PropTypes.func,
translate: PropTypes.func,
- }
+ setCurrentState: PropTypes.func,
+ };
static defaultProps = {
onChange () {},
- initialState: new Map(),
+ initialState: {},
+ initialLayersTreeState: new Map(),
fetchPropertyValues () {},
fetchPropertyRange () {},
translate: translateMock({
'visualizer.layerstree.group.selector': 'No layer found',
}),
- }
+ setCurrentState () {},
+ };
constructor (props) {
super(props);
- const { initialState: layersTreeState } = this.props;
+ const { initialLayersTreeState: layersTreeState } = this.props;
this.state = { layersTreeState };
}
@@ -51,11 +60,11 @@ export class LayersTreeProvider extends React.Component {
this.initLayersState();
}
- componentDidUpdate ({ initialState: prevInitialState, layersTree: prevLayersTree }) {
- const { initialState, layersTree } = this.props;
+ componentDidUpdate ({ initialLayersTreeState: prevLayersTreeState, layersTree: prevLayersTree }) {
+ const { initialLayersTreeState, layersTree } = this.props;
- if (initialState !== prevInitialState) {
- this.initLayersState(initialState);
+ if (initialLayersTreeState !== prevLayersTreeState) {
+ this.initLayersState(initialLayersTreeState);
}
if (layersTree !== prevLayersTree) {
@@ -71,12 +80,12 @@ export class LayersTreeProvider extends React.Component {
this.resetState(({ layersTreeState }) => ({
layersTreeState: setLayerStateAction(layer, newState, layersTreeState, reset),
}));
- }
+ };
getLayerState = ({ layer }) => {
const { layersTreeState } = this.state;
return layersTreeState.get(layer) || {};
- }
+ };
fetchPropertyValues = async (layer, property) => {
const { fetchPropertyValues } = this.props;
@@ -91,7 +100,7 @@ export class LayersTreeProvider extends React.Component {
property.values = [...properties];
const { layersTreeState: newLayersTreeState } = this.state;
this.resetState(new Map(newLayersTreeState));
- }
+ };
fetchPropertyRange = async (layer, property) => {
const { fetchPropertyRange } = this.props;
@@ -110,29 +119,44 @@ export class LayersTreeProvider extends React.Component {
/* eslint-enable no-param-reassign */
const { layersTreeState: newLayersTreeState } = this.state;
this.resetState(new Map(newLayersTreeState));
- }
+ };
- initLayersState = initialState => {
+ initLayersState = initialLayersTreeState => {
this.resetState(({ layersTreeState }) => {
- const { layersTree } = this.props;
- const state = initialState || layersTreeState;
+ const { layersTree, initialState } = this.props;
+ const state = initialLayersTreeState || layersTreeState;
if (!layersTree) return {};
return {
layersTreeState: state.size
? state
- : initLayersStateAction(layersTree),
+ : initLayersStateAction(layersTree, initialState),
};
});
- }
+ };
resetState (state, callback = () => {}) {
+ const { setCurrentState } = this.props;
+
this.setState(state, () => {
callback();
const { onChange } = this.props;
const { layersTreeState } = this.state;
+ // Simplify the state from the map
+ const activeLayers = [];
+ let table = null;
+ layersTreeState && layersTreeState.forEach((layerState, { layers: [layerId] = [] }) => {
+ if (layerState.active) {
+ activeLayers.push(layerId);
+ }
+ if (layerState.table) {
+ table = layerId;
+ }
+ });
+ setCurrentState({ layers: activeLayers, table: table || undefined });
+
onChange(layersTreeState);
});
}
@@ -169,4 +193,4 @@ export class LayersTreeProvider extends React.Component {
}
}
-export default LayersTreeProvider;
+export default connectState('initialState', 'setCurrentState')(LayersTreeProvider);
diff --git a/src/modules/Visualizer/LayersTree/LayersTreeProvider/LayersTreeProvider.test.js b/src/modules/Visualizer/LayersTree/LayersTreeProvider/LayersTreeProvider.test.js
index 47a2ff68..d2b54bb0 100644
--- a/src/modules/Visualizer/LayersTree/LayersTreeProvider/LayersTreeProvider.test.js
+++ b/src/modules/Visualizer/LayersTree/LayersTreeProvider/LayersTreeProvider.test.js
@@ -1,14 +1,12 @@
import React from 'react';
import renderer from 'react-test-renderer';
-import LayersTreeProvider from './LayersTreeProvider';
+import { LayersTreeProvider } from './LayersTreeProvider';
import { connectLayersTree } from './context';
-import { setLayerStateAction, initLayersStateAction } from '../../services/layersTreeUtils';
+import * as layersTreeUtils from '../../services/layersTreeUtils';
-jest.mock('../../services/layersTreeUtils', () => ({
- initLayersStateAction: jest.fn(),
- setLayerStateAction: jest.fn(),
-}));
+const initLayersStateAction = jest.spyOn(layersTreeUtils, 'initLayersStateAction').mockImplementation(jest.fn());
+const setLayerStateAction = jest.spyOn(layersTreeUtils, 'setLayerStateAction').mockImplementation(jest.fn());
beforeEach(() => {
initLayersStateAction.mockClear();
@@ -40,12 +38,12 @@ it('should update', () => {
expect(instance.resetState).not.toHaveBeenCalled();
expect(instance.initLayersState).not.toHaveBeenCalled();
- instance.props.initialState = {};
- instance.componentDidUpdate({ });
- expect(instance.initLayersState).toHaveBeenCalledWith(instance.props.initialState);
+ instance.props.initialLayersTreeState = {};
+ instance.componentDidUpdate({});
+ expect(instance.initLayersState).toHaveBeenCalledWith(instance.props.initialLayersTreeState);
instance.props.layersTree = [];
- instance.componentDidUpdate({ });
+ instance.componentDidUpdate({});
expect(instance.initLayersState).toHaveBeenCalledWith();
});
@@ -139,6 +137,7 @@ it('should fetch property ranges', async () => {
it('should init layers state', () => {
const instance = new LayersTreeProvider({ initialState: new Map() });
+ instance.props.getInitialState = jest.fn();
instance.resetState = jest.fn();
instance.initLayersState();
expect(instance.resetState).toHaveBeenCalled();
@@ -159,3 +158,124 @@ it('should init layers state', () => {
});
expect(initLayersStateAction).toHaveBeenCalled();
});
+
+it('should get layers state from hash', () => {
+ initLayersStateAction.mockRestore();
+
+ const layer1 = { layers: ['thatlayerid'] };
+ const layer2 = { layers: ['t'] };
+ const layersTreeState = new Map();
+
+ const initialState = {
+ layers: ['thatlayerid', 'b'],
+ };
+
+ const instance = new LayersTreeProvider({
+ initialState,
+ layersTree: [layer1, layer2],
+ });
+
+ instance.resetState = jest.fn(stateFn => stateFn({ layersTreeState }));
+ instance.initLayersState();
+ expect(instance.resetState.mock.calls[0][0]({ layersTreeState: new Map() })).toEqual({
+ layersTreeState: new Map([
+ [layer1, {
+ active: true,
+ opacity: 1,
+ }],
+ [layer2, {
+ active: false,
+ opacity: 1,
+ }],
+ ]),
+ });
+
+ instance.props.initialState = {
+ layers: 'thatlayerid',
+ };
+ instance.initLayersState();
+ expect(instance.resetState.mock.calls[1][0]({ layersTreeState: new Map() })).toEqual({
+ layersTreeState: new Map([
+ [layer1, {
+ active: true,
+ opacity: 1,
+ }],
+ [layer2, {
+ active: false,
+ opacity: 1,
+ }],
+ ]),
+ });
+
+ instance.props.initialState = {
+ layers: ['thatlayerid', 't'],
+ table: 'thatlayerid',
+ };
+ instance.initLayersState();
+ expect(instance.resetState.mock.calls[2][0]({ layersTreeState: new Map() })).toEqual({
+ layersTreeState: new Map([
+ [layer1, {
+ active: true,
+ opacity: 1,
+ table: true,
+ }],
+ [layer2, {
+ active: true,
+ opacity: 1,
+ }],
+ ]),
+ });
+});
+
+it('should set layers state from hash', () => {
+ const setCurrentState = jest.fn();
+ const layer1 = { layers: ['thatlayerid'] };
+ const layer2 = { layers: ['t'] };
+ const layer3 = {};
+ const instance = new LayersTreeProvider({
+ setCurrentState,
+ onChange: jest.fn(),
+ });
+
+ instance.state = {
+ layersTreeState: new Map([
+ [layer1, {
+ active: true,
+ opacity: 1,
+ }],
+ [layer2, {
+ active: false,
+ opacity: 1,
+ }],
+ [layer3, {}],
+ ]),
+ };
+ instance.setState = jest.fn(
+ (stateFn, callback) => callback(stateFn(instance.state)),
+ );
+ instance.initLayersState();
+
+ expect(setCurrentState).toHaveBeenCalledWith({
+ layers: ['thatlayerid'],
+ table: undefined,
+ });
+
+ instance.state = {
+ layersTreeState: new Map([
+ [layer1, {
+ active: true,
+ opacity: 1,
+ }],
+ [layer2, {
+ active: true,
+ opacity: 1,
+ table: true,
+ }],
+ ]),
+ };
+ instance.initLayersState();
+ expect(setCurrentState).toHaveBeenCalledWith({
+ layers: ['thatlayerid', 't'],
+ table: 't',
+ });
+});
diff --git a/src/modules/Visualizer/services/layersTreeUtils.js b/src/modules/Visualizer/services/layersTreeUtils.js
index 8836e979..b2d49c5c 100644
--- a/src/modules/Visualizer/services/layersTreeUtils.js
+++ b/src/modules/Visualizer/services/layersTreeUtils.js
@@ -4,11 +4,20 @@ export const INITIAL_FILTERS = new Map();
export const isCluster = (source, layerId) => !!source.match(new RegExp(`^${layerId}-${PREFIX_SOURCE}-[0-9]+`));
-export function initLayersStateAction (layersTree) {
+/**
+ * Returns a flattened map of layers state from a layers tree
+ *
+ * @param {object} layersTree The layers tree config object
+ * @param {string|string[]} layers Active layer(s) from hash
+ * @param {string} table Active table from hash (layer id)
+ * @return {Map} A reduced layer tree state
+ */
+export const initLayersStateAction = (layersTree, { layers, table } = {}) => {
const layersTreeState = new Map();
+
function reduceLayers (group, map) {
return group.reduce((layersStateMap, layer) => {
- const { initialState = {} } = layer;
+ const { initialState = {}, layers: [layerId] = [] } = layer;
if (layer.group) {
return reduceLayers(layer.layers, layersStateMap);
}
@@ -16,6 +25,15 @@ export function initLayersStateAction (layersTree) {
? 1
: initialState.opacity;
+ if (layers) {
+ initialState.active = (
+ Array.isArray(layers) && layers.includes(layerId))
+ || layers === layerId;
+ }
+ if (table && table === layerId) {
+ initialState.table = true;
+ }
+
layersStateMap.set(layer, {
active: false,
opacity: 1,
@@ -24,8 +42,9 @@ export function initLayersStateAction (layersTree) {
return layersStateMap;
}, map);
}
+
return reduceLayers(layersTree, layersTreeState);
-}
+};
export function setGroupLayerStateAction (layer, layerState, prevLayersTreeState) {
const { layers } = layer;