diff --git a/docs/developer-guide/mapstore-migration-guide.md b/docs/developer-guide/mapstore-migration-guide.md index 0f26c98785..17366d03d0 100644 --- a/docs/developer-guide/mapstore-migration-guide.md +++ b/docs/developer-guide/mapstore-migration-guide.md @@ -91,12 +91,90 @@ formats: - '*' ``` +### Replacing BurgerMenu with SidebarMenu +There were several changes applied to the application layout, one of them is the Sidebar Menu that comes to replace Burger menu on map viewer and in contexts. +Following actions need to be applied to make a switch: +- Update localConfig.json and add "SidebarMenu" entry to the "desktop" section: +```json +{ + "desktop": [ + ... + "SidebarMenu", + ... + ] +} +``` +- Remove "BurgerMenu" entry from "desktop" section. + +#### Updating contexts to use Sidebar Menu + +Contents of your `pluginsConfig.json` need to be reviewed to allow usage of new "SidebarMenu" in contexts. + +- Find "BurgerMenu" plugin confuguration in `pluginsConfig.json` and remove `"hidden": true` line from it: + +```json + { + "name": "BurgerMenu", + "glyph": "menu-hamburger", + "title": "plugins.BurgerMenu.title", + "description": "plugins.BurgerMenu.description", + "dependencies": [ + "OmniBar" + ] +} +``` + +- Add `SidebarMenu` entry to the "plugins" array: + +```json +{ + "plugins": [ + ... + { + "name": "SidebarMenu", + "hidden": true + } + ... + ] +} +``` + +- Go through all plugins definitions and replace `BurgerMenu` dependency with `SidebarMenu`, e.g.: + +```json + { + "name": "MapExport", + "glyph": "download", + "title": "plugins.MapExport.title", + "description": "plugins.MapExport.description", + "dependencies": [ + "SidebarMenu" + ] + } +``` + +- Also the `StreetView` plugin needs to depend from `SidebarMenu`. + +```json +{ + "name": "StreetView", + "glyph": "road", + "title": "plugins.StreetView.title", + "description": "plugins.StreetView.description", + "dependencies": [ + "SidebarMenu" + ] +} +``` + + ## Migration from 2022.01.00 to 2022.01.01 ### MailingLists plugin has been removed `MailingLists` plugin has ben removed from the core of MapStore. This means you can remove it from your `localConfig.json` (if present, it will be anyway ignored by the plugin system). + ## Migration from 2021.02.02 to 2022.01.00 This release includes several libraries upgrade on the backend side, diff --git a/package.json b/package.json index 27f7083c3c..71cdb05e3e 100644 --- a/package.json +++ b/package.json @@ -221,7 +221,7 @@ "react-codemirror2": "4.0.0", "react-color": "2.17.0", "react-confirm-button": "0.0.2", - "react-container-dimensions": "1.3.2", + "react-container-dimensions": "1.4.1", "react-contenteditable": "3.3.2", "react-copy-to-clipboard": "5.0.0", "react-data-grid": "5.0.4", diff --git a/web/client/actions/__tests__/sidebarmenu-test.js b/web/client/actions/__tests__/sidebarmenu-test.js new file mode 100644 index 0000000000..592c055096 --- /dev/null +++ b/web/client/actions/__tests__/sidebarmenu-test.js @@ -0,0 +1,19 @@ +/** + * 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 {SET_LAST_ACTIVE_ITEM, setLastActiveItem} from "../sidebarmenu"; + +describe('Test correctness of the sidebar actions', () => { + + it('test setLastActiveItem', () => { + const action = setLastActiveItem("annotations"); + expect(action.type).toBe(SET_LAST_ACTIVE_ITEM); + expect(action.value).toBe("annotations"); + }); +}); diff --git a/web/client/actions/sidebarmenu.js b/web/client/actions/sidebarmenu.js new file mode 100644 index 0000000000..037b7dd326 --- /dev/null +++ b/web/client/actions/sidebarmenu.js @@ -0,0 +1,16 @@ +/* + * 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. +*/ + +export const SET_LAST_ACTIVE_ITEM = 'SIDEBARMENU:SET_LAST_ACTIVE_ITEM'; + +export function setLastActiveItem(value) { + return { + type: SET_LAST_ACTIVE_ITEM, + value + }; +} diff --git a/web/client/components/TOC/Toolbar.jsx b/web/client/components/TOC/Toolbar.jsx index e3d32850ca..e99f152458 100644 --- a/web/client/components/TOC/Toolbar.jsx +++ b/web/client/components/TOC/Toolbar.jsx @@ -23,6 +23,7 @@ class Toolbar extends React.Component { static propTypes = { groups: PropTypes.array, items: PropTypes.array, + layers: PropTypes.array, selectedLayers: PropTypes.array, generalInfoFormat: PropTypes.string, selectedGroups: PropTypes.array, @@ -41,6 +42,7 @@ class Toolbar extends React.Component { static defaultProps = { groups: [], items: [], + layers: [], selectedLayers: [], selectedGroups: [], onToolsActions: { diff --git a/web/client/components/data/identify/IdentifyContainer.jsx b/web/client/components/data/identify/IdentifyContainer.jsx index 6a72bc39dc..a5cce27a8e 100644 --- a/web/client/components/data/identify/IdentifyContainer.jsx +++ b/web/client/components/data/identify/IdentifyContainer.jsx @@ -8,17 +8,17 @@ import React from 'react'; -import { Row } from 'react-bootstrap'; +import {Row} from 'react-bootstrap'; import { get } from 'lodash'; import Toolbar from '../../misc/toolbar/Toolbar'; import Message from '../../I18N/Message'; -import DockablePanel from '../../misc/panels/DockablePanel'; import GeocodeViewer from './GeocodeViewer'; import ResizableModal from '../../misc/ResizableModal'; import Portal from '../../misc/Portal'; import Coordinate from './coordinates/Coordinate'; import { responseValidForEdit } from '../../../utils/IdentifyUtils'; import LayerSelector from './LayerSelector'; +import ResponsivePanel from "../../misc/panels/ResponsivePanel"; /** * Component for rendering Identify Container inside a Dockable container @@ -108,102 +108,106 @@ export default props => { const missingResponses = requests.length - responses.length; const revGeocodeDisplayName = reverseGeocodeData.error ? : reverseGeocodeData.display_name; return ( -
- { - onClose(); - toggleHighlightFeature(false); - }} - dock={dock} - style={dockStyle} - showFullscreen={showFullscreen} - zIndex={zIndex} - header={[ - -
- - - -
-
, - !disableCoordinatesRow && - ( - -
- -
- + { + onClose(); + toggleHighlightFeature(false); + }} + dock={dock} + style={dockStyle} + showFullscreen={showFullscreen} + zIndex={zIndex} + header={[ + +
+ + +
+
, + !disableCoordinatesRow && + ( + +
+ +
+ + -
) - ].filter(headRow => headRow)}> - -
- - } - size="xs" - show={warning} - onClose={clearWarning} - buttons={[{ - text: , - onClick: clearWarning, - bsStyle: 'primary' - }]}> -
-
- + }/> + ) + ].filter(headRow => headRow)} + siblings={ + + } + size="xs" + show={warning} + onClose={clearWarning} + buttons={[{ + text: , + onClick: clearWarning, + bsStyle: 'primary' + }]}> +
+
+ +
-
- - -
+
+
+ } + > + + ); }; diff --git a/web/client/components/details/DetailsPanel.jsx b/web/client/components/details/DetailsPanel.jsx index 9dd98a5847..90309fefb7 100644 --- a/web/client/components/details/DetailsPanel.jsx +++ b/web/client/components/details/DetailsPanel.jsx @@ -11,7 +11,7 @@ import PropTypes from 'prop-types'; import Message from '../I18N/Message'; import { Panel } from 'react-bootstrap'; import BorderLayout from '../layout/BorderLayout'; -import DockPanel from "../misc/panels/DockPanel"; +import ResponsivePanel from "../misc/panels/ResponsivePanel"; class DetailsPanel extends React.Component { static propTypes = { @@ -21,7 +21,8 @@ class DetailsPanel extends React.Component { dockStyle: PropTypes.object, panelClassName: PropTypes.string, style: PropTypes.object, - onClose: PropTypes.func + onClose: PropTypes.func, + width: PropTypes.number }; static contextTypes = { @@ -39,14 +40,18 @@ class DetailsPanel extends React.Component { onClose: () => { }, active: false, - panelClassName: "details-panel" + panelClassName: "details-panel", + width: 550 }; render() { return ( - } @@ -59,7 +64,7 @@ class DetailsPanel extends React.Component { {this.props.children} - + ); } } diff --git a/web/client/components/home/Home.jsx b/web/client/components/home/Home.jsx index f5872ccdcc..39643db06b 100644 --- a/web/client/components/home/Home.jsx +++ b/web/client/components/home/Home.jsx @@ -26,7 +26,9 @@ class Home extends React.Component { onCloseUnsavedDialog: PropTypes.func, displayUnsavedDialog: PropTypes.bool, renderUnsavedMapChangesDialog: PropTypes.bool, - tooltipPosition: PropTypes.string + tooltipPosition: PropTypes.string, + bsStyle: PropTypes.string, + hidden: PropTypes.bool }; static contextTypes = { @@ -39,19 +41,21 @@ class Home extends React.Component { onCheckMapChanges: () => {}, onCloseUnsavedDialog: () => {}, renderUnsavedMapChangesDialog: true, - tooltipPosition: 'left' + tooltipPosition: 'left', + bsStyle: 'primary', + hidden: false }; render() { - const { tooltipPosition, ...restProps} = this.props; + const { tooltipPosition, hidden, ...restProps} = this.props; let tooltip = {}; - return ( + return hidden ? false : (

-
diff --git a/web/client/components/mapcontrols/measure/MeasureDialog.jsx b/web/client/components/mapcontrols/measure/MeasureDialog.jsx index 341cf2e701..3fc408127e 100644 --- a/web/client/components/mapcontrols/measure/MeasureDialog.jsx +++ b/web/client/components/mapcontrols/measure/MeasureDialog.jsx @@ -11,10 +11,10 @@ import PropTypes from 'prop-types'; import React from 'react'; import { isEqual } from 'lodash'; import MeasureComponent from './MeasureComponent'; -import DockablePanel from '../../misc/panels/DockablePanel'; import Message from '../../I18N/Message'; import Dialog from '../../misc/Dialog'; import { Glyphicon } from 'react-bootstrap'; +import ResponsivePanel from "../../misc/panels/ResponsivePanel"; class MeasureDialog extends React.Component { static propTypes = { @@ -25,7 +25,9 @@ class MeasureDialog extends React.Component { onInit: PropTypes.func, showCoordinateEditor: PropTypes.bool, defaultOptions: PropTypes.object, - style: PropTypes.object + style: PropTypes.object, + dockStyle: PropTypes.object, + size: PropTypes.number }; static contextTypes = { @@ -41,10 +43,8 @@ class MeasureDialog extends React.Component { showCoordinateEditor: false, showAddAsAnnotation: false, closeGlyph: "1-close", - style: { - // Needs map layout selector see Identify Plugin - height: 'calc(100% - 30px)' - } + dockStyle: {}, + size: 550 }; onClose = () => { @@ -77,18 +77,21 @@ class MeasureDialog extends React.Component { // TODO FIX TRANSALATIONS TITLE return this.props.show ? ( this.props.showCoordinateEditor ? - } glyph="1-ruler" - size={660} + size={this.props.size} open={this.props.show} onClose={this.onClose} - style={this.props.style}> + style={this.props.dockStyle} + > - + : (
  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} > {this.props.user.name} @@ -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 : ( +
+ + { ({ height }) => + <>{props.children}} + toolStyle="tray" + activeStyle="primary" + stateSelector="sidebarMenu" + tool={SidebarElement} + tools={this.getTools('sidebar', height)} + panels={this.getPanels(this.props.items)} + /> } + +
+ + ); + } + + 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 ( - Fork me on GitHub + Fork me on GitHub ); } 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);