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

geosolutions-it / MapStore2 / 14217089396

02 Apr 2025 10:11AM UTC coverage: 92.673% (+15.7%) from 76.984%
14217089396

push

github

web-flow
Fix #10819 Manager menu is merged into Login/user menu and Manager menu is now deprecated  (#10963)

245 of 385 branches covered (63.64%)

66 of 77 new or added lines in 3 files covered. (85.71%)

18 existing lines in 5 files now uncovered.

2011 of 2170 relevant lines covered (92.67%)

825.05 hits per line

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

51.46
/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
    className
40
}) {
41
    return menuItem
×
42
        ? (
43
            <MenuItem
44
                active={active}
45
                className={className}
46
                onClick={() => onClick(!active)}
×
47
            >
48
                <Glyphicon glyph={glyph}/><Message msgId={labelId}/>
49
            </MenuItem>
50
        )
51
        : (
52
            <ButtonWithTooltip
53
                className={`square-button${className ? ` ${className}` : ''}`}
×
54
                bsStyle={active ? 'primary' : 'tray'}
×
55
                active={active}
56
                onClick={() => onClick(!active)}
×
57
                tooltipId={labelId}
58
                tooltipPosition="left"
59
            >
60
                <Glyphicon glyph={glyph}/>
61
            </ButtonWithTooltip>
62
        );
63
}
64

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

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

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

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

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

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

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

137
        if (visibleElements === 0 && !hidden) {
×
138
            onDetach();
×
139
            this.setState((state) => ({ ...state, hidden: true})); // eslint-disable-line -- need to be fixed
×
140
        } else if (visibleElements > 0 && hidden) {
×
141
            onInit();
×
142
            this.setState((state) => ({ ...state, hidden: false})); // eslint-disable-line -- need to be fixed
×
143
        }
144
    }
145

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

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

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

167
    };
168

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

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

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

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

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

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

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

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

284
        );
285
    }
286

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

299

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

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

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