diff --git a/web/client/components/misc/enhancers/tooltip.jsx b/web/client/components/misc/enhancers/tooltip.jsx
index 8c71b15503..3a6403c194 100644
--- a/web/client/components/misc/enhancers/tooltip.jsx
+++ b/web/client/components/misc/enhancers/tooltip.jsx
@@ -39,5 +39,5 @@ export default branch(
placement={tooltipPosition}
overlay={
{tooltipId ? : tooltip}}>
),
// avoid to pass non needed props
- (Wrapped) => (props) =>
{props.children}
+ (Wrapped) => (props) =>
{props.children}
);
diff --git a/web/client/components/misc/panels/DockContainer.jsx b/web/client/components/misc/panels/DockContainer.jsx
new file mode 100644
index 0000000000..bc14197160
--- /dev/null
+++ b/web/client/components/misc/panels/DockContainer.jsx
@@ -0,0 +1,33 @@
+import React from 'react';
+import classnames from "classnames";
+
+/**
+ * Wrapper for DockablePanel with main intension to support applying of custom styling to make dock panels have proper
+ * offset depending on the sidebars presence on the page
+ * @memberof components.misc.panels
+ * @name DockContainer
+ * @param id {string} - id applied to the container
+ * @param children {JSX.Element}
+ * @param dockStyle {object} - style object obtained from mapLayoutValuesSelector and used to calculate offsets
+ * @param className {string} - class name
+ * @param style - style object to apply to the container. Can be used to overwrite styles applied by dockStyle calculations
+ * @returns {JSX.Element}
+ * @constructor
+ */
+const DockContainer = ({ id, children, dockStyle, className, style = {}}) => {
+ const persistentStyle = {
+ width: `calc(100% - ${(dockStyle?.right ?? 0) + (dockStyle?.left ?? 0)}px)`,
+ transform: `translateX(-${(dockStyle?.right ?? 0)}px)`,
+ pointerEvents: 'none'
+ };
+ return (
+
+ {children}
+
+ );
+};
+
+export default DockContainer;
diff --git a/web/client/components/misc/panels/ResponsivePanel.jsx b/web/client/components/misc/panels/ResponsivePanel.jsx
new file mode 100644
index 0000000000..590b97f3d1
--- /dev/null
+++ b/web/client/components/misc/panels/ResponsivePanel.jsx
@@ -0,0 +1,61 @@
+/*
+ * Copyright 2022, GeoSolutions Sas.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree.
+*/
+
+import DockContainer from "./DockContainer";
+import ContainerDimensions from "react-container-dimensions";
+import DockablePanel from "./DockablePanel";
+import React from "react";
+
+/**
+ * Component for rendering DockPanel that supposed to:
+ * - Get dynamic width if panel cannot fit to the screen width. It will be automatically resized to the window width.
+ * - Get proper offsets based on the current map layout: with or without sidebar
+ * @memberof components.misc.panels
+ * @name ResponsivePanel
+ * @param {boolean} dock - rendered as a dock if true, otherwise rendered as a modal window
+ * @param {object} containerStyle - style object to be applied to the DockContainer.
+ * @param {string} containerClassName - class name to be applied to the DockContainer.
+ * @param {string} containerId - id to be applied to the DockContainer.
+ * @param {number} size - maximum width of the dock panel.
+ * @param {JSX.Element} children - components to be rendered inside the dock panel.
+ * @param {JSX.Element} siblings - components to be rendered inside container after dock panel.
+ * @param {object} panelProps - props that will be applied to the DockablePanel component.
+ * @returns {JSX.Element}
+ */
+export default ({
+ children,
+ containerClassName,
+ containerId,
+ containerStyle,
+ dock = true,
+ siblings,
+ size,
+ ...panelProps}) => {
+ return (
+
+
+ {({ width }) => (
+ <>
+ 1 ? width : size}
+ {...panelProps}
+ >
+ { children }
+
+ { siblings }
+ >
+ )}
+
+
+ );
+};
diff --git a/web/client/components/misc/panels/__tests__/DockContainer-test.jsx b/web/client/components/misc/panels/__tests__/DockContainer-test.jsx
new file mode 100644
index 0000000000..84511838e4
--- /dev/null
+++ b/web/client/components/misc/panels/__tests__/DockContainer-test.jsx
@@ -0,0 +1,31 @@
+/*
+ * Copyright 2022, GeoSolutions Sas.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree.
+ */
+
+import expect from 'expect';
+import React from 'react';
+import ReactDOM from 'react-dom';
+import DockContainer from '../DockContainer';
+
+describe("test DockContainer", () => {
+ beforeEach((done) => {
+ document.body.innerHTML = '
';
+ setTimeout(done);
+ });
+
+ afterEach((done) => {
+ ReactDOM.unmountComponentAtNode(document.getElementById("container"));
+ document.body.innerHTML = '';
+ setTimeout(done);
+ });
+
+ it('test rendering', () => {
+ ReactDOM.render(
, document.getElementById("container"));
+ const domComp = document.getElementsByClassName('dock-container')[0];
+ expect(domComp).toExist();
+ });
+});
diff --git a/web/client/components/misc/spinners/GlobalSpinner/css/GlobalSpinner.css b/web/client/components/misc/spinners/GlobalSpinner/css/GlobalSpinner.css
index 02544671b1..fd4f1eff55 100644
--- a/web/client/components/misc/spinners/GlobalSpinner/css/GlobalSpinner.css
+++ b/web/client/components/misc/spinners/GlobalSpinner/css/GlobalSpinner.css
@@ -3,4 +3,10 @@
width: 40px !important;
position:static !important;
border-radius: 0 !important;
+ display: flex;
+ justify-content: center;
+ align-items: center;
+}
+#mapstore-globalspinner > .spinner {
+ margin: 0;
}
diff --git a/web/client/components/plugins/PluginsContainer.jsx b/web/client/components/plugins/PluginsContainer.jsx
index 9f89975c15..5136b15e3c 100644
--- a/web/client/components/plugins/PluginsContainer.jsx
+++ b/web/client/components/plugins/PluginsContainer.jsx
@@ -171,7 +171,7 @@ class PluginsContainer extends React.Component {
filterLoaded = (plugin) => plugin && !plugin.impl.loadPlugin;
filterRoot = (plugin) => {
const container = PluginsUtils.getMorePrioritizedContainer(
- plugin.impl,
+ plugin,
PluginsUtils.getPluginConfiguration(this.getPluginsConfig(this.props), plugin.name).override,
this.getPluginsConfig(this.props),
0
diff --git a/web/client/components/security/UserMenu.jsx b/web/client/components/security/UserMenu.jsx
index 6a6b43d560..421ccd56ca 100644
--- a/web/client/components/security/UserMenu.jsx
+++ b/web/client/components/security/UserMenu.jsx
@@ -29,11 +29,14 @@ class UserMenu extends React.Component {
showAccountInfo: PropTypes.bool,
showPasswordChange: PropTypes.bool,
showLogout: PropTypes.bool,
+ hidden: PropTypes.bool,
+ displayUnsavedDialog: PropTypes.bool,
/**
* displayAttributes function to filter attributes to show
*/
displayAttributes: PropTypes.func,
bsStyle: PropTypes.string,
+ tooltipPosition: PropTypes.string,
renderButtonText: PropTypes.bool,
nav: PropTypes.bool,
menuProps: PropTypes.object,
@@ -48,18 +51,21 @@ class UserMenu extends React.Component {
onCheckMapChanges: PropTypes.func,
className: PropTypes.string,
renderUnsavedMapChangesDialog: PropTypes.bool,
- onLogoutConfirm: PropTypes.func
+ onLogoutConfirm: PropTypes.func,
+ onCloseUnsavedDialog: PropTypes.func
};
static defaultProps = {
user: {
},
+ tooltipPosition: 'bottom',
showAccountInfo: true,
showPasswordChange: true,
showLogout: true,
onLogout: () => {},
onCheckMapChanges: () => {},
onPasswordChange: () => {},
+ onCloseUnsavedDialog: () => {},
displayName: "name",
bsStyle: "primary",
displayAttributes: (attr) => {
@@ -85,22 +91,12 @@ class UserMenu extends React.Component {
useModal: false,
closeGlyph: "1-close"
}],
- renderUnsavedMapChangesDialog: true
+ renderUnsavedMapChangesDialog: true,
+ renderButtonText: false,
+ hidden: false,
+ displayUnsavedDialog: true
};
- checkUnsavedChanges = () => {
- if (this.props.renderUnsavedMapChangesDialog) {
- this.props.onCheckMapChanges(this.props.onLogout);
- } else {
- this.logout();
- }
- }
-
- logout = () => {
- this.props.onCloseUnsavedDialog();
- this.props.onLogout();
- }
-
renderGuestTools = () => {
let DropDown = this.props.nav ? TNavDropdown : TDropdownButton;
return (
@@ -111,7 +107,7 @@ class UserMenu extends React.Component {
title={this.renderButtonText()}
id="dropdown-basic-primary"
tooltipId="user.login"
- tooltipPosition="bottom"
+ tooltipPosition={this.props.tooltipPosition}
{...this.props.menuProps}>
);
@@ -141,7 +137,7 @@ class UserMenu extends React.Component {
bsStyle="success"
title={this.renderButtonText()}
tooltipId="user.userMenu"
- tooltipPosition="bottom"
+ tooltipPosition={this.props.tooltipPosition}
{...this.props.menuProps}
>
@@ -174,13 +170,27 @@ class UserMenu extends React.Component {
renderButtonText = () => {
return this.props.renderButtonContent ?
- this.props.renderButtonContent() :
+ this.props.renderButtonContent(this.props) :
[
, this.props.renderButtonText ? this.props.user && this.props.user[this.props.displayName] || "Guest" : null];
};
render() {
+ if (this.props.hidden) return false;
return this.props.user && this.props.user[this.props.displayName] ? this.renderLoggedTools() : this.renderGuestTools();
}
+
+ logout = () => {
+ this.props.onCloseUnsavedDialog();
+ this.props.onLogout();
+ }
+
+ checkUnsavedChanges = () => {
+ if (this.props.renderUnsavedMapChangesDialog) {
+ this.props.onCheckMapChanges(this.props.onLogout);
+ } else {
+ this.logout();
+ }
+ }
}
export default UserMenu;
diff --git a/web/client/components/sidebarmenu/SidebarElement.jsx b/web/client/components/sidebarmenu/SidebarElement.jsx
new file mode 100644
index 0000000000..4fe340e577
--- /dev/null
+++ b/web/client/components/sidebarmenu/SidebarElement.jsx
@@ -0,0 +1,22 @@
+import tooltip from "../misc/enhancers/tooltip";
+import {Button} from "react-bootstrap";
+import React from "react";
+import classnames from "classnames";
+import {omit} from "lodash";
+
+const TooltipButton = tooltip(Button);
+
+
+const Container = ({children, className, bsStyle = 'link', tooltipId, tooltipPosition = 'left', ...props}) => (
+
+ {children}
+
+);
+
+export default Container;
diff --git a/web/client/components/sidebarmenu/__tests__/SidebarElement-test.jsx b/web/client/components/sidebarmenu/__tests__/SidebarElement-test.jsx
new file mode 100644
index 0000000000..af5990ea9b
--- /dev/null
+++ b/web/client/components/sidebarmenu/__tests__/SidebarElement-test.jsx
@@ -0,0 +1,38 @@
+/**
+ * Copyright 2022, GeoSolutions Sas.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree.
+ */
+
+import expect from 'expect';
+import React from 'react';
+import ReactDOM from 'react-dom';
+import SidebarElement from "../SidebarElement";
+
+describe("The SidebarElement component", () => {
+ beforeEach((done) => {
+ document.body.innerHTML = '
';
+ setTimeout(done);
+ });
+
+ afterEach((done) => {
+ ReactDOM.unmountComponentAtNode(document.getElementById("container"));
+ document.body.innerHTML = '';
+ setTimeout(done);
+ });
+
+ it('is created with defaults', () => {
+ ReactDOM.render(
, document.getElementById("container"));
+ const domComp = document.getElementsByClassName('btn')[0];
+ expect(domComp).toExist();
+
+ });
+
+ it('should have proper style', () => {
+ ReactDOM.render(
, document.getElementById("container"));
+ const domComp = document.getElementsByClassName('btn-primary')[0];
+ expect(domComp).toExist();
+ });
+});
diff --git a/web/client/configs/localConfig.json b/web/client/configs/localConfig.json
index fa5936dc15..6f81cce85d 100644
--- a/web/client/configs/localConfig.json
+++ b/web/client/configs/localConfig.json
@@ -378,6 +378,7 @@
}
}
},
+ "SidebarMenu",
"FilterLayer",
"AddGroup",
"TOCItemsSettings",
@@ -474,7 +475,7 @@
"declineUrl" : "http://www.google.com"
}
},
- "OmniBar", "Login", "Save", "SaveAs", "BurgerMenu", "Expander", "Undo", "Redo", "FullScreen", "GlobeViewSwitcher", "SearchServicesConfig", "SearchByBookmark", "WidgetsBuilder", "Widgets",
+ "OmniBar", "Login", "Save", "SaveAs", "Expander", "Undo", "Redo", "FullScreen", "GlobeViewSwitcher", "SearchServicesConfig", "SearchByBookmark", "WidgetsBuilder", "Widgets",
"WidgetsTray",
"Timeline",
"Playback",
diff --git a/web/client/configs/pluginsConfig.json b/web/client/configs/pluginsConfig.json
index daadfeaeba..eda0ef0143 100644
--- a/web/client/configs/pluginsConfig.json
+++ b/web/client/configs/pluginsConfig.json
@@ -93,7 +93,7 @@
"title": "plugins.TOCItemsSettings.title",
"description": "plugins.TOCItemsSettings.description",
"children": [
- "StyleEditor"
+ "StyleEditor"
]
},
{
@@ -142,7 +142,7 @@
"title": "plugins.HelpLink.title",
"description": "plugins.HelpLink.description",
"dependencies": [
- "BurgerMenu"
+ "SidebarMenu"
]
},
{
@@ -151,7 +151,7 @@
"title": "plugins.Share.title",
"description": "plugins.Share.description",
"dependencies": [
- "BurgerMenu"
+ "SidebarMenu"
]
},
{
@@ -171,7 +171,7 @@
"title": "plugins.Annotations.title",
"description": "plugins.Annotations.description",
"dependencies": [
- "BurgerMenu"
+ "SidebarMenu"
]
},
{
@@ -191,14 +191,19 @@
"glyph": "1-position-1",
"title": "plugins.Locate.title",
"description": "plugins.Locate.description",
- "dependencies": ["Toolbar"]
+ "dependencies": [
+ "Toolbar"
+ ]
},
{
"name": "Home",
"glyph": "home",
"title": "plugins.Home.title",
"description": "plugins.Home.description",
- "dependencies": ["OmniBar"]
+ "dependencies": [
+ "OmniBar",
+ "SidebarMenu"
+ ]
},
{
"name": "LayerDownload",
@@ -250,12 +255,15 @@
"glyph": "add-folder",
"title": "plugins.AddGroup.title",
"description": "plugins.AddGroup.description"
- }, {
+ },
+ {
"name": "FilterLayer",
"glyph": "filter-layer",
"title": "plugins.FilterLayer.title",
"description": "plugins.FilterLayer.description",
- "dependencies": ["QueryPanel"]
+ "dependencies": [
+ "QueryPanel"
+ ]
},
{
"name": "Tutorial",
@@ -269,7 +277,7 @@
"title": "plugins.Measure.title",
"description": "plugins.Measure.description",
"dependencies": [
- "BurgerMenu"
+ "SidebarMenu"
]
},
{
@@ -278,7 +286,7 @@
"title": "plugins.Print.title",
"description": "plugins.Print.description",
"dependencies": [
- "BurgerMenu"
+ "SidebarMenu"
]
},
{
@@ -287,7 +295,7 @@
"title": "plugins.MapCatalog.title",
"description": "plugins.MapCatalog.description",
"dependencies": [
- "BurgerMenu"
+ "SidebarMenu"
]
},
{
@@ -296,7 +304,7 @@
"title": "plugins.MapImport.title",
"description": "plugins.MapImport.description",
"dependencies": [
- "BurgerMenu"
+ "SidebarMenu"
]
},
{
@@ -305,7 +313,7 @@
"title": "plugins.MapExport.title",
"description": "plugins.MapExport.description",
"dependencies": [
- "BurgerMenu"
+ "SidebarMenu"
]
},
{
@@ -313,9 +321,11 @@
"glyph": "cog",
"title": "plugins.Settings.title",
"description": "plugins.Settings.description",
- "children": ["Version"],
+ "children": [
+ "Version"
+ ],
"dependencies": [
- "BurgerMenu"
+ "SidebarMenu"
],
"defaultConfig": {
"wrap": true
@@ -335,14 +345,18 @@
"glyph": "info-sign",
"title": "plugins.About.title",
"description": "plugins.About.description",
- "dependencies": ["BurgerMenu"]
+ "dependencies": [
+ "SidebarMenu"
+ ]
},
{
"name": "MousePosition",
"glyph": "mouse",
"title": "plugins.MousePosition.title",
"description": "plugins.MousePosition.description",
- "dependencies": ["MapFooter"],
+ "dependencies": [
+ "MapFooter"
+ ],
"defaultConfig": {
"editCRS": true,
"showLabels": true,
@@ -359,7 +373,9 @@
"glyph": "crs",
"title": "plugins.CRSSelector.title",
"description": "plugins.CRSSelector.description",
- "dependencies": ["MapFooter"],
+ "dependencies": [
+ "MapFooter"
+ ],
"defaultConfig": {
"additionalCRS": {},
"filterAllowedCRS": [
@@ -386,7 +402,9 @@
"OmniBar",
"SearchServicesConfig"
],
- "children": ["SearchByBookmark"],
+ "children": [
+ "SearchByBookmark"
+ ],
"defaultConfig": {
"withToggle": [
"max-width: 768px",
@@ -411,7 +429,9 @@
"name": "ScaleBox",
"title": "plugins.ScaleBox.title",
"description": "plugins.ScaleBox.description",
- "dependencies": ["MapFooter"]
+ "dependencies": [
+ "MapFooter"
+ ]
},
{
"name": "GlobeViewSwitcher",
@@ -465,7 +485,7 @@
"title": "plugins.Save.title",
"description": "plugins.Save.description",
"dependencies": [
- "BurgerMenu",
+ "SidebarMenu",
"SaveAs"
]
},
@@ -476,7 +496,7 @@
"hidden": true,
"description": "plugins.SaveAs.description",
"dependencies": [
- "BurgerMenu"
+ "SidebarMenu"
]
},
{
@@ -486,12 +506,11 @@
"hidden": true,
"description": "plugins.DeleteMap.description",
"dependencies": [
- "BurgerMenu"
+ "SidebarMenu"
]
},
{
"name": "BurgerMenu",
- "hidden": true,
"glyph": "menu-hamburger",
"title": "plugins.BurgerMenu.title",
"description": "plugins.BurgerMenu.description",
@@ -538,7 +557,9 @@
"glyph": "time",
"title": "plugins.Timeline.title",
"description": "plugins.Timeline.description",
- "dependencies": ["Playback"]
+ "dependencies": [
+ "Playback"
+ ]
},
{
"name": "Playback",
@@ -569,27 +590,40 @@
"name": "MapTemplates",
"title": "Map Templates",
"dependencies": [
- "BurgerMenu"
+ "SidebarMenu"
]
- }, {
+ },
+ {
"name": "UserExtensions",
"glyph": "1-user-add",
"title": "plugins.UserExtensions.title",
"hidden": true,
"description": "plugins.UserExtensions.description",
- "dependencies": ["BurgerMenu"]
- }, {
+ "dependencies": [
+ "SidebarMenu"
+ ]
+ },
+ {
"name": "UserSession",
"glyph": "floppy-save",
"title": "plugins.UserSession.title",
"description": "plugins.UserSession.description",
- "dependencies": ["BurgerMenu"]
- }, {
+ "dependencies": [
+ "SidebarMenu"
+ ]
+ },
+ {
"name": "StreetView",
"glyph": "road",
"title": "plugins.StreetView.title",
"description": "plugins.StreetView.description",
- "dependencies": ["Toolbar"]
+ "dependencies": [
+ "SidebarMenu"
+ ]
+ },
+ {
+ "name": "SidebarMenu",
+ "hidden": true
}
]
}
diff --git a/web/client/configs/simple.json b/web/client/configs/simple.json
index ad7f86b0e2..f427ce7c74 100644
--- a/web/client/configs/simple.json
+++ b/web/client/configs/simple.json
@@ -223,6 +223,8 @@
"OmniBar",
{
"name": "BurgerMenu"
+ }, {
+ "name": "SidebarMenu"
}, {
"name": "Expander"
}, {
diff --git a/web/client/epics/__tests__/annotations-test.js b/web/client/epics/__tests__/annotations-test.js
index aec48946fe..f898d0bfcd 100644
--- a/web/client/epics/__tests__/annotations-test.js
+++ b/web/client/epics/__tests__/annotations-test.js
@@ -47,7 +47,7 @@ import {
setEditingFeature
} from '../../actions/annotations';
-import { TOGGLE_CONTROL, toggleControl, SET_CONTROL_PROPERTY } from '../../actions/controls';
+import { toggleControl, SET_CONTROL_PROPERTY } from '../../actions/controls';
import { STYLE_POINT_MARKER } from '../../utils/AnnotationsUtils';
import annotationsEpics from '../annotations';
import { testEpic, addTimeoutEpic, TEST_TIMEOUT } from './epicTestUtils';
@@ -1156,7 +1156,7 @@ describe('annotations Epics', () => {
store.subscribe(() => {
const actions = store.getActions();
if (actions.length >= 2) {
- expect(actions[1].type).toBe(TOGGLE_CONTROL);
+ expect(actions[1].type).toBe(SET_CONTROL_PROPERTY);
expect(actions[1].control).toBe("measure");
done();
}
diff --git a/web/client/epics/__tests__/catalog-test.js b/web/client/epics/__tests__/catalog-test.js
index 73b2c6e7e0..8a9d98f648 100644
--- a/web/client/epics/__tests__/catalog-test.js
+++ b/web/client/epics/__tests__/catalog-test.js
@@ -21,7 +21,7 @@ const {
} = catalog(API);
import {SHOW_NOTIFICATION} from '../../actions/notifications';
import {CLOSE_FEATURE_GRID} from '../../actions/featuregrid';
-import {setControlProperty, SET_CONTROL_PROPERTY} from '../../actions/controls';
+import {SET_CONTROL_PROPERTY, toggleControl} from '../../actions/controls';
import {ADD_LAYER, CHANGE_LAYER_PROPERTIES, selectNode} from '../../actions/layers';
import {PURGE_MAPINFO_RESULTS, HIDE_MAPINFO_MARKER} from '../../actions/mapInfo';
import {testEpic, addTimeoutEpic, TEST_TIMEOUT} from './epicTestUtils';
@@ -96,13 +96,13 @@ describe('catalog Epics', () => {
});
it('openCatalogEpic', (done) => {
const NUM_ACTIONS = 3;
- testEpic(openCatalogEpic, NUM_ACTIONS, setControlProperty("metadataexplorer", "enabled", true), (actions) => {
+ testEpic(openCatalogEpic, NUM_ACTIONS, toggleControl("metadataexplorer", "enabled"), (actions) => {
expect(actions.length).toBe(NUM_ACTIONS);
expect(actions[0].type).toBe(CLOSE_FEATURE_GRID);
expect(actions[1].type).toBe(PURGE_MAPINFO_RESULTS);
expect(actions[2].type).toBe(HIDE_MAPINFO_MARKER);
done();
- }, { });
+ }, { controls: { metadataexplorer: { enabled: true } }});
});
it('recordSearchEpic with two layers', (done) => {
diff --git a/web/client/epics/__tests__/featuregrid-test.js b/web/client/epics/__tests__/featuregrid-test.js
index a7647d6ba2..3bade5170b 100644
--- a/web/client/epics/__tests__/featuregrid-test.js
+++ b/web/client/epics/__tests__/featuregrid-test.js
@@ -918,20 +918,48 @@ describe('featuregrid Epics', () => {
case SET_CONTROL_PROPERTY: {
switch (i) {
case 0: {
- expect(action.control).toBe('metadataexplorer');
+ expect(action.control).toBe('userExtensions');
expect(action.property).toBe('enabled');
expect(action.value).toBe(false);
expect(action.toggle).toBe(undefined);
break;
}
case 1: {
- expect(action.control).toBe('annotations');
+ expect(action.control).toBe('details');
expect(action.property).toBe('enabled');
expect(action.value).toBe(false);
expect(action.toggle).toBe(undefined);
break;
}
case 2: {
+ expect(action.control).toBe('mapTemplates');
+ expect(action.property).toBe('enabled');
+ expect(action.value).toBe(false);
+ expect(action.toggle).toBe(undefined);
+ break;
+ }
+ case 3: {
+ expect(action.control).toBe('mapCatalog');
+ expect(action.property).toBe('enabled');
+ expect(action.value).toBe(false);
+ expect(action.toggle).toBe(undefined);
+ break;
+ }
+ case 4: {
+ expect(action.control).toBe('metadataexplorer');
+ expect(action.property).toBe('enabled');
+ expect(action.value).toBe(false);
+ expect(action.toggle).toBe(undefined);
+ break;
+ }
+ case 5: {
+ expect(action.control).toBe('annotations');
+ expect(action.property).toBe('enabled');
+ expect(action.value).toBe(false);
+ expect(action.toggle).toBe(undefined);
+ break;
+ }
+ case 6: {
expect(action.control).toBe('details');
expect(action.property).toBe('enabled');
expect(action.value).toBe(false);
diff --git a/web/client/epics/__tests__/maplayout-test.js b/web/client/epics/__tests__/maplayout-test.js
index d1a9aba8db..0ef7fe8df1 100644
--- a/web/client/epics/__tests__/maplayout-test.js
+++ b/web/client/epics/__tests__/maplayout-test.js
@@ -26,11 +26,18 @@ describe('map layout epics', () => {
expect(actions.length).toBe(1);
actions.map((action) => {
expect(action.type).toBe(UPDATE_MAP_LAYOUT);
- expect(action.layout).toEqual({ left: 600, right: 658, bottom: 0, transform: 'none', height: 'calc(100% - 30px)', boundingMapRect: {
- bottom: 0,
- left: 600,
- right: 658
- }});
+ expect(action.layout).toEqual(
+ { left: 600, right: 548, bottom: 0, transform: 'none', height: 'calc(100% - 30px)',
+ boundingMapRect: {
+ bottom: 0,
+ left: 600,
+ right: 548
+ },
+ boundingSidebarRect: { right: 0, left: 0, bottom: 0 },
+ leftPanel: true,
+ rightPanel: true
+ }
+ );
});
} catch (e) {
done(e);
@@ -41,6 +48,34 @@ describe('map layout epics', () => {
testEpic(updateMapLayoutEpic, 1, toggleControl("queryPanel"), epicResult, state);
});
+ it('tests layout with sidebar', (done) => {
+ const epicResult = actions => {
+ try {
+ expect(actions.length).toBe(1);
+ actions.map((action) => {
+ expect(action.type).toBe(UPDATE_MAP_LAYOUT);
+ expect(action.layout).toEqual(
+ { left: 600, right: 588, bottom: 0, transform: 'none', height: 'calc(100% - 30px)',
+ boundingMapRect: {
+ bottom: 0,
+ left: 600,
+ right: 588
+ },
+ boundingSidebarRect: { right: 40, left: 0, bottom: 0 },
+ leftPanel: true,
+ rightPanel: true
+ }
+ );
+ });
+ } catch (e) {
+ done(e);
+ }
+ done();
+ };
+ const state = {controls: { metadataexplorer: {enabled: true}, queryPanel: {enabled: true}, sidebarMenu: {enabled: true} }};
+ testEpic(updateMapLayoutEpic, 1, toggleControl("queryPanel"), epicResult, state);
+ });
+
it('tests layout with prop', (done) => {
ConfigUtils.setConfigProp('mapLayout', {
left: { sm: 300, md: 500, lg: 600 },
@@ -53,11 +88,15 @@ describe('map layout epics', () => {
actions.map((action) => {
expect(action.type).toBe(UPDATE_MAP_LAYOUT);
expect(action.layout).toEqual({
- left: 600, right: 330, bottom: 0, transform: 'none', height: 'calc(100% - 120px)', boundingMapRect: {
+ left: 0, right: 330, bottom: 0, transform: 'none', height: 'calc(100% - 120px)',
+ boundingMapRect: {
bottom: 0,
- left: 600,
+ left: 0,
right: 330
- }
+ },
+ boundingSidebarRect: { right: 0, left: 0, bottom: 0 },
+ leftPanel: false,
+ rightPanel: true
});
});
} catch (e) {
@@ -65,7 +104,7 @@ describe('map layout epics', () => {
}
done();
};
- const state = { controls: { metadataexplorer: { enabled: true }, queryPanel: { enabled: true } } };
+ const state = { controls: { metadataexplorer: { enabled: true }, queryPanel: { enabled: false } } };
testEpic(updateMapLayoutEpic, 1, toggleControl("queryPanel"), epicResult, state);
});
@@ -128,11 +167,15 @@ describe('map layout epics', () => {
actions.map((action) => {
expect(action.type).toBe(UPDATE_MAP_LAYOUT);
expect(action.layout).toEqual({
- left: 512, right: 0, bottom: 0, transform: 'none', height: 'calc(100% - 30px)', boundingMapRect: {
+ left: 512, right: 0, bottom: 0, transform: 'none', height: 'calc(100% - 30px)',
+ boundingMapRect: {
left: 512,
right: 0,
bottom: 0
- }
+ },
+ boundingSidebarRect: { right: 0, left: 0, bottom: 0 },
+ rightPanel: false,
+ leftPanel: true
});
});
} catch (e) {
@@ -145,17 +188,21 @@ describe('map layout epics', () => {
});
describe('tests layout updated for right panels', () => {
- const epicResult = (done, right = 658) => actions => {
+ const epicResult = (done, right = 548) => actions => {
try {
expect(actions.length).toBe(1);
actions.map((action) => {
expect(action.type).toBe(UPDATE_MAP_LAYOUT);
expect(action.layout).toEqual({
- left: 0, right, bottom: 0, transform: 'none', height: 'calc(100% - 30px)', boundingMapRect: {
+ left: 0, right, bottom: 0, transform: 'none', height: 'calc(100% - 30px)',
+ boundingMapRect: {
bottom: 0,
left: 0,
right
- }
+ },
+ boundingSidebarRect: { right: 0, left: 0, bottom: 0 },
+ rightPanel: !!right,
+ leftPanel: false
});
});
} catch (e) {
@@ -171,16 +218,41 @@ describe('map layout epics', () => {
const state = { controls: { userExtensions: { enabled: true, group: "parent" } } };
testEpic(updateMapLayoutEpic, 1, setControlProperties("userExtensions", "enabled", true, "group", "parent"), epicResult(done), state);
});
- it('annotations', (done) => {
- const state = { controls: { annotations: { enabled: true, group: "parent" } } };
- testEpic(updateMapLayoutEpic, 1, setControlProperties("annotations", "enabled", true, "group", "parent"), epicResult(done, 329), state);
- });
it('details', (done) => {
const state = { controls: { details: { enabled: true, group: "parent" } } };
testEpic(updateMapLayoutEpic, 1, setControlProperties("details", "enabled", true, "group", "parent"), epicResult(done), state);
});
});
+ describe('tests layout updated for left panels', () => {
+ const epicResult = (done, left = 300) => actions => {
+ try {
+ expect(actions.length).toBe(1);
+ actions.map((action) => {
+ expect(action.type).toBe(UPDATE_MAP_LAYOUT);
+ expect(action.layout).toEqual({
+ right: 0, left, bottom: 0, transform: 'none', height: 'calc(100% - 30px)',
+ boundingMapRect: {
+ bottom: 0,
+ right: 0,
+ left
+ },
+ boundingSidebarRect: { right: 0, left: 0, bottom: 0 },
+ leftPanel: true,
+ rightPanel: false
+ });
+ });
+ } catch (e) {
+ done(e);
+ }
+ done();
+ };
+ it('annotations', (done) => {
+ const state = { controls: { annotations: { enabled: true, group: "parent" } } };
+ testEpic(updateMapLayoutEpic, 1, setControlProperties("annotations", "enabled", true, "group", "parent"), epicResult(done), state);
+ });
+ });
+
it('tests layout updated on noQueryableLayers', (done) => {
const epicResult = actions => {
@@ -205,7 +277,10 @@ describe('map layout epics', () => {
expect(action.type).toBe(UPDATE_MAP_LAYOUT);
expect(action.layout).toEqual({
left: 0, right: 0, bottom: '100%', dockSize: 100, transform: "translate(0, -30px)", height: "calc(100% - 30px)",
- boundingMapRect: {bottom: "100%", dockSize: 100, left: 0, right: 0}
+ boundingMapRect: {bottom: "100%", dockSize: 100, left: 0, right: 0},
+ boundingSidebarRect: { right: 0, left: 0, bottom: 0 },
+ leftPanel: false,
+ rightPanel: false
});
});
} catch (e) {
diff --git a/web/client/epics/__tests__/measurement-test.jsx b/web/client/epics/__tests__/measurement-test.jsx
index 1ae7692670..effa563e4e 100644
--- a/web/client/epics/__tests__/measurement-test.jsx
+++ b/web/client/epics/__tests__/measurement-test.jsx
@@ -191,6 +191,7 @@ describe('measurement epics', () => {
const state = {
controls: {
measure: {
+ enabled: true,
showCoordinateEditor: true
}
}
diff --git a/web/client/epics/annotations.js b/web/client/epics/annotations.js
index 18000711ec..89adea62e8 100644
--- a/web/client/epics/annotations.js
+++ b/web/client/epics/annotations.js
@@ -80,13 +80,14 @@ import { MEASURE_TYPE } from '../utils/MeasurementUtils';
import { createSvgUrl } from '../utils/VectorStyleUtils';
import { isFeatureGridOpen } from '../selectors/featuregrid';
-import { queryPanelSelector, measureSelector } from '../selectors/controls';
+import { queryPanelSelector } from '../selectors/controls';
import { annotationsLayerSelector, multiGeometrySelector, symbolErrorsSelector, editingSelector } from '../selectors/annotations';
import { mapNameSelector } from '../selectors/map';
import { groupsSelector } from '../selectors/layers';
import symbolMissing from '../product/assets/symbols/symbolMissing.svg';
+import {isActiveSelector} from "../selectors/measurement";
/**
* Epics for annotations
* @name epics.annotations
@@ -537,8 +538,8 @@ export default {
if (isFeatureGridOpen(state)) { // if FeatureGrid is open, close it
actions.push(closeFeatureGrid());
}
- if (measureSelector(state)) { // if measure is open, close it
- actions.push(toggleControl("measure"));
+ if (isActiveSelector(state)) { // if measure is open, close it
+ actions.push(setControlProperty('measure', "enabled", false));
}
return actions.length ? Rx.Observable.from(actions) : Rx.Observable.empty();
}),
diff --git a/web/client/epics/catalog.js b/web/client/epics/catalog.js
index a041d438e0..bde7fc18e2 100644
--- a/web/client/epics/catalog.js
+++ b/web/client/epics/catalog.js
@@ -36,7 +36,7 @@ import {
} from '../actions/catalog';
import {showLayerMetadata, SELECT_NODE, changeLayerProperties, addLayer as addNewLayer} from '../actions/layers';
import { error, success } from '../actions/notifications';
-import { SET_CONTROL_PROPERTY, setControlProperties, setControlProperty } from '../actions/controls';
+import {SET_CONTROL_PROPERTY, setControlProperties, setControlProperty, TOGGLE_CONTROL} from '../actions/controls';
import { closeFeatureGrid } from '../actions/featuregrid';
import { purgeMapInfoResults, hideMapinfoMarker } from '../actions/mapInfo';
import { allowBackgroundsDeletion } from '../actions/backgroundselector';
@@ -51,7 +51,7 @@ import {
searchOptionsSelector,
catalogSearchInfoSelector,
getFormatUrlUsedSelector,
- activeSelector
+ isActiveSelector
} from '../selectors/catalog';
import { metadataSourceSelector } from '../selectors/backgroundselector';
import { currentMessagesSelector } from "../selectors/locale";
@@ -291,9 +291,9 @@ export default (API) => ({
- GFI
- FeatureGrid
*/
- openCatalogEpic: (action$) =>
- action$.ofType(SET_CONTROL_PROPERTY)
- .filter((action) => action.control === "metadataexplorer" && action.value)
+ openCatalogEpic: (action$, store) =>
+ action$.ofType(SET_CONTROL_PROPERTY, TOGGLE_CONTROL)
+ .filter((action) => action.control === "metadataexplorer" && isActiveSelector(store.getState()))
.switchMap(() => {
return Rx.Observable.of(closeFeatureGrid(), purgeMapInfoResults(), hideMapinfoMarker());
}),
@@ -468,7 +468,7 @@ export default (API) => ({
* @return {external:Observable}
*/
updateGroupSelectedMetadataExplorerEpic: (action$, store) => action$.ofType(SELECT_NODE)
- .filter(() => activeSelector(store.getState()))
+ .filter(() => isActiveSelector(store.getState()))
.switchMap(({ nodeType, id }) => {
const state = store.getState();
const selectedNodes = selectedNodesSelector(state);
diff --git a/web/client/epics/featuregrid.js b/web/client/epics/featuregrid.js
index 6eb681d007..87d7282d0a 100644
--- a/web/client/epics/featuregrid.js
+++ b/web/client/epics/featuregrid.js
@@ -808,6 +808,10 @@ export const closeRightPanelOnFeatureGridOpen = (action$, store) =>
action$.ofType(OPEN_FEATURE_GRID)
.switchMap( () => {
let actions = [
+ setControlProperty('userExtensions', 'enabled', false),
+ setControlProperty('details', 'enabled', false),
+ setControlProperty('mapTemplates', 'enabled', false),
+ setControlProperty('mapCatalog', 'enabled', false),
setControlProperty('metadataexplorer', 'enabled', false),
setControlProperty('annotations', 'enabled', false),
setControlProperty('details', 'enabled', false)
diff --git a/web/client/epics/mapcatalog.js b/web/client/epics/mapcatalog.js
index 9f4af91397..896c80d701 100644
--- a/web/client/epics/mapcatalog.js
+++ b/web/client/epics/mapcatalog.js
@@ -17,6 +17,10 @@ import {
setFilterReloadDelay,
triggerReload
} from '../actions/mapcatalog';
+import {SET_CONTROL_PROPERTY, TOGGLE_CONTROL} from "../actions/controls";
+import {isActiveSelector} from "../selectors/mapcatalog";
+import {closeFeatureGrid} from "../actions/featuregrid";
+import {hideMapinfoMarker, purgeMapInfoResults} from "../actions/mapInfo";
// the delay in epics below is needed to temporarily mitigate georchestra backend issues
@@ -53,3 +57,11 @@ export const saveMapEpic = (action$) => action$
message: 'mapCatalog.updateError'
})))
);
+
+export const openMapCatalogEpic = (action$, store) =>
+ action$.ofType(SET_CONTROL_PROPERTY, TOGGLE_CONTROL)
+ .filter((action) => action.control === "mapCatalog" && isActiveSelector(store.getState()))
+ .switchMap(() => {
+ return Rx.Observable.of(closeFeatureGrid(), purgeMapInfoResults(), hideMapinfoMarker());
+ });
+
diff --git a/web/client/epics/maplayout.js b/web/client/epics/maplayout.js
index b71bdf8b5a..1111528ac9 100644
--- a/web/client/epics/maplayout.js
+++ b/web/client/epics/maplayout.js
@@ -36,6 +36,7 @@ import { mapInfoDetailsSettingsFromIdSelector, isMouseMoveIdentifyActiveSelector
import { head, get } from 'lodash';
import { isFeatureGridOpen, getDockSize } from '../selectors/featuregrid';
+import {DEFAULT_MAP_LAYOUT} from "../utils/MapUtils";
/**
* Capture that cause layout change to update the proper object.
@@ -77,7 +78,22 @@ export const updateMapLayoutEpic = (action$, store) =>
}));
}
- const mapLayout = ConfigUtils.getConfigProp("mapLayout") || {left: {sm: 300, md: 500, lg: 600}, right: {md: 658}, bottom: {sm: 30}};
+ // Calculating sidebar's rectangle to be used by dock panels
+ const rightSidebars = head([
+ get(state, "controls.sidebarMenu.enabled") && {right: 40} || null
+ ]) || {right: 0};
+ const leftSidebars = head([
+ null
+ ]) || {left: 0};
+
+ const boundingSidebarRect = {
+ ...rightSidebars,
+ ...leftSidebars,
+ bottom: 0
+ };
+ /* ---------------------- */
+
+ const mapLayout = ConfigUtils.getConfigProp("mapLayout") || DEFAULT_MAP_LAYOUT;
if (get(state, "mode") === 'embedded') {
const height = {height: 'calc(100% - ' + mapLayout.bottom.sm + 'px)'};
@@ -95,6 +111,7 @@ export const updateMapLayoutEpic = (action$, store) =>
const leftPanels = head([
get(state, "controls.queryPanel.enabled") && {left: mapLayout.left.lg} || null,
+ get(state, "controls.annotations.enabled") && {left: mapLayout.left.sm} || null,
get(state, "controls.widgetBuilder.enabled") && {left: mapLayout.left.md} || null,
get(state, "layers.settings.expanded") && {left: mapLayout.left.md} || null,
get(state, "controls.drawer.enabled") && { left: resizedDrawer || mapLayout.left.sm} || null
@@ -102,7 +119,6 @@ export const updateMapLayoutEpic = (action$, store) =>
const rightPanels = head([
get(state, "controls.details.enabled") && !mapInfoDetailsSettingsFromIdSelector(state)?.showAsModal && {right: mapLayout.right.md} || null,
- get(state, "controls.annotations.enabled") && {right: mapLayout.right.md / 2} || null,
get(state, "controls.metadataexplorer.enabled") && {right: mapLayout.right.md} || null,
get(state, "controls.measure.enabled") && showCoordinateEditorSelector(state) && {right: mapLayout.right.md} || null,
get(state, "controls.userExtensions.enabled") && { right: mapLayout.right.md } || null,
@@ -124,13 +140,23 @@ export const updateMapLayoutEpic = (action$, store) =>
...rightPanels
};
+ Object.keys(boundingMapRect).forEach(key => {
+ if (['left', 'right', 'dockSize'].includes(key)) {
+ boundingMapRect[key] = boundingMapRect[key] + (boundingSidebarRect[key] ?? 0);
+ } else {
+ const totalOffset = (parseFloat(boundingMapRect[key]) + parseFloat(boundingSidebarRect[key] ?? 0));
+ boundingMapRect[key] = totalOffset ? totalOffset + '%' : 0;
+ }
+ });
+
return Rx.Observable.of(updateMapLayout({
- ...leftPanels,
- ...rightPanels,
- ...bottom,
+ ...boundingMapRect,
...transform,
...height,
- boundingMapRect
+ boundingMapRect,
+ boundingSidebarRect,
+ rightPanel: rightPanels.right > 0,
+ leftPanel: leftPanels.left > 0
}));
});
diff --git a/web/client/epics/maptemplates.js b/web/client/epics/maptemplates.js
index ce7ef272a9..4917c64502 100644
--- a/web/client/epics/maptemplates.js
+++ b/web/client/epics/maptemplates.js
@@ -15,16 +15,18 @@ import { error as showError } from '../actions/notifications';
import { isLoggedIn } from '../selectors/security';
import { setTemplates, setMapTemplatesLoaded, setTemplateData, setTemplateLoading, CLEAR_MAP_TEMPLATES, OPEN_MAP_TEMPLATES_PANEL,
MERGE_TEMPLATE, REPLACE_TEMPLATE, SET_ALLOWED_TEMPLATES } from '../actions/maptemplates';
-import { templatesSelector, allTemplatesSelector } from '../selectors/maptemplates';
+import {templatesSelector, allTemplatesSelector, isActiveSelector} from '../selectors/maptemplates';
import { mapSelector } from '../selectors/map';
import { layersSelector, groupsSelector } from '../selectors/layers';
import { backgroundListSelector } from '../selectors/backgroundselector';
import { textSearchConfigSelector, bookmarkSearchConfigSelector } from '../selectors/searchconfig';
import { mapOptionsToSaveSelector } from '../selectors/mapsave';
-import { setControlProperty } from '../actions/controls';
+import {SET_CONTROL_PROPERTY, setControlProperty, TOGGLE_CONTROL} from '../actions/controls';
import { configureMap } from '../actions/config';
import { wrapStartStop } from '../observables/epics';
import { toMapConfig } from '../utils/ogc/WMC';
+import {closeFeatureGrid} from "../actions/featuregrid";
+import {hideMapinfoMarker, purgeMapInfoResults} from "../actions/mapInfo";
const errorToMessageId = (e = {}, getState = () => {}) => {
let message = `context.errors.template.unknownError`;
@@ -174,3 +176,10 @@ export const replaceTemplateEpic = (action$, store) => action$
}
));
});
+
+export const openMapTemplatesEpic = (action$, store) =>
+ action$.ofType(SET_CONTROL_PROPERTY, TOGGLE_CONTROL)
+ .filter((action) => action.control === "mapTemplates" && isActiveSelector(store.getState()))
+ .switchMap(() => {
+ return Observable.of(closeFeatureGrid(), purgeMapInfoResults(), hideMapinfoMarker());
+ });
diff --git a/web/client/epics/measurement.js b/web/client/epics/measurement.js
index c5554c9a57..99e314fda5 100644
--- a/web/client/epics/measurement.js
+++ b/web/client/epics/measurement.js
@@ -17,10 +17,15 @@ import {STYLE_TEXT} from '../utils/AnnotationsUtils';
import {toggleControl, setControlProperty, SET_CONTROL_PROPERTY, TOGGLE_CONTROL} from '../actions/controls';
import {closeFeatureGrid} from '../actions/featuregrid';
import {purgeMapInfoResults, hideMapinfoMarker} from '../actions/mapInfo';
-import {showCoordinateEditorSelector, measureSelector} from '../selectors/controls';
-import {geomTypeSelector} from '../selectors/measurement';
+import {measureSelector} from '../selectors/controls';
+import {geomTypeSelector, isActiveSelector} from '../selectors/measurement';
import { CLICK_ON_MAP } from '../actions/map';
-import {newAnnotation, setEditingFeature, cleanHighlight, toggleVisibilityAnnotation} from '../actions/annotations';
+import {
+ newAnnotation,
+ setEditingFeature,
+ cleanHighlight,
+ toggleVisibilityAnnotation
+} from '../actions/annotations';
export const addAnnotationFromMeasureEpic = (action$) =>
action$.ofType(ADD_MEASURE_AS_ANNOTATION)
@@ -60,10 +65,10 @@ export const addAsLayerEpic = (action$) =>
});
export const openMeasureEpic = (action$, store) =>
- action$.ofType(SET_CONTROL_PROPERTY)
- .filter((action) => action.control === "measure" && action.value && showCoordinateEditorSelector(store.getState()))
+ action$.ofType(SET_CONTROL_PROPERTY, TOGGLE_CONTROL)
+ .filter((action) => action.control === "measure" && isActiveSelector(store.getState()))
.switchMap(() => {
- return Rx.Observable.of(closeFeatureGrid(), purgeMapInfoResults(), hideMapinfoMarker());
+ return Rx.Observable.of(closeFeatureGrid(), purgeMapInfoResults(), hideMapinfoMarker(), setControlProperty('annotations', 'enabled', false));
});
export const closeMeasureEpics = (action$, store) =>
diff --git a/web/client/epics/sidebarmenu.js b/web/client/epics/sidebarmenu.js
new file mode 100644
index 0000000000..a6dca95555
--- /dev/null
+++ b/web/client/epics/sidebarmenu.js
@@ -0,0 +1,41 @@
+/*
+ * Copyright 2022, GeoSolutions Sas.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree.
+ */
+
+import { Observable } from 'rxjs';
+import { keys, findIndex, get } from 'lodash';
+import {SET_CONTROL_PROPERTIES, SET_CONTROL_PROPERTY, setControlProperty, TOGGLE_CONTROL} from '../actions/controls';
+import ConfigUtils from "../utils/ConfigUtils";
+
+const customExclusivePanels = get(ConfigUtils.getConfigProp('miscSettings'), 'exclusiveDockPanels', []);
+const exclusiveDockPanels = ['measure', 'mapCatalog', 'mapTemplates', 'metadataexplorer', 'userExtensions', 'details', 'cadastrapp']
+ .concat(...(Array.isArray(customExclusivePanels) ? customExclusivePanels : []));
+
+export const resetOpenDockPanels = (action$, store) => action$
+ .ofType(SET_CONTROL_PROPERTIES, SET_CONTROL_PROPERTY, TOGGLE_CONTROL)
+ .filter(({control, property, properties = [], type}) => {
+ const state = store.getState();
+ const controlState = state.controls[control].enabled;
+ switch (type) {
+ case SET_CONTROL_PROPERTY:
+ case TOGGLE_CONTROL:
+ return (property === 'enabled' || !property) && controlState && exclusiveDockPanels.includes(control);
+ default:
+ return findIndex(keys(properties), prop => prop === 'enabled') > -1 && controlState && exclusiveDockPanels.includes(control);
+ }
+ })
+ .switchMap(({control}) => {
+ const actions = [];
+ const state = store.getState();
+ exclusiveDockPanels.forEach((controlName) => {
+ const enabled = get(state, ['controls', controlName, 'enabled'], false);
+ enabled && control !== controlName && actions.push(setControlProperty(controlName, 'enabled', null));
+ });
+ return Observable.from(actions);
+ });
+
+export default { resetOpenDockPanels };
diff --git a/web/client/epics/userextensions.js b/web/client/epics/userextensions.js
new file mode 100644
index 0000000000..854e6ecfcd
--- /dev/null
+++ b/web/client/epics/userextensions.js
@@ -0,0 +1,20 @@
+/**
+ * Copyright 2022, GeoSolutions Sas.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree.
+ */
+
+import {SET_CONTROL_PROPERTY, TOGGLE_CONTROL} from "../actions/controls";
+import {isActiveSelector} from "../selectors/userextensions";
+import {Observable} from "rxjs";
+import {closeFeatureGrid} from "../actions/featuregrid";
+import {hideMapinfoMarker, purgeMapInfoResults} from "../actions/mapInfo";
+
+export const openUserExtensionsEpic = (action$, store) =>
+ action$.ofType(SET_CONTROL_PROPERTY, TOGGLE_CONTROL)
+ .filter((action) => action.control === "userExtensions" && isActiveSelector(store.getState()))
+ .switchMap(() => {
+ return Observable.of(closeFeatureGrid(), purgeMapInfoResults(), hideMapinfoMarker());
+ });
diff --git a/web/client/plugins/Annotations.jsx b/web/client/plugins/Annotations.jsx
index 3590567faa..1b4755195b 100644
--- a/web/client/plugins/Annotations.jsx
+++ b/web/client/plugins/Annotations.jsx
@@ -9,14 +9,10 @@
import React from 'react';
import assign from 'object-assign';
import PropTypes from 'prop-types';
-import { Glyphicon } from 'react-bootstrap';
import { createSelector } from 'reselect';
import isEmpty from 'lodash/isEmpty';
-import ContainerDimensions from 'react-container-dimensions';
-import Dock from 'react-dock';
import { createPlugin, connect } from '../utils/PluginsUtils';
-import Message from '../components/I18N/Message';
import { on, toggleControl } from '../actions/controls';
import AnnotationsEditorComp from '../components/mapcontrols/annotations/AnnotationsEditor';
import AnnotationsComp from '../components/mapcontrols/annotations/Annotations';
@@ -86,6 +82,11 @@ import { annotationsInfoSelector, annotationsListSelector } from '../selectors/a
import { mapLayoutValuesSelector } from '../selectors/maplayout';
import { ANNOTATIONS } from '../utils/AnnotationsUtils';
import { registerRowViewer } from '../utils/MapInfoUtils';
+import ResponsivePanel from "../components/misc/panels/ResponsivePanel";
+import {Glyphicon, Tooltip} from "react-bootstrap";
+import Button from "../components/misc/Button";
+import OverlayTrigger from "../components/misc/OverlayTrigger";
+import Message from "../components/I18N/Message";
const commonEditorActions = {
onUpdateSymbols: updateSymbols,
@@ -191,6 +192,7 @@ class AnnotationsPanel extends React.Component {
buttonStyle: PropTypes.object,
style: PropTypes.object,
dockProps: PropTypes.object,
+ dockStyle: PropTypes.object,
// side panel properties
width: PropTypes.number
@@ -213,12 +215,10 @@ class AnnotationsPanel extends React.Component {
closeGlyph: "1-close",
// side panel properties
- width: 330,
+ width: 300,
dockProps: {
dimMode: "none",
- size: 0.30,
- fluid: true,
- position: "right",
+ position: "left",
zIndex: 1030
},
dockStyle: {}
@@ -235,19 +235,18 @@ class AnnotationsPanel extends React.Component {
render() {
return this.props.active ? (
-
- { ({ width }) =>
-
- 1 ? 1 : this.props.width / width} >
-
-
-
- }
-
+
+
+
) : null;
}
}
@@ -308,7 +307,7 @@ const annotationsSelector = createSelector([
], (active, dockStyle, list) => ({
active,
dockStyle,
- width: !isEmpty(list?.selected) ? 660 : 330
+ width: !isEmpty(list?.selected) ? 600 : 300
}));
const AnnotationsPlugin = connect(annotationsSelector, {
@@ -318,18 +317,45 @@ const AnnotationsPlugin = connect(annotationsSelector, {
export default createPlugin('Annotations', {
component: assign(AnnotationsPlugin, {
disablePluginIf: "{state('mapType') === 'cesium' || state('mapType') === 'leaflet' }"
- }, {
- BurgerMenu: {
- name: 'annotations',
- position: 40,
- text:
,
- tooltip: "annotations.tooltip",
- icon:
,
- action: conditionalToggle,
- priority: 2,
- doNotHide: true
- }
}),
+ containers: {
+ TOC: {
+ doNotHide: true,
+ name: "Annotations",
+ target: 'toolbar',
+ selector: () => true,
+ Component: connect(() => {}, {
+ onClick: conditionalToggle
+ })(({onClick, layers, selectedLayers, status}) => {
+ if (status === 'DESELECT' && layers.filter(l => l.id === 'annotations').length === 0) {
+ return (
}>
+
+ );
+ }
+ if (selectedLayers[0]?.id === 'annotations') {
+ return (
+
}>
+
+ );
+ }
+ return false;
+ })
+ }
+ },
reducers: {
annotations: annotationsReducer
},
diff --git a/web/client/plugins/BurgerMenu.jsx b/web/client/plugins/BurgerMenu.jsx
index 7b730c8fef..6867eeafcf 100644
--- a/web/client/plugins/BurgerMenu.jsx
+++ b/web/client/plugins/BurgerMenu.jsx
@@ -42,6 +42,8 @@ import ToolsContainer from './containers/ToolsContainer';
import Message from './locale/Message';
import { createPlugin } from '../utils/PluginsUtils';
import './burgermenu/burgermenu.css';
+import {setControlProperty} from "../actions/controls";
+import {burgerMenuSelector} from "../selectors/controls";
class BurgerMenu extends React.Component {
static propTypes = {
@@ -50,6 +52,8 @@ class BurgerMenu extends React.Component {
items: PropTypes.array,
title: PropTypes.node,
onItemClick: PropTypes.func,
+ onInit: PropTypes.func,
+ onDetach: PropTypes.func,
controls: PropTypes.object,
mapType: PropTypes.string,
panelStyle: PropTypes.object,
@@ -75,9 +79,27 @@ class BurgerMenu extends React.Component {
position: "absolute",
overflow: "auto"
},
- panelClassName: "toolbar-panel"
+ panelClassName: "toolbar-panel",
+ onInit: () => {},
+ onDetach: () => {}
};
+ componentDidMount() {
+ const { onInit } = this.props;
+ onInit();
+ }
+
+ componentDidUpdate(prevProps) {
+ const { onInit } = this.props;
+ prevProps.isActive === false && onInit();
+ }
+
+ componentWillUnmount() {
+ const { onDetach } = this.props;
+ onDetach();
+ }
+
+
getPanels = items => {
return items.filter((item) => item.panel)
.map((item) => assign({}, item, {panel: item.panel === true ? item.plugin : item.panel})).concat(
@@ -177,8 +199,12 @@ export default createPlugin(
'BurgerMenu',
{
component: connect((state) =>({
- controls: state.controls
- }))(BurgerMenu),
+ controls: state.controls,
+ active: burgerMenuSelector(state)
+ }), {
+ onInit: setControlProperty.bind(null, 'burgermenu', 'enabled', true),
+ onDetach: setControlProperty.bind(null, 'burgermenu', 'enabled', false)
+ })(BurgerMenu),
containers: {
OmniBar: {
name: "burgermenu",
diff --git a/web/client/plugins/DeleteMap.jsx b/web/client/plugins/DeleteMap.jsx
index 58661a173c..25b832b15b 100644
--- a/web/client/plugins/DeleteMap.jsx
+++ b/web/client/plugins/DeleteMap.jsx
@@ -84,7 +84,24 @@ export default createPlugin('DeleteMap', {
selector: (state) => {
const { canDelete = false } = state?.map?.present?.info || {};
return canDelete ? {} : { style: {display: "none"} };
- }
+ },
+ priority: 2,
+ doNotHide: true
+ },
+ SidebarMenu: {
+ name: 'mapDelete',
+ position: 36,
+ text:
,
+ icon:
,
+ action: toggleControl.bind(null, 'mapDelete', null),
+ toggle: true,
+ tooltip: "manager.deleteMap",
+ selector: (state) => {
+ const { canDelete = false } = state?.map?.present?.info || {};
+ return canDelete ? {} : { style: {display: "none"} };
+ },
+ priority: 1,
+ doNotHide: true
}
}
});
diff --git a/web/client/plugins/Details.jsx b/web/client/plugins/Details.jsx
index 1dd6bce57a..113d89dd7b 100644
--- a/web/client/plugins/Details.jsx
+++ b/web/client/plugins/Details.jsx
@@ -31,6 +31,7 @@ import { createPlugin } from '../utils/PluginsUtils';
import details from '../reducers/details';
import * as epics from '../epics/details';
+import {createStructuredSelector} from "reselect";
/**
* Allow to show details for the map.
@@ -70,6 +71,7 @@ const DetailsPlugin = ({
{viewer}
:
@@ -78,11 +80,11 @@ const DetailsPlugin = ({
};
export default createPlugin('Details', {
- component: connect((state) => ({
- active: get(state, "controls.details.enabled"),
- dockStyle: mapLayoutValuesSelector(state, {height: true}),
- detailsText: detailsTextSelector(state),
- showAsModal: mapInfoDetailsSettingsFromIdSelector(state)?.showAsModal
+ component: connect(createStructuredSelector({
+ active: state => get(state, "controls.details.enabled"),
+ dockStyle: state => mapLayoutValuesSelector(state, { height: true, right: true }, true),
+ detailsText: detailsTextSelector,
+ showAsModal: state => mapInfoDetailsSettingsFromIdSelector(state)?.showAsModal
}), {
onClose: closeDetailsPanel
})(DetailsPlugin),
@@ -90,7 +92,7 @@ export default createPlugin('Details', {
BurgerMenu: {
name: 'details',
position: 1000,
- priority: 1,
+ priority: 2,
doNotHide: true,
text: ,
tooltip: "details.tooltip",
@@ -122,6 +124,30 @@ export default createPlugin('Details', {
}
return { style: {display: "none"} };
}
+ },
+ SidebarMenu: {
+ name: "details",
+ position: 4,
+ text: ,
+ tooltip: "details.tooltip",
+ alwaysVisible: true,
+ icon: ,
+ action: openDetailsPanel,
+ selector: (state) => {
+ const mapId = mapIdSelector(state);
+ const detailsUri = mapId && mapInfoDetailsUriFromIdSelector(state, mapId);
+ if (detailsUri) {
+ return {
+ bsStyle: state.controls.details && state.controls.details.enabled ? 'primary' : 'tray',
+ active: state.controls.details && state.controls.details.enabled || false
+ };
+ }
+ return {
+ style: {display: "none"}
+ };
+ },
+ doNotHide: true,
+ priority: 1
}
},
epics,
diff --git a/web/client/plugins/FeatureEditor.jsx b/web/client/plugins/FeatureEditor.jsx
index 8c141dc145..06a413e198 100644
--- a/web/client/plugins/FeatureEditor.jsx
+++ b/web/client/plugins/FeatureEditor.jsx
@@ -177,7 +177,7 @@ const FeatureDock = (props = {
minDockSize: 0.1,
position: "bottom",
setDockSize: () => {},
- zIndex: 1030
+ zIndex: 1060
};
// columns={[]}
const items = props?.items ?? [];
diff --git a/web/client/plugins/Help.jsx b/web/client/plugins/Help.jsx
index 12ea1cffff..01f8b981e7 100644
--- a/web/client/plugins/Help.jsx
+++ b/web/client/plugins/Help.jsx
@@ -34,6 +34,15 @@ export default {
priority: 1
},
BurgerMenu: {
+ name: 'help',
+ position: 1000,
+ text: ,
+ icon: ,
+ action: toggleControl.bind(null, 'help', null),
+ priority: 3,
+ doNotHide: true
+ },
+ SidebarMenu: {
name: 'help',
position: 1000,
text: ,
diff --git a/web/client/plugins/HelpLink.jsx b/web/client/plugins/HelpLink.jsx
index 3ffdf56066..f7631d08b8 100644
--- a/web/client/plugins/HelpLink.jsx
+++ b/web/client/plugins/HelpLink.jsx
@@ -45,6 +45,20 @@ export default createPlugin('HelpLink', {
},
priority: 2,
doNotHide: true
+ },
+ SidebarMenu: {
+ name: 'helplink',
+ position: 1100,
+ tooltip: "docsTooltip",
+ text: ,
+ icon: ,
+ action: () => ({type: ''}),
+ selector: (state, ownProps) => {
+ const docsUrl = get(ownProps, 'docsUrl', 'https://mapstore.readthedocs.io/en/latest/');
+ return { href: docsUrl, target: 'blank'};
+ },
+ priority: 1,
+ doNotHide: true
}
}
});
diff --git a/web/client/plugins/Home.jsx b/web/client/plugins/Home.jsx
index b29287ecd3..f1960b9d24 100644
--- a/web/client/plugins/Home.jsx
+++ b/web/client/plugins/Home.jsx
@@ -17,9 +17,10 @@ import Home, {getPath} from '../components/home/Home';
import { connect } from 'react-redux';
import { checkPendingChanges } from '../actions/pendingChanges';
import { setControlProperty } from '../actions/controls';
-import { unsavedMapSelector, unsavedMapSourceSelector } from '../selectors/controls';
+import {burgerMenuSelector, unsavedMapSelector, unsavedMapSourceSelector} from '../selectors/controls';
import { feedbackMaskSelector } from '../selectors/feedbackmask';
import ConfigUtils from '../utils/ConfigUtils';
+import {sidebarIsActiveSelector} from "../selectors/sidebarmenu";
const checkUnsavedMapChanges = (action) => {
return dispatch => {
@@ -70,7 +71,24 @@ export default {
OmniBar: {
name: 'home',
position: 4,
- tool: true,
+ tool: connect((state) => ({
+ hidden: sidebarIsActiveSelector(state),
+ bsStyle: 'primary',
+ tooltipPosition: 'bottom'
+ }))(HomeConnected),
+ priority: 3
+ },
+ SidebarMenu: {
+ name: 'home',
+ position: 1,
+ tool: connect(() => ({
+ bsStyle: 'tray',
+ tooltipPosition: 'left',
+ text:
+ }))(HomeConnected),
+ selector: (state) => ({
+ style: { display: burgerMenuSelector(state) ? 'none' : null }
+ }),
priority: 3
}
}),
diff --git a/web/client/plugins/Identify.jsx b/web/client/plugins/Identify.jsx
index 14529c72fc..3361754989 100644
--- a/web/client/plugins/Identify.jsx
+++ b/web/client/plugins/Identify.jsx
@@ -89,7 +89,7 @@ const selector = createStructuredSelector({
reverseGeocodeData: (state) => state.mapInfo && state.mapInfo.reverseGeocodeData,
warning: (state) => state.mapInfo && state.mapInfo.warning,
currentLocale: currentLocaleSelector,
- dockStyle: state => mapLayoutValuesSelector(state, { height: true }),
+ dockStyle: (state) => mapLayoutValuesSelector(state, { height: true, right: true }, true),
formatCoord: (state) => state.mapInfo && state.mapInfo.formatCoord || ConfigUtils.getConfigProp('defaultCoordinateFormat'),
showCoordinateEditor: (state) => state.mapInfo && state.mapInfo.showCoordinateEditor,
showEmptyMessageGFI: state => showEmptyMessageGFISelector(state),
@@ -164,7 +164,7 @@ const identifyDefaultProps = defaultProps({
showMoreInfo: true,
showEdit: false,
position: 'right',
- size: 660,
+ size: 550,
getToolButtons,
getFeatureButtons,
showFullscreen: false,
diff --git a/web/client/plugins/Login.jsx b/web/client/plugins/Login.jsx
index 3f725c4bb0..ff1e3bcd2a 100644
--- a/web/client/plugins/Login.jsx
+++ b/web/client/plugins/Login.jsx
@@ -16,6 +16,10 @@ import epics from '../epics/login';
import { comparePendingChanges } from '../epics/pendingChanges';
import security from '../reducers/security';
import { Login, LoginNav, PasswordReset, UserDetails, UserMenu } from './login/index';
+import {connect} from "../utils/PluginsUtils";
+import {Glyphicon} from "react-bootstrap";
+import {burgerMenuSelector} from "../selectors/controls";
+import {sidebarIsActiveSelector} from "../selectors/sidebarmenu";
/**
* Login Plugin. Allow to login/logout or show user info and reset password tools.
@@ -62,7 +66,29 @@ export default {
OmniBar: {
name: "login",
position: 3,
- tool: LoginNav,
+ tool: connect((state) => ({
+ hidden: sidebarIsActiveSelector(state),
+ renderButtonContent: () => {return ; },
+ bsStyle: 'primary'
+ }))(LoginNav),
+ tools: [UserDetails, PasswordReset, Login],
+ priority: 1
+ },
+ SidebarMenu: {
+ name: "login",
+ position: 2,
+ tool: connect(() => ({
+ bsStyle: 'tray',
+ tooltipPosition: 'left',
+ renderButtonContent: (props) => [, props.renderButtonText ? props.user && props.user[props.displayName] || "Guest" : null],
+ renderButtonText: true,
+ menuProps: {
+ noCaret: true
+ }
+ }))(LoginNav),
+ selector: (state) => ({
+ style: { display: burgerMenuSelector(state) ? 'none' : null }
+ }),
tools: [UserDetails, PasswordReset, Login],
priority: 1
}
diff --git a/web/client/plugins/MapCatalog.jsx b/web/client/plugins/MapCatalog.jsx
index 240ae31f85..7fc5d1dd5c 100644
--- a/web/client/plugins/MapCatalog.jsx
+++ b/web/client/plugins/MapCatalog.jsx
@@ -7,7 +7,7 @@
*/
import React from 'react';
-import { Glyphicon } from 'react-bootstrap';
+import {Glyphicon} from 'react-bootstrap';
import { connect } from 'react-redux';
import { createStructuredSelector } from 'reselect';
@@ -26,12 +26,14 @@ import {
} from '../selectors/mapcatalog';
import MapCatalogPanel from '../components/mapcatalog/MapCatalogPanel';
-import DockPanel from '../components/misc/panels/DockPanel';
import Message from '../components/I18N/Message';
import { createPlugin } from '../utils/PluginsUtils';
import mapcatalog from '../reducers/mapcatalog';
import * as epics from '../epics/mapcatalog';
+import {mapLayoutValuesSelector} from "../selectors/maplayout";
+import * as PropTypes from "prop-types";
+import ResponsivePanel from "../components/misc/panels/ResponsivePanel";
/**
* Allows users to existing maps directly on the map.
@@ -39,48 +41,81 @@ import * as epics from '../epics/mapcatalog';
* @class
* @name MapCatalog
*/
-const MapCatalogComponent = ({
- allow3d,
- active,
- mapType,
- user,
- triggerReloadValue,
- filterReloadDelay,
- onToggleControl = () => {},
- onTriggerReload = () => {},
- onDelete = () => {},
- onSave = () => {},
- ...props
-}) => {
- return (
- }
- onClose={() => onToggleControl()}
- style={{ height: 'calc(100% - 30px)' }}>
- map.contextName ?
- `context/${map.contextName}/${map.id}` :
- `viewer/${mapType}/${map.id}`
- }
- toggleCatalog={() => onToggleControl()}
- shareApi/>
-
- );
-};
+class MapCatalogComponent extends React.Component {
+ static propTypes = {
+ allow3d: PropTypes.any,
+ active: PropTypes.any,
+ mapType: PropTypes.any,
+ user: PropTypes.any,
+ triggerReloadValue: PropTypes.any,
+ filterReloadDelay: PropTypes.any,
+ onToggleControl: PropTypes.func,
+ onTriggerReload: PropTypes.func,
+ onDelete: PropTypes.func,
+ onSave: PropTypes.func,
+ dockStyle: PropTypes.object,
+ size: PropTypes.number
+ };
+ static defaultProps = {
+ onToggleControl: () => {
+ }, onTriggerReload: () => {
+ }, onDelete: () => {
+ }, onSave: () => {
+ }, dockStyle: {},
+ size: 550
+ };
+
+ render() {
+ const {
+ allow3d,
+ active,
+ mapType,
+ user,
+ triggerReloadValue,
+ filterReloadDelay,
+ onToggleControl,
+ onTriggerReload,
+ onDelete,
+ onSave,
+ dockStyle,
+ size,
+ ...props
+ } = this.props;
+ return (
+ }
+ onClose={() => onToggleControl()}
+ style={dockStyle}
+ >
+ map.contextName ?
+ `context/${map.contextName}/${map.id}` :
+ `viewer/${mapType}/${map.id}`
+ }
+ toggleCatalog={() => onToggleControl()}
+ shareApi/>
+
+ );
+ }
+}
+
export default createPlugin('MapCatalog', {
component: connect(createStructuredSelector({
@@ -88,7 +123,8 @@ export default createPlugin('MapCatalog', {
mapType: mapTypeSelector,
user: userSelector,
triggerReloadValue: triggerReloadValueSelector,
- filterReloadDelay: filterReloadDelaySelector
+ filterReloadDelay: filterReloadDelaySelector,
+ dockStyle: state => mapLayoutValuesSelector(state, { height: true, right: true }, true)
}), {
setFilterReloadDelay,
onToggleControl: toggleControl.bind(null, 'mapCatalog', 'enabled'),
@@ -98,7 +134,7 @@ export default createPlugin('MapCatalog', {
})(MapCatalogComponent),
containers: {
BurgerMenu: {
- name: 'mapcatalog',
+ name: 'mapCatalog',
position: 6,
text: ,
icon: ,
@@ -106,6 +142,17 @@ export default createPlugin('MapCatalog', {
action: () => toggleControl('mapCatalog', 'enabled'),
priority: 2,
doNotHide: true
+ },
+ SidebarMenu: {
+ name: "mapCatalog",
+ position: 6,
+ icon: ,
+ text: ,
+ tooltip: "mapCatalog.tooltip",
+ action: () => toggleControl('mapCatalog', 'enabled'),
+ toggle: true,
+ priority: 1,
+ doNotHide: true
}
},
reducers: {
diff --git a/web/client/plugins/MapExport.jsx b/web/client/plugins/MapExport.jsx
index 258622a588..3d81ab136a 100644
--- a/web/client/plugins/MapExport.jsx
+++ b/web/client/plugins/MapExport.jsx
@@ -8,7 +8,6 @@
import React from 'react';
import { Glyphicon } from 'react-bootstrap';
-import assign from 'object-assign';
import { pick, get } from 'lodash';
import { connect } from 'react-redux';
import { compose, withState, defaultProps } from 'recompose';
@@ -24,6 +23,7 @@ import { createControlEnabledSelector } from '../selectors/controls';
import ExportPanel from '../components/export/ExportPanel';
import * as epics from '../epics/mapexport';
+import {createPlugin} from "../utils/PluginsUtils";
const DEFAULTS = ["mapstore2", "wmc"];
const isEnabled = createControlEnabledSelector('export');
@@ -86,26 +86,46 @@ const MapExport = enhanceExport(
* @name MapExport
* @property {string[]} cfg.enabledFormats the list of allowed formats. By default ["mapstore2", "wmc"]
*/
-const MapExportPlugin = {
- MapExportPlugin: assign(MapExport, {
- disablePluginIf: "{state('mapType') === 'cesium'}",
- BurgerMenu: config => {
+const MapExportPlugin = createPlugin('MapExport', {
+ component: MapExport,
+ options: {
+ disablePluginIf: "{state('mapType') === 'cesium'}"
+ },
+ containers: {
+ SidebarMenu: config => {
const enabledFormats = get(config, 'cfg.enabledFormats', DEFAULTS);
return {
- name: 'export',
+ name: "export",
position: 4,
+ tooltip: "mapExport.tooltip",
text: ,
+ icon: ,
+ action: enabledFormats.length > 1 ?
+ () => toggleControl('export') :
+ () => exportMap(enabledFormats[0] || 'mapstore2'),
+ priority: 1,
+ toggle: true,
+ doNotHide: true
+ };
+ },
+ BurgerMenu: config => {
+ const enabledFormats = get(config, 'cfg.enabledFormats', DEFAULTS);
+ return {
+ name: "export",
+ position: 4,
tooltip: "mapExport.tooltip",
+ text: ,
icon: ,
action: enabledFormats.length > 1 ?
() => toggleControl('export') :
() => exportMap(enabledFormats[0] || 'mapstore2'),
priority: 2,
+ toggle: true,
doNotHide: true
};
}
- }),
+ },
epics: epics
-};
+});
export default MapExportPlugin;
diff --git a/web/client/plugins/MapImport.jsx b/web/client/plugins/MapImport.jsx
index 4dd8c43e1f..650836c38e 100644
--- a/web/client/plugins/MapImport.jsx
+++ b/web/client/plugins/MapImport.jsx
@@ -93,6 +93,17 @@ export default {
action: toggleControl.bind(null, 'mapimport', null),
priority: 2,
doNotHide: true
+ },
+ SidebarMenu: {
+ name: "mapimport",
+ position: 4,
+ tooltip: "mapImport.tooltip",
+ text: ,
+ icon: ,
+ action: toggleControl.bind(null, 'mapimport', null),
+ toggle: true,
+ priority: 1,
+ doNotHide: true
}
}),
reducers: {
diff --git a/web/client/plugins/MapTemplates.jsx b/web/client/plugins/MapTemplates.jsx
index 8a26998d73..e1f56fd409 100644
--- a/web/client/plugins/MapTemplates.jsx
+++ b/web/client/plugins/MapTemplates.jsx
@@ -6,24 +6,26 @@
* LICENSE file in the root directory of this source tree.
*/
-import React, { useEffect } from 'react';
+import React from 'react';
import { get } from 'lodash';
import { connect } from 'react-redux';
import { Glyphicon } from 'react-bootstrap';
import { createSelector } from 'reselect';
import { createPlugin } from '../utils/PluginsUtils';
-import { toggleControl } from '../actions/controls';
+import {setControlProperty, toggleControl} from '../actions/controls';
import { templatesSelector, mapTemplatesLoadedSelector } from '../selectors/maptemplates';
import { openMapTemplatesPanel, mergeTemplate, replaceTemplate, toggleFavouriteTemplate, setAllowedTemplates } from '../actions/maptemplates';
import Message from '../components/I18N/Message';
import Loader from '../components/misc/Loader';
-import DockPanel from '../components/misc/panels/DockPanel';
import MapTemplatesPanel from '../components/maptemplates/MapTemplatesPanel';
import maptemplates from '../reducers/maptemplates';
import * as epics from '../epics/maptemplates';
+import {mapLayoutValuesSelector} from "../selectors/maplayout";
+import PropTypes from "prop-types";
+import ResponsivePanel from "../components/misc/panels/ResponsivePanel";
/**
* Provides a list of map templates available inside of a currently loaded context.
@@ -34,49 +36,94 @@ import * as epics from '../epics/maptemplates';
* @name MapTemplates
* @prop {object[]} cfg.allowedTemplates: A list of objects with map template ids used to load templates when not in context
*/
-const mapTemplates = ({
- active,
- templates = [],
- allowedTemplates = [],
- templatesLoaded,
- onToggleControl = () => {},
- onMergeTemplate = () => {},
- onReplaceTemplate = () => {},
- onToggleFavourite = () => {},
- onSetAllowedTemplates = () => {}
-}) => {
- useEffect(() => {
- if (active) {
- onSetAllowedTemplates(allowedTemplates);
+class MapTemplatesComponent extends React.Component {
+ static propTypes = {
+ active: PropTypes.bool,
+ templatesLoaded: PropTypes.bool,
+ templates: PropTypes.array,
+ allowedTemplates: PropTypes.array,
+ dockStyle: PropTypes.object,
+ onToggleControl: PropTypes.func,
+ onMergeTemplate: PropTypes.func,
+ onReplaceTemplate: PropTypes.func,
+ onToggleFavourite: PropTypes.func,
+ onSetAllowedTemplates: PropTypes.func,
+ size: PropTypes.number
+ };
+
+ static defaultProps = {
+ active: false,
+ templatesLoaded: false,
+ templates: [],
+ allowedTemplates: [],
+ dockStyle: {},
+ onToggleControl: () => {},
+ onMergeTemplate: () => {},
+ onReplaceTemplate: () => {},
+ onToggleFavourite: () => {},
+ onSetAllowedTemplates: () => {},
+ size: 550
+ };
+
+ componentDidUpdate(prevProps) {
+ const { active, allowedTemplates, onSetAllowedTemplates } = this.props;
+ const { active: prevActive } = prevProps;
+ if (active !== prevActive) {
+ if (active) {
+ onSetAllowedTemplates(allowedTemplates);
+ }
}
- }, [ active ]);
- return (
- }
- style={{ height: 'calc(100% - 30px)' }}
- onClose={onToggleControl}>
- {!templatesLoaded &&
}
- {templatesLoaded && }
-
- );
-};
+
+ }
+
+ render() {
+ const {
+ active,
+ templates,
+ templatesLoaded,
+ onToggleControl,
+ onMergeTemplate,
+ onReplaceTemplate,
+ onToggleFavourite,
+ dockStyle,
+ size
+ } = this.props;
+ return (
+ }
+ style={dockStyle}
+ onClose={onToggleControl}
+ >
+ {!templatesLoaded &&
}
+ {templatesLoaded && }
+
+ );
+ }
+}
const MapTemplatesPlugin = connect(createSelector(
+ state => mapLayoutValuesSelector(state, { height: true, right: true }, true),
state => get(state, 'controls.mapTemplates.enabled'),
templatesSelector,
mapTemplatesLoadedSelector,
- (active, templates, templatesLoaded) => ({
+
+ (dockStyle, active, templates, templatesLoaded) => ({
active,
templates,
- templatesLoaded
+ templatesLoaded,
+ dockStyle
})
), {
onToggleControl: toggleControl.bind(null, 'mapTemplates', 'enabled'),
@@ -84,7 +131,7 @@ const MapTemplatesPlugin = connect(createSelector(
onReplaceTemplate: replaceTemplate,
onToggleFavourite: toggleFavouriteTemplate,
onSetAllowedTemplates: setAllowedTemplates
-})(mapTemplates);
+})(MapTemplatesComponent);
export default createPlugin('MapTemplates', {
component: MapTemplatesPlugin,
@@ -98,6 +145,17 @@ export default createPlugin('MapTemplates', {
priority: 2,
doNotHide: true,
tooltip:
+ },
+ SidebarMenu: {
+ name: 'mapTemplates',
+ position: 998,
+ text: ,
+ icon: ,
+ action: setControlProperty.bind(null, "mapTemplates", "enabled", true, true),
+ toggle: true,
+ priority: 1,
+ doNotHide: true,
+ tooltip: "mapTemplates.tooltip"
}
},
reducers: {
diff --git a/web/client/plugins/Measure.jsx b/web/client/plugins/Measure.jsx
index 76e4195439..9454bc8063 100644
--- a/web/client/plugins/Measure.jsx
+++ b/web/client/plugins/Measure.jsx
@@ -34,6 +34,7 @@ import {
import ConfigUtils from '../utils/ConfigUtils';
import Message from './locale/Message';
import { MeasureDialog } from './measure/index';
+import {mapLayoutValuesSelector} from "../selectors/maplayout";
const selector = (state) => {
return {
@@ -60,7 +61,8 @@ const selector = (state) => {
showAddAsLayer: isOpenlayers(state),
isCoordEditorEnabled: state.measurement && !state.measurement.isDrawing,
geomType: state.measurement && state.measurement.geomType,
- format: state.measurement && state.measurement.format
+ format: state.measurement && state.measurement.format,
+ dockStyle: mapLayoutValuesSelector(state, { height: true, right: true }, true)
};
};
const toggleMeasureTool = toggleControl.bind(null, 'measure', null);
@@ -126,7 +128,24 @@ export default {
tooltip: "measureComponent.tooltip",
text: ,
icon: ,
- action: () => setControlProperty("measure", "enabled", true)
+ action: () => setControlProperty("measure", "enabled", true),
+ doNotHide: true,
+ priority: 2
+ },
+ SidebarMenu: {
+ name: 'measurement',
+ position: 9,
+ panel: false,
+ help: ,
+ tooltip: "measureComponent.tooltip",
+ text: ,
+ icon: ,
+ action: toggleControl.bind(null, 'measure', null),
+ toggle: true,
+ toggleControl: 'measure',
+ toggleProperty: 'enabled',
+ doNotHide: true,
+ priority: 1
}
}),
reducers: {measurement: require('../reducers/measurement').default},
diff --git a/web/client/plugins/MetadataExplorer.jsx b/web/client/plugins/MetadataExplorer.jsx
index 5c907224ad..f22ce1ac20 100644
--- a/web/client/plugins/MetadataExplorer.jsx
+++ b/web/client/plugins/MetadataExplorer.jsx
@@ -12,7 +12,6 @@ import assign from 'object-assign';
import PropTypes from 'prop-types';
import React from 'react';
import { Glyphicon, Panel } from 'react-bootstrap';
-import ContainerDimensions from 'react-container-dimensions';
import { connect } from 'react-redux';
import { branch, compose, defaultProps, renderComponent, withProps } from 'recompose';
import { createStructuredSelector } from 'reselect';
@@ -47,10 +46,9 @@ import API from '../api/catalog';
import CatalogComp from '../components/catalog/Catalog';
import CatalogServiceEditor from '../components/catalog/CatalogServiceEditor';
import Message from '../components/I18N/Message';
-import DockPanel from '../components/misc/panels/DockPanel';
import { metadataSourceSelector, modalParamsSelector } from '../selectors/backgroundselector';
import {
- activeSelector,
+ isActiveSelector,
authkeyParamNameSelector,
groupSelector,
layerErrorSelector,
@@ -81,6 +79,7 @@ import { isLocalizedLayerStylesEnabledSelector } from '../selectors/localizedLay
import { projectionSelector } from '../selectors/map';
import { mapLayoutValuesSelector } from '../selectors/maplayout';
import { DEFAULT_FORMAT_WMS } from '../api/WMS';
+import ResponsivePanel from "../components/misc/panels/ResponsivePanel";
export const DEFAULT_ALLOWED_PROVIDERS = ["OpenStreetMap", "OpenSeaMap", "Stamen"];
@@ -93,8 +92,8 @@ const metadataExplorerSelector = createStructuredSelector({
services: servicesSelector,
servicesWithBackgrounds: servicesSelectorWithBackgrounds,
layerError: layerErrorSelector,
- active: activeSelector,
- dockStyle: state => mapLayoutValuesSelector(state, { height: true }),
+ active: isActiveSelector,
+ dockStyle: state => mapLayoutValuesSelector(state, { height: true, right: true }, true),
searchText: searchTextSelector,
group: groupSelector,
source: metadataSourceSelector,
@@ -191,7 +190,7 @@ class MetadataExplorerComponent extends React.Component {
zoomToLayer: true,
// side panel properties
- width: 660,
+ width: 550,
dockProps: {
dimMode: "none",
fluid: false,
@@ -217,24 +216,23 @@ class MetadataExplorerComponent extends React.Component {
/>
);
return (
-
-
- {({ width }) => ( 1 ? width : this.props.width}
- position="right"
- bsStyle="primary"
- title={}
- onClose={() => this.props.closeCatalog()}
- glyph="folder-open"
- zIndex={1031}
- style={this.props.dockStyle}>
-
- {panel}
-
- )}
-
-
+ }
+ onClose={() => this.props.closeCatalog()}
+ glyph="folder-open"
+ style={this.props.dockStyle}
+ >
+
+ {panel}
+
+
);
}
}
@@ -297,15 +295,19 @@ export default {
tooltip: "catalog.tooltip",
icon: ,
action: setControlProperty.bind(null, "metadataexplorer", "enabled", true, true),
- doNotHide: true
+ doNotHide: true,
+ priority: 2
},
- BackgroundSelector: {
- name: 'MetadataExplorer',
- doNotHide: true
- },
- TOC: {
- name: 'MetadataExplorer',
- doNotHide: true
+ SidebarMenu: {
+ name: 'metadataexplorer',
+ position: 5,
+ text: ,
+ tooltip: "catalog.tooltip",
+ icon: ,
+ action: setControlProperty.bind(null, "metadataexplorer", "enabled", true, true),
+ toggle: true,
+ doNotHide: true,
+ priority: 1
}
}),
reducers: {catalog: require('../reducers/catalog').default},
diff --git a/web/client/plugins/OmniBar.jsx b/web/client/plugins/OmniBar.jsx
index 8d84ffa267..55ab4b63bf 100644
--- a/web/client/plugins/OmniBar.jsx
+++ b/web/client/plugins/OmniBar.jsx
@@ -9,13 +9,14 @@ import React from 'react';
import PropTypes from 'prop-types';
import './omnibar/omnibar.css';
-import assign from 'object-assign';
import ToolsContainer from './containers/ToolsContainer';
+import {createPlugin} from "../utils/PluginsUtils";
class OmniBar extends React.Component {
static propTypes = {
className: PropTypes.string,
style: PropTypes.object,
+ containerWrapperStyle: PropTypes.object,
items: PropTypes.array,
id: PropTypes.string,
mapType: PropTypes.string
@@ -25,6 +26,7 @@ class OmniBar extends React.Component {
items: [],
className: "navbar-dx shadow",
style: {},
+ containerWrapperStyle: {},
id: "mapstore-navbar",
mapType: "leaflet"
};
@@ -49,6 +51,7 @@ class OmniBar extends React.Component {
render() {
return ( {props.children}
}
@@ -70,12 +73,12 @@ class OmniBar extends React.Component {
* @class
* @memberof plugins
*/
-export default {
- OmniBarPlugin: assign(
- OmniBar,
- {
+export default createPlugin(
+ 'OmniBar',
+ {
+ component: OmniBar,
+ options: {
disablePluginIf: "{state('featuregridmode') === 'EDIT' || (state('router') && state('router').includes('/geostory/shared') && state('geostorymode') !== 'edit')}"
}
- ),
- reducers: {}
-};
+ }
+);
diff --git a/web/client/plugins/Print.jsx b/web/client/plugins/Print.jsx
index e6cf0c9031..fcfcb92b87 100644
--- a/web/client/plugins/Print.jsx
+++ b/web/client/plugins/Print.jsx
@@ -32,6 +32,7 @@ import { getMessageById } from '../utils/LocaleUtils';
import { defaultGetZoomForExtent, getResolutions, mapUpdated, dpi2dpu, DEFAULT_SCREEN_DPI } from '../utils/MapUtils';
import { isInsideResolutionsLimits } from '../utils/LayersUtils';
import { has, includes } from 'lodash';
+import {additionalLayersSelector} from "../selectors/additionallayers";
/**
* Print plugin. This plugin allows to print current map view. **note**: this plugin requires the **printing module** to work.
@@ -337,7 +338,10 @@ export default {
UNSAFE_componentWillReceiveProps(nextProps) {
const hasBeenOpened = nextProps.open && !this.props.open;
const mapHasChanged = this.props.open && this.props.syncMapPreview && mapUpdated(this.props.map, nextProps.map);
- const specHasChanged = nextProps.printSpec.defaultBackground !== this.props.printSpec.defaultBackground;
+ const specHasChanged = (
+ nextProps.printSpec.defaultBackground !== this.props.printSpec.defaultBackground ||
+ nextProps.printSpec.additionalLayers !== this.props.printSpec.additionalLayers
+ );
if (hasBeenOpened || mapHasChanged || specHasChanged) {
this.configurePrintMap(nextProps);
}
@@ -608,18 +612,19 @@ export default {
(state) => state.print && state.print.error,
mapSelector,
layersSelector,
+ additionalLayersSelector,
scalesSelector,
(state) => state.browser && (!state.browser.ie || state.browser.ie11),
currentLocaleSelector,
mapTypeSelector
- ], (open, capabilities, printSpec, pdfUrl, error, map, layers, scales, usePreview, currentLocale, mapType) => ({
+ ], (open, capabilities, printSpec, pdfUrl, error, map, layers, additionalLayers, scales, usePreview, currentLocale, mapType) => ({
open,
capabilities,
printSpec,
pdfUrl,
error,
map,
- layers: layers.filter(l => !l.loadingError),
+ layers: [...layers.filter(l => !l.loadingError), ...(printSpec?.additionalLayers ? additionalLayers.map(l => l.options).filter(l => !l.loadingError) : [])],
scales,
usePreview,
currentLocale,
@@ -659,8 +664,19 @@ export default {
text: ,
icon: ,
action: toggleControl.bind(null, 'print', null),
- priority: 2,
+ priority: 3,
doNotHide: true
+ },
+ SidebarMenu: {
+ name: "print",
+ position: 3,
+ tooltip: "printbutton",
+ text: ,
+ icon: ,
+ action: toggleControl.bind(null, 'print', null),
+ doNotHide: true,
+ toggle: true,
+ priority: 2
}
}),
reducers: {print: printReducers}
diff --git a/web/client/plugins/Save.jsx b/web/client/plugins/Save.jsx
index 8541d1120a..d611804b28 100644
--- a/web/client/plugins/Save.jsx
+++ b/web/client/plugins/Save.jsx
@@ -39,7 +39,7 @@ export default createPlugin('Save', {
}))(SaveBaseDialog),
containers: {
BurgerMenu: {
- name: 'save',
+ name: 'mapSave',
position: 30,
text: ,
icon: ,
@@ -52,7 +52,28 @@ export default createPlugin('Save', {
(loggedIn, {canEdit, id} = {}) => ({
style: loggedIn && id && canEdit ? {} : { display: "none" }// the resource is new (no resource) or if present, is editable
})
- )
+ ),
+ priority: 2,
+ doNotHide: true
+ },
+ SidebarMenu: {
+ name: 'mapSave',
+ position: 30,
+ icon: ,
+ text: ,
+ action: toggleControl.bind(null, 'mapSave', null),
+ toggle: true,
+ tooltip: "saveDialog.saveTooltip",
+ // display the button only if the map can be edited
+ selector: createSelector(
+ isLoggedIn,
+ mapInfoSelector,
+ (loggedIn, {canEdit, id} = {}) => ({
+ style: loggedIn && id && canEdit ? {} : { display: "none" }// the resource is new (no resource) or if present, is editable
+ })
+ ),
+ priority: 1,
+ doNotHide: true
}
}
});
diff --git a/web/client/plugins/SaveAs.jsx b/web/client/plugins/SaveAs.jsx
index 15b1692a37..11388322e2 100644
--- a/web/client/plugins/SaveAs.jsx
+++ b/web/client/plugins/SaveAs.jsx
@@ -58,7 +58,28 @@ export default createPlugin('SaveAs', {
return indexOf(state.controls.saveAs.allowedRoles, state && state.security && state.security.user && state.security.user.role) !== -1 ? {} : { style: {display: "none"} };
}
return { style: isLoggedIn(state) ? {} : {display: "none"} };
- }
+ },
+ priority: 2,
+ doNotHide: true
+ },
+ SidebarMenu: {
+ name: 'saveAs',
+ position: 31,
+ icon: ,
+ text: ,
+ action: toggleControl.bind(null, 'mapSaveAs', null),
+ tooltip: "saveDialog.saveAsTooltip",
+ // display the button only if the map can be edited
+ selector: (state) => {
+ return {
+ style: isLoggedIn(state) ? {} : {display: "none"},
+ bsStyle: state.controls.mapSaveAs && state.controls.mapSaveAs.enabled ? 'primary' : 'tray',
+ active: state.controls.mapSaveAs && state.controls.mapSaveAs.enabled || false
+
+ };
+ },
+ priority: 1,
+ doNotHide: true
}
}
});
diff --git a/web/client/plugins/Search.jsx b/web/client/plugins/Search.jsx
index 27b43df416..2ddecf7c1b 100644
--- a/web/client/plugins/Search.jsx
+++ b/web/client/plugins/Search.jsx
@@ -6,13 +6,12 @@
* LICENSE file in the root directory of this source tree.
*/
-import { get, isArray } from 'lodash';
+import { get } from 'lodash';
import assign from 'object-assign';
import PropTypes from 'prop-types';
import React from 'react';
import { connect } from 'react-redux';
-import MediaQuery from 'react-responsive';
-import { createSelector } from 'reselect';
+import {createSelector, createStructuredSelector} from 'reselect';
import { removeAdditionalLayer } from '../actions/additionallayers';
import { configureMap } from '../actions/config';
@@ -46,10 +45,13 @@ import {
import mapInfoReducers from '../reducers/mapInfo';
import searchReducers from '../reducers/search';
import { layersSelector } from '../selectors/layers';
-import { mapSelector } from '../selectors/map';
+import {mapSelector, mapSizeValuesSelector} from '../selectors/map';
import ConfigUtils from '../utils/ConfigUtils';
import { defaultIconStyle } from '../utils/SearchUtils';
import ToggleButton from './searchbar/ToggleButton';
+import {mapLayoutValuesSelector} from "../selectors/maplayout";
+import {sidebarIsActiveSelector} from "../selectors/sidebarmenu";
+import classnames from "classnames";
const searchSelector = createSelector([
state => state.search || null,
@@ -110,7 +112,6 @@ const SearchResultList = connect(selector, {
* {
* "name": "Search",
* "cfg": {
- * "withToggle": ["max-width: 768px", "min-width: 768px"],
* "resultsStyle": {
* "iconUrl": "https://cdn.rawgit.com/pointhi/leaflet-color-markers/master/img/marker-icon-red.png",
* "shadowUrl": "https://cdnjs.cloudflare.com/ajax/libs/leaflet/1.3.1/images/marker-shadow.png",
@@ -286,8 +287,6 @@ An example to require the data api:
* Note that, in the following cases, the point used for GFI request is a point on surface of the geometry of the selected record
* - "single_layer", it performs the GFI request for one layer only with only that record as a result, info_format is forced to be application/json
* - "all_layers", it performs the GFI for all layers, as a normal GFI triggered by clicking on the map
-
-* @prop {array|boolean} cfg.withToggle when boolean, true uses a toggle to display the searchbar. When array, e.g `["max-width: 768px", "min-width: 768px"]`, `max-width` and `min-width` are the limits where to show/hide the toggle (useful for mobile)
*/
const SearchPlugin = connect((state) => ({
enabled: state.controls && state.controls.search && state.controls.search.enabled || false,
@@ -310,7 +309,9 @@ const SearchPlugin = connect((state) => ({
userServices: PropTypes.array,
withToggle: PropTypes.oneOfType([PropTypes.bool, PropTypes.array]),
enabled: PropTypes.bool,
- textSearchConfig: PropTypes.object
+ textSearchConfig: PropTypes.object,
+ style: PropTypes.object,
+ sidebarIsActive: PropTypes.bool
};
static defaultProps = {
@@ -328,7 +329,9 @@ const SearchPlugin = connect((state) => ({
},
fitResultsToMapSize: true,
withToggle: false,
- enabled: true
+ enabled: true,
+ style: {},
+ sidebarIsActive: false
};
componentDidMount() {
@@ -353,6 +356,12 @@ const SearchPlugin = connect((state) => ({
return selectedServices && selectedServices.length > 0 ? assign({}, searchOptions, {services: selectedServices}) : searchOptions;
};
+ searchFitToTheScreen = () => {
+ const { offsets: { right: rightOffset, left: leftOffset}, mapSize: { width: mapWidth = window.innerWidth } } = this.props;
+ // @todo make searchbar width configurable via configuration?
+ return (mapWidth - rightOffset - leftOffset - 60) >= 500;
+ }
+
getSearchAndToggleButton = () => {
const search = ( ({
placeholder={this.getServiceOverrides("placeholder")}
placeholderMsgId={this.getServiceOverrides("placeholderMsgId")}
/>);
- if (this.props.withToggle === true) {
- return [].concat(this.props.enabled ? [search] : null);
- }
- if (isArray(this.props.withToggle)) {
- return (
-
-
- {this.props.enabled ? search : null}
-
-
- {search}
-
-
- );
- }
- return search;
+ return (
+ !this.searchFitToTheScreen() ?
+ (
+ <>
+
+ {this.props.enabled ? search : null}
+ >
+ ) : (search)
+ );
};
render() {
- return (
+ return (
{this.getSearchAndToggleButton()}
({
});
export default {
- SearchPlugin: assign(SearchPlugin, {
- OmniBar: {
- name: 'search',
- position: 1,
- tool: true,
- priority: 1
- }
- }),
+ SearchPlugin: assign(
+ connect(createStructuredSelector({
+ style: state => mapLayoutValuesSelector(state, { right: true }),
+ offsets: state => mapLayoutValuesSelector(state, { right: true, left: true }),
+ mapSize: state => mapSizeValuesSelector({ width: true })(state),
+ sidebarIsActive: state => sidebarIsActiveSelector(state)
+ }), {})(SearchPlugin), {
+ OmniBar: {
+ name: 'search',
+ position: 1,
+ tool: true,
+ priority: 1
+ }
+ }),
epics: {searchEpic, searchOnStartEpic, searchItemSelected, zoomAndAddPointEpic, textSearchShowGFIEpic},
reducers: {
search: searchReducers,
diff --git a/web/client/plugins/Settings.jsx b/web/client/plugins/Settings.jsx
index 9829b00225..27214329a8 100644
--- a/web/client/plugins/Settings.jsx
+++ b/web/client/plugins/Settings.jsx
@@ -207,6 +207,17 @@ export default {
tooltip: "settingsTooltip",
icon: ,
action: toggleControl.bind(null, 'settings', null),
+ priority: 4,
+ doNotHide: true
+ },
+ SidebarMenu: {
+ name: 'settings',
+ position: 100,
+ tooltip: "settingsTooltip",
+ text: ,
+ icon: ,
+ toggle: true,
+ action: toggleControl.bind(null, 'settings', null),
priority: 3,
doNotHide: true
}
diff --git a/web/client/plugins/ShapeFile.jsx b/web/client/plugins/ShapeFile.jsx
index b6503c383b..f4e521da68 100644
--- a/web/client/plugins/ShapeFile.jsx
+++ b/web/client/plugins/ShapeFile.jsx
@@ -85,6 +85,16 @@ export default createPlugin(
text: ,
icon: ,
action: toggleControl.bind(null, 'shapefile', null),
+ priority: 3,
+ doNotHide: true
+ },
+ SidebarMenu: {
+ name: 'shapefile',
+ position: 4,
+ text: ,
+ icon: ,
+ action: toggleControl.bind(null, 'shapefile', null),
+ toggle: true,
priority: 2,
doNotHide: true
}
diff --git a/web/client/plugins/Share.jsx b/web/client/plugins/Share.jsx
index 9919dad64e..565a367427 100644
--- a/web/client/plugins/Share.jsx
+++ b/web/client/plugins/Share.jsx
@@ -106,13 +106,24 @@ export const SharePlugin = assign(Share, {
BurgerMenu: {
name: 'share',
position: 1000,
- priority: 1,
+ priority: 2,
doNotHide: true,
text: ,
tooltip: "share.tooltip",
icon: ,
action: toggleControl.bind(null, 'share', null)
},
+ SidebarMenu: {
+ name: 'share',
+ position: 1000,
+ priority: 1,
+ doNotHide: true,
+ tooltip: "share.tooltip",
+ text: ,
+ icon: ,
+ action: toggleControl.bind(null, 'share', null),
+ toggle: true
+ },
Toolbar: {
name: 'share',
alwaysVisible: true,
diff --git a/web/client/plugins/SidebarMenu.jsx b/web/client/plugins/SidebarMenu.jsx
new file mode 100644
index 0000000000..95695ff03a
--- /dev/null
+++ b/web/client/plugins/SidebarMenu.jsx
@@ -0,0 +1,305 @@
+/*
+ * Copyright 2022, GeoSolutions Sas.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree.
+ */
+import React from 'react';
+
+import PropTypes from 'prop-types';
+import ContainerDimensions from 'react-container-dimensions';
+import {DropdownButton, Glyphicon, MenuItem} from "react-bootstrap";
+import {connect} from "react-redux";
+import assign from "object-assign";
+import {createSelector} from "reselect";
+import {bindActionCreators} from "redux";
+
+import ToolsContainer from "./containers/ToolsContainer";
+import SidebarElement from "../components/sidebarmenu/SidebarElement";
+import {mapLayoutValuesSelector} from "../selectors/maplayout";
+import tooltip from "../components/misc/enhancers/tooltip";
+import {setControlProperty} from "../actions/controls";
+import {createPlugin} from "../utils/PluginsUtils";
+import sidebarMenuReducer from "../reducers/sidebarmenu";
+import sidebarMenuEpics from "../epics/sidebarmenu";
+
+import './sidebarmenu/sidebarmenu.less';
+import {lastActiveToolSelector, sidebarIsActiveSelector} from "../selectors/sidebarmenu";
+import {setLastActiveItem} from "../actions/sidebarmenu";
+import Message from "../components/I18N/Message";
+
+const TDropdownButton = tooltip(DropdownButton);
+
+class SidebarMenu extends React.Component {
+ static propTypes = {
+ className: PropTypes.string,
+ style: PropTypes.object,
+ items: PropTypes.array,
+ id: PropTypes.string,
+ mapType: PropTypes.string,
+ onInit: PropTypes.func,
+ onDetach: PropTypes.func,
+ sidebarWidth: PropTypes.number,
+ state: PropTypes.object,
+ setLastActiveItem: PropTypes.func,
+ lastActiveTool: PropTypes.oneOfType([PropTypes.string, PropTypes.bool])
+ };
+
+ static contextTypes = {
+ messages: PropTypes.object,
+ router: PropTypes.object
+ };
+
+ static defaultProps = {
+ items: [],
+ style: {},
+ id: "mapstore-sidebar-menu",
+ mapType: "openlayers",
+ onInit: () => {},
+ onDetach: () => {},
+ eventSelector: "onClick",
+ toolStyle: "default",
+ activeStyle: "primary",
+ stateSelector: 'sidebarMenu',
+ tool: SidebarElement,
+ toolCfg: {},
+ sidebarWidth: 40
+ };
+
+ constructor() {
+ super();
+ this.defaultTool = SidebarElement;
+ this.defaultTarget = 'sidebar';
+ this.state = {
+ lastVisible: false,
+ hidden: false
+ };
+ }
+
+ componentDidMount() {
+ const { onInit } = this.props;
+ onInit();
+ }
+
+ shouldComponentUpdate(nextProps) {
+ const newSize = nextProps.state.map?.present?.size?.height !== this.props.state.map?.present?.size?.height;
+ const newHeight = nextProps.style.bottom !== this.props.style.bottom;
+ const newItems = nextProps.items !== this.props.items;
+ const burgerMenuState = nextProps.state?.controls?.burgermenu?.enabled !== this.props.state?.controls?.burgermenu?.enabled;
+ const newVisibleItems = !newItems ? nextProps.items.reduce((prev, cur, idx) => {
+ if (this.isNotHidden(cur, nextProps.state) !== this.isNotHidden(this.props.items[idx], this.props.state)) {
+ prev.push(cur);
+ }
+ return prev;
+ }, []).length > 0 : false;
+ return newSize || newItems || newVisibleItems || newHeight || burgerMenuState;
+ }
+
+ componentDidUpdate(prevProps) {
+ const { onInit, onDetach } = this.props;
+ const { hidden } = this.state;
+ const visibleElements = this.visibleItems('sidebar').length;
+ visibleElements && prevProps.isActive === false && onInit();
+
+ if (visibleElements === 0 && !hidden) {
+ onDetach();
+ this.setState((state) => ({ ...state, hidden: true}));
+ } else if (visibleElements > 0 && hidden) {
+ onInit();
+ this.setState((state) => ({ ...state, hidden: false}));
+ }
+ }
+
+ componentWillUnmount() {
+ const { onDetach } = this.props;
+ onDetach();
+ }
+
+ getStyle = (style) => {
+ const hasBottomOffset = parseInt(style?.bottom, 10) !== 0;
+ return { ...style, height: hasBottomOffset ? 'auto' : '100%', maxHeight: style?.height ?? null, bottom: hasBottomOffset ? `calc(${style.bottom} + 30px)` : null };
+ };
+
+ getPanels = items => {
+ return items.filter((item) => item.panel)
+ .map((item) => assign({}, item, {panel: item.panel === true ? item.plugin : item.panel})).concat(
+ items.filter((item) => item.tools).reduce((previous, current) => {
+ return previous.concat(
+ current.tools.map((tool, index) => ({
+ name: current.name + index,
+ panel: tool,
+ cfg: current.cfg.toolsCfg ? current.cfg.toolsCfg[index] : {}
+ }))
+ );
+ }, [])
+ );
+ };
+
+ visibleItems = (target) => {
+ return this.props.items.reduce(( prev, current) => {
+ if (!current?.components && this.targetMatch(target, current.target)
+ && this.isNotHidden(current, this.props.state)
+ ) {
+ prev.push({
+ ...current,
+ target
+ });
+ return prev;
+ }
+ if (current?.components && Array.isArray(current.components)) {
+ current.components.forEach((component) => {
+ if (this.targetMatch(target, component?.target)
+ && this.isNotHidden(component?.selector ? component : current, this.props.state)
+ ) {
+ prev.push({
+ plugin: current?.plugin || this.defaultTool,
+ position: current?.position,
+ cfg: current?.cfg,
+ name: current.name,
+ help: current?.help,
+ items: current?.items,
+ ...component
+ });
+ }
+ return prev;
+ });
+ }
+ return prev;
+ }, []);
+ }
+
+ getItems = (_target, height) => {
+ const itemsToRender = Math.floor(height / this.props.sidebarWidth) - 1;
+ const target = _target ? _target : this.defaultTarget;
+ const filtered = this.visibleItems(target);
+
+ if (itemsToRender < filtered.length) {
+ const sorted = filtered.sort((i1, i2) => (i1.position ?? 0) - (i2.position ?? 0));
+ this.swapLastActiveItem(sorted, itemsToRender);
+ const toRender = sorted.slice(0, itemsToRender);
+ const extra = {
+ name: "moreItems",
+ position: 9999,
+ icon: ,
+ tool: () => this.renderExtraItems(filtered.slice(itemsToRender)),
+ priority: 1
+ };
+ toRender.splice(itemsToRender, 0, extra);
+ return toRender;
+ }
+
+ return filtered.sort((i1, i2) => (i1.position ?? 0) - (i2.position ?? 0));
+ };
+
+ targetMatch = (target, elementTarget) => elementTarget === target || !elementTarget && target === this.defaultTarget;
+
+ getTools = (namespace = 'sidebar', height) => {
+ return this.getItems(namespace, height).sort((a, b) => a.position - b.position);
+ };
+
+ renderExtraItems = (items) => {
+ const dummySelector = () => {};
+ const menuItems = items.map((item) => {
+ const ConnectedItem = connect((item?.selector ?? dummySelector),
+ (dispatch, ownProps) => {
+ const actions = {};
+ if (ownProps.action) {
+ actions.onClick = () => {
+ this.props.setLastActiveItem(item?.name ?? item?.toggleProperty);
+ bindActionCreators(ownProps.action, dispatch)();
+ };
+ }
+ return actions;
+ })(MenuItem);
+ return {item?.icon}{item?.text};
+ });
+ return (
+ }
+ tooltipPosition="left"
+ title={}
+ >
+ {menuItems}
+ );
+ };
+
+ render() {
+ return this.state.hidden ? false : (
+
+
+ );
+ }
+
+ swapLastActiveItem = (items, itemsToRender) => {
+ const name = this.props.lastActiveTool;
+ if (name) {
+ const idx = items.findIndex((el) => el?.name === name || el?.toggleProperty === name);
+ if (idx !== -1 && idx > (itemsToRender - 1)) {
+ const item = items[idx];
+ items[idx] = items[itemsToRender - 1];
+ items[itemsToRender - 2] = item;
+ }
+ }
+ }
+
+
+ isNotHidden = (element, state) => {
+ return element?.selector ? element.selector(state)?.style?.display !== 'none' : true;
+ };
+}
+
+const sidebarMenuSelector = createSelector([
+ state => state,
+ state => lastActiveToolSelector(state),
+ state => mapLayoutValuesSelector(state, {bottom: true, height: true}),
+ sidebarIsActiveSelector
+], (state, lastActiveTool, style, isActive) => ({
+ style,
+ lastActiveTool,
+ state,
+ isActive
+}));
+
+/**
+ * Generic bar that can contains other plugins.
+ * used by {@link #plugins.Login|Login}, {@link #plugins.Home|Home},
+ * {@link #plugins.Login|Login} and many other, on map viewer pages.
+ * @name SidebarMenu
+ * @class
+ * @memberof plugins
+ */
+export default createPlugin(
+ 'SidebarMenu',
+ {
+ cfg: {},
+ component: connect(sidebarMenuSelector, {
+ onInit: setControlProperty.bind(null, 'sidebarMenu', 'enabled', true),
+ onDetach: setControlProperty.bind(null, 'sidebarMenu', 'enabled', false),
+ setLastActiveItem
+ })(SidebarMenu),
+ epics: sidebarMenuEpics,
+ reducers: {
+ sidebarmenu: sidebarMenuReducer
+ }
+ }
+);
diff --git a/web/client/plugins/Snapshot.jsx b/web/client/plugins/Snapshot.jsx
index 5c681e4fa1..298d7567c1 100644
--- a/web/client/plugins/Snapshot.jsx
+++ b/web/client/plugins/Snapshot.jsx
@@ -83,6 +83,17 @@ export default {
action: toggleControl.bind(null, 'snapshot', null),
tools: [SnapshotPlugin],
priority: 2
+ },
+ SidebarMenu: {
+ name: 'snapshot',
+ position: 3,
+ panel: SnapshotPanel,
+ text: ,
+ icon: ,
+ tooltip: "snapshot.tooltip",
+ action: toggleControl.bind(null, 'snapshot', null),
+ toggle: true,
+ priority: 1
}
}),
reducers: {
diff --git a/web/client/plugins/StreetView/StreetView.jsx b/web/client/plugins/StreetView/StreetView.jsx
index 2535f2ff52..cd14a73602 100644
--- a/web/client/plugins/StreetView/StreetView.jsx
+++ b/web/client/plugins/StreetView/StreetView.jsx
@@ -72,6 +72,22 @@ export default createPlugin(
tooltip: "streetView.tooltip",
icon: ,
action: () => toggleStreetView()
+ },
+ SidebarMenu: {
+ position: 40,
+ priority: 1,
+ doNotHide: true,
+ name: CONTROL_NAME,
+ text: ,
+ tooltip: "streetView.tooltip",
+ icon: ,
+ action: () => toggleStreetView(),
+ selector: (state) => {
+ return {
+ bsStyle: state.controls["street-view"] && state.controls["street-view"].enabled ? 'primary' : 'tray',
+ active: state.controls["street-view"] && state.controls["street-view"].enabled || false
+ };
+ }
}
}
}
diff --git a/web/client/plugins/TOC.jsx b/web/client/plugins/TOC.jsx
index f2741dfda4..bdc76aa905 100644
--- a/web/client/plugins/TOC.jsx
+++ b/web/client/plugins/TOC.jsx
@@ -64,7 +64,7 @@ import { isObject, head, find, round } from 'lodash';
import { setControlProperties, setControlProperty } from '../actions/controls';
import { createWidget } from '../actions/widgets';
import { getMetadataRecordById } from '../actions/catalog';
-import { activeSelector } from '../selectors/catalog';
+import { isActiveSelector } from '../selectors/catalog';
import { isCesium } from '../selectors/maptype';
const addFilteredAttributesGroups = (nodes, filters) => {
@@ -106,7 +106,7 @@ const tocSelector = createSelector(
layerFilterSelector,
layersSelector,
mapNameSelector,
- activeSelector,
+ isActiveSelector,
widgetBuilderAvailable,
generalInfoFormatSelector,
isCesium,
@@ -129,9 +129,10 @@ const tocSelector = createSelector(
selectedNodes,
filterText,
generalInfoFormat,
+ layers,
selectedLayers: layers.filter((l) => head(selectedNodes.filter(s => s === l.id))),
noFilterResults: layers.filter((l) => filterLayersByTitle(l, filterText, currentLocale)).length === 0,
- updatableLayersCount: layers.filter(l => l.group !== 'background' && (l.type === 'wms' || l.type === 'wmts')).length,
+ updatableLayersCount: layers.filter(l => l.group !== 'background' && (l.type === 'wms' || l.type === 'wmts')).length > 0,
selectedGroups: selectedNodes.map(n => getNode(groups, n)).filter(n => n && n.nodes),
mapName,
filteredGroups: addFilteredAttributesGroups(groups, [
@@ -174,6 +175,7 @@ class LayerTree extends React.Component {
static propTypes = {
id: PropTypes.number,
items: PropTypes.array,
+ layers: PropTypes.array,
buttonContent: PropTypes.node,
groups: PropTypes.array,
settings: PropTypes.object,
@@ -268,6 +270,7 @@ class LayerTree extends React.Component {
static defaultProps = {
items: [],
+ layers: [],
groupPropertiesChangeHandler: () => {},
layerPropertiesChangeHandler: () => {},
retrieveLayerData: () => {},
@@ -415,6 +418,7 @@ class LayerTree extends React.Component {
target === "toolbar")}
groups={this.props.groups}
+ layers={this.props.layers}
selectedLayers={this.props.selectedLayers}
selectedGroups={this.props.selectedGroups}
generalInfoFormat={this.props.generalInfoFormat}
diff --git a/web/client/plugins/Tutorial.jsx b/web/client/plugins/Tutorial.jsx
index e187c06863..f203c4f8bc 100644
--- a/web/client/plugins/Tutorial.jsx
+++ b/web/client/plugins/Tutorial.jsx
@@ -171,6 +171,23 @@ export default {
action: toggleTutorial,
priority: 2,
doNotHide: true
+ },
+ SidebarMenu: {
+ name: 'tutorial',
+ position: 1200,
+ tooltip: "tutorial.title",
+ text: ,
+ icon: ,
+ action: toggleTutorial,
+ selector: (state) => {
+ return {
+ bsStyle: state.tutorial.enabled ? 'primary' : 'tray',
+ active: state.tutorial.enabled || false
+
+ };
+ },
+ priority: 1,
+ doNotHide: true
}
}),
reducers: {
diff --git a/web/client/plugins/UserExtensions.jsx b/web/client/plugins/UserExtensions.jsx
index 54f78c52ad..a4155de03b 100644
--- a/web/client/plugins/UserExtensions.jsx
+++ b/web/client/plugins/UserExtensions.jsx
@@ -7,41 +7,73 @@
*/
import React from 'react';
+import PropTypes from "prop-types";
import { connect } from 'react-redux';
-import { createPlugin } from '../utils/PluginsUtils';
+import { createSelector } from 'reselect';
+import get from 'lodash/get';
+
import { Glyphicon } from 'react-bootstrap';
import Message from '../components/I18N/Message';
+import ExtensionsPanel from './userExtensions/ExtensionsPanel';
+import { createPlugin } from '../utils/PluginsUtils';
import { setControlProperty, toggleControl } from '../actions/controls';
+import * as epics from '../epics/userextensions';
+import {mapLayoutValuesSelector} from "../selectors/maplayout";
+import ResponsivePanel from "../components/misc/panels/ResponsivePanel";
-import { createSelector } from 'reselect';
-import get from 'lodash/get';
-import DockPanel from '../components/misc/panels/DockPanel';
-import ExtensionsPanel from './userExtensions/ExtensionsPanel';
+class Extensions extends React.Component {
+ static propTypes = {
+ active: PropTypes.bool,
+ onClose: PropTypes.func,
+ dockStyle: PropTypes.object,
+ size: PropTypes.number
+ }
-const Extensions = ({
- active,
- onClose = () => { }
-}) => (
- }
- onClose={() => onClose()}
- glyph="plug"
- style={{ height: 'calc(100% - 30px)' }}>
-
- );
+ static defaultProps = {
+ active: false,
+ onClose: () => {},
+ dockStyle: {},
+ size: 550
+ }
+
+ render() {
+ let {
+ active,
+ onClose,
+ dockStyle,
+ size
+ } = this.props;
+ return (
+ }
+ onClose={() => onClose()}
+ glyph="plug"
+ style={dockStyle}
+ >
+
+
+ );
+ }
+}
const ExtensionsPlugin = connect(
- createSelector([
- state => get(state, 'controls.userExtensions.enabled')
- ],
- (active, extensions) => ({ active, extensions })),
+ createSelector(
+ state => get(state, 'controls.userExtensions.enabled'),
+ state => mapLayoutValuesSelector(state, { height: true, right: true }, true),
+ (active, dockStyle) => ({
+ active,
+ dockStyle
+ })),
{
onClose: toggleControl.bind(null, 'userExtensions', 'enabled')
}
@@ -67,6 +99,18 @@ export default createPlugin('UserExtensions', {
action: setControlProperty.bind(null, "userExtensions", "enabled", true, true),
priority: 2,
doNotHide: true
+ },
+ SidebarMenu: {
+ name: 'userExtensions',
+ position: 999,
+ tooltip: "userExtensions.title",
+ icon: ,
+ text: ,
+ action: setControlProperty.bind(null, "userExtensions", "enabled", true, true),
+ priority: 1,
+ doNotHide: true,
+ toggle: true
}
- }
+ },
+ epics
});
diff --git a/web/client/plugins/UserSession.jsx b/web/client/plugins/UserSession.jsx
index 4385ac24d2..e361a0598f 100644
--- a/web/client/plugins/UserSession.jsx
+++ b/web/client/plugins/UserSession.jsx
@@ -104,6 +104,19 @@ export default createPlugin('UserSession', {
},
priority: 2,
doNotHide: true
+ },
+ SidebarMenu: {
+ name: 'UserSession',
+ position: 1500,
+ icon: ,
+ text: ,
+ action: toggleControl.bind(null, 'resetUserSession', null),
+ tooltip: "userSession.tooltip",
+ selector: (state) => {
+ return { style: hasSession(state) ? {} : {display: "none"} };
+ },
+ priority: 1,
+ doNotHide: true
}
},
reducers: {
diff --git a/web/client/plugins/Widgets.jsx b/web/client/plugins/Widgets.jsx
index f0921cfe7f..ee033c8158 100644
--- a/web/client/plugins/Widgets.jsx
+++ b/web/client/plugins/Widgets.jsx
@@ -21,10 +21,11 @@ import { editWidget, updateWidgetProperty, deleteWidget, changeLayout, exportCSV
import editOptions from './widgets/editOptions';
import autoDisableWidgets from './widgets/autoDisableWidgets';
-const RIGHT_MARGIN = 70;
+const RIGHT_MARGIN = 55;
import { widthProvider, heightProvider } from '../components/layout/enhancers/gridLayout';
import WidgetsViewBase from '../components/widgets/view/WidgetsView';
+import {mapLayoutValuesSelector} from "../selectors/maplayout";
const WidgetsView =
compose(
@@ -35,12 +36,14 @@ compose(
getFloatingWidgetsLayout,
getMaximizedState,
dependenciesSelector,
- (id, widgets, layouts, maximized, dependencies) => ({
+ (state) => mapLayoutValuesSelector(state, { right: true}),
+ (id, widgets, layouts, maximized, dependencies, mapLayout) => ({
id,
widgets,
layouts,
maximized,
- dependencies
+ dependencies,
+ mapLayout
})
), {
editWidget,
@@ -57,7 +60,8 @@ compose(
compose(
heightProvider({ debounceTime: 20, closest: true, querySelector: '.fill' }),
widthProvider({ overrideWidthProvider: false }),
- withProps(({width, height, maximized} = {}) => {
+ withProps(({width, height, maximized, mapLayout} = {}) => {
+ const rightOffset = mapLayout?.right ?? 0;
const divHeight = height - 120;
const nRows = 4;
const rowHeight = Math.floor(divHeight / nRows - 20);
@@ -78,7 +82,7 @@ compose(
breakpoints: { xxs: 0 },
cols: { xxs: 1 }
} : {};
- const viewWidth = width && width > 800 ? width - (500 + RIGHT_MARGIN) : width - RIGHT_MARGIN;
+ const viewWidth = width && width > 800 ? width - (500 + rightOffset + RIGHT_MARGIN) : width - rightOffset - RIGHT_MARGIN;
const widthOptions = width ? {width: viewWidth - 1} : {};
return ({
rowHeight,
diff --git a/web/client/plugins/__tests__/MapTemplates-test.jsx b/web/client/plugins/__tests__/MapTemplates-test.jsx
index 6995d661eb..74fc75b677 100644
--- a/web/client/plugins/__tests__/MapTemplates-test.jsx
+++ b/web/client/plugins/__tests__/MapTemplates-test.jsx
@@ -33,7 +33,7 @@ describe('MapTemplates Plugins', () => {
}
});
ReactDOM.render(, document.getElementById("container"));
- expect(document.getElementsByClassName('map-templates-panel')[0]).toExist();
+ expect(document.getElementsByClassName('map-templates-loader')[0]).toExist();
});
it('shows MapTemplates loaded, empty', () => {
const { Plugin } = getPluginForTest(MapTemplates, {
diff --git a/web/client/plugins/__tests__/Share-test.jsx b/web/client/plugins/__tests__/Share-test.jsx
index 863a275ab5..0ed60e5fa3 100644
--- a/web/client/plugins/__tests__/Share-test.jsx
+++ b/web/client/plugins/__tests__/Share-test.jsx
@@ -55,12 +55,14 @@ describe('Share Plugin', () => {
};
const { containers } = getPluginForTest(SharePlugin, { controls }, {
ToolbarPlugin: {},
- BurgerMenuPlugin: {}
+ BurgerMenuPlugin: {},
+ SidebarMenuPlugin: {}
});
- expect(Object.keys(containers).length).toBe(2);
- expect(Object.keys(containers)).toEqual(['BurgerMenu', 'Toolbar']);
+ expect(Object.keys(containers).length).toBe(3);
+ expect(Object.keys(containers)).toEqual(['BurgerMenu', 'SidebarMenu', 'Toolbar']);
expect(containers.Toolbar).toContain({alwaysVisible: true, doNotHide: true});
- expect(containers.BurgerMenu).toContain({position: 1000, priority: 1, doNotHide: true});
+ expect(containers.BurgerMenu).toContain({position: 1000, priority: 2, doNotHide: true});
+ expect(containers.SidebarMenu).toContain({position: 1000, priority: 1, doNotHide: true});
});
it('test Share plugin on close', (done) => {
diff --git a/web/client/plugins/__tests__/SidebarMenu-test.jsx b/web/client/plugins/__tests__/SidebarMenu-test.jsx
new file mode 100644
index 0000000000..331c04d3a2
--- /dev/null
+++ b/web/client/plugins/__tests__/SidebarMenu-test.jsx
@@ -0,0 +1,42 @@
+/*
+ * Copyright 2022, GeoSolutions Sas.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree.
+ */
+import expect from 'expect';
+import React from 'react';
+import ReactDOM from 'react-dom';
+import SidebarMenu from "../SidebarMenu";
+import { getPluginForTest } from './pluginsTestUtils';
+
+describe('SidebarMenu Plugin', () => {
+ beforeEach(() => {
+ document.body.innerHTML = '';
+ });
+
+ afterEach(() => {
+ ReactDOM.unmountComponentAtNode(document.getElementById("container"));
+ document.body.innerHTML = '';
+ });
+
+ it('default configuration', () => {
+ document.getElementById('container').style.height = '600px';
+ const { Plugin } = getPluginForTest(SidebarMenu, {});
+ const items = [{
+ name: 'test',
+ position: 1,
+ text: 'Test Item'
+ }, {
+ name: 'test2',
+ position: 2,
+ text: 'Test Item 2'
+ }];
+ ReactDOM.render(, document.getElementById("container"));
+ const sidebarMenuContainer = document.getElementById('mapstore-sidebar-menu-container');
+ expect(sidebarMenuContainer).toExist();
+ const elements = document.querySelectorAll('#mapstore-sidebar-menu > button, #mapstore-sidebar-menu #extra-items + .dropdown-menu li');
+ expect(elements.length).toBe(2);
+ });
+});
diff --git a/web/client/plugins/burgermenu/burgermenu.css b/web/client/plugins/burgermenu/burgermenu.css
index 095e7384d5..e53e54342a 100644
--- a/web/client/plugins/burgermenu/burgermenu.css
+++ b/web/client/plugins/burgermenu/burgermenu.css
@@ -8,7 +8,7 @@
display: none;
position: absolute;
left: -160px;
- top: 0px;
+ top: 0;
width: 160px;
box-shadow: 0 6px 12px rgba(0, 0, 0, 0.175);
}
diff --git a/web/client/plugins/login/index.js b/web/client/plugins/login/index.js
index 525197a2ca..4d69cc4221 100644
--- a/web/client/plugins/login/index.js
+++ b/web/client/plugins/login/index.js
@@ -5,9 +5,6 @@
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree.
*/
-import React from 'react';
-import { Glyphicon } from 'react-bootstrap';
-
import { setControlProperty } from '../../actions/controls';
import { checkPendingChanges } from '../../actions/pendingChanges';
import { changePassword, login, loginFail, logout, logoutWithReload, resetError } from '../../actions/security';
@@ -73,9 +70,6 @@ export const Login = connect((state) => ({
export const LoginNav = connect((state) => ({
user: state.security && state.security.user,
nav: false,
- renderButtonText: false,
- renderButtonContent: () => {return ; },
- bsStyle: "primary",
className: "square-button",
renderUnsavedMapChangesDialog: ConfigUtils.getConfigProp('unsavedMapChangesDialog'),
displayUnsavedDialog: unsavedMapSelector(state)
diff --git a/web/client/plugins/manager/ManagerMenu.jsx b/web/client/plugins/manager/ManagerMenu.jsx
index 5b23335ffc..b4ca186ae6 100644
--- a/web/client/plugins/manager/ManagerMenu.jsx
+++ b/web/client/plugins/manager/ManagerMenu.jsx
@@ -54,7 +54,7 @@ class ManagerMenu extends React.Component {
};
static defaultProps = {
- id: "mapstore-burger-menu",
+ id: "mapstore-manager-menu",
entries: [{
"msgId": "users.title",
"glyph": "1-group-mod",
diff --git a/web/client/plugins/maploading/maploading.css b/web/client/plugins/maploading/maploading.css
index 3010c0d89e..d16bf1f56b 100644
--- a/web/client/plugins/maploading/maploading.css
+++ b/web/client/plugins/maploading/maploading.css
@@ -31,6 +31,4 @@
.ms2-loading .sk-circle-wrapper {
width: 30px;
height: 30px;
- margin-left: 10px !important;
- margin-top: 10px !important;
}
diff --git a/web/client/plugins/metadataexplorer/css/style.css b/web/client/plugins/metadataexplorer/css/style.css
index eecebca4b1..c36b58cf14 100644
--- a/web/client/plugins/metadataexplorer/css/style.css
+++ b/web/client/plugins/metadataexplorer/css/style.css
@@ -17,9 +17,6 @@ div.record-grid .record-item .panel-body{
#mapstore-catalog-panel .record-item {
min-height: 150px;
}
-#catalog-root {
- position: static!important;
-}
/*
!important is needed because the library we used
diff --git a/web/client/plugins/print/index.js b/web/client/plugins/print/index.js
index 38b85e9240..6c085a37e5 100644
--- a/web/client/plugins/print/index.js
+++ b/web/client/plugins/print/index.js
@@ -125,6 +125,16 @@ export const DefaultBackgrounOption = connect((state) => ({
onChangeParameter: setPrintParameter
})(Option);
+export const AdditionalLayers = connect((state) => ({
+ spec: state.print?.spec || {},
+ path: "",
+ property: "additionalLayers",
+ additionalProperty: false,
+ label: "print.additionalLayers"
+}), {
+ onChangeParameter: setPrintParameter
+})(Option);
+
export const PrintSubmit = connect((state) => ({
spec: state?.print?.spec || {},
loading: state.print && state.print.isLoading || false,
@@ -140,6 +150,7 @@ export const PrintPreview = connect((state) => ({
scale: state.controls && state.controls.print && state.controls.print.viewScale || 0.5,
currentPage: state.controls && state.controls.print && state.controls.print.currentPage || 0,
pages: state.controls && state.controls.print && state.controls.print.pages || 1,
+ additionalLayers: state.print?.spec?.additionalLayers ?? false,
outputFormat: state.print?.spec?.outputFormat || "pdf"
}), {
back: printCancel,
@@ -178,6 +189,13 @@ export const standardItems = {
"projections": [{"name": "EPSG:3857", "value": "EPSG:3857"}, {"name": "EPSG:4326", "value": "EPSG:4326"}]
},
position: 4
+ }, {
+ id: "overlayLayers",
+ plugin: AdditionalLayers,
+ cfg: {
+ enabled: false
+ },
+ position: 5
}],
"left-panel-accordion": [{
id: "layout",
diff --git a/web/client/plugins/sidebarmenu/sidebarmenu.less b/web/client/plugins/sidebarmenu/sidebarmenu.less
new file mode 100644
index 0000000000..04fdf249a6
--- /dev/null
+++ b/web/client/plugins/sidebarmenu/sidebarmenu.less
@@ -0,0 +1,37 @@
+@import '../../themes/default/ms-variables.less';
+
+#mapstore-sidebar-menu-container {
+ z-index: 1030;
+ position: absolute;
+ background: inherit;
+ right: 0;
+ top: 0;
+ width: @square-btn-size;
+ height: 100%;
+
+ #mapstore-sidebar-menu {
+ position: absolute;
+ top: 0;
+ right: 0;
+ height: auto;
+ z-index: 10;
+
+ & > .btn-tray, & > .btn,
+ & > .btn-group .btn {
+ border-bottom: 0;
+ height: @square-btn-size;
+ width: @square-btn-size;
+
+ span:not(.glyphicon) {
+ display: none;
+ }
+ }
+
+ .snapshot-panel {
+ position: absolute;
+ right: @square-btn-size;
+ top: 60px;
+ background: #ffffffab;
+ }
+ }
+}
diff --git a/web/client/plugins/widgets/WidgetsTray.jsx b/web/client/plugins/widgets/WidgetsTray.jsx
index 7dd473a36e..9dfbba6333 100644
--- a/web/client/plugins/widgets/WidgetsTray.jsx
+++ b/web/client/plugins/widgets/WidgetsTray.jsx
@@ -20,6 +20,7 @@ import { filterHiddenWidgets } from './widgetsPermission';
import BorderLayout from '../../components/layout/BorderLayout';
import WidgetsBar from './WidgetsBar';
import BButton from '../../components/misc/Button';
+import {mapLayoutValuesSelector} from "../../selectors/maplayout";
const Button = tooltip(BButton);
@@ -78,20 +79,22 @@ class WidgetsTray extends React.Component {
toolsOptions: PropTypes.object,
items: PropTypes.array,
expanded: PropTypes.bool,
- setExpanded: PropTypes.func
+ setExpanded: PropTypes.func,
+ layout: PropTypes.object
};
static defaultProps = {
enabled: true,
items: [],
expanded: false,
- setExpanded: () => { }
+ setExpanded: () => { },
+ layout: {}
};
render() {
return this.props.enabled
? ( ({ widgets })
+ (state) => mapLayoutValuesSelector(state, { right: true }),
+ (widgets, layout = []) => ({ widgets, layout })
), {
toggleTray
}),
diff --git a/web/client/product/plugins.js b/web/client/product/plugins.js
index 8587c54073..0ffba31ac9 100644
--- a/web/client/product/plugins.js
+++ b/web/client/product/plugins.js
@@ -126,7 +126,9 @@ export default {
WidgetsTrayPlugin: require('../plugins/WidgetsTray').default,
ZoomAllPlugin: require('../plugins/ZoomAll').default,
ZoomInPlugin: require('../plugins/ZoomIn').default,
- ZoomOutPlugin: require('../plugins/ZoomOut').default
+ ZoomOutPlugin: require('../plugins/ZoomOut').default,
+ SidebarMenuPlugin: require('../plugins/SidebarMenu').default
+
},
requires: {
ReactSwipe: require('react-swipeable-views').default,
diff --git a/web/client/product/plugins/About.jsx b/web/client/product/plugins/About.jsx
index ed1596aa05..3c15ec337c 100644
--- a/web/client/product/plugins/About.jsx
+++ b/web/client/product/plugins/About.jsx
@@ -39,8 +39,19 @@ export default {
text:
,
icon:
,
action: toggleControl.bind(null, 'about', null),
- priority: 1,
+ priority: 2,
doNotHide: true
+ },
+ SidebarMenu: {
+ name: 'about',
+ position: 1500,
+ tooltip: "aboutTooltip",
+ text:
,
+ icon:
,
+ action: toggleControl.bind(null, 'about', null),
+ priority: 1,
+ doNotHide: true,
+ toggle: true
}
}),
reducers: {}
diff --git a/web/client/product/plugins/Fork.jsx b/web/client/product/plugins/Fork.jsx
index c75131a0d4..cce5a1754f 100644
--- a/web/client/product/plugins/Fork.jsx
+++ b/web/client/product/plugins/Fork.jsx
@@ -17,7 +17,7 @@ class ForkPlugin extends React.Component {
render() {
return (
-
+
);
}
diff --git a/web/client/reducers/__tests__/sidebarmenu-test.js b/web/client/reducers/__tests__/sidebarmenu-test.js
new file mode 100644
index 0000000000..11cd316e2b
--- /dev/null
+++ b/web/client/reducers/__tests__/sidebarmenu-test.js
@@ -0,0 +1,22 @@
+/**
+ * Copyright 2022, GeoSolutions Sas.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree.
+ */
+import expect from 'expect';
+
+import sidebarmenu from '../sidebarmenu';
+import {SET_LAST_ACTIVE_ITEM} from "../../actions/sidebarmenu";
+
+describe('SidebarMenu REDUCERS', () => {
+ it('should set last active item', () => {
+ const action = {
+ type: SET_LAST_ACTIVE_ITEM,
+ value: 'annotations'
+ };
+ const state = sidebarmenu({}, action);
+ expect(state.lastActiveItem).toBe('annotations');
+ });
+});
diff --git a/web/client/reducers/maplayout.js b/web/client/reducers/maplayout.js
index 727797047d..8a535085e5 100644
--- a/web/client/reducers/maplayout.js
+++ b/web/client/reducers/maplayout.js
@@ -17,11 +17,11 @@ import assign from 'object-assign';
*
* @memberof reducers
*/
-function mapLayout(state = { layout: {}, boundingMapRect: {} }, action) {
+function mapLayout(state = { layout: {}, boundingMapRect: {}, boundingSidebarRect: {} }, action) {
switch (action.type) {
case UPDATE_MAP_LAYOUT: {
- const {boundingMapRect = {}, ...layout} = action.layout;
- return assign({}, state, {layout: assign({}, layout, layout), boundingMapRect: {...boundingMapRect}});
+ const {boundingMapRect = {}, boundingSidebarRect = {}, ...layout} = action.layout;
+ return assign({}, state, {layout: assign({}, layout, layout), boundingMapRect: {...boundingMapRect}, boundingSidebarRect: {...boundingSidebarRect}});
}
default:
return state;
diff --git a/web/client/reducers/sidebarmenu.js b/web/client/reducers/sidebarmenu.js
new file mode 100644
index 0000000000..fa4ad9d6c3
--- /dev/null
+++ b/web/client/reducers/sidebarmenu.js
@@ -0,0 +1,24 @@
+/**
+ * Copyright 2022, GeoSolutions Sas.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree.
+ */
+
+import { SET_LAST_ACTIVE_ITEM } from '../actions/sidebarmenu';
+
+export default (state = {
+ lastActiveItem: null
+}, action) => {
+ switch (action.type) {
+ case SET_LAST_ACTIVE_ITEM: {
+ return {
+ ...state,
+ lastActiveItem: action.value
+ };
+ }
+ default:
+ return state;
+ }
+};
diff --git a/web/client/selectors/__tests__/catalog-test.js b/web/client/selectors/__tests__/catalog-test.js
index 5284ad7ec5..d686aa0f92 100644
--- a/web/client/selectors/__tests__/catalog-test.js
+++ b/web/client/selectors/__tests__/catalog-test.js
@@ -9,7 +9,7 @@
import expect from 'expect';
import {
- activeSelector,
+ isActiveSelector,
authkeyParamNameSelector,
delayAutoSearchSelector,
groupSelector,
@@ -217,8 +217,8 @@ describe('Test catalog selectors', () => {
const retVal = layerErrorSelector(state);
expect(retVal).toBe(null);
});
- it('test activeSelector', () => {
- const retVal = activeSelector(state);
+ it('test isActiveSelector', () => {
+ const retVal = isActiveSelector(state);
expect(retVal).toExist();
expect(retVal).toBeTruthy();
});
@@ -327,4 +327,14 @@ describe('Test catalog selectors', () => {
});
expect(urlUsed).toBe(url);
});
+ it('test isActiveSelector ', () => {
+ const toolState = isActiveSelector({
+ controls: {
+ metadataexplorer: {
+ enabled: true
+ }
+ }
+ });
+ expect(toolState).toBe(true);
+ });
});
diff --git a/web/client/selectors/__tests__/mapcatalog-test.js b/web/client/selectors/__tests__/mapcatalog-test.js
index 9038904bf3..3477b91759 100644
--- a/web/client/selectors/__tests__/mapcatalog-test.js
+++ b/web/client/selectors/__tests__/mapcatalog-test.js
@@ -8,7 +8,8 @@
import expect from 'expect';
import {
- triggerReloadValueSelector
+ triggerReloadValueSelector,
+ isActiveSelector
} from '../mapcatalog';
const testState = {
@@ -21,4 +22,14 @@ describe('mapcatalog selectors', () => {
it('triggerReloadValueSelector', () => {
expect(triggerReloadValueSelector(testState)).toBe(true);
});
+ it('test isActiveSelector ', () => {
+ const toolState = isActiveSelector({
+ controls: {
+ mapCatalog: {
+ enabled: true
+ }
+ }
+ });
+ expect(toolState).toBe(true);
+ });
});
diff --git a/web/client/selectors/__tests__/maplayout-test.js b/web/client/selectors/__tests__/maplayout-test.js
index 0bdb1f1152..5c658264ad 100644
--- a/web/client/selectors/__tests__/maplayout-test.js
+++ b/web/client/selectors/__tests__/maplayout-test.js
@@ -13,6 +13,7 @@ import {
mapLayoutValuesSelector,
checkConditionsSelector,
rightPanelOpenSelector,
+ leftPanelOpenSelector,
bottomPanelOpenSelector,
boundingMapRectSelector,
mapPaddingSelector
@@ -56,18 +57,24 @@ describe('Test map layout selectors', () => {
});
it('test rightPanelOpenSelector', () => {
- expect(rightPanelOpenSelector({maplayout: { layout: {right: 658, bottom: 500}}})).toBe(true);
- expect(rightPanelOpenSelector({maplayout: { layout: {left: 300, bottom: 30}}})).toBe(false);
+ expect(rightPanelOpenSelector({maplayout: { layout: {rightPanel: true, leftPanel: false}}})).toBe(true);
+ expect(rightPanelOpenSelector({maplayout: { layout: {rightPanel: false, leftPanel: false}}})).toBe(false);
expect(rightPanelOpenSelector({})).toBe(false);
});
+ it('test leftPanelOpenSelector', () => {
+ expect(leftPanelOpenSelector({maplayout: { layout: {rightPanel: true, leftPanel: true}}})).toBe(true);
+ expect(leftPanelOpenSelector({maplayout: { layout: {rightPanel: false, leftPanel: false}}})).toBe(false);
+ expect(leftPanelOpenSelector({})).toBe(false);
+ });
+
it('test bottomPanelOpenSelector', () => {
expect(bottomPanelOpenSelector({maplayout: { layout: {left: 300, bottom: 500}}})).toBe(true);
expect(bottomPanelOpenSelector({maplayout: { layout: {left: 300, bottom: 30}}})).toBe(false);
expect(bottomPanelOpenSelector({})).toBe(false);
});
- it('test bottomPanelOpenSelector', () => {
+ it('test boundingMapRectSelector', () => {
expect(boundingMapRectSelector({
maplayout: {
diff --git a/web/client/selectors/__tests__/maptemplates-test.js b/web/client/selectors/__tests__/maptemplates-test.js
index 8a2be9a9c2..e863ecc6c9 100644
--- a/web/client/selectors/__tests__/maptemplates-test.js
+++ b/web/client/selectors/__tests__/maptemplates-test.js
@@ -7,7 +7,7 @@
*/
import expect from 'expect';
-import {allTemplatesSelector} from '../maptemplates';
+import {allTemplatesSelector, isActiveSelector} from '../maptemplates';
describe('maptemplates selectors', () => {
it('should return allowed templates when they are provided', () => {
@@ -37,4 +37,14 @@ describe('maptemplates selectors', () => {
};
expect(allTemplatesSelector(state)[0]).toBe("CONTEXT TEST TEMPLATE");
});
+ it('test isActiveSelector ', () => {
+ const toolState = isActiveSelector({
+ controls: {
+ mapTemplates: {
+ enabled: true
+ }
+ }
+ });
+ expect(toolState).toBe(true);
+ });
});
diff --git a/web/client/selectors/__tests__/measurement-test.js b/web/client/selectors/__tests__/measurement-test.js
index 1e0aa2f534..35c19a8fda 100644
--- a/web/client/selectors/__tests__/measurement-test.js
+++ b/web/client/selectors/__tests__/measurement-test.js
@@ -13,7 +13,8 @@ import {
isCoordinateEditorEnabledSelector,
showAddAsAnnotationSelector,
measurementSelector,
- getValidFeatureSelector
+ getValidFeatureSelector,
+ isActiveSelector
} from '../measurement';
import {
@@ -80,4 +81,14 @@ describe('Test maptype', () => {
});
expect(retval.feature.geometry.coordinates).toEqual( lineFeature3.geometry.coordinates );
});
+ it('test isActiveSelector ', () => {
+ const toolState = isActiveSelector({
+ controls: {
+ measure: {
+ enabled: true
+ }
+ }
+ });
+ expect(toolState).toBe(true);
+ });
});
diff --git a/web/client/selectors/__tests__/sidebarmenu-test.js b/web/client/selectors/__tests__/sidebarmenu-test.js
new file mode 100644
index 0000000000..737610931b
--- /dev/null
+++ b/web/client/selectors/__tests__/sidebarmenu-test.js
@@ -0,0 +1,33 @@
+/*
+* Copyright 2022, GeoSolutions Sas.
+* All rights reserved.
+*
+* This source code is licensed under the BSD-style license found in the
+* LICENSE file in the root directory of this source tree.
+*/
+import expect from 'expect';
+
+import {lastActiveToolSelector, sidebarIsActiveSelector} from "../sidebarmenu";
+
+describe('SidebarMenu SELECTORS', () => {
+ it('should test lastActiveToolSelector', () => {
+ const state = {
+ sidebarmenu: {
+ lastActiveItem: 'mapCatalog'
+ }
+ };
+
+ expect(lastActiveToolSelector(state)).toEqual(state.sidebarmenu.lastActiveItem);
+ });
+ it('should test sidebarIsActiveSelector', () => {
+ const state = {
+ controls: {
+ sidebarMenu: {
+ enabled: true
+ }
+ }
+ };
+
+ expect(sidebarIsActiveSelector(state)).toEqual(state.controls.sidebarMenu.enabled);
+ });
+});
diff --git a/web/client/selectors/__tests__/userextensions-test.js b/web/client/selectors/__tests__/userextensions-test.js
new file mode 100644
index 0000000000..ab9677d1e3
--- /dev/null
+++ b/web/client/selectors/__tests__/userextensions-test.js
@@ -0,0 +1,23 @@
+/*
+* Copyright 2022, GeoSolutions Sas.
+* All rights reserved.
+*
+* This source code is licensed under the BSD-style license found in the
+* LICENSE file in the root directory of this source tree.
+*/
+import expect from 'expect';
+
+import {isActiveSelector} from "../userextensions";
+
+describe('UserExtensions SELECTORS', () => {
+ it('should test isActiveSelector', () => {
+ const state = {
+ controls: {
+ userExtensions: {
+ enabled: true
+ }
+ }
+ };
+ expect(isActiveSelector(state)).toEqual(state.controls.userExtensions.enabled);
+ });
+});
diff --git a/web/client/selectors/catalog.js b/web/client/selectors/catalog.js
index 2f178cd25b..a180c251a5 100644
--- a/web/client/selectors/catalog.js
+++ b/web/client/selectors/catalog.js
@@ -45,7 +45,7 @@ export const selectedServiceSelector = (state) => get(state, "catalog.selectedSe
export const modeSelector = (state) => get(state, "catalog.mode", "view");
export const layerErrorSelector = (state) => get(state, "catalog.layerError");
export const searchTextSelector = (state) => get(state, "catalog.searchOptions.text", "");
-export const activeSelector = (state) => get(state, "controls.toolbar.active") === "metadataexplorer" || get(state, "controls.metadataexplorer.enabled");
+export const isActiveSelector = (state) => get(state, "controls.toolbar.active") === "metadataexplorer" || get(state, "controls.metadataexplorer.enabled");
export const authkeyParamNameSelector = (state) => {
return (get(state, "localConfig.authenticationRules") || []).filter(a => a.method === "authkey").map(r => r.authkeyParamName) || [];
};
diff --git a/web/client/selectors/controls.js b/web/client/selectors/controls.js
index 674b3e14cf..344290d606 100644
--- a/web/client/selectors/controls.js
+++ b/web/client/selectors/controls.js
@@ -33,3 +33,4 @@ export const unsavedMapSelector = (state) => get(state, "controls.unsavedMap.ena
export const unsavedMapSourceSelector = (state) => get(state, "controls.unsavedMap.source", "");
export const isIdentifyAvailable = (state) => get(state, "controls.info.available");
export const showConfirmDeleteMapModalSelector = (state) => get(state, "controls.mapDelete.enabled", false);
+export const burgerMenuSelector = (state) => get(state, "controls.burgermenu.enabled", false);
diff --git a/web/client/selectors/map.js b/web/client/selectors/map.js
index 72bf0a6cd4..718ed4b265 100644
--- a/web/client/selectors/map.js
+++ b/web/client/selectors/map.js
@@ -9,7 +9,7 @@
import CoordinatesUtils from '../utils/CoordinatesUtils';
import { createSelector } from 'reselect';
-import { get } from 'lodash';
+import {get, memoize} from 'lodash';
import {detectIdentifyInMapPopUp} from "../utils/MapUtils";
/**
@@ -96,6 +96,19 @@ export const mapVersionSelector = (state) => state.map && state.map.present && s
*/
export const mapNameSelector = (state) => state.map && state.map.present && state.map.present.info && state.map.present.info.name || '';
+export const mapSizeSelector = (state) => state?.map?.present?.size ?? 0;
+
+export const mapSizeValuesSelector = memoize((attributes = {}) => createSelector(
+ mapSizeSelector,
+ (sizes) => {
+ return sizes && Object.keys(sizes).filter(key =>
+ attributes[key]).reduce((a, key) => {
+ return ({...a, [key]: sizes[key]});
+ },
+ {}) || {};
+ }
+), (attributes) => JSON.stringify(attributes));
+
export const mouseMoveListenerSelector = (state) => get(mapSelector(state), 'eventListeners.mousemove', []);
export const isMouseMoveActiveSelector = (state) => !!mouseMoveListenerSelector(state).length;
diff --git a/web/client/selectors/mapcatalog.js b/web/client/selectors/mapcatalog.js
index fa8d146f33..2f0e50d96d 100644
--- a/web/client/selectors/mapcatalog.js
+++ b/web/client/selectors/mapcatalog.js
@@ -6,7 +6,9 @@
* LICENSE file in the root directory of this source tree.
*/
import { mapTypeSelector as mtSelector, isCesium, last2dMapTypeSelector } from '../selectors/maptype';
+import {get} from "lodash";
+export const isActiveSelector = (state) => get(state, "controls.mapCatalog.enabled");
export const triggerReloadValueSelector = state => state.mapcatalog?.triggerReloadValue;
export const filterReloadDelaySelector = state => state.mapcatalog?.filterReloadDelay;
export const mapTypeSelector = state => {
diff --git a/web/client/selectors/maplayout.js b/web/client/selectors/maplayout.js
index c5b50b506c..821a605976 100644
--- a/web/client/selectors/maplayout.js
+++ b/web/client/selectors/maplayout.js
@@ -5,10 +5,11 @@
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree.
*/
-import { head } from 'lodash';
+import {head, memoize} from 'lodash';
import { mapSelector } from './map';
-import { parseLayoutValue } from '../utils/MapUtils';
+import {DEFAULT_MAP_LAYOUT, parseLayoutValue} from '../utils/MapUtils';
+import ConfigUtils from "../utils/ConfigUtils";
/**
* selects map layout state
@@ -36,20 +37,39 @@ export const mapLayoutSelector = (state) => state.maplayout && state.maplayout.l
*/
export const boundingMapRectSelector = (state) => state.maplayout && state.maplayout.boundingMapRect || {};
+/**
+ * Get map layout bounds left, top, bottom and right
+ * @function
+ * @memberof selectors.mapLayout
+ * @param {object} state the state
+ * @return {object} boundingMapRect {left, top, bottom, right}
+ */
+export const boundingSidebarRectSelector = (state) => state.maplayout && state.maplayout.boundingSidebarRect || {};
+
/**
* Retrieve only specific attribute from map layout
* @function
* @memberof selectors.mapLayout
* @param {object} state the state
* @param {object} attributes attributes to retrieve, bool {left: true}
+ * @param {boolean} isDock flag to use dock paddings instead of toolbar paddings
* @return {object} selected attributes of layout of the map
*/
-export const mapLayoutValuesSelector = (state, attributes = {}) => {
+export const mapLayoutValuesSelector = memoize((state, attributes = {}, isDock = false) => {
const layout = mapLayoutSelector(state);
+ const boundingSidebarRect = boundingSidebarRectSelector(state);
return layout && Object.keys(layout).filter(key =>
- attributes[key]).reduce((a, key) => ({...a, [key]: layout[key]}),
- {}) || {};
-};
+ attributes[key]).reduce((a, key) => {
+ if (isDock) {
+ return ({...a, [key]: (boundingSidebarRect[key] ?? layout[key])});
+ }
+ return ({...a, [key]: layout[key]});
+ },
+ {}) || {};
+}, (state, attributes, isDock) =>
+ JSON.stringify(mapLayoutSelector(state)) +
+ JSON.stringify(boundingSidebarRectSelector(state)) +
+ JSON.stringify(attributes) + (isDock ? '_isDock' : ''));
/**
* Check if conditions match with the current layout
@@ -78,9 +98,20 @@ export const checkConditionsSelector = (state, conditions = []) => {
* @return {boolean} returns true if right panels are open
*/
export const rightPanelOpenSelector = state => {
- // need to remove 658 and manage it from the state with all dafault layout variables
- return checkConditionsSelector(state, [{ key: 'right', value: 658 }]);
+ return !!mapLayoutSelector(state)?.rightPanel;
};
+
+/**
+ * Check if left panels are open
+ * @function
+ * @memberof selectors.mapLayout
+ * @param {object} state the state
+ * @return {boolean} returns true if left panels are open
+ */
+export const leftPanelOpenSelector = state => {
+ return !!mapLayoutSelector(state)?.leftPanel;
+};
+
/**
* Check if bottom panel is open
* @function
@@ -89,8 +120,9 @@ export const rightPanelOpenSelector = state => {
* @return {boolean} returns true if bottom panel is open
*/
export const bottomPanelOpenSelector = state => {
- // need to remove 30 and manage it from the state with all dafault layout variables
- return checkConditionsSelector(state, [{ key: 'bottom', value: 30, type: 'not' }]);
+ const mapLayout = ConfigUtils.getConfigProp("mapLayout") || DEFAULT_MAP_LAYOUT;
+ const bottomMapOffset = mapLayout?.bottom.sm ?? 0;
+ return checkConditionsSelector(state, [{ key: 'bottom', value: bottomMapOffset, type: 'not' }]);
};
/**
diff --git a/web/client/selectors/maptemplates.js b/web/client/selectors/maptemplates.js
index 8492b274ce..3cb679c95c 100644
--- a/web/client/selectors/maptemplates.js
+++ b/web/client/selectors/maptemplates.js
@@ -7,7 +7,9 @@
*/
import { createSelector } from 'reselect';
import { templatesSelector as contextTemplatesSelector } from './context';
+import {get} from "lodash";
+export const isActiveSelector = (state) => get(state, "controls.mapTemplates.enabled");
export const mapTemplatesLoadedSelector = state => state.maptemplates && state.maptemplates.mapTemplatesLoaded;
export const mapTemplatesLoadErrorSelector = state => state.maptemplates && state.maptemplates.mapTemplatesLoadError;
export const templatesSelector = state => state.maptemplates && state.maptemplates.templates;
diff --git a/web/client/selectors/measurement.js b/web/client/selectors/measurement.js
index 73884d4197..9705479d60 100644
--- a/web/client/selectors/measurement.js
+++ b/web/client/selectors/measurement.js
@@ -6,11 +6,12 @@
* LICENSE file in the root directory of this source tree.
*/
-import { isOpenlayers } from '../selectors/maptype';
+import { isOpenlayers } from './maptype';
-import { showCoordinateEditorSelector } from '../selectors/controls';
+import { showCoordinateEditorSelector } from './controls';
import { set } from '../utils/ImmutableUtils';
import { validateFeatureCoordinates } from '../utils/MeasureUtils';
+import {get} from "lodash";
/**
* selects measurement state
@@ -19,6 +20,8 @@ import { validateFeatureCoordinates } from '../utils/MeasureUtils';
* @static
*/
+export const isActiveSelector = (state) => get(state, "controls.measure.enabled");
+
/**
* selects the showCoordinateEditor flag from state
* @memberof selectors.measurement
diff --git a/web/client/selectors/sidebarmenu.js b/web/client/selectors/sidebarmenu.js
new file mode 100644
index 0000000000..f93cf8488e
--- /dev/null
+++ b/web/client/selectors/sidebarmenu.js
@@ -0,0 +1,5 @@
+import {get} from "lodash";
+
+export const lastActiveToolSelector = (state) => get(state, "sidebarmenu.lastActiveItem", false);
+
+export const sidebarIsActiveSelector = (state) => get(state, 'controls.sidebarMenu.enabled', false);
diff --git a/web/client/selectors/userextensions.js b/web/client/selectors/userextensions.js
new file mode 100644
index 0000000000..5593df1dfb
--- /dev/null
+++ b/web/client/selectors/userextensions.js
@@ -0,0 +1,11 @@
+/**
+ * Copyright 2022, GeoSolutions Sas.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree.
+ */
+
+import {get} from "lodash";
+
+export const isActiveSelector = (state) => get(state, "controls.userExtensions.enabled");
diff --git a/web/client/themes/default/bootstrap-theme.less b/web/client/themes/default/bootstrap-theme.less
index d2985b8f53..99a3f1a317 100644
--- a/web/client/themes/default/bootstrap-theme.less
+++ b/web/client/themes/default/bootstrap-theme.less
@@ -52,6 +52,12 @@
// Custom theme
+// Navigation
+
+.navbar {
+ min-height: @square-btn-size;
+}
+
// Button
button.close {
opacity: 1.0;
diff --git a/web/client/themes/default/less/annotations.less b/web/client/themes/default/less/annotations.less
index 6b67dad1f4..cea4a3594c 100644
--- a/web/client/themes/default/less/annotations.less
+++ b/web/client/themes/default/less/annotations.less
@@ -262,7 +262,8 @@
.mapstore-annotations-panel-card-title{
margin-top: 5px;
text-overflow: ellipsis;
- width: 100px;
+ width: 150px;
+ overflow: hidden;
}
.mapstore-side-card-desc{
border-bottom: none;
@@ -513,7 +514,7 @@
}
.mapstore-annotations-info-viewer-expanded {
flex: 1;
- order: -1;
+ order: 2;
border-right-width: 1px;
border-right-style: solid;
display: flex;
@@ -524,6 +525,7 @@
.tab-container{
flex: 1;
padding-top: 8px;
+ padding-right: 8px;
position: relative;
overflow-y: auto;
overflow-x: hidden;
diff --git a/web/client/themes/default/less/createnewmap.less b/web/client/themes/default/less/createnewmap.less
index b374d5c6e3..4ae6577f2d 100644
--- a/web/client/themes/default/less/createnewmap.less
+++ b/web/client/themes/default/less/createnewmap.less
@@ -26,7 +26,7 @@
// **************
.create-new-map-container {
.dropdown-toggle {
- height: 52px;
+ height: @square-btn-size;
}
.modal-body {
@@ -77,4 +77,4 @@
align-items: center;
height: 100%;
}
-}
\ No newline at end of file
+}
diff --git a/web/client/themes/default/less/loaders.less b/web/client/themes/default/less/loaders.less
index d631be3987..8f048681ca 100644
--- a/web/client/themes/default/less/loaders.less
+++ b/web/client/themes/default/less/loaders.less
@@ -64,7 +64,7 @@
// Layout
// **************
div#mapstore-globalspinner {
- display: inline-block;
+ display: flex;
margin-bottom: 0;
vertical-align: middle;
width: @square-btn-size !important;
diff --git a/web/client/themes/default/less/map-search-bar.less b/web/client/themes/default/less/map-search-bar.less
index 8408e6a7b8..b6f0c220b5 100644
--- a/web/client/themes/default/less/map-search-bar.less
+++ b/web/client/themes/default/less/map-search-bar.less
@@ -68,6 +68,44 @@
// Layout
// **************
/* search */
+#search-bar-container {
+ position: absolute;
+ top: 0;
+ margin-right: 5px;
+ box-shadow: -1px 1px 5px 1px #5e5e5e;
+ z-index: 1031;
+}
+
+#search-bar-container.no-sidebar {
+ margin-right: 0;
+ position: relative;
+ float: left;
+ box-shadow: none;
+ z-index: unset;
+
+ &.toggled {
+ #map-search-bar {
+ position: fixed;
+ top: 52px;
+ width: 100%;
+ }
+ }
+
+ #map-search-bar {
+ top: 0;
+ left: 0;
+ position: relative;
+ box-shadow: none;
+ }
+
+ @media (max-width: 767px ) {
+ #map-search-bar, .search-result-list {
+ width: 400px;
+ }
+ }
+
+}
+
#mapstore-navbar .navbar-dx .MapSearchBar .input-group {
border-radius: 0;
position: relative;
@@ -113,7 +151,7 @@ div.MapSearchBar .form-control:focus {
position: relative;
-webkit-box-shadow: unset;
box-shadow: unset;
- flex: 1 1 0%;
+ flex: 1 1 0;
margin-right: 8px;
display: table;
border-collapse: separate;
@@ -133,31 +171,21 @@ div.MapSearchBar .form-control:focus {
z-index: 1;
}
-@media (max-width: 768px) {
- #mapstore-navbar .search-toggle {
+ #mapstore-navbar .toggled .search-toggle {
display: inline-block;
}
- #mapstore-navbar .MapSearchBar {
+ #mapstore-navbar .toggled .MapSearchBar {
width: 400px;
top: @square-btn-size;
left: auto;
}
- #mapstore-navbar .search-result-list {
- top: 85px;
- left: auto;
- width: 400px;
- }
-}
-
-/* Small devices (tablets, 768px and up) */
-@media (max-width: 768px) {
- #mapstore-navbar .search-toggle {
+ #mapstore-navbar .toggled .search-toggle {
display: inline-block;
}
- #mapstore-navbar .navbar-dx .MapSearchBar {
+ #mapstore-navbar .toggled .MapSearchBar {
position: fixed;
left: 1px;
right: 1px;
@@ -165,43 +193,29 @@ div.MapSearchBar .form-control:focus {
width: auto;
}
- #mapstore-navbar .navbar-dx .MapSearchBar .input-group {
+ #mapstore-navbar .toggled .MapSearchBar .input-group {
width: 100%;
}
- #mapstore-navbar .navbar-dx .search-result-list {
+ #mapstore-navbar .toggled .search-result-list {
position: fixed;
left: 15px;
right: 15px;
top: 105px;
width: 95%;
}
-}
-/* Medium devices (desktops, 992px and up) */
-@media (min-width: 992px) {
- #mapstore-navbar .MapSearchBar {
- width: 500px;
- right: auto;
- }
-
- #mapstore-navbar .search-result-list {
- width: 500px;
- right: auto;
- }
+#mapstore-navbar .MapSearchBar {
+ width: 500px;
+ right: auto;
}
-/* Large devices (large desktops, 1200px and up) */
-@media (min-width: 1200px) {
- #mapstore-navbar .MapSearchBar {
- width: 500px;
- position: absolute;
- }
-
- #mapstore-navbar .search-result-list {
- width: 500px;
- right: auto;
- }
+#mapstore-navbar .search-result-list {
+ width: 500px;
+ top: 35px;
+ right: 0;
+ left: auto;
+ margin: 0;
}
#mapstore-navbar .form-group {
@@ -215,7 +229,7 @@ div.MapSearchBar .form-control:focus {
flex: 1;
height: 100%;
position: relative;
- min-height: 52px;
+ min-height: @square-btn-size;
align-items: center;
}
@@ -224,7 +238,7 @@ div.MapSearchBar .form-control:focus {
flex: 1;
height: 100%;
position: relative;
- min-height: 52px;
+ min-height: @square-btn-size;
align-items: center;
}
@@ -234,7 +248,7 @@ div.MapSearchBar .form-control:focus {
box-sizing: border-box;
margin: 6px 0 !important;
display: flex;
- flex: 1 1 0%;
+ flex: 1 1 0;
justify-content: space-between;
padding: 0 6px;
@@ -242,7 +256,7 @@ div.MapSearchBar .form-control:focus {
margin-left: unset;
margin-right: unset;
padding: 5px;
- flex: 1 1 0%;
+ flex: 1 1 0;
.coordinateLabel {
margin-top: 9px;
@@ -349,7 +363,7 @@ div.MapSearchBar .form-control:focus {
.input-group{
.input-group-addon{
width: auto;
- border: 0px solid transparent;
+ border: 0 solid transparent;
.selectedItem-text{
max-width: 200px;
}
diff --git a/web/client/themes/default/less/maps-properties.less b/web/client/themes/default/less/maps-properties.less
index e31603e52f..ddf8bcd288 100644
--- a/web/client/themes/default/less/maps-properties.less
+++ b/web/client/themes/default/less/maps-properties.less
@@ -323,10 +323,10 @@
margin: 4px 0;
padding-left: 4px;
padding-right: 4px;
- height: @square-btn-size * 3.5;
+ height: @square-btn-size * 4.5;
overflow: visible;
.gridcard {
- height: @square-btn-size * 3.5;
+ height: @square-btn-size * 4.5;
transition: all 0.3s;
&:hover {
.shadow-far;
diff --git a/web/client/themes/default/less/navbar.less b/web/client/themes/default/less/navbar.less
index 71a74f6d77..055be3c0ce 100644
--- a/web/client/themes/default/less/navbar.less
+++ b/web/client/themes/default/less/navbar.less
@@ -59,4 +59,21 @@ ol {
#mapstore-navbar-container {
height: @square-btn-size;
+
+ .nav {
+ &.pull-left {
+ display: flex;
+ flex: 1;
+
+ >li {
+ >a {
+ display: flex;
+ align-items: center;
+ height: @square-btn-size;
+ padding: 0 15px;
+ }
+ }
+ }
+ }
+
}
diff --git a/web/client/themes/default/less/panels.less b/web/client/themes/default/less/panels.less
index 59880f6e6f..5f87d294c1 100644
--- a/web/client/themes/default/less/panels.less
+++ b/web/client/themes/default/less/panels.less
@@ -214,6 +214,18 @@
}
}
+.dock-container {
+ position: absolute;
+ z-index: 1025;
+ width: 100%;
+ height: 100%;
+ pointer-events: none;
+
+ &.identify-active {
+ z-index: 1026;
+ }
+}
+
#mapstore-print-panel, #measure-dialog, #mapstore-about, #share-panel-dialog, #bookmark-panel-dialog {
position: fixed;
top: 0%;
diff --git a/web/client/themes/default/less/searchbar.less b/web/client/themes/default/less/searchbar.less
index 817ad13b70..aea3d39b4c 100644
--- a/web/client/themes/default/less/searchbar.less
+++ b/web/client/themes/default/less/searchbar.less
@@ -98,77 +98,20 @@
#mapstore-navbar .MapSearchBar {
top: 0;
left: -500px;
-}
-
-#mapstore-navbar .search-result-list{
- top: 35px;
- left: -500px;
+ right: 0;
}
.search-toggle {
display: none;
}
-@media (max-width: 768px ) {
- #mapstore-navbar .search-toggle {
- display: inline-block;
- }
- #mapstore-navbar .MapSearchBar {
- width: 400px;
- top: 50px;
- left: auto;
- }
-
- #mapstore-navbar .search-result-list{
- top: 85px;
- left: auto;
- width: 400px;
- }
-}
-
-/* Small devices (tablets, 768px and up) */
-@media (min-width: 768px ) {
- .MapSearchBar {
- width: 500px;
- right: auto;
- }
- .search-result-list{
- width: 500px;
- right: auto;
- }
-}
-
-/* Medium devices (desktops, 992px and up) */
-@media (min-width: 992px) {
- .MapSearchBar {
- width: 500px;
- right: auto;
- }
- .search-result-list{
- width: 500px;
- right: auto;
- }
-}
-
-/* Large devices (large desktops, 1200px and up) */
-@media (min-width: 1200px) {
- .MapSearchBar {
- width: 500px;
- position:absolute;
- }
- .search-result-list{
- width: 500px;
- right: auto;
- }
-}
-
/* Maps Search */
.maps-search.MapSearchBar{
width: 90%;
- left: 0px;
+ left: 0;
position: relative;
- right: 0px;
- top: 0px;
+ right: 0;
+ top: 0;
margin-left: auto;
margin-right: auto;
}
@@ -189,10 +132,10 @@
/* User Search */
.user-search.MapSearchBar{
width: 90%;
- left: 0px;
+ left: 0;
position: relative;
- right: 0px;
- top: 0px;
+ right: 0;
+ top: 0;
margin-left: auto;
margin-right: auto;
}
diff --git a/web/client/themes/default/less/sidegrid.less b/web/client/themes/default/less/sidegrid.less
index 08d19a8064..9df46a91cc 100644
--- a/web/client/themes/default/less/sidegrid.less
+++ b/web/client/themes/default/less/sidegrid.less
@@ -85,7 +85,7 @@
border-width: 1px;
border-style: dashed;
&.ms-sm {
- height: @square-btn-size;
+ min-height: @card-height;
}
}
@@ -114,7 +114,7 @@
flex-direction: column;
.ms-head {
display: flex;
- height: @square-btn-size * 2;
+ min-height: @card-height * 2;
}
.mapstore-side-card-container {
display: flex;
@@ -229,11 +229,11 @@
&.ms-sm {
.ms-head {
- height: @square-btn-size;
+ min-height: @card-height;
}
.mapstore-side-preview {
- width: @square-btn-size;
- height: @square-btn-size;
+ width: @card-height;
+ height: @card-height;
padding: 8px;
> .glyphicon {
text-align: center;
diff --git a/web/client/themes/default/less/toc.less b/web/client/themes/default/less/toc.less
index 812a9c0037..3afb5b861c 100644
--- a/web/client/themes/default/less/toc.less
+++ b/web/client/themes/default/less/toc.less
@@ -312,15 +312,15 @@
}
&.toc-head-sections-1 {
- height: @square-btn-size;
+ height: @card-height;
}
&.toc-head-sections-2 {
- height: @square-btn-size * 2;
+ height: @card-height * 2;
}
&.toc-head-sections-3 {
- height: @square-btn-size * 3;
+ height: @card-height * 3;
}
.toc-inline-loader {
@@ -332,7 +332,7 @@
display: table-cell;
vertical-align: middle;
width: 270px;
- height: @square-btn-size;
+ height: @card-height;
color: @ms-primary;
font-weight: bold;
.glyphicon {
@@ -342,7 +342,7 @@
}
.col-xs-12 {
- height: @square-btn-size;
+ height: @card-height;
border-top: 1px solid @ms-main-border-color;
.btn-sm {
@@ -463,15 +463,15 @@
}
&.toc-body-sections-1 .mapstore-layers-container {
- height: ~"calc(100% - @{square-btn-size})";
+ height: ~"calc(100% - @{card-height})";
}
&.toc-body-sections-2 .mapstore-layers-container {
- height: ~"calc(100% - @{square-btn-size} * 2 )";
+ height: ~"calc(100% - @{card-height} * 2 )";
}
&.toc-body-sections-3 .mapstore-layers-container {
- height: ~"calc(100% - @{square-btn-size} * 3 )";
+ height: ~"calc(100% - @{card-height} * 3 )";
}
.toc-filter-no-results {
@@ -559,9 +559,9 @@
.toc-default-group-head {
background-color: @ms-main-bg;
- height: @square-btn-size;
+ height: @card-height;
width: 100%;
- padding: floor(((@square-btn-size - @icon-size-md) / 2)) 0;
+ padding: floor(((@card-height - @icon-size-md) / 2)) 0;
.toc-group-title {
overflow: hidden;
@@ -644,12 +644,12 @@
}
.toc-default-layer-head {
- height: @square-btn-size;
+ height: @card-height;
width: 100%;
color: @ms-main-color;
background-color: @ms-main-bg;
margin-bottom: 0;
- padding: floor(((@square-btn-size - @icon-size-md) / 2)) 0;
+ padding: floor(((@card-height - @icon-size-md) / 2)) 0;
.toc-title {
overflow: hidden;
@@ -778,7 +778,7 @@
.is-dragging.toc-default-layer.selected {
background-color: @ms-main-bg !important;
border: 1px dashed @ms-primary !important;
- height: @square-btn-size !important;
+ height: @card-height !important;
overflow: hidden;
border-radius: @border-radius-base !important;
opacity: 0.8;
@@ -804,7 +804,7 @@
.is-placeholder.toc-default-layer.selected {
background-color: transparent !important;
border: 1px dashed @ms-primary !important;
- height: @square-btn-size !important;
+ height: @card-height !important;
overflow: hidden;
border-radius: @border-radius-base !important;
.toc-default-layer-head {
@@ -823,7 +823,7 @@
.is-dragging.toc-default-group.selected {
background-color: @ms-main-bg !important;
border: 1px dashed @ms-primary !important;
- height: @square-btn-size !important;
+ height: @card-height !important;
overflow: hidden;
border-radius: @border-radius-base !important;
opacity: 0.8;
@@ -850,7 +850,7 @@
background-color: @ms-main-bg !important;
border: none !important;
border-bottom: 1px solid @ms-primary !important;
- height: @square-btn-size + 5 !important;
+ height: @card-height + 5 !important;
overflow: hidden;
border-radius: @border-radius-base !important;
.toc-default-group-head {
diff --git a/web/client/themes/default/ms-variables.less b/web/client/themes/default/ms-variables.less
index 409697327e..be3b058970 100644
--- a/web/client/themes/default/ms-variables.less
+++ b/web/client/themes/default/ms-variables.less
@@ -333,12 +333,12 @@
// ******************************************
@small-icon-size: 14px;
-@icon-size: 26px;
-@square-btn-size: 52px;
+@icon-size: 24px;
+@square-btn-size: 40px;
@padding-left-square: floor(((@square-btn-size - @icon-size) / 2));
-@icon-size-md: 16px;
-@square-btn-medium-size: 32px;
+@icon-size-md: 15px;
+@square-btn-medium-size: 30px;
@padding-left-square-md: floor(((@square-btn-medium-size - @icon-size-md) / 2));
@icon-size-sm: 14px;
@@ -349,6 +349,8 @@
@grid-btn-size: 32px;
@grid-btn-padding-left: floor(((@grid-btn-size - @grid-icon-size) / 2));
+@card-height: 52px;
+
// ******************************************
diff --git a/web/client/translations/data.de-DE.json b/web/client/translations/data.de-DE.json
index 854b3b6d77..d8cf269295 100644
--- a/web/client/translations/data.de-DE.json
+++ b/web/client/translations/data.de-DE.json
@@ -525,6 +525,8 @@
"toggleGroupVisibility": "Gruppensichtbarkeit umschalten",
"displayLegendAndTools": "Legende einblenden",
"zoomToLayerExtent": "Zoome auf Ausdehung der Ebene",
+ "addAnnotations": "Anmerkungen hinzufügen",
+ "editAnnotations": "Anmerkungen bearbeiten",
"addLayer": "Ebene hinzufügen",
"addLayerToGroup": "Ebene zur ausgewählten Gruppe hinzufügen",
"addGroup": "Gruppe hinzufügen",
@@ -693,7 +695,8 @@
"previewFormatUnsupported": "Nicht unterstütztes Format für die Vorschau",
"projection": "Koordinatensystem",
"projectionmismatch": "Nichtübereinstimmung des Koordinatensystems zwischen gedruckter und Bildschirmkarte",
- "graticule": "Raster mit Etiketten hinzufügen"
+ "graticule": "Raster mit Etiketten hinzufügen",
+ "additionalLayers": "Überlagerungen einschließen"
},
"backgroundSwitcher":{
"tooltip": "Wähle Hintergrund"
@@ -3505,6 +3508,9 @@
"noDataForPosition": "keine Street-View-Daten für diese Position",
"unknownError": "Unbekannter Fehler, siehe Konsole"
}
+ },
+ "sidebarMenu": {
+ "showMoreItems": "Weitere Elemente anzeigen"
}
}
}
diff --git a/web/client/translations/data.en-US.json b/web/client/translations/data.en-US.json
index 7f61eaeb64..da474a5349 100644
--- a/web/client/translations/data.en-US.json
+++ b/web/client/translations/data.en-US.json
@@ -487,6 +487,8 @@
"toggleGroupVisibility": "Toggle group visibility",
"displayLegendAndTools": "Display legend",
"zoomToLayerExtent": "Zoom to layer extent",
+ "addAnnotations": "Add annotations",
+ "editAnnotations": "Edit annotations",
"addLayer": "Add layer",
"addLayerToGroup": "Add layer to selected group",
"addGroup": "Add group",
@@ -654,7 +656,8 @@
"previewFormatUnsupported": "Unsupported format for preview",
"projection": "Coordinates System",
"projectionmismatch": "Coordinate system mismatch among printed and screen map",
- "graticule": "add grid with labels"
+ "graticule": "add grid with labels",
+ "additionalLayers": "Include overlays"
},
"backgroundSwitcher":{
"tooltip": "Select Background"
@@ -3478,6 +3481,9 @@
"noDataForPosition": "no street-view data for this position",
"unknownError": "unknown error, see console"
}
+ },
+ "sidebarMenu": {
+ "showMoreItems": "Show more items"
}
}
}
diff --git a/web/client/translations/data.es-ES.json b/web/client/translations/data.es-ES.json
index e451da0a34..1bbb8989ff 100644
--- a/web/client/translations/data.es-ES.json
+++ b/web/client/translations/data.es-ES.json
@@ -487,6 +487,8 @@
"toggleGroupVisibility": "Alternar la visibilidad del grupo",
"displayLegendAndTools": "Mostrar la leyenda",
"zoomToLayerExtent": "Zoom a la extensión de la capa",
+ "addAnnotations": "Agregar anotaciones",
+ "editAnnotations": "Editar anotaciones",
"addLayer": "Añadir capa",
"addLayerToGroup": "Añadir capa al grupo seleccionado",
"addGroup": "Añadir grupo",
@@ -654,7 +656,8 @@
"previewFormatUnsupported": "Formato no compatible para la vista previa",
"projection": "Sistema de coordenadas",
"projectionmismatch": "Falta de coincidencia del sistema de coordenadas entre el mapa impreso y en pantalla",
- "graticule": "agregar cuadrícula con etiquetas"
+ "graticule": "agregar cuadrícula con etiquetas",
+ "additionalLayers": "Incluir superposiciones"
},
"backgroundSwitcher":{
"tooltip": "Selección del fondo"
@@ -3467,6 +3470,9 @@
"noDataForPosition": "no hay datos de Street View para esta",
"unknownError": "error desconocido, ver consola"
}
+ },
+ "sidebarMenu": {
+ "showMoreItems": "Mostrar más elementos"
}
}
}
diff --git a/web/client/translations/data.fr-FR.json b/web/client/translations/data.fr-FR.json
index f3e01ee609..33f9416e25 100644
--- a/web/client/translations/data.fr-FR.json
+++ b/web/client/translations/data.fr-FR.json
@@ -487,6 +487,8 @@
"toggleGroupVisibility": "Changer la visibilité du groupe",
"displayLegendAndTools": "Afficher la légende",
"zoomToLayerExtent": "Zoomer sur l'étendue de la couche",
+ "addAnnotations": "Ajouter des annotations",
+ "editAnnotations": "Modifier les annotations",
"addLayer": "Ajouter une couche",
"addLayerToGroup": "Ajouter une couche au groupe sélectionné",
"addGroup": "Ajouter un groupe",
@@ -654,7 +656,8 @@
"previewFormatUnsupported": "Format non pris en charge pour l'aperçu",
"projection": "Système de coordonnées",
"projectionmismatch": "Non-concordance du système de coordonnées entre la carte imprimée et la carte à l'écran",
- "graticule": "ajouter une grille avec des étiquettes"
+ "graticule": "ajouter une grille avec des étiquettes",
+ "additionalLayers": "Inclure les superpositions"
},
"backgroundSwitcher": {
"tooltip": "Sélection du fond de plan"
@@ -3468,6 +3471,9 @@
"noDataForPosition": "pas de données Street View pour cette position",
"unknownError": "erreur inconnue, voir console"
}
+ },
+ "sidebarMenu": {
+ "showMoreItems": "Afficher plus d'éléments"
}
}
}
diff --git a/web/client/translations/data.it-IT.json b/web/client/translations/data.it-IT.json
index 76c51a862e..819f2f8e69 100644
--- a/web/client/translations/data.it-IT.json
+++ b/web/client/translations/data.it-IT.json
@@ -487,6 +487,8 @@
"toggleGroupVisibility": "Attiva o disattiva la visibilità del gruppo",
"displayLegendAndTools": "Visualizza legenda",
"zoomToLayerExtent": "Zoom all' estensione del livello",
+ "addAnnotations": "Aggiungi annotazioni",
+ "editAnnotations": "Modifica annotazioni",
"addLayer": "Aggiungi Livello",
"addLayerToGroup": "Aggiungi livello al gruppo selezionato",
"addGroup": "Aggiungi Gruppo",
@@ -654,7 +656,8 @@
"previewFormatUnsupported": "Formato non supportato per la preview",
"projection": "Sistema di coordinate",
"projectionmismatch": "Sistema di coordinate di stampa diverso da quello a schermo",
- "graticule": "aggiungi griglia con label"
+ "graticule": "aggiungi griglia con label",
+ "additionalLayers": "Includi sovrapposizioni"
},
"backgroundSwitcher":{
"tooltip": "Scegli lo sfondo"
@@ -3468,6 +3471,9 @@
"noDataForPosition": "Non ci sono dati Street View per questa posizione",
"unknownError": "Errore sconosciuto"
}
- }
+ },
+ "sidebarMenu": {
+ "showMoreItems": "Mostra più elementi"
+ }
}
}
diff --git a/web/client/utils/MapUtils.js b/web/client/utils/MapUtils.js
index 562d9cc71c..4d3653bcfb 100644
--- a/web/client/utils/MapUtils.js
+++ b/web/client/utils/MapUtils.js
@@ -38,6 +38,8 @@ import {
} from './LayersUtils';
import assign from 'object-assign';
+export const DEFAULT_MAP_LAYOUT = {left: {sm: 300, md: 500, lg: 600}, right: {md: 548}, bottom: {sm: 30}};
+
export const DEFAULT_SCREEN_DPI = 96;
export const METERS_PER_UNIT = {
diff --git a/web/client/utils/PluginsUtils.js b/web/client/utils/PluginsUtils.js
index 2c34f1c966..9db6fd5d70 100644
--- a/web/client/utils/PluginsUtils.js
+++ b/web/client/utils/PluginsUtils.js
@@ -233,22 +233,30 @@ const includeLoaded = (name, loadedPlugins, plugin, stateSelector) => {
return plugin;
};
+const executeDeferredProp = (pluginImpl, pluginConfig, name) => pluginImpl && isFunction(pluginImpl[name]) ?
+ ({...pluginImpl, [name]: pluginImpl[name](pluginConfig)}) :
+ pluginImpl;
+
const getPriority = (plugin, override = {}, container) => {
+ const pluginImpl = executeDeferredProp(plugin.impl, plugin.config, container);
return (
get(override, container + ".priority") ||
- get(plugin, container + ".priority") ||
+ get(pluginImpl, container + ".priority") ||
0
);
};
-export const getMorePrioritizedContainer = (pluginImpl, override = {}, plugins, priority) => {
+export const getMorePrioritizedContainer = (plugin, override = {}, plugins, priority) => {
+ const pluginImpl = plugin.impl;
return plugins.reduce((previous, current) => {
const containerName = current.name || current;
- const pluginPriority = getPriority(pluginImpl, override, containerName);
+ const pluginPriority = getPriority(plugin, override, containerName);
return pluginPriority > previous.priority ? {
plugin: {
name: containerName,
- impl: assign({}, pluginImpl[containerName], override[containerName])
+ impl: {
+ ...(isFunction(pluginImpl[containerName]) ? pluginImpl[containerName](plugin.config) : pluginImpl[containerName]),
+ ...(override[containerName] ?? {})}
},
priority: pluginPriority} : previous;
}, {plugin: null, priority: priority});
@@ -271,8 +279,8 @@ const canContain = (container, plugin, override = {}) => {
return plugin[container] || override[container] || false;
};
-const isMorePrioritizedContainer = (pluginImpl, override, plugins, priority) => {
- return getMorePrioritizedContainer(pluginImpl,
+const isMorePrioritizedContainer = (plugin, override, plugins, priority) => {
+ return getMorePrioritizedContainer(plugin,
override,
plugins,
priority).plugin === null;
@@ -282,10 +290,6 @@ const isValidConfiguration = (cfg) => {
return cfg && isString(cfg) || (isObject(cfg) && cfg.name);
};
-const executeDeferredProp = (pluginImpl, pluginConfig, name) => pluginImpl && isFunction(pluginImpl[name]) ?
- ({...pluginImpl, [name]: pluginImpl[name](pluginConfig)}) :
- pluginImpl;
-
export const getPluginItems = (state, plugins = {}, pluginsConfig = {}, containerName, containerId, isDefault, loadedPlugins = {}, filter) => {
return Object.keys(plugins)
// extract basic info for each plugins (name, implementation and config)
@@ -325,8 +329,8 @@ export const getPluginItems = (state, plugins = {}, pluginsConfig = {}, containe
return [...acc, curr];
}, [])
// include only plugins for which container is the preferred container
- .filter((plugin) => isMorePrioritizedContainer(plugin.impl, plugin.config.override, pluginsConfig,
- getPriority(plugin.impl, plugin.config.override, containerName)))
+ .filter((plugin) => isMorePrioritizedContainer(plugin, plugin.config.override, pluginsConfig,
+ getPriority(plugin, plugin.config.override, containerName)))
.map((plugin) => {
const pluginName = getPluginSimpleName(plugin.name);
const pluginImpl = includeLoaded(pluginName, loadedPlugins, plugin.impl);