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

geosolutions-it / MapStore2 / 14797665772

02 May 2025 02:55PM UTC coverage: 76.933% (-0.06%) from 76.991%
14797665772

Pull #11067

github

web-flow
Merge 99dcd7f92 into d28bf7035
Pull Request #11067: Fix #10966 basic auth for services

30858 of 48053 branches covered (64.22%)

104 of 172 new or added lines in 24 files covered. (60.47%)

3 existing lines in 3 files now uncovered.

38384 of 49893 relevant lines covered (76.93%)

35.93 hits per line

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

82.5
/web/client/reducers/widgets.js
1
/*
2
 * Copyright 2017, 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 uuidv1 from 'uuid/v1';
10
import {
11
    EDIT_NEW,
12
    INSERT,
13
    EDIT,
14
    UPDATE_PROPERTY,
15
    UPDATE_LAYER,
16
    DELETE,
17
    EDITOR_CHANGE,
18
    EDITOR_SETTING_CHANGE,
19
    INIT,
20
    CHANGE_LAYOUT,
21
    CLEAR_WIDGETS,
22
    DEFAULT_TARGET,
23
    ADD_DEPENDENCY,
24
    REMOVE_DEPENDENCY,
25
    LOAD_DEPENDENCIES,
26
    RESET_DEPENDENCIES,
27
    TOGGLE_COLLAPSE,
28
    TOGGLE_MAXIMIZE,
29
    TOGGLE_COLLAPSE_ALL,
30
    TOGGLE_TRAY,
31
    toggleCollapse,
32
    REPLACE,
33
    WIDGETS_REGEX
34
} from '../actions/widgets';
35
import { REFRESH_SECURITY_LAYERS } from '../actions/layers';
36
import { CLEAR_SECURITY } from '../actions/security';
37
import { MAP_CONFIG_LOADED } from '../actions/config';
38
import { DASHBOARD_LOADED, DASHBOARD_RESET } from '../actions/dashboard';
39
import assign from 'object-assign';
40
import set from 'lodash/fp/set';
41
import { get, find, omit, mapValues, castArray, isEmpty } from 'lodash';
42
import { arrayUpsert, compose, arrayDelete } from '../utils/ImmutableUtils';
43
import {
44
    convertDependenciesMappingForCompatibility as convertToCompatibleWidgets,
45
    editorChange
46
} from "../utils/WidgetsUtils";
47

48
const emptyState = {
1✔
49
    dependencies: {
50
        viewport: "map.bbox",
51
        center: "map.center",
52
        zoom: "map.zoom"
53
    },
54
    containers: {
55
        floating: {
56
            widgets: []
57
        }
58
    },
59
    builder: {
60
        map: null,
61
        settings: {
62
            step: 0
63
        }
64
    }
65
};
66

67

68
/**
69
 * Manages the state of the widgets
70
 * @prop {array} widgets version identifier
71
 *
72
 * @example
73
 *{
74
 *  widgets: {
75
 *    containers: {
76
 *       floating: {
77
 *          widgets: [{
78
 *              //...
79
 *          }]
80
 *       }
81
 *    }
82
 *  }
83
 *}
84
 * @memberof reducers
85
 */
86
function widgetsReducer(state = emptyState, action) {
21✔
87
    switch (action.type) {
51!
88
    case INIT: {
89
        return set(`defaults`, action.cfg, state);
1✔
90
    }
91
    case EDITOR_SETTING_CHANGE: {
92
        return set(`builder.settings.${action.key}`, action.value, state);
1✔
93
    }
94
    case EDIT_NEW: {
95
        return set(`builder.editor`, action.widget,
1✔
96
            set("builder.settings", action.settings || emptyState.settings, state));
1!
97
    }
98
    case EDIT: {
99
        return set(`builder.editor`, {
4✔
100
            ...action.widget,
101
            // for backward compatibility widgets without widgetType are charts
102
            widgetType: action.widget && action.widget.widgetType || 'chart'
10✔
103
        }, set("builder.settings.step", 0, state));
104
    }
105
    case EDITOR_CHANGE: {
106
        return editorChange(action, state);
5✔
107
    }
108
    case INSERT: {
109
        let widget = {...action.widget};
3✔
110
        if (widget.widgetType === 'chart') {
3!
111
            widget = omit(widget, ["layer", "url"]);
×
112
        }
113
        const w = state?.defaults?.initialSize?.w ?? 1;
3✔
114
        const h = state?.defaults?.initialSize?.h ?? 1;
3✔
115
        return arrayUpsert(`containers[${action.target}].widgets`, {
3✔
116
            id: action.id,
117
            ...widget,
118
            dataGrid: action.id && {
6✔
119
                w,
120
                h,
121
                x: 0,
122
                y: 0
123
            }
124
        }, {
125
            id: action.widget.id || action.id
3!
126
        }, state);
127
    }
128

129
    case REPLACE:
130
        const widgetsPath = `containers[${action.target}].widgets`;
×
131
        const widgets = get(state, widgetsPath);
×
132
        if (widgets) {
×
133
            return set(widgetsPath, action.widgets, state);
×
134
        }
135
        return state;
×
136
    case UPDATE_PROPERTY:
137
        // if "merge" update map by merging a partial map object coming from
138
        // onMapViewChanges handler for MapWidget
139
        // if "replace" update the widget setting the value to the existing object
140
        const oldWidget = find(get(state, `containers[${action.target}].widgets`), {
5✔
141
            id: action.id
142
        });
143
        let uValue = action.value;
5✔
144
        if (action.mode === "merge") {
5✔
145
            uValue = action.key === "maps"
1!
146
                ? oldWidget.maps.map(m => m.mapId === action.value?.mapId ? {...m, ...action?.value} : m)
×
147
                : assign({}, oldWidget[action.key], action.value);
148
        }
149
        return arrayUpsert(`containers[${action.target}].widgets`,
5✔
150
            set(action.key, uValue, oldWidget), { id: action.id },
151
            state
152
        );
153
    case UPDATE_LAYER: {
154
        if (action.layer) {
1!
155
            const _widgets = get(state, `containers[${DEFAULT_TARGET}].widgets`);
1✔
156
            if (_widgets) {
1!
157
                return set(`containers[${DEFAULT_TARGET}].widgets`,
1✔
158
                    _widgets.map(w => {
159
                        if (w.widgetType === "chart" && w?.charts) {
5✔
160
                            // every chart stores the layer object configuration
161
                            // so we need to loop around them to update correctly the layer properties
162
                            // including the layerFilter
163
                            let chartsCopy = w?.charts?.length ? [...w.charts] : [];
1!
164
                            chartsCopy = chartsCopy.map(chart=>{
1✔
165
                                let chartItem = {...chart};
3✔
166
                                chartItem.traces = chartItem?.traces?.map(trace=>
3✔
167
                                    get(trace, "layer.id") === action.layer.id ? set("layer", action.layer, trace) : trace
1!
168
                                );
169
                                return chartItem;
3✔
170
                            });
171
                            return set("charts", chartsCopy, w);
1✔
172
                        }
173
                        return get(w, "layer.id") === action.layer.id ? set("layer", action.layer, w) : w;
4✔
174
                    }), state);
175
            }
176
        }
177
        return state;
×
178
    }
179
    case DELETE:
180
        const path = `containers[${DEFAULT_TARGET}].widgets`;
3✔
181
        const updatedState = arrayDelete(`containers[${action.target}].widgets`, {
3✔
182
            id: action.widget.id
183
        }, state);
184
        const allWidgets = get(updatedState, path, []);
3✔
185
        return set(path, allWidgets.map(m => {
3✔
186
            if (m.dependenciesMap) {
3!
187
                const [, dependentWidgetId] = WIDGETS_REGEX.exec((Object.values(m.dependenciesMap) || [])[0]) || [];
3!
188
                if (dependentWidgetId) {
3✔
189
                    if (action.widget.id === dependentWidgetId) {
2!
190
                        return {...omit(m, "dependenciesMap"), mapSync: false};
2✔
191
                    }
192
                }
193
            }
194
            return m;
1✔
195
        }), state);
196
    case DASHBOARD_LOADED:
197
        const { data } = action;
1✔
198
        return set(`containers[${DEFAULT_TARGET}]`, {
1✔
199
            ...data
200
        }, state);
201
    case REFRESH_SECURITY_LAYERS: {
202
        let newWidgets = state?.containers?.[DEFAULT_TARGET].widgets;
1✔
203
        newWidgets = newWidgets.map(w => {
1✔
204
            const newMaps = w.maps.map(map => {
1✔
205
                return {
1✔
206
                    ...map,
207
                    layers: map.layers.map(l => {
208
                        return l.security ? {
1!
209
                            ...l,
210
                            security: {
211
                                ...l.security,
212
                                rand: uuidv1()
213
                            }
214
                        } : l;
215
                    })
216
                };
217
            });
218
            return {...w, maps: newMaps};
1✔
219
        });
220
        const newMaps = state.builder?.editor?.maps?.map(map => {
1✔
221
            return {
1✔
222
                ...map,
223
                layers: map.layers.map(l => {
224
                    return l.security ? {
1!
225
                        ...l,
226
                        security: {
227
                            ...l.security,
228
                            rand: uuidv1()
229
                        }
230
                    } : l;
231
                })
232
            };
233
        });
234
        return set(`containers[${DEFAULT_TARGET}].widgets`, newWidgets, set(`builder.editor.maps`, newMaps, state));}
1✔
235
    case CLEAR_SECURITY: {
NEW
236
        let newWidgets = state?.containers?.[DEFAULT_TARGET].widgets;
×
NEW
237
        newWidgets = newWidgets.map(w => {
×
NEW
238
            const maps = w.maps.map(map => {
×
NEW
239
                return {
×
240
                    ...map,
241
                    layers: map.layers.map(l => {
NEW
242
                        return l?.security?.sourceId === action.protectedId ? {
×
243
                            ...l,
244
                            security: undefined
245
                        } : l;
246
                    })
247
                };
248
            });
NEW
249
            return {...w, maps};
×
250
        });
NEW
251
        const maps = state.builder?.editor?.maps?.map(map => {
×
NEW
252
            return {
×
253
                ...map,
254
                layers: map.layers.map(l => {
NEW
255
                    return l?.security?.sourceId === action.protectedId ? {
×
256
                        ...l,
257
                        security: undefined
258
                    } : l;
259
                })
260
            };
261
        });
NEW
262
        return set(`containers[${DEFAULT_TARGET}].widgets`, newWidgets, set(`builder.editor.maps`, maps, state));
×
263
    }
264
    case MAP_CONFIG_LOADED:
265
        let { widgetsConfig } = (action.config || {});
4!
266
        if (!isEmpty(widgetsConfig)) {
4✔
267
            widgetsConfig = convertToCompatibleWidgets(widgetsConfig);
2✔
268
        }
269
        return set(`containers[${DEFAULT_TARGET}]`, {
4✔
270
            ...widgetsConfig
271
        }, state);
272
    case CHANGE_LAYOUT: {
273
        return set(`containers[${action.target}].layout`, action.layout)(set(`containers[${action.target}].layouts`, action.allLayouts, state));
2✔
274
    }
275
    case CLEAR_WIDGETS:
276
    case DASHBOARD_RESET: {
277
        return set(`containers[${DEFAULT_TARGET}]`, emptyState.containers[DEFAULT_TARGET], state);
1✔
278
    }
279
    case ADD_DEPENDENCY: {
280
        const {key, value} = action;
1✔
281
        return set(`dependencies[${key}]`, value, state);
1✔
282
    }
283
    case REMOVE_DEPENDENCY: {
284
        const {key} = action;
1✔
285
        return set(`dependencies[${key}]`, null, state);
1✔
286
    }
287
    case LOAD_DEPENDENCIES:
288
        const {dependencies} = action;
1✔
289
        return set(`dependencies`, dependencies, state);
1✔
290
    case RESET_DEPENDENCIES:
291
        return set('dependencies', emptyState.dependencies, state);
1✔
292
    case TOGGLE_COLLAPSE: {
293
        /*
294
             * Collapse functionality has been implemented keeping the widget unchanged, adding it's layout is added to a map of collapsed objects.
295
             * The widgets plugin filters out the collapsed widget from the widgets list to render
296
             * So the containers triggers a layout change that removes the layout.
297
             * So when we want to expand again the widget, we have to restore the original layout settings.
298
             * This allows to save (and restore) collapsed state of the widgets in one unique separated object.
299
             */
300
        const {widget = {}} = action;
5!
301

302
        // locked widgets can not be collapsed
303
        if (widget.dataGrid && widget.dataGrid.static) {
5✔
304
            return state;
1✔
305
        }
306
        const widgetCollapsedState = get(state, `containers[${action.target}].collapsed[${widget.id}`);
4✔
307
        if (widgetCollapsedState) {
4✔
308
            // EXPAND
309

310
            const newLayoutValue = [
1✔
311
                ...get(state, `containers[${action.target}].layout`, []),
312
                ...castArray(
313
                    get(widgetCollapsedState, `layout`, [])
314
                ) // add stored old layout, if exists
315
            ];
316
            const updatedLayoutsMap = mapValues(
1✔
317
                get(state, `containers[${action.target}].layouts`, {}),
318
                (v = [], k) => ([
1!
319
                    ...v,
320
                    ...castArray(
321
                        get(widgetCollapsedState, `layouts[${k}]`, [])
322
                    )
323
                ])
324
            );
325
            return omit(
1✔
326
                compose(
327
                    // restore original layout for the widget
328
                    set(
329
                        `containers[${action.target}].layout`,
330
                        newLayoutValue
331
                    ),
332
                    // restore original layout for each break point
333
                    set(
334
                        `containers[${action.target}].layouts`,
335
                        updatedLayoutsMap
336
                    )
337
                    // restore original layout for each breakpoint (md, xs, ...) for the widget
338
                )(state),
339
                `containers[${action.target}].collapsed[${widget.id}]`);
340
        }
341

342
        return set(`containers[${action.target}].collapsed[${widget.id}]`, {
3✔
343
            // COLLAPSE
344

345
            // NOTE: when the collapse is toggled, the widget is not visible anymore
346
            // because it is filtered out from the view ( by the selector)
347
            // this causes a second action CHANGE_LAYOUT, automatically triggered
348
            // by react-grid-layout that removes the layout objects from the
349
            // `layout` and `layouts` state parts
350

351
            // get layout object for each k for the widget
352
            layout: find(
353
                get(state, `containers[${action.target}].layout`, []),
354
                { i: widget.id }
355
            ),
356
            // get layout object for each breakpoint (md, xs...) for the widget
357
            layouts: mapValues(
358
                get(state, `containers[${action.target}].layouts`, {}),
359
                v => find(v, {i: widget.id})
3✔
360
            )
361
        }, state);
362
    }
363
    case TOGGLE_MAXIMIZE: {
364
        const widget = action.widget;
3✔
365
        const maximized = state?.containers?.[action.target]?.maximized;
3✔
366

367
        if (!widget || widget.dataGrid?.static) {
3✔
368
            return state;
1✔
369
        }
370

371
        if (maximized?.widget) {
2✔
372
            return compose(
1✔
373
                set(`containers[${action.target}].layout`, maximized.layout),
374
                set(`containers[${action.target}].layouts`, maximized.layouts),
375
                set(`containers[${action.target}].maximized`, {}),
376
                set(`containers[${action.target}].widgets`, state?.containers?.[action.target]?.widgets?.map(w => w.id === maximized.widget.id ?
3✔
377
                    {
378
                        ...w,
379
                        dataGrid: {
380
                            ...w.dataGrid,
381
                            isDraggable: true,
382
                            isResizable: true
383
                        }
384
                    } : w)
385
                )
386
            )(state);
387
        }
388

389
        if (state?.containers?.[action.target]?.collapsed?.[widget.id]) {
1!
390
            return state;
×
391
        }
392

393
        // we assume that react-grid-layout has just one cell with one xxs breakpoint at 0, that is covering
394
        // the area that is supposed to be taken by maximized widget, when maximized state is present
395
        const newLayoutValues = {
1✔
396
            x: 0,
397
            y: 0,
398
            w: 1,
399
            h: 1
400
        };
401
        const oldLayoutValue = find(state?.containers?.[action.target]?.layout, {i: widget.id});
1✔
402
        const newLayoutValue = {
1✔
403
            ...oldLayoutValue,
404
            ...newLayoutValues
405
        };
406

407
        return compose(
1✔
408
            set(`containers[${action.target}].maximized`, {
409
                widget,
410
                layout: state?.containers?.[action.target]?.layout,
411
                layouts: state?.containers?.[action.target]?.layouts
412
            }),
413
            set(`containers[${action.target}].layout`, [newLayoutValue]),
414
            set(`containers[${action.target}].layouts`, {
415
                xxs: [newLayoutValue]
416
            }),
417
            set(`containers[${action.target}].widgets`, state?.containers?.[action.target]?.widgets?.map(w => w.id === widget.id ?
3✔
418
                {
419
                    ...w,
420
                    dataGrid: {
421
                        ...w.dataGrid,
422
                        isDraggable: false,
423
                        isResizable: false
424
                    }
425
                } : w)
426
            )
427
        )(state);
428
    }
429
    case TOGGLE_COLLAPSE_ALL: {
430
        // get widgets excluding static widgets
431
        const widgetsStatic = get(state, `containers[${action.target}].widgets`, [])
1✔
432
            .filter( w => !w.dataGrid || !w.dataGrid.static );
3✔
433
        const collapsedWidgets = widgetsStatic.filter(w => get(state, `containers[${action.target}].collapsed[${w.id}]`));
2✔
434
        const expandedWidgets = widgetsStatic.filter(w => !get(state, `containers[${action.target}].collapsed[${w.id}]`));
2✔
435
        const shouldExpandAll = expandedWidgets.length === 0;
1✔
436
        if (shouldExpandAll) {
1!
437
            return collapsedWidgets.reduce((acc, w) => widgetsReducer(
×
438
                acc,
439
                toggleCollapse(w)
440
            ), state);
441
        } else if (expandedWidgets.length > 0) {
1!
442
            return expandedWidgets.reduce((acc, w) => widgetsReducer(
2✔
443
                acc,
444
                toggleCollapse(w)
445
            ), state);
446
        }
447
        return state;
×
448
    }
449
    case TOGGLE_TRAY: {
450
        return set('tray', action.value, state);
2✔
451
    }
452
    default:
453
        return state;
3✔
454
    }
455
}
456

457
export default widgetsReducer;
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