From 2b62f6547a761f82f441b3807db5d8a6cff80820 Mon Sep 17 00:00:00 2001 From: Alexander Date: Thu, 5 May 2022 12:29:45 +0300 Subject: [PATCH 1/2] #8086 Layout changes and other improvements (#8085) * # 150 Layout changes of the UI - Moving annotations to the left sidebar (TOC), adding annotation management tools into its toolbar. * Updated test to be align with the changed position of annotations panel * Changes for print plugin required to support printing of additional layers. * Minor customizations for "Home" and "Login" plugins to make them properly styled both for Omnibar and SidebarMenu Adding abstract element for sidebar menu that could be used as a tool. Making OmniBar disappear on map viewer page if sidebarMenu was instantiated. * Update of the rest plugins that were presented in BurgerMenu to appear in SidebarMenu * rollback, was causing problems when some glyphicons were not rendered * Corrections of style for legacy burger menu * - Hide burger menu when SidebarMenu is active, keep OmniBar visible as it is a container for search plugin - Updated configuration for right panels: aligned their width (all to 550px), connected them to the layout state so that they track for right/bottom paddings. - Provided different way to apply paddings for dock panels and for toolbars (omnibar, map toolbar) - New component to wrap dock panels. - Style changes * Skip attempt to sum-up dockSize as percentage amount. * Moved layout-based style from omnibar to search plugin to prevent infinite loop * Added toggle support for part of the tools; Style fixes. * Selector & reducer update * Sidebar menu with tracking of the window height and hiding elements into dropdown dynamically. * - Sidebar - keep last active item tracked. - Burger menu, fix for visibility condition - Minor fix to the selector to get paddings for docks and toolbars depending on open tools. Definitely not the most beauty way to resolve the issue, but it helps to prevent gazzilions of unneeded re-renders for components when store state changes by ANY action. - All menu elements are togglable. - Search bar with new behavior (- media queries, + checks if width fit to the screen) - Fix for react-container-dimensions & dock components by updating it to the latest version (only used by docks, so it's easy to test/safe to update) - Slightly changes styles of preview plugin to make it look fine with the sidebar menu. * Prevent reset of last active item from additional list when user toggles item in the main list * Support of having plugins list that define dock panel that needs to be open exclusively in sidebar. All other panels from the list will be deactivated automatically. Configurable/extendible via "miscSettings.excludsiveDockPanels" in localConfig.json * - Updated annotations panels order (since it's moved from right to left) - Deactivate annotations when measure is on and vice versa - Toggle off any open panel in right sidebar when feature editor is open - Toggle off Identify panel when any right panel is getting activated. - Z-index readjustments * - Fixes for searchbar when omnibar contains elements; - Add cadastrapp to the list of exclusive panels - Re-checking that sidebar is properly instantiated on its update. Sometimes onInit isn't running on component mount. - Making user extensions plugin close feature editor * Locale strings for sidebar menu * Fixes search to be styled consistently with or without sidebar menu. Correction of elements order in omnibar on home page. * Fixes search to be styled consistently with or without sidebar menu. * Amending existing tests to make them work after layout changes; Minor fixes to the epic to keep old logic for "bottom" offset. * Basic test coverage for implemented functionality * Reverting flex styles for omnibar. Using simpler approach for proper searchbar positioning. * Fix of the bug that prevents prioritized container to be properly selected if more than one container is defined as a function in plugin. * - Changes in plugin priorities to make them appear in BurgerMenu by default. - SLight update to the burger menu to make it's possible to track whether it's mounted on the page or not. - Resolving issue with Home and Login plugins to be rendered in sidebarMenu IF there is no BurgerMenu, otherwise they should be rendered in omnibar; - * Reducing button sizes; Adding shadow to the sidebar; Replaced save tool icon. * Making option to print additional layers optional and disabled by default, updating option name. * Removed monitored state. * Removed BurgerMenu from desktop plugins. * Share plugin tests fix. * Order matters * 15/30px size for medium-size buttons * - Separate panel component with support of tracking of container width - Fix for measure plugin; - Fix of map grid element height on home page. * Fix for annotations plugin tests * Revert of hardcoded toolbar buttons in TOC for annotations. Defining buttons in Annotations plugin instead. * Correction to make cards size approximately same as it was before button sizes change * Using common button size in less stylesheet of SidebarMenu; Migration guide. * - Tracking of the case when left/right panels are open via state. - Style fix for mapstore logo, spinner; - Widgets plugin corrections, removed hardcoded offset, using offset provided by state. - Amended tests * Updated migration documentation with changes to the pluginsConfig.json * - Map export, Save, SaveAs plugins - making them work again by adding doNotHide flag into container configuration. * DockContainer and ResponsivePanel documentation * Added card-height variable. Applied it to the min-height of the sidegrid cards & TOC layers list * Missed lines * Missed lines /2 * Use card-height in TOC styles * Suggested change to the documentation with minor fix * Update web/client/configs/pluginsConfig.json Co-authored-by: Lorenzo Natali * Properly hide overlay layers printing options by default (it is enabled if opposite is not passed by props) * Properly hide overlay layers printing options by default (it is enabled if opposite is not passed by props) * Missing doNotHide for DeleteMap plugin Co-authored-by: Lorenzo Natali (cherry picked from commit 8d438d9ea2a0256115c34dfafc2680e6ae107224) --- .../mapstore-migration-guide.md | 78 +++++ package.json | 2 +- .../actions/__tests__/sidebarmenu-test.js | 19 ++ web/client/actions/sidebarmenu.js | 16 + web/client/components/TOC/Toolbar.jsx | 2 + .../data/identify/IdentifyContainer.jsx | 190 +++++------ .../components/details/DetailsPanel.jsx | 17 +- web/client/components/home/Home.jsx | 14 +- .../map/openlayers/snapshot/Preview.jsx | 1 + .../mapcontrols/annotations/Annotations.jsx | 11 +- .../mapcontrols/measure/MeasureDialog.jsx | 23 +- .../components/misc/enhancers/tooltip.jsx | 2 +- .../components/misc/panels/DockContainer.jsx | 33 ++ .../misc/panels/ResponsivePanel.jsx | 61 ++++ .../panels/__tests__/DockContainer-test.jsx | 31 ++ .../GlobalSpinner/css/GlobalSpinner.css | 6 + .../components/plugins/PluginsContainer.jsx | 2 +- web/client/components/security/UserMenu.jsx | 46 +-- .../components/sidebarmenu/SidebarElement.jsx | 22 ++ .../__tests__/SidebarElement-test.jsx | 38 +++ web/client/configs/localConfig.json | 3 +- web/client/configs/pluginsConfig.json | 98 ++++-- web/client/configs/simple.json | 2 + .../epics/__tests__/annotations-test.js | 4 +- web/client/epics/__tests__/catalog-test.js | 6 +- .../epics/__tests__/featuregrid-test.js | 32 +- web/client/epics/__tests__/maplayout-test.js | 113 +++++-- .../epics/__tests__/measurement-test.jsx | 1 + web/client/epics/annotations.js | 7 +- web/client/epics/catalog.js | 12 +- web/client/epics/featuregrid.js | 4 + web/client/epics/mapcatalog.js | 12 + web/client/epics/maplayout.js | 38 ++- web/client/epics/maptemplates.js | 13 +- web/client/epics/measurement.js | 17 +- web/client/epics/sidebarmenu.js | 41 +++ web/client/epics/userextensions.js | 20 ++ web/client/plugins/Annotations.jsx | 92 ++++-- web/client/plugins/BurgerMenu.jsx | 32 +- web/client/plugins/DeleteMap.jsx | 19 +- web/client/plugins/Details.jsx | 38 ++- web/client/plugins/FeatureEditor.jsx | 2 +- web/client/plugins/Help.jsx | 9 + web/client/plugins/HelpLink.jsx | 14 + web/client/plugins/Home.jsx | 22 +- web/client/plugins/Identify.jsx | 4 +- web/client/plugins/Login.jsx | 28 +- web/client/plugins/MapCatalog.jsx | 139 +++++--- web/client/plugins/MapExport.jsx | 36 ++- web/client/plugins/MapImport.jsx | 11 + web/client/plugins/MapTemplates.jsx | 136 +++++--- web/client/plugins/Measure.jsx | 23 +- web/client/plugins/MetadataExplorer.jsx | 66 ++-- web/client/plugins/OmniBar.jsx | 19 +- web/client/plugins/Print.jsx | 24 +- web/client/plugins/Save.jsx | 25 +- web/client/plugins/SaveAs.jsx | 23 +- web/client/plugins/Search.jsx | 82 +++-- web/client/plugins/Settings.jsx | 11 + web/client/plugins/ShapeFile.jsx | 10 + web/client/plugins/Share.jsx | 13 +- web/client/plugins/SidebarMenu.jsx | 305 ++++++++++++++++++ web/client/plugins/Snapshot.jsx | 11 + web/client/plugins/StreetView/StreetView.jsx | 16 + web/client/plugins/TOC.jsx | 10 +- web/client/plugins/Tutorial.jsx | 17 + web/client/plugins/UserExtensions.jsx | 94 ++++-- web/client/plugins/UserSession.jsx | 13 + web/client/plugins/Widgets.jsx | 14 +- .../plugins/__tests__/MapTemplates-test.jsx | 2 +- web/client/plugins/__tests__/Share-test.jsx | 10 +- .../plugins/__tests__/SidebarMenu-test.jsx | 42 +++ web/client/plugins/burgermenu/burgermenu.css | 2 +- web/client/plugins/login/index.js | 6 - web/client/plugins/manager/ManagerMenu.jsx | 2 +- web/client/plugins/maploading/maploading.css | 2 - .../plugins/metadataexplorer/css/style.css | 3 - web/client/plugins/print/index.js | 18 ++ .../plugins/sidebarmenu/sidebarmenu.less | 37 +++ web/client/plugins/widgets/WidgetsTray.jsx | 12 +- web/client/product/plugins.js | 4 +- web/client/product/plugins/About.jsx | 13 +- web/client/product/plugins/Fork.jsx | 2 +- .../reducers/__tests__/sidebarmenu-test.js | 22 ++ web/client/reducers/maplayout.js | 6 +- web/client/reducers/sidebarmenu.js | 24 ++ .../selectors/__tests__/catalog-test.js | 16 +- .../selectors/__tests__/mapcatalog-test.js | 13 +- .../selectors/__tests__/maplayout-test.js | 13 +- .../selectors/__tests__/maptemplates-test.js | 12 +- .../selectors/__tests__/measurement-test.js | 13 +- .../selectors/__tests__/sidebarmenu-test.js | 33 ++ .../__tests__/userextensions-test.js | 23 ++ web/client/selectors/catalog.js | 2 +- web/client/selectors/controls.js | 1 + web/client/selectors/map.js | 15 +- web/client/selectors/mapcatalog.js | 2 + web/client/selectors/maplayout.js | 52 ++- web/client/selectors/maptemplates.js | 2 + web/client/selectors/measurement.js | 7 +- web/client/selectors/sidebarmenu.js | 5 + web/client/selectors/userextensions.js | 11 + .../themes/default/bootstrap-theme.less | 6 + .../themes/default/less/annotations.less | 6 +- .../themes/default/less/createnewmap.less | 4 +- web/client/themes/default/less/loaders.less | 2 +- .../themes/default/less/map-search-bar.less | 104 +++--- .../themes/default/less/maps-properties.less | 4 +- web/client/themes/default/less/navbar.less | 17 + web/client/themes/default/less/panels.less | 12 + web/client/themes/default/less/searchbar.less | 71 +--- web/client/themes/default/less/sidegrid.less | 10 +- web/client/themes/default/less/toc.less | 32 +- web/client/themes/default/ms-variables.less | 10 +- web/client/translations/data.de-DE.json | 8 +- web/client/translations/data.en-US.json | 8 +- web/client/translations/data.es-ES.json | 8 +- web/client/translations/data.fr-FR.json | 8 +- web/client/translations/data.it-IT.json | 10 +- web/client/utils/MapUtils.js | 2 + web/client/utils/PluginsUtils.js | 28 +- 121 files changed, 2481 insertions(+), 691 deletions(-) create mode 100644 web/client/actions/__tests__/sidebarmenu-test.js create mode 100644 web/client/actions/sidebarmenu.js create mode 100644 web/client/components/misc/panels/DockContainer.jsx create mode 100644 web/client/components/misc/panels/ResponsivePanel.jsx create mode 100644 web/client/components/misc/panels/__tests__/DockContainer-test.jsx create mode 100644 web/client/components/sidebarmenu/SidebarElement.jsx create mode 100644 web/client/components/sidebarmenu/__tests__/SidebarElement-test.jsx create mode 100644 web/client/epics/sidebarmenu.js create mode 100644 web/client/epics/userextensions.js create mode 100644 web/client/plugins/SidebarMenu.jsx create mode 100644 web/client/plugins/__tests__/SidebarMenu-test.jsx create mode 100644 web/client/plugins/sidebarmenu/sidebarmenu.less create mode 100644 web/client/reducers/__tests__/sidebarmenu-test.js create mode 100644 web/client/reducers/sidebarmenu.js create mode 100644 web/client/selectors/__tests__/sidebarmenu-test.js create mode 100644 web/client/selectors/__tests__/userextensions-test.js create mode 100644 web/client/selectors/sidebarmenu.js create mode 100644 web/client/selectors/userextensions.js diff --git a/docs/developer-guide/mapstore-migration-guide.md b/docs/developer-guide/mapstore-migration-guide.md index 5e7de140fb..c3971bff42 100644 --- a/docs/developer-guide/mapstore-migration-guide.md +++ b/docs/developer-guide/mapstore-migration-guide.md @@ -21,12 +21,90 @@ This is a list of things to check if you want to update from a previous version - Follow the instructions below, in order, from your version to the one you want to update to. +### 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 aad5da14a8..b379193829 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 b4027d016e..2b1a817788 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 effee2091d..ae87a77ae4 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. @@ -302,7 +303,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); } @@ -572,18 +576,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, @@ -623,8 +628,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 779c863a7b..3f6d918632 100644 --- a/web/client/plugins/print/index.js +++ b/web/client/plugins/print/index.js @@ -96,6 +96,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, @@ -111,6 +121,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, @@ -145,6 +156,13 @@ export const standardItems = { "title": "print.legendoptions" }, position: 2 + }, { + id: "overlayLayers", + plugin: AdditionalLayers, + cfg: { + enabled: false + }, + position: 5 }], "right-panel": [{ id: "resolution", 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 c41b2d5dfd..0a24851879 100644 --- a/web/client/translations/data.de-DE.json +++ b/web/client/translations/data.de-DE.json @@ -524,6 +524,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", @@ -692,7 +694,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" @@ -3500,6 +3503,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 fc9f0cc34d..084f3f6232 100644 --- a/web/client/translations/data.en-US.json +++ b/web/client/translations/data.en-US.json @@ -486,6 +486,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", @@ -653,7 +655,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" @@ -3473,6 +3476,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 a094b2c646..5fb479ddbc 100644 --- a/web/client/translations/data.es-ES.json +++ b/web/client/translations/data.es-ES.json @@ -486,6 +486,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", @@ -653,7 +655,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" @@ -3462,6 +3465,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 a6c96a7912..0919890446 100644 --- a/web/client/translations/data.fr-FR.json +++ b/web/client/translations/data.fr-FR.json @@ -486,6 +486,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", @@ -653,7 +655,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" @@ -3463,6 +3466,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 fa97e176c0..06d1db386e 100644 --- a/web/client/translations/data.it-IT.json +++ b/web/client/translations/data.it-IT.json @@ -486,6 +486,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", @@ -653,7 +655,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" @@ -3463,6 +3466,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); From 122e5dbd0ef9be98dc9dc1cce36924d70fd549c3 Mon Sep 17 00:00:00 2001 From: Alexander Date: Fri, 6 May 2022 17:47:36 +0300 Subject: [PATCH 2/2] 8086 Bugfixes for layout changes (#8169) * Context creator - map configuration - use SidebarMenu instead of burger menu. Properly refresh sidebar menu activity state on map reload * Class name should be applied to the container as it's important for proper styling of catalog in modal map viewer in geostory * Custom compass offset on Cesium viewer in case if sidebar is active. Passing sidebar state to the map plugin. * Get back original containers, applying same priority to the containers and hiding menu item in sidebar if burger menu exists on the page Rolling back updatableLayersCount selector item. * Revert "Custom compass offset on Cesium viewer in case if sidebar is active. Passing sidebar state to the map plugin." This reverts commit 704c2052e480308b49470287254c256192d7c44f. * Static position update of compass * Update of the pluginsFilterOverride to make it works with a custom list of plugins passed by props (cherry picked from commit 29cbb12ce0be7081816afc077d747f16966b2026) --- .../contextcreator/ContextCreator.jsx | 22 +++++++++------- web/client/components/security/UserMenu.jsx | 2 +- web/client/plugins/MetadataExplorer.jsx | 18 +++++++++++-- web/client/plugins/SidebarMenu.jsx | 26 +++++++++---------- web/client/plugins/TOC.jsx | 2 +- web/client/themes/default/less/cesium.less | 6 ++++- 6 files changed, 48 insertions(+), 28 deletions(-) diff --git a/web/client/components/contextcreator/ContextCreator.jsx b/web/client/components/contextcreator/ContextCreator.jsx index d2ef9910ee..f0e1890fce 100644 --- a/web/client/components/contextcreator/ContextCreator.jsx +++ b/web/client/components/contextcreator/ContextCreator.jsx @@ -17,8 +17,9 @@ import ConfigureMap from './ConfigureMapStep'; import ConfigureThemes from './ConfigureThemes'; import {CONTEXT_TUTORIALS} from '../../actions/contextcreator'; /** - * Filters plugins and applies overrides. - * The resulting array will filter the pluginsConfig returning only the ones present in viewerPlugins. + * Merges plugins "cfg" from pluginsConfigs and applies "overrides" from viewerPlugins. + * The resulting array will return viewerPlugins with "cfg" applied directly from the pluginsConfigs and optionally overridden + * with "overrides" from viewerPlugins. * If some viewerPlugin is an object, it can contain a special entry "overrides". If so, the configuration here * will override the ones in the original plugin config. * Actually overrides are supported only for "cfg" values. @@ -46,17 +47,20 @@ import {CONTEXT_TUTORIALS} from '../../actions/contextcreator'; * "cfg": { activateQueryTool: false, otherOptions: true } * }, * ``` + * NOTE: Logic has changed to support custom list of plugins to be active on the map configuration step of context wizard. + * The final result will match previous implementation except that it will also contain plugins that are missing + * in "viewer"/"desktop" array inside localConfig.json -> plugins * @param {array} pluginsConfigs array of plugins (Strings or objects) to override * @param {array} viewerPlugins list of plugins to use */ export const pluginsFilterOverride = (pluginsConfigs, viewerPlugins) => { - return pluginsConfigs.map(p => { - const pName = isObject(p) ? p.name : p; + return viewerPlugins.map((viewerPlugin) => { + const pName = isObject(viewerPlugin) ? viewerPlugin.name : viewerPlugin; // find out - const viewerPlugin = find(viewerPlugins, vp => { - return pName === (isObject(vp) ? vp.name : vp); + const p = find(pluginsConfigs, plugin => { + return pName === (isObject(plugin) ? plugin.name : plugin); }); - if (viewerPlugin) { + if (p) { if (isObject(viewerPlugin) && viewerPlugin.overrides) { const newP = isObject(p) ? p : { name: p }; const cfg = { @@ -70,8 +74,8 @@ export const pluginsFilterOverride = (pluginsConfigs, viewerPlugins) => { } return p; } - return null; - }).filter(p => p); // remove plugins not found + return viewerPlugin; + }); }; export default class ContextCreator extends React.Component { diff --git a/web/client/components/security/UserMenu.jsx b/web/client/components/security/UserMenu.jsx index 421ccd56ca..ffcf8ab6e8 100644 --- a/web/client/components/security/UserMenu.jsx +++ b/web/client/components/security/UserMenu.jsx @@ -94,7 +94,7 @@ class UserMenu extends React.Component { renderUnsavedMapChangesDialog: true, renderButtonText: false, hidden: false, - displayUnsavedDialog: true + displayUnsavedDialog: false }; renderGuestTools = () => { diff --git a/web/client/plugins/MetadataExplorer.jsx b/web/client/plugins/MetadataExplorer.jsx index f22ce1ac20..a241c7f6cb 100644 --- a/web/client/plugins/MetadataExplorer.jsx +++ b/web/client/plugins/MetadataExplorer.jsx @@ -75,6 +75,7 @@ import { } from '../selectors/catalog'; import { layersSelector } from '../selectors/layers'; import { currentLocaleSelector, currentMessagesSelector } from '../selectors/locale'; +import {burgerMenuSelector} from "../selectors/controls"; import { isLocalizedLayerStylesEnabledSelector } from '../selectors/localizedLayerStyles'; import { projectionSelector } from '../selectors/map'; import { mapLayoutValuesSelector } from '../selectors/maplayout'; @@ -219,7 +220,7 @@ class MetadataExplorerComponent extends React.Component { , action: setControlProperty.bind(null, "metadataexplorer", "enabled", true, true), doNotHide: true, - priority: 2 + priority: 1 + }, + BackgroundSelector: { + name: 'MetadataExplorer', + doNotHide: true, + priority: 1 + }, + TOC: { + name: 'MetadataExplorer', + doNotHide: true, + priority: 1 }, SidebarMenu: { name: 'metadataexplorer', @@ -305,6 +316,9 @@ export default { tooltip: "catalog.tooltip", icon: , action: setControlProperty.bind(null, "metadataexplorer", "enabled", true, true), + selector: (state) => ({ + style: { display: burgerMenuSelector(state) ? 'none' : null } + }), toggle: true, doNotHide: true, priority: 1 diff --git a/web/client/plugins/SidebarMenu.jsx b/web/client/plugins/SidebarMenu.jsx index 95695ff03a..c3e990fbe0 100644 --- a/web/client/plugins/SidebarMenu.jsx +++ b/web/client/plugins/SidebarMenu.jsx @@ -11,7 +11,6 @@ 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"; @@ -83,6 +82,7 @@ class SidebarMenu extends React.Component { } shouldComponentUpdate(nextProps) { + const markedAsInactive = nextProps.isActive === false; 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; @@ -93,7 +93,7 @@ class SidebarMenu extends React.Component { } return prev; }, []).length > 0 : false; - return newSize || newItems || newVisibleItems || newHeight || burgerMenuState; + return newSize || newItems || newVisibleItems || newHeight || burgerMenuState || markedAsInactive; } componentDidUpdate(prevProps) { @@ -121,19 +121,17 @@ class SidebarMenu extends React.Component { 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] : {} - })) - ); - }, []) + getPanels = () => { + return this.props.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?.[index] || {} + })) ); + }, []); + }; visibleItems = (target) => { diff --git a/web/client/plugins/TOC.jsx b/web/client/plugins/TOC.jsx index bdc76aa905..63cf6abbe6 100644 --- a/web/client/plugins/TOC.jsx +++ b/web/client/plugins/TOC.jsx @@ -132,7 +132,7 @@ const tocSelector = createSelector( 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 > 0, + updatableLayersCount: layers.filter(l => l.group !== 'background' && (l.type === 'wms' || l.type === 'wmts')).length, selectedGroups: selectedNodes.map(n => getNode(groups, n)).filter(n => n && n.nodes), mapName, filteredGroups: addFilteredAttributesGroups(groups, [ diff --git a/web/client/themes/default/less/cesium.less b/web/client/themes/default/less/cesium.less index 31c8334467..eff5d50cf7 100644 --- a/web/client/themes/default/less/cesium.less +++ b/web/client/themes/default/less/cesium.less @@ -28,4 +28,8 @@ text-shadow: none; display: table-cell; vertical-align: middle; -} \ No newline at end of file +} + +#map .cesium-viewer .compass { + right: 40px; +}