• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

geosolutions-it / MapStore2 / 19258616339

11 Nov 2025 07:41AM UTC coverage: 76.91% (+0.1%) from 76.761%
19258616339

Pull #11483

github

web-flow
Merge 4fab52f9e into c63877182
Pull Request #11483: Fix #11479 Add validation and support for editing restrictions in attribute table

32172 of 49976 branches covered (64.37%)

134 of 158 new or added lines in 15 files covered. (84.81%)

650 existing lines in 48 files now uncovered.

39990 of 51996 relevant lines covered (76.91%)

37.59 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

63.86
/web/client/jsapi/MapStore2.js
1
/*
2
 * Copyright 2016, GeoSolutions Sas.
3
 * All rights reserved.
4
 *
5
 * This source code is licensed under the BSD-style license found in the
6
 * LICENSE file in the root directory of this source tree.
7
 */
8

9
import url from 'url';
10

11
import { merge, partialRight } from 'lodash';
12
import React from 'react';
13
import ReactDOM from 'react-dom';
14
import { connect } from 'react-redux';
15

16
import { configureMap, loadMapConfig } from '../actions/config';
17
import { initMap } from '../actions/map';
18
import StandardApp from '../components/app/StandardApp';
19
import defaultConfig from '../configs/config.json';
20
import { generateActionTrigger } from '../epics/jsapi';
21
import localConfig from '../configs/localConfig.json';
22
import { standardEpics, standardReducers, standardRootReducerFunc } from '../stores/defaultOptions';
23
import ConfigUtils from '../utils/ConfigUtils';
24
import { renderFromLess } from '../utils/ThemeUtils';
25
import { getApi } from '../api/userPersistedStorage';
26

27
const defaultPlugins = {
1✔
28
    "mobile": localConfig.plugins.embedded,
29
    "desktop": localConfig.plugins.embedded
30
};
31
let triggerAction;
32
function mergeDefaultConfig(pluginName, cfg) {
33
    var propertyName;
34
    var i;
35
    var result;
UNCOV
36
    for (i = 0; i < defaultPlugins.desktop.length; i++) {
×
37
        if (defaultPlugins.desktop[i].name === pluginName) {
×
38
            result = defaultPlugins.desktop[i].cfg;
×
39
            for (propertyName in cfg) {
×
40
                if (cfg.hasOwnProperty(propertyName)) {
×
41
                    result[propertyName] = cfg[propertyName];
×
42
                }
43
            }
UNCOV
44
            return result;
×
45
        }
46
    }
UNCOV
47
    return cfg;
×
48
}
49

50
function loadConfigFromStorage(name = 'mapstore.embedded') {
3✔
51
    if (name) {
3!
52
        let loaded = false;
3✔
53
        try {
3✔
54
            loaded = getApi().getItem(name);
3✔
55
            if (loaded) {
2✔
56
                return JSON.parse(loaded);
1✔
57
            }
58
        } catch (e) {
59
            console.error(e);
1✔
60
            return null;
1✔
61
        }
62
    }
63
    return null;
1✔
64
}
65

66
function getParamFromRequest(paramName) {
UNCOV
67
    const urlQuery = url.parse(window.location.href, true).query;
×
68
    return urlQuery[paramName] || null;
×
69
}
70

71
function buildPluginsCfg(plugins, cfg) {
UNCOV
72
    var pluginsCfg = [];
×
73
    var i;
UNCOV
74
    for (i = 0; i < plugins.length; i++) {
×
75
        if (cfg[plugins[i] + "Plugin"]) {
×
76
            pluginsCfg.push({
×
77
                name: plugins[i],
78
                cfg: mergeDefaultConfig(plugins[i], cfg[plugins[i] + "Plugin"])
79
            });
80
        } else {
UNCOV
81
            pluginsCfg.push({
×
82
                name: plugins[i],
83
                cfg: mergeDefaultConfig(plugins[i], {})
84
            });
85
        }
86
    }
UNCOV
87
    return {
×
88
        mobile: pluginsCfg,
89
        desktop: pluginsCfg
90
    };
91
}
92

93
const actionListeners = {};
1✔
94
let stateChangeListeners = [];
1✔
95

96
const getInitialActions = (options) => {
1✔
97
    if (!options.initialState || !options.initialState.defaultState.map) {
4!
98
        if (options.configUrl) {
4!
99
            return [initMap, loadMapConfig.bind(null, options.configUrl || defaultConfig, options.mapId)];
4!
100
        }
UNCOV
101
        return [configureMap.bind(null, options.config || defaultConfig, options.mapId)];
×
102
    }
UNCOV
103
    return [];
×
104
};
105

106

107
/**
108
 * MapStore2 JavaScript API. Allows embedding MapStore2 functionalities into
109
 * a standard HTML page.
110
 *
111
 * ATTENTION: As of July 2020 a number of MapStore2 plugins (i.e. TOC layer settings, Identify) use react-dock for providing
112
 * Dock panel functionality, that assumes that we use the whole window, so the panels won't show up at all or will
113
 * not be constrained within the container.
114
 * @class
115
 */
116
const MapStore2 = {
1✔
117
    /**
118
     * Instantiates an embedded MapStore2 application in the given container.
119
     * MapStore2 api doesn't use StandardRouter but It relies on StandardContainer
120
     * @memberof MapStore2
121
     * @static
122
     * @param {string} container id of the DOM element that should contain the embedded MapStore2
123
     * @param {object} options set of options of the embedded app
124
     *  * The options object can contain the following properties, to configure the app UI and state:
125
     *  * **plugins**: list of plugins (and the related configuration) to be included in the app
126
     *    look at [Plugins documentation](https://mapstore.readthedocs.io/en/latest/developer-guide/plugins-documentation/) for further details
127
     *  * **config**: map configuration object for the application (look at [Map Configuration](https://mapstore.readthedocs.io/en/latest/developer-guide/maps-configuration/) for details)
128
     *  * **configUrl**: map configuration url for the application (look at [Map Configuration](https://mapstore.readthedocs.io/en/latest/developer-guide/maps-configuration/) for details)
129
     *  * **originalUrl**: url of the original instance of MapStore. If present it will be linked inside the map using the "GoFull" plugin, present by default.
130
     *  * **initialState**: allows setting the initial application state (look at [State Configuration](https://mapstore.readthedocs.io/en/latest/developer-guide/local-config/) for details)
131
     *
132
     * Styling can be configured either using a **theme**, or a complete custom **less stylesheet**, using the
133
     * following options properties:
134
     *  * **style**: less style to be applied
135
     *  * **startAction**: the actionType to wait before start triggering actions. By default CHANGE_MAP_VIEW
136
     *  * **theme**: theme configuration options:
137
     *    * path: path/url of the themes folder related to the current page
138
     *    * theme: theme name to be used
139
     *
140
     * ```javascript
141
     * {
142
     *      plugins: ['Map', 'ZoomIn', 'ZoomOut'],
143
     *      config: {
144
     *          map: {
145
     *              ...
146
     *          }
147
     *      },
148
     *      configUrl: '...',
149
     *      initialState: {
150
     *          defaultState: {
151
     *              ...
152
     *          }
153
     *      },
154
     *      style: '<custom style>',
155
     *      theme: {
156
     *          theme: 'mytheme',
157
     *          path: 'dist/themes'
158
     *      }
159
     * }
160
     * ```
161
     * @param {object} [plugins] optional plugins definition (defaults to local plugins list)
162
     * @param {object} [component] optional page component (defaults to MapStore2 Embedded Page)
163
     * @example
164
     * MapStore2.create('container', {
165
     *      plugins: ['Map']
166
     * });
167
     */
168
    create(container, opts, pluginsDef, component) {
169
        const embedded = require('../containers/Embedded').default;
4✔
170
        const options = merge({}, this.defaultOptions || {}, opts);
4✔
171
        const {initialState, storeOpts} = options;
4✔
172

173
        const {loadVersion} = require('../actions/version');
4✔
174
        const {versionSelector} = require('../selectors/version');
4✔
175
        const {loadAfterThemeSelector} = require('../selectors/config');
4✔
176
        const componentConfig = {
4✔
177
            component: component || embedded,
8✔
178
            config: {
179
                pluginsConfig: options.plugins || defaultPlugins
8✔
180
            }
181
        };
182
        const StandardContainer = connect((state) => ({
14✔
183
            locale: state.locale || {},
26✔
184
            componentConfig,
185
            version: versionSelector(state),
186
            loadAfterTheme: loadAfterThemeSelector(state)
187
        }))(require('../components/app/StandardContainer').default);
188
        const actionTrigger = generateActionTrigger(options.startAction || "CHANGE_MAP_VIEW");
4✔
189
        triggerAction = actionTrigger.trigger;
4✔
190
        const appStore = require('../stores/StandardStore').default.bind(null, {
4✔
191
            initialState: initialState || {},
8✔
192
            appReducers: {
193
                security: require('../reducers/security').default,
194
                version: require('../reducers/version').default,
195
                ...standardReducers
196
            },
197
            appEpics: {
198
                jsAPIEpic: actionTrigger.epic,
199
                ...(options.epics || {}),
6✔
200
                ...standardEpics
201
            },
202
            rootReducerFunc: standardRootReducerFunc
203
        });
204
        const initialActions = [...getInitialActions(options), loadVersion.bind(null, options.versionURL)];
4✔
205
        const appConfig = {
4✔
206
            storeOpts: Object.assign({}, storeOpts, {notify: true, noRouter: true}),
207
            appStore,
208
            pluginsDef,
209
            initialActions,
210
            appComponent: StandardContainer,
211
            printingEnabled: options.printingEnabled || false
8✔
212
        };
213
        if (options.style) {
4!
UNCOV
214
            let dom = document.getElementById('custom_theme');
×
215
            if (!dom) {
×
216
                dom = document.createElement('style');
×
217
                dom.id = 'custom_theme';
×
218
                document.head.appendChild(dom);
×
219
            }
UNCOV
220
            renderFromLess(options.style, 'custom_theme', 'themes/default/');
×
221
        }
222
        const defaultThemeCfg = {
4✔
223
            prefixContainer: '#' + container
224
        };
225

226
        const themeCfg = options.theme && Object.assign({}, defaultThemeCfg, options.theme) || defaultThemeCfg;
4!
227
        const onStoreInit = (store) => {
4✔
228
            store.addActionListener((action) => {
4✔
229
                const act = action.type === "PERFORM_ACTION" && action.action || action; // Needed to works also in debug
36!
230
                (actionListeners[act.type] || []).concat(actionListeners['*'] || []).forEach((listener) => {
36✔
231
                    listener.call(null, act);
3✔
232
                });
233
            });
234
            store.subscribe(() => {
4✔
235
                stateChangeListeners.forEach(({listener, selector}) => {
36✔
236
                    listener.call(null, selector(store.getState()));
9✔
237
                });
238
            });
239
        };
240
        if (options.noLocalConfig) {
4!
UNCOV
241
            ConfigUtils.setLocalConfigurationFile('');
×
242
            ConfigUtils.setConfigProp('proxyUrl', options.proxy || null);
×
243
        }
244

245
        if (options.translations) {
4!
UNCOV
246
            ConfigUtils.setConfigProp('translationsPath', options.translations);
×
247
        }
248
        if (options.originalUrl) {
4!
249
            ConfigUtils.setConfigProp('originalUrl', options.originalUrl);
4✔
250
        }
251
        ReactDOM.render(<StandardApp onStoreInit={onStoreInit} themeCfg={themeCfg} className="fill" {...appConfig}/>, document.getElementById(container));
4✔
252
    },
253
    buildPluginsCfg,
254
    getParamFromRequest,
255
    loadConfigFromStorage,
256
    /**
257
     * Adds a listener that will be notified of all the MapStore2 events (**actions**), or only some of them.
258
     *
259
     * @memberof MapStore2
260
     * @static
261
     * @param {string} type type of actions to be captured (* for all)
262
     * @param {function} listener function to be called for each launched action; it will receive
263
     *  the action as the only argument
264
     * @example
265
     * MapStore2.onAction('CHANGE_MAP_VIEW', function(action) {
266
     *      console.log(action.zoom);
267
     * });
268
     */
269
    onAction: (type, listener) => {
270
        const listeners = actionListeners[type] || [];
2✔
271
        listeners.push(listener);
2✔
272
        actionListeners[type] = listeners;
2✔
273
    },
274
    /**
275
     * Removes an action listener.
276
     *
277
     * @memberof MapStore2
278
     * @static
279
     * @param {string} type type of actions that is captured by the listener (* for all)
280
     * @param {function} listener listener to be removed
281
     * @example
282
     * MapStore2.offAction('CHANGE_MAP_VIEW', listener);
283
     */
284
    offAction: (type, listener) => {
285
        const listeners = (actionListeners[type] || []).filter((l) => l !== listener);
2!
286
        actionListeners[type] = listeners;
1✔
287
    },
288
    /**
289
     * Adds a listener that will be notified of each state update.
290
     *
291
     * @memberof MapStore2
292
     * @static
293
     * @param {function} listener function to be called for each state udpate; it will receive
294
     *  the new state as the only argument
295
     * @param {function} [selector] optional function that will produce a partial/derived state
296
     * from the global state before calling the listeners
297
     * @example
298
     * MapStore2.onStateChange(function(map) {
299
     *      console.log(map.zoom);
300
     * }, function(state) {
301
     *      return (state.map && state.map.present) || state.map || {};
302
     * });
303
     */
304
    onStateChange: (listener, selector = (state) => state) => {
9✔
305
        stateChangeListeners.push({listener, selector});
1✔
306
    },
307
    /**
308
     * Removes a state listener.
309
     *
310
     * @memberof MapStore2
311
     * @static
312
     * @param {function} listener listener to be removed
313
     * @example
314
     * MapStore2.offStateChange(listener);
315
     */
316
    offStateChange: (listener) => {
UNCOV
317
        stateChangeListeners = stateChangeListeners.filter((l) => l !== listener);
×
318
    },
319
    /**
320
     * Returns a new custom API object using the given plugins list.
321
     *
322
     * @memberof MapStore2
323
     * @static
324
     * @param {object} plugins list of included plugins
325
     * @param {object} [options] default options (to be overridden on create)
326
     * @example
327
     * MapStore2.withPlugins({...});
328
     */
329
    withPlugins: (plugins, options) => {
UNCOV
330
        return Object.assign({}, MapStore2, {create: partialRight(MapStore2.create, partialRight.placeholder, partialRight.placeholder, plugins), defaultOptions: options || {}});
×
331
    },
332
    /**
333
     * Triggers an action
334
     * @param  {object} action The action to trigger.
335
     * @example
336
     * triggerAction({
337
     *       type: 'ZOOM_TO_EXTENT',
338
     *       extent: {
339
     *         minx: '-124.731422',
340
     *         miny: '24.955967',
341
     *         maxx: '-66.969849',
342
     *         maxy: '49.371735'
343
     *       },
344
     *       crs: 'EPSG:4326'
345
     *   })
346
     */
UNCOV
347
    triggerAction: (action) => triggerAction(action)
×
348
};
349

350
export default MapStore2;
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc