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

geosolutions-it / MapStore2 / 12669623181

08 Jan 2025 11:25AM UTC coverage: 76.812% (-0.3%) from 77.108%
12669623181

Pull #10731

github

web-flow
Merge f1474727f into d1ebec1ff
Pull Request #10731: Fix #10631 New MapStore Home Page

30930 of 48379 branches covered (63.93%)

827 of 1286 new or added lines in 70 files covered. (64.31%)

161 existing lines in 14 files now uncovered.

38448 of 50055 relevant lines covered (76.81%)

34.24 hits per line

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

53.4
/web/client/plugins/SidebarMenu.jsx
1
/*
2
 * Copyright 2022, 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
import React from 'react';
9

10
import PropTypes from 'prop-types';
11
import ContainerDimensions from 'react-container-dimensions';
12
import {DropdownButton, Glyphicon, MenuItem} from "react-bootstrap";
13
import {connect} from "react-redux";
14
import {createSelector} from "reselect";
15
import {bindActionCreators} from "redux";
16

17
import ToolsContainer from "./containers/ToolsContainer";
18
import SidebarElement from "../components/sidebarmenu/SidebarElement";
19
import {mapLayoutValuesSelector} from "../selectors/maplayout";
20
import tooltip from "../components/misc/enhancers/tooltip";
21
import {setControlProperty} from "../actions/controls";
22
import {createPlugin} from "../utils/PluginsUtils";
23
import sidebarMenuReducer from "../reducers/sidebarmenu";
24

25
import './sidebarmenu/sidebarmenu.less';
26
import {lastActiveToolSelector, sidebarIsActiveSelector, isSidebarWithFullHeight} from "../selectors/sidebarmenu";
27
import {setLastActiveItem} from "../actions/sidebarmenu";
28
import Message from "../components/I18N/Message";
29
import { ButtonWithTooltip } from '../components/misc/Button';
30

31
const TDropdownButton = tooltip(DropdownButton);
1✔
32

33
function SidebarMenuItem({
34
    active,
35
    onClick,
36
    menuItem,
37
    glyph,
38
    labelId
39
}) {
NEW
40
    return menuItem
×
41
        ? (
42
            <MenuItem
43
                active={active}
NEW
44
                onClick={() => onClick(!active)}
×
45
            >
46
                <Glyphicon glyph={glyph}/><Message msgId={labelId}/>
47
            </MenuItem>
48
        )
49
        : (
50
            <ButtonWithTooltip
51
                className="square-button"
52
                bsStyle={active ? 'primary' : 'tray'}
×
53
                active={active}
NEW
54
                onClick={() => onClick(!active)}
×
55
                tooltipId={labelId}
56
                tooltipPosition="left"
57
            >
58
                <Glyphicon glyph={glyph}/>
59
            </ButtonWithTooltip>
60
        );
61
}
62

63
class SidebarMenu extends React.Component {
64
    static propTypes = {
1✔
65
        className: PropTypes.string,
66
        style: PropTypes.object,
67
        items: PropTypes.array,
68
        id: PropTypes.string,
69
        onInit: PropTypes.func,
70
        onDetach: PropTypes.func,
71
        sidebarWidth: PropTypes.number,
72
        state: PropTypes.object,
73
        setLastActiveItem: PropTypes.func,
74
        isSidebarFullHeight: PropTypes.bool
75
    };
76

77
    static contextTypes = {
1✔
78
        messages: PropTypes.object,
79
        router: PropTypes.object
80
    };
81

82
    static defaultProps = {
1✔
83
        items: [],
84
        style: {},
85
        id: "mapstore-sidebar-menu",
86
        onInit: () => {},
87
        onDetach: () => {},
88
        eventSelector: "onClick",
89
        toolStyle: "default",
90
        activeStyle: "primary",
91
        stateSelector: 'sidebarMenu',
92
        tool: SidebarElement,
93
        toolCfg: {},
94
        sidebarWidth: 40,
95
        isSidebarFullHeight: false
96
    };
97

98
    constructor() {
99
        super();
5✔
100
        this.defaultTool = SidebarElement;
5✔
101
        this.defaultTarget = 'sidebar';
5✔
102
        this.state = {
5✔
103
            lastVisible: false,
104
            hidden: false
105
        };
106
    }
107

108
    componentDidMount() {
109
        const { onInit } = this.props;
5✔
110
        onInit();
5✔
111
    }
112

113
    shouldComponentUpdate(nextProps) {
114
        const markedAsInactive = nextProps.isActive === false;
5✔
115
        const newSize = nextProps.state.map?.present?.size?.height !== this.props.state.map?.present?.size?.height;
5✔
116
        const newHeight = nextProps.style.bottom !== this.props.style.bottom;
5✔
117
        const newItems = nextProps.items !== this.props.items;
5✔
118
        const burgerMenuState = nextProps.state?.controls?.burgermenu?.enabled !== this.props.state?.controls?.burgermenu?.enabled;
5✔
119
        const newVisibleItems = !newItems ? nextProps.items.reduce((prev, cur, idx) => {
5!
120
            if (this.isNotHidden(cur, nextProps.state) !== this.isNotHidden(this.props.items[idx], this.props.state)) {
7!
121
                prev.push(cur);
×
122
            }
123
            return prev;
7✔
124
        }, []).length > 0 : false;
125
        const nextIsSideBarFullHeight = !this.props.isSidebarFullHeight && nextProps.isSidebarFullHeight;
5✔
126
        return newSize || newItems || newVisibleItems || newHeight || burgerMenuState || markedAsInactive || nextIsSideBarFullHeight;
5✔
127
    }
128

129
    componentDidUpdate(prevProps) {
130
        const { onInit, onDetach } = this.props;
×
131
        const { hidden } = this.state;
×
132
        const visibleElements = this.visibleItems('sidebar').length;
×
133
        visibleElements && prevProps.isActive === false && onInit();
×
134

135
        if (visibleElements === 0 && !hidden) {
×
136
            onDetach();
×
137
            this.setState((state) => ({ ...state, hidden: true}));
×
138
        } else if (visibleElements > 0 && hidden) {
×
139
            onInit();
×
140
            this.setState((state) => ({ ...state, hidden: false}));
×
141
        }
142
    }
143

144
    componentWillUnmount() {
145
        const { onDetach } = this.props;
5✔
146
        onDetach();
5✔
147
    }
148

149
    getStyle = (style) => {
5✔
150
        const hasBottomOffset = style?.dockSize > 0;
5✔
151
        return { ...style, height: hasBottomOffset ? 'auto' : '100%', maxHeight: style?.height ?? null, bottom: hasBottomOffset ? `calc(${style.dockSize}vh + 30px)` : null };
5!
152
    };
153

154
    getPanels = () => {
5✔
155
        return this.props.items.filter((item) => item.tools).reduce((previous, current) => {
7✔
156
            return previous.concat(
3✔
157
                current.tools.map((tool, index) => ({
9✔
158
                    name: current.name + index,
159
                    panel: tool,
160
                    cfg: current?.cfg?.toolsCfg?.[index] || {}
18✔
161
                }))
162
            );
163
        }, []);
164

165
    };
166

167
    visibleItems = (target) => {
5✔
168
        return this.props.items.reduce(( prev, current) => {
5✔
169
            if (!current?.components && this.targetMatch(target, current.target)
7!
170
                && this.isNotHidden(current, this.props.state)
171
            ) {
172
                prev.push({
7✔
173
                    ...current,
174
                    target
175
                });
176
                return prev;
7✔
177
            }
178
            if (current?.components && Array.isArray(current.components)) {
×
179
                current.components.forEach((component) => {
×
180
                    if (this.targetMatch(target, component?.target)
×
181
                        && this.isNotHidden(component?.selector ? component : current, this.props.state)
×
182
                    ) {
183
                        prev.push({
×
184
                            plugin: current?.plugin || this.defaultTool,
×
185
                            position: current?.position,
186
                            cfg: current?.cfg,
187
                            name: current.name,
188
                            help: current?.help,
189
                            items: current?.items,
190
                            ...component
191
                        });
192
                    }
193
                    return prev;
×
194
                });
195
            }
196
            return prev;
×
197
        }, []);
198
    }
199

200
    getItems = (_target, height) => {
5✔
201
        const itemsToRender = Math.floor(height / this.props.sidebarWidth) - 1;
5✔
202
        const target = _target ? _target : this.defaultTarget;
5!
203
        const filtered = this.visibleItems(target);
5✔
204

205
        if (itemsToRender < filtered.length) {
5!
206
            const sorted = filtered.sort((i1, i2) => (i1.position ?? 0) - (i2.position ?? 0));
×
207
            this.swapLastActiveItem(sorted, itemsToRender);
×
208
            const toRender = sorted.slice(0, itemsToRender);
×
209
            const extra = {
×
210
                name: "moreItems",
211
                position: 9999,
212
                icon: <Glyphicon glyph="option-horizontal" />,
213
                tool: () => this.renderExtraItems(filtered.slice(itemsToRender)),
×
214
                priority: 1
215
            };
216
            toRender.splice(itemsToRender, 0, extra);
×
217
            return toRender;
×
218
        }
219

220
        return filtered.sort((i1, i2) => (i1.position ?? 0) - (i2.position ?? 0));
5!
221
    };
222

223
    targetMatch = (target, elementTarget) => elementTarget === target || !elementTarget && target === this.defaultTarget;
7✔
224

225
    getTools = (namespace = 'sidebar', height) => {
5!
226
        return this.getItems(namespace, height).sort((a, b) => a.position - b.position);
5✔
227
    };
228

229
    renderExtraItems = (items) => {
5✔
230
        const dummySelector = () => ({});
×
231
        const menuItems = items.map((item) => {
×
232
            if (item.tool) {
×
233
                const CustomMenuItem = item.tool;
×
234
                return <CustomMenuItem key={item.name} menuItem />;
×
235
            }
236
            const ConnectedItem = connect((item?.selector ?? dummySelector),
×
237
                (dispatch, ownProps) => {
238
                    const actions = {};
×
239
                    if (ownProps.action) {
×
240
                        actions.onClick = () => {
×
241
                            this.props.setLastActiveItem(item?.name ?? item?.toggleProperty);
×
242
                            bindActionCreators(ownProps.action, dispatch)();
×
243
                        };
244
                    }
245
                    return actions;
×
246
                })(MenuItem);
247
            return <ConnectedItem action={item.action}>{item?.icon}{item?.text}</ConnectedItem>;
×
248
        });
249
        return (
×
250
            <TDropdownButton
251
                dropup
252
                pullRight
253
                bsStyle="tray"
254
                id="extra-items"
255
                tooltip={<Message msgId="sidebarMenu.showMoreItems" />}
256
                tooltipPosition="left"
257
                title={<Glyphicon glyph="option-horizontal" />}
258
            >
259
                {menuItems}
260
            </TDropdownButton>);
261
    };
262

263
    render() {
264
        return this.state.hidden ? false : (
5!
265
            <div id="mapstore-sidebar-menu-container" className={`shadow-soft ${this.props.isSidebarFullHeight ? "fullHeightSideBar" : ""}`} style={this.getStyle(this.props.style)}>
5!
266
                <ContainerDimensions>
267
                    { ({ height }) =>
268
                        <ToolsContainer id={this.props.id}
5✔
269
                            className={this.props.className}
270
                            container={(props) => <>{props.children}</>}
5✔
271
                            toolStyle="tray"
272
                            activeStyle="primary"
273
                            stateSelector="sidebarMenu"
274
                            tool={SidebarElement}
275
                            tools={this.getTools('sidebar', height)}
276
                            panels={this.getPanels(this.props.items)}
277
                            toolComponent={SidebarMenuItem}
278
                        /> }
279
                </ContainerDimensions>
280
            </div>
281

282
        );
283
    }
284

285
    swapLastActiveItem = (items, itemsToRender) => {
5✔
286
        const name = this.props.lastActiveTool;
×
287
        if (name) {
×
288
            const idx = items.findIndex((el) => el?.name === name || el?.toggleProperty === name);
×
289
            if (idx !== -1 && idx > (itemsToRender - 1)) {
×
290
                const item = items[idx];
×
291
                items[idx] = items[itemsToRender - 1];
×
292
                items[itemsToRender - 2] = item;
×
293
            }
294
        }
295
    }
296

297

298
    isNotHidden = (element, state) => {
5✔
299
        return element?.selector ? element.selector(state)?.style?.display !== 'none' : true;
21✔
300
    };
301
}
302

303
const sidebarMenuSelector = createSelector([
1✔
304
    state => state,
10✔
305
    state => lastActiveToolSelector(state),
10✔
306
    state => mapLayoutValuesSelector(state, {dockSize: true, bottom: true, height: true}),
10✔
307
    sidebarIsActiveSelector,
308
    isSidebarWithFullHeight
309
], (state, lastActiveTool, style, isActive, isSidebarFullHeight ) => ({
10✔
310
    style,
311
    lastActiveTool,
312
    state,
313
    isActive,
314
    isSidebarFullHeight
315
}));
316

317
/**
318
 * Generic bar that can contains other plugins.
319
 * used by {@link #plugins.Login|Login}, {@link #plugins.Home|Home},
320
 * {@link #plugins.Login|Login} and many other, on map viewer pages.
321
 * @name SidebarMenu
322
 * @class
323
 * @memberof plugins
324
 */
325
export default createPlugin(
326
    'SidebarMenu',
327
    {
328
        cfg: {},
329
        component: connect(sidebarMenuSelector, {
330
            onInit: setControlProperty.bind(null, 'sidebarMenu', 'enabled', true),
331
            onDetach: setControlProperty.bind(null, 'sidebarMenu', 'enabled', false),
332
            setLastActiveItem
333
        })(SidebarMenu),
334
        reducers: {
335
            sidebarmenu: sidebarMenuReducer
336
        }
337
    }
338
);
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

© 2026 Coveralls, Inc