From 4525991a7a5d913157c08ee5f5b106afc12c13d2 Mon Sep 17 00:00:00 2001 From: Lorenzo Natali Date: Fri, 17 Jan 2020 09:31:53 +0100 Subject: [PATCH] #4720 - #4719 User Extensions - TOC plugins (#4723) --- web/client/actions/context.js | 11 ++ web/client/examples/featuregrid/plugins.js | 2 +- web/client/localConfig.json | 13 +- web/client/plugins/AddGroup.jsx | 13 +- web/client/plugins/FeatureEditor.jsx | 60 ++++--- web/client/plugins/FilterLayer.jsx | 29 +++ web/client/plugins/MetadataExplorer.jsx | 17 +- web/client/plugins/TOC.jsx | 106 ++++++++--- web/client/plugins/TOCItemsSettings.jsx | 52 ++++-- web/client/plugins/UserExtensions.js | 64 +++++++ web/client/plugins/WidgetsBuilder.jsx | 44 +++-- web/client/plugins/__tests__/TOC-test.jsx | 167 ++++++++++++++++++ .../userExtensions/ExtensionsPanel.jsx | 104 +++++++++++ web/client/pluginsConfig.json | 20 ++- web/client/product/plugins.js | 8 +- web/client/reducers/context.js | 12 +- .../themes/default/less/user-extensions.less | 3 + web/client/themes/default/ms2-theme.less | 1 + 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 | 8 + 23 files changed, 649 insertions(+), 117 deletions(-) create mode 100644 web/client/plugins/FilterLayer.jsx create mode 100644 web/client/plugins/UserExtensions.js create mode 100644 web/client/plugins/userExtensions/ExtensionsPanel.jsx create mode 100644 web/client/themes/default/less/user-extensions.less diff --git a/web/client/actions/context.js b/web/client/actions/context.js index 3b4a5a6c15..055ee03dbf 100644 --- a/web/client/actions/context.js +++ b/web/client/actions/context.js @@ -62,3 +62,14 @@ export const CLEAR_CONTEXT = 'CONTEXT:CLEAR_CONTEXT'; * Clears current context state */ export const clearContext = () => ({ type: CLEAR_CONTEXT }); + +export const UPDATE_USER_PLUGIN = "CONTEXT:UPDATE_USER_PLUGIN"; +/** + * Updates a value for a user plugin. It can be used to activate/deactivate user plugins, or to set specific + * properties. + * @param {string} name the name of the plugin + * @param {object} values key-values map to update. + * @example + * updateUserPlugin("Annotations", {active: true}) + */ +export const updateUserPlugin = (name, values) => ({ type: UPDATE_USER_PLUGIN, name, values}); diff --git a/web/client/examples/featuregrid/plugins.js b/web/client/examples/featuregrid/plugins.js index 84f87d9632..42aca6bebc 100644 --- a/web/client/examples/featuregrid/plugins.js +++ b/web/client/examples/featuregrid/plugins.js @@ -11,7 +11,7 @@ module.exports = { LayerSelectorPlugin: require('./plugins/LayerSelector'), MapPlugin: require('../../plugins/Map'), WFSDownload: require('../../plugins/WFSDownload'), - FeatureEditor: require('../../plugins/FeatureEditor'), + FeatureEditor: require('../../plugins/FeatureEditor').default, QueryPanel: require('../../plugins/QueryPanel'), Notifications: require('../../plugins/Notifications') }, diff --git a/web/client/localConfig.json b/web/client/localConfig.json index 033ca8244f..f53abcbe44 100644 --- a/web/client/localConfig.json +++ b/web/client/localConfig.json @@ -342,17 +342,10 @@ }, { "name": "TOC", "cfg": { - "activateQueryTool": true, - "activateAddLayerButton": true, - "activateAddGroupButton": true, - "activateMetedataTool": false, - "addLayersPermissions": true, - "removeLayersPermissions": true, - "sortingPermissions": true, - "addGroupsPermissions": true, - "removeGroupsPermissions": true + "activateMetedataTool": false } }, + "FilterLayer", "AddGroup", "TOCItemsSettings", "Tutorial", "MapFooter", { @@ -632,7 +625,7 @@ }, "Login", "Language", "NavMenu", "DashboardSave", "DashboardSaveAs", "Attribution", "Home", { "name": "DashboardEditor", "cfg": { - "catalog": { + "catalog": { "url": "https://gs-stable.geo-solutions.it/geoserver/csw", "type": "csw", "title": "GeoSolutions GeoServer CSW", diff --git a/web/client/plugins/AddGroup.jsx b/web/client/plugins/AddGroup.jsx index 8dd7f6c64d..069b3533a5 100644 --- a/web/client/plugins/AddGroup.jsx +++ b/web/client/plugins/AddGroup.jsx @@ -96,6 +96,17 @@ const AddGroupPlugin = connect((state) => ({ onAdd: addGroup })(AddGroup); +/** + * AddGrouo. Add to the TOC the possibility to add layer group. + * @memberof plugins + * @requires plugins.TOC + */ export default createPlugin('AddGroup', { - component: AddGroupPlugin + component: AddGroupPlugin, + containers: { + TOC: { + doNotHide: true, + name: "AddGroup" + } + } }); diff --git a/web/client/plugins/FeatureEditor.jsx b/web/client/plugins/FeatureEditor.jsx index 40d22dbc63..2069bbb035 100644 --- a/web/client/plugins/FeatureEditor.jsx +++ b/web/client/plugins/FeatureEditor.jsx @@ -5,25 +5,33 @@ * This source code is licensed under the BSD-style license found in the * LICENSE file in the root directory of this source tree. */ -const React = require('react'); -const {connect} = require('react-redux'); -const {createSelector, createStructuredSelector} = require('reselect'); -const {bindActionCreators} = require('redux'); -const { get, pick } = require('lodash'); +import React from 'react'; +import {connect} from 'react-redux'; +import {createSelector, createStructuredSelector} from 'reselect'; +import {bindActionCreators} from 'redux'; +import { get, pick } from 'lodash'; +import {compose, lifecycle} from 'recompose'; +import ReactDock from 'react-dock'; -const {compose, lifecycle} = require('recompose'); -const Grid = require('../components/data/featuregrid/FeatureGrid'); -const {paginationInfo, describeSelector, wfsURLSelector, typeNameSelector} = require('../selectors/query'); -const {modeSelector, changesSelector, newFeaturesSelector, hasChangesSelector, selectedFeaturesSelector, getDockSize} = require('../selectors/featuregrid'); -const { toChangesMap} = require('../utils/FeatureGridUtils'); -const {getPanels, getHeader, getFooter, getDialogs, getEmptyRowsView, getFilterRenderers} = require('./featuregrid/panels/index'); -const BorderLayout = require('../components/layout/BorderLayout'); +import { createPlugin } from '../utils/PluginsUtils'; + +import * as epics from '../epics/featuregrid'; +import * as featuregrid from '../reducers/featuregrid'; + +import Grid from '../components/data/featuregrid/FeatureGrid'; +import {paginationInfo, describeSelector, wfsURLSelector, typeNameSelector} from '../selectors/query'; +import {modeSelector, changesSelector, newFeaturesSelector, hasChangesSelector, selectedFeaturesSelector, getDockSize} from '../selectors/featuregrid'; +import { toChangesMap} from '../utils/FeatureGridUtils'; +import {getPanels, getHeader, getFooter, getDialogs, getEmptyRowsView, getFilterRenderers} from './featuregrid/panels/index'; +import BorderLayout from '../components/layout/BorderLayout'; const EMPTY_ARR = []; const EMPTY_OBJ = {}; -const {gridTools, gridEvents, pageEvents, toolbarEvents} = require('./featuregrid/index'); -const { initPlugin, sizeChange, setUp} = require('../actions/featuregrid'); -const ContainerDimensions = require('react-container-dimensions').default; -const {mapLayoutValuesSelector} = require('../selectors/maplayout'); +import {gridTools, gridEvents, pageEvents, toolbarEvents} from './featuregrid/index'; +import { initPlugin, sizeChange, setUp} from '../actions/featuregrid'; +import ContainerDimensions from 'react-container-dimensions'; +import {mapLayoutValuesSelector} from '../selectors/maplayout'; + + const Dock = connect(createSelector( getDockSize, state => mapLayoutValuesSelector(state, {transform: true}), @@ -32,7 +40,7 @@ const Dock = connect(createSelector( dockStyle }) ) -)(require('react-dock').default); +)(ReactDock); /** * @name FeatureEditor * @memberof plugins @@ -242,11 +250,17 @@ const EditorPlugin = compose( }) )(FeatureDock); - -module.exports = { - FeatureEditorPlugin: EditorPlugin, - epics: require('../epics/featuregrid'), +export default createPlugin('FeatureEditor', { + component: EditorPlugin, + epics, reducers: { - featuregrid: require('../reducers/featuregrid') + featuregrid + }, + containers: { + TOC: { + doNotHide: true, + name: "FeatureEditor" + } } -}; +}); + diff --git a/web/client/plugins/FilterLayer.jsx b/web/client/plugins/FilterLayer.jsx new file mode 100644 index 0000000000..1a192a0621 --- /dev/null +++ b/web/client/plugins/FilterLayer.jsx @@ -0,0 +1,29 @@ +/* + * Copyright 2019, 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 { createPlugin } from '../utils/PluginsUtils'; + +// dummy plugin +const FilterLayer = () => null; + +/** + * Plugin that activate the FilterLayer button in the TOC. + * Requires the QueryPanel Plugin To Work + * @memberof plugins + * @requires plugins.QueryPanel + */ +export default createPlugin('FilterLayer', + { + component: FilterLayer, + containers: { + TOC: { + name: "FilterLayer" + } + } + } +); diff --git a/web/client/plugins/MetadataExplorer.jsx b/web/client/plugins/MetadataExplorer.jsx index 35464d8ffb..e7fd314c54 100644 --- a/web/client/plugins/MetadataExplorer.jsx +++ b/web/client/plugins/MetadataExplorer.jsx @@ -252,25 +252,16 @@ const API = { */ module.exports = { MetadataExplorerPlugin: assign(MetadataExplorerPlugin, { - Toolbar: { - name: 'metadataexplorer', - position: 10, - exclusive: true, - panel: true, - tooltip: "catalog.tooltip", - wrap: true, - title: 'catalog.title', - help: , - icon: , - priority: 1 - }, BurgerMenu: { name: 'metadataexplorer', position: 5, text: , icon: , action: setControlProperty.bind(null, "metadataexplorer", "enabled", true, true), - priority: 2, + doNotHide: true + }, + TOC: { + name: 'MetadataExplorer', doNotHide: true } }), diff --git a/web/client/plugins/TOC.jsx b/web/client/plugins/TOC.jsx index 7af8ac1a34..323e73f124 100644 --- a/web/client/plugins/TOC.jsx +++ b/web/client/plugins/TOC.jsx @@ -9,6 +9,8 @@ const PropTypes = require('prop-types'); const React = require('react'); const {connect} = require('react-redux'); const {createSelector} = require('reselect'); +const { compose, branch, withPropsOnChange} = require('recompose'); + const {Glyphicon} = require('react-bootstrap'); const {changeLayerProperties, changeGroupProperties, toggleNode, contextNode, @@ -35,7 +37,7 @@ const assign = require('object-assign'); const layersIcon = require('./toolbar/assets/img/layers.png'); -const {isObject, head} = require('lodash'); +const {isObject, head, find} = require('lodash'); const { setControlProperties} = require('../actions/controls'); const {createWidget} = require('../actions/widgets'); @@ -144,6 +146,7 @@ const DefaultLayerOrGroup = require('../components/TOC/DefaultLayerOrGroup'); class LayerTree extends React.Component { static propTypes = { id: PropTypes.number, + items: PropTypes.array, buttonContent: PropTypes.node, groups: PropTypes.array, settings: PropTypes.object, @@ -227,6 +230,7 @@ class LayerTree extends React.Component { }; static defaultProps = { + items: [], groupPropertiesChangeHandler: () => {}, layerPropertiesChangeHandler: () => {}, retrieveLayerData: () => {}, @@ -258,7 +262,7 @@ class LayerTree extends React.Component { activateQueryTool: true, activateDownloadTool: false, activateWidgetTool: false, - activateLayerFilterTool: true, + activateLayerFilterTool: false, maxDepth: 3, visibilityCheckType: "glyph", settingsOptions: { @@ -479,28 +483,78 @@ class LayerTree extends React.Component { } } -const securityEnhancer = (Component) => (props) => { - const { addLayersPermissions = true, - removeLayersPermissions = true, - sortingPermissions = true, - addGroupsPermissions = true, - removeGroupsPermissions = true, user, ...other} = props; +/** + * enhances the TOC to check `Permissions` properties and enable/disable + * the proper tools. + * @memberof plugins.TOC + */ +const securityEnhancer = withPropsOnChange( + [ + "user", + "addLayersPermissions", "activateAddLayerButton", + "removeLayersPermissions", "activateRemoveLayer", + "sortingPermission", "activateRemoveLayer", + "addGroupsPermissions", "activateAddGroupButton", + "removeGroupsPermissions", "activateRemoveGroup" + ], + (props) => { + const { + addLayersPermissions = true, + removeLayersPermissions = true, + sortingPermissions = true, + addGroupsPermissions = true, + removeGroupsPermissions = true, + activateAddLayerButton, + activateRemoveLayer, + activateSortLayer, + activateAddGroupButton, + activateRemoveGroup, + user + } = props; - const activateParameter = (allow, activate) => { - const isUserAdmin = user && user.role === 'ADMIN' || false; - return (allow || isUserAdmin) ? activate : false; - }; + const activateParameter = (allow, activate) => { + const isUserAdmin = user && user.role === 'ADMIN' || false; + return (allow || isUserAdmin) ? activate : false; + }; - const activateProps = { - activateAddLayerButton: activateParameter(addLayersPermissions, props.activateAddLayerButton), - activateRemoveLayer: activateParameter(removeLayersPermissions, props.activateRemoveLayer), - activateSortLayer: activateParameter(sortingPermissions, props.activateSortLayer), - activateAddGroupButton: activateParameter(addGroupsPermissions, props.activateAddGroupButton), - activateRemoveGroup: activateParameter(removeGroupsPermissions, props.activateRemoveGroup) - }; + return { + activateAddLayerButton: activateParameter(addLayersPermissions, activateAddLayerButton), + activateRemoveLayer: activateParameter(removeLayersPermissions, activateRemoveLayer), + activateSortLayer: activateParameter(sortingPermissions, activateSortLayer), + activateAddGroupButton: activateParameter(addGroupsPermissions, activateAddGroupButton), + activateRemoveGroup: activateParameter(removeGroupsPermissions, activateRemoveGroup) + }; + }); - return ; -}; + +/** + * enhances the TOC to check the presence of TOC plugins to display/add buttons to the toolbar. + * NOTE: the flags are required because of old configurations about permissions. + * TODO: delegate button rendering and actions to the plugins (now this is only a check and some plugins are dummy, only to allow plug/unplug). Also permissions should be delegated to the related plugins + * @memberof plugins.TOC + */ +const checkPluginsEnhancer = branch( + ({ checkPlugins = true }) => checkPlugins, + withPropsOnChange( + ["items", "activateAddLayerButton", "activateAddGroupButton", "activateLayerFilterTool", "activateSettingsTool", "FeatureEditor"], + ({ + items = [], + activateAddLayerButton = true, + activateAddGroupButton = true, + activateQueryTool = true, + activateSettingsTool = true, + activateLayerFilterTool = true, + activateWidgetTool = true + }) => ({ + activateAddLayerButton: activateAddLayerButton && !!find(items, { name: "MetadataExplorer" }) || false, // requires MetadataExplorer (Catalog) + activateAddGroupButton: activateAddGroupButton && !!find(items, { name: "AddGroup" }) || false, + activateSettingsTool: activateSettingsTool && !!find(items, { name: "TOCItemsSettings"}) || false, + activateQueryTool: activateQueryTool && !!find(items, {name: "FeatureEditor"}) || false, + activateLayerFilterTool: activateLayerFilterTool && !!find(items, {name: "FilterLayer"}) || false, + activateWidgetTool: activateWidgetTool && !!find(items, { name: "WidgetBuilder" }) // NOTE: activateWidgetTool is already controlled by a selector. TODO: Simplify investigating on the best approch + }) + ) +); /** @@ -520,8 +574,9 @@ const securityEnhancer = (Component) => (props) => { * @prop {boolean} cfg.activateQueryTool: activate query tool options, default `false` * @prop {boolean} cfg.activateDownloadTool: activate a button to download layer data through wfs, default `false` * @prop {boolean} cfg.activateSortLayer: activate drag and drop to sort layers, default `true` - * @prop {boolean} cfg.activateAddLayerButton: activate a button to open the catalog, default `false` - * @prop {boolean} cfg.activateAddGroupButton: activate a button to add a new group, default `false` + * @prop {boolean} cfg.checkPlugins if true, check if AddLayer, AddGroup ... plugins are present to auto-configure the toolbar + * @prop {boolean} cfg.activateAddLayerButton: activate a button to open the catalog, default `true` + * @prop {boolean} cfg.activateAddGroupButton: activate a button to add a new group, default `true` * @prop {boolean} cfg.showFullTitleOnExpand shows full length title in the legend. default `false`. * @prop {boolean} cfg.hideOpacityTooltip hide toolip on opacity sliders * @prop {string[]|string|object|function} cfg.metadataTemplate custom template for displaying metadata @@ -644,7 +699,10 @@ const TOCPlugin = connect(tocSelector, { hideLayerMetadata, onNewWidget: () => createWidget(), refreshLayerVersion -})(securityEnhancer(LayerTree)); +})(compose( + securityEnhancer, + checkPluginsEnhancer +)(LayerTree)); const API = { csw: require('../api/CSW'), diff --git a/web/client/plugins/TOCItemsSettings.jsx b/web/client/plugins/TOCItemsSettings.jsx index 4ce39c3cb5..c1e5b30899 100644 --- a/web/client/plugins/TOCItemsSettings.jsx +++ b/web/client/plugins/TOCItemsSettings.jsx @@ -6,22 +6,23 @@ * LICENSE file in the root directory of this source tree. */ -const {connect} = require('react-redux'); -const {createSelector} = require('reselect'); -const {compose, defaultProps} = require('recompose'); -const {hideSettings, updateSettings, updateNode, updateSettingsParams} = require('../actions/layers'); -const {getLayerCapabilities} = require('../actions/layerCapabilities'); -const {updateSettingsLifecycle} = require("../components/TOC/enhancers/tocItemsSettings"); -const TOCItemsSettings = require('../components/TOC/TOCItemsSettings'); -const defaultSettingsTabs = require('./tocitemssettings/defaultSettingsTabs'); -const LayersUtils = require('../utils/LayersUtils'); -const { initialSettingsSelector, originalSettingsSelector, activeTabSettingsSelector } = require('../selectors/controls'); -const {layerSettingSelector, layersSelector, groupsSelector, elementSelector} = require('../selectors/layers'); -const {mapLayoutValuesSelector} = require('../selectors/maplayout'); -const {currentLocaleSelector} = require('../selectors/locale'); -const {isAdminUserSelector} = require('../selectors/security'); -const {setControlProperty} = require('../actions/controls'); -const {toggleStyleEditor} = require('../actions/styleeditor'); +import {connect} from 'react-redux'; +import {createSelector} from 'reselect'; +import {compose, defaultProps} from 'recompose'; +import { createPlugin } from '../utils/PluginsUtils'; +import LayersUtils from '../utils/LayersUtils'; +import {hideSettings, updateSettings, updateNode, updateSettingsParams} from '../actions/layers'; +import {getLayerCapabilities} from '../actions/layerCapabilities'; +import {updateSettingsLifecycle} from "../components/TOC/enhancers/tocItemsSettings"; +import TOCItemsSettings from '../components/TOC/TOCItemsSettings'; +import defaultSettingsTabs from './tocitemssettings/defaultSettingsTabs'; +import { initialSettingsSelector, originalSettingsSelector, activeTabSettingsSelector } from '../selectors/controls'; +import {layerSettingSelector, layersSelector, groupsSelector, elementSelector} from '../selectors/layers'; +import {mapLayoutValuesSelector} from '../selectors/maplayout'; +import {currentLocaleSelector} from '../selectors/locale'; +import {isAdminUserSelector} from '../selectors/security'; +import {setControlProperty} from '../actions/controls'; +import {toggleStyleEditor} from '../actions/styleeditor'; const tocItemsSettingsSelector = createSelector([ layerSettingSelector, @@ -89,6 +90,19 @@ const TOCItemsSettingsPlugin = compose( }) )(TOCItemsSettings); -module.exports = { - TOCItemsSettingsPlugin -}; +/** + * TOCItemsSettings. Add to the TOC the possibility to edit layers. + * @memberof plugins + * @requires plugins.TOC + */ +export default createPlugin('TOCItemsSettings', { + component: TOCItemsSettingsPlugin, + containers: { + TOC: { + doNotHide: true, + name: "TOCItemsSettings" + } + } +}); + + diff --git a/web/client/plugins/UserExtensions.js b/web/client/plugins/UserExtensions.js new file mode 100644 index 0000000000..ece92cc478 --- /dev/null +++ b/web/client/plugins/UserExtensions.js @@ -0,0 +1,64 @@ +/* + * Copyright 2019, 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 { connect } from 'react-redux'; +import { createPlugin } from '../utils/PluginsUtils'; +import { Glyphicon } from 'react-bootstrap'; + +import Message from '../components/I18N/Message'; + +import { setControlProperty, toggleControl } from '../actions/controls'; + +import { createSelector } from 'reselect'; +import get from 'lodash/get'; +import DockPanel from '../components/misc/panels/DockPanel'; +import ExtensionsPanel from './userExtensions/ExtensionsPanel'; + + +const Extensions = ({ + active, + onClose = () => { } +}) => ( + } + onClose={() => onClose()} + glyph="plug" + style={{ height: 'calc(100% - 30px)' }} + noResize> + + ); + +const ExtensionsPlugin = connect( + createSelector([ + state => get(state, 'controls.userExtensions.enabled') + ], + (active, extensions) => ({ active, extensions })), + { + onClose: toggleControl.bind(null, 'userExtensions', 'enabled') + } +)(Extensions); + +export default createPlugin('UserExtensions', { + component: ExtensionsPlugin, + containers: { + BurgerMenu: { + name: 'userExtensions', + position: 999, + text: , + icon: , + action: setControlProperty.bind(null, "userExtensions", "enabled", true, true), + priority: 2, + doNotHide: true + } + } +}); diff --git a/web/client/plugins/WidgetsBuilder.jsx b/web/client/plugins/WidgetsBuilder.jsx index 0ed531c17e..e663c4d25d 100644 --- a/web/client/plugins/WidgetsBuilder.jsx +++ b/web/client/plugins/WidgetsBuilder.jsx @@ -6,22 +6,25 @@ * LICENSE file in the root directory of this source tree. */ -const React = require('react'); -const PropTypes = require('prop-types'); +import React from 'react'; +import PropTypes from 'prop-types'; +import * as epics from '../epics/widgetsbuilder'; +import { createPlugin } from '../utils/PluginsUtils'; -const DockPanel = require("../components/misc/panels/DockPanel"); +import DockPanel from "../components/misc/panels/DockPanel"; -const {connect} = require('react-redux'); -const {createSelector} = require('reselect'); -const { compose } = require('recompose'); +import {connect} from 'react-redux'; +import {createSelector} from 'reselect'; +import { compose } from 'recompose'; -const {setControlProperty} = require('../actions/controls'); +import {setControlProperty} from '../actions/controls'; -const {mapLayoutValuesSelector} = require('../selectors/maplayout'); -const {widgetBuilderSelector} = require('../selectors/controls'); -const { dependenciesSelector, availableDependenciesSelector} = require('../selectors/widgets'); -const { toggleConnection } = require('../actions/widgets'); -const withMapExitButton = require('./widgetbuilder/enhancers/withMapExitButton'); +import {mapLayoutValuesSelector} from '../selectors/maplayout'; +import {widgetBuilderSelector} from '../selectors/controls'; +import { dependenciesSelector, availableDependenciesSelector} from '../selectors/widgets'; +import { toggleConnection } from '../actions/widgets'; +import withMapExitButton from './widgetbuilder/enhancers/withMapExitButton'; +import WidgetTypeBuilder from './widgetbuilder/WidgetTypeBuilder'; const Builder = compose( connect( createSelector( @@ -30,7 +33,7 @@ const Builder = compose( (dependencies, availableDependenciesProps) => ({ dependencies, ...availableDependenciesProps })) , { toggleConnection }), withMapExitButton -)(require('./widgetbuilder/WidgetTypeBuilder')); +)(WidgetTypeBuilder); class SideBarComponent extends React.Component { static propTypes = { @@ -112,7 +115,14 @@ const Plugin = connect( } )(SideBarComponent); -module.exports = { - WidgetsBuilderPlugin: Plugin, - epics: require('../epics/widgetsbuilder') -}; + +export default createPlugin('WidgetsBuilder', { + component: Plugin, + epics, + containers: { + TOC: { + doNotHide: true, + name: "WidgetBuilder" + } + } +}); diff --git a/web/client/plugins/__tests__/TOC-test.jsx b/web/client/plugins/__tests__/TOC-test.jsx index 51aa6d31e3..b52e79cddb 100644 --- a/web/client/plugins/__tests__/TOC-test.jsx +++ b/web/client/plugins/__tests__/TOC-test.jsx @@ -185,4 +185,171 @@ describe('TOCPlugin Plugin', () => { expect(layerNode02.innerHTML).toBe('title_02'); expect(layerNodeDummy.innerHTML).toBe('dummy'); }); + describe('render items from other plugins', () => { + const TOOL_BUTTON_SELECTOR = '.btn-group button'; + const SELECTED_LAYER_STATE = { + layers: { + flat: [ + { + id: 'topp:states__6', + format: 'image/png8', + search: { + url: 'https://something/geoserver/wfs', + type: 'wfs' + }, + name: 'topp:states', + type: 'wms', + url: 'https://something/geoserver/wms', + bbox: { + crs: 'EPSG:4326', + bounds: { + minx: -124.73142200000001, + miny: 24.955967, + maxx: -66.969849, + maxy: 49.371735 + } + }, + visibility: true + } + ], + groups: [ + { + id: 'Default', + title: 'Default', + name: 'Default', + nodes: [ + 'topp:states__6' + ], + expanded: true + } + ], + selected: [ + 'topp:states__6' + ], + settings: { + expanded: false, + node: null, + nodeType: null, + options: {} + }, + layerMetadata: { + expanded: false, + metadataRecord: {}, + maskLoading: false + } + } + }; + it('AddLayer and AddGroup do not show without proper plugins', () => { + const { Plugin } = getPluginForTest(TOCPlugin, { + layers: { + groups: [{ id: 'default', title: 'Default', nodes: [] }], + flat: [] + }, + maptype: { + mapType: 'openlayers' + } + }); + const WrappedPlugin = dndContext(Plugin); + ReactDOM.render(, document.getElementById("container")); + expect(document.querySelectorAll(TOOL_BUTTON_SELECTOR).length).toBe(0); + }); + it('render AddLayer', () => { + const { Plugin } = getPluginForTest(TOCPlugin, { + layers: { + groups: [{ id: 'default', title: 'Default', nodes: [] }], + flat: [] + }, + maptype: { + mapType: 'openlayers' + } + }); + const WrappedPlugin = dndContext(Plugin); + ReactDOM.render(, document.getElementById("container")); + expect(document.querySelectorAll(TOOL_BUTTON_SELECTOR).length).toBe(1); + expect(document.querySelector(`${TOOL_BUTTON_SELECTOR} .glyphicon-add-layer`)).toExist(); + }); + it('render AddGroup', () => { + const { Plugin } = getPluginForTest(TOCPlugin, { + layers: { + groups: [{ id: 'default', title: 'Default', nodes: [] }], + flat: [] + }, + maptype: { + mapType: 'openlayers' + } + }); + const WrappedPlugin = dndContext(Plugin); + ReactDOM.render(, document.getElementById("container")); + expect(document.querySelectorAll(TOOL_BUTTON_SELECTOR).length).toBe(1); + expect(document.querySelector(`${TOOL_BUTTON_SELECTOR} .glyphicon-add-folder`)).toExist(); + }); + const ZOOM_TO_SELECTOR = `${TOOL_BUTTON_SELECTOR} .glyphicon-zoom-to`; + const FEATURES_GRID_SELECTOR = `${TOOL_BUTTON_SELECTOR} .glyphicon-features-grid`; + const REMOVE_SELECTOR = `${TOOL_BUTTON_SELECTOR } .glyphicon-trash`; + const SETTINGS_SELECTOR = `${TOOL_BUTTON_SELECTOR} .glyphicon-wrench`; + const FILTER_LAYER_SELECTOR = `${TOOL_BUTTON_SELECTOR} .glyphicon-filter-layer`; + const WIDGET_BUILDER_SELECTOR = `${TOOL_BUTTON_SELECTOR} .glyphicon-stats`; + it('render default tools (zoomToLayer and remove layer, for selected layer', () => { + const { Plugin } = getPluginForTest(TOCPlugin, SELECTED_LAYER_STATE); + const WrappedPlugin = dndContext(Plugin); + ReactDOM.render(, document.getElementById("container")); + // check zoom and remove selector + expect(document.querySelectorAll(TOOL_BUTTON_SELECTOR).length).toBe(2); + expect(document.querySelector(ZOOM_TO_SELECTOR)).toExist(); + expect(document.querySelector(REMOVE_SELECTOR)).toExist(); + + }); + it('render FeatureEditor', () => { + const { Plugin } = getPluginForTest(TOCPlugin, SELECTED_LAYER_STATE); + const WrappedPlugin = dndContext(Plugin); + ReactDOM.render(, document.getElementById("container")); + // check tools + expect(document.querySelectorAll(TOOL_BUTTON_SELECTOR).length).toBe(3); + expect(document.querySelector(ZOOM_TO_SELECTOR)).toExist(); + expect(document.querySelector(FEATURES_GRID_SELECTOR)).toExist(); + expect(document.querySelector(REMOVE_SELECTOR)).toExist(); + }); + it('render TOCItemsSettings', () => { + const { Plugin } = getPluginForTest(TOCPlugin, SELECTED_LAYER_STATE); + const WrappedPlugin = dndContext(Plugin); + ReactDOM.render(, document.getElementById("container")); + // check tools + expect(document.querySelectorAll(TOOL_BUTTON_SELECTOR).length).toBe(3); + expect(document.querySelector(ZOOM_TO_SELECTOR)).toExist(); + expect(document.querySelector(SETTINGS_SELECTOR)).toExist(); + expect(document.querySelector(REMOVE_SELECTOR)).toExist(); + }); + it('render FilterLayer', () => { + const { Plugin } = getPluginForTest(TOCPlugin, SELECTED_LAYER_STATE); + const WrappedPlugin = dndContext(Plugin); + ReactDOM.render(, document.getElementById("container")); + // check tools + expect(document.querySelectorAll(TOOL_BUTTON_SELECTOR).length).toBe(3); + expect(document.querySelector(ZOOM_TO_SELECTOR)).toExist(); + expect(document.querySelector(FILTER_LAYER_SELECTOR)).toExist(); + expect(document.querySelector(REMOVE_SELECTOR)).toExist(); + }); + it.skip('render WidgetBuilder', () => { // this test fails only on travis (not locally) + const { Plugin } = getPluginForTest(TOCPlugin, { ...SELECTED_LAYER_STATE, controls: { widgetBuilder: {available: true}}}); + const WrappedPlugin = dndContext(Plugin); + ReactDOM.render(, document.getElementById("container")); + // check tools + + expect(document.querySelector(ZOOM_TO_SELECTOR)).toExist("zoom doesn't exist"); + expect(document.querySelector(WIDGET_BUILDER_SELECTOR)).toExist("widget doesn't exist"); + expect(document.querySelector(REMOVE_SELECTOR)).toExist("remove doesn't exist"); + }); + }); }); diff --git a/web/client/plugins/userExtensions/ExtensionsPanel.jsx b/web/client/plugins/userExtensions/ExtensionsPanel.jsx new file mode 100644 index 0000000000..c191a425e6 --- /dev/null +++ b/web/client/plugins/userExtensions/ExtensionsPanel.jsx @@ -0,0 +1,104 @@ +/* + * Copyright 2019, 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, { useState } from 'react'; +import { connect } from 'react-redux'; +import { createSelector } from 'reselect'; +import { Glyphicon } from 'react-bootstrap'; + +import { updateUserPlugin } from '../../actions/context'; +import { userPluginsSelector } from '../../selectors/context'; + +import Message from '../../components/I18N/Message'; +import BorderLayout from '../../components/layout/BorderLayout'; +import SideGrid from '../../components/misc/cardgrids/SideGrid'; +import BaseFilter from '../../components/misc/Filter'; +import Toolbar from '../../components/misc/toolbar/Toolbar'; +import emptyState from '../../components/misc/enhancers/emptyState'; +import localizedProps from '../../components/misc/enhancers/localizedProps'; + +const Filter = localizedProps('filterPlaceholder')(BaseFilter); + +const ExtensionList = emptyState(({ filteredItems, filterText }) => filterText && filteredItems.length === 0, { + glyph: 'filter', + title: , + description: +})(({ filteredItems, onSelect }) => { + + return ( + ({ + preview:
+ +
, + title: extension.name, + description: extension.description, + selected: extension.active, + loading: extension.loading, + onClick: () => onSelect(extension), + tools: ( + { + event.stopPropagation(); + onSelect(extension); + } + } + ]} /> + ) + }))} /> + ); +}); + +const match = (filterText, extension) => + ['name', 'title', 'description'] + .map( k => extension[k]) + .map((string = "") => string.toLowerCase().indexOf(filterText.toLowerCase()) !== -1) + .reduce((p, n) => p || n, false); + +const ExtensionsPanel = ({ + extensions = [], + onSelect = () => { } +}) => { + const [filterText, onFilter] = useState(''); + const filteredItems = extensions + .filter((ext) => { + return !filterText + || filterText && + match(filterText, ext); + }); + return ( + + + }> + + ); +}; +export default connect( + createSelector(userPluginsSelector, extensions => ({extensions})), + { + onSelect: (extension) => updateUserPlugin(extension.name, { active: !extension.active }) + } +)(ExtensionsPanel); diff --git a/web/client/pluginsConfig.json b/web/client/pluginsConfig.json index e75da4e7dc..e33af02a99 100644 --- a/web/client/pluginsConfig.json +++ b/web/client/pluginsConfig.json @@ -43,10 +43,7 @@ "title": "Table of contents", "description": "plugins.TOC.description", "defaultConfig": { - "activateQueryTool": true, "activateAddLayerButton": true, - "activateAddGroupButton": true, - "activateMetedataTool": false, "addLayersPermissions": true, "removeLayersPermissions": true, "sortingPermissions": true, @@ -56,11 +53,13 @@ "children": [ "TOCItemsSettings", "FeatureEditor", + "FilterLayer", "AddGroup" ], "autoEnableChildren": [ "TOCItemsSettings", "FeatureEditor", + "FilterLayer", "AddGroup" ], "dependencies": [ @@ -69,6 +68,9 @@ }, { "name": "FeatureEditor", + "dependencies": [ + "QueryPanel" + ], "defaultConfig": {} }, { @@ -76,6 +78,7 @@ }, { "name": "MapFooter", + "mandatory": true, "hidden": true }, { @@ -92,6 +95,9 @@ }, { "name": "WidgetsBuilder", + "dependencies": [ + "QueryPanel" + ], "hidden": false }, { @@ -181,6 +187,9 @@ }, { "name": "AddGroup" + }, { + "name": "FilterLayer", + "dependencies": ["QueryPanel"] }, { "name": "Tutorial" @@ -325,7 +334,7 @@ }, { "name": "BurgerMenu", - "hidden": true, + "active": true, "dependencies": [ "OmniBar" ] @@ -377,6 +386,9 @@ "dependencies": [ "Toolbar" ] + }, { + "name": "UserExtensions", + "dependencies": ["BurgerMenu"] } ] } diff --git a/web/client/product/plugins.js b/web/client/product/plugins.js index 3edf894e2f..fc1b3d0fc6 100644 --- a/web/client/product/plugins.js +++ b/web/client/product/plugins.js @@ -44,9 +44,10 @@ module.exports = { DetailsPlugin: require('../plugins/Details'), DrawerMenuPlugin: require('../plugins/DrawerMenu'), ExpanderPlugin: require('../plugins/Expander'), - FeatureEditorPlugin: require('../plugins/FeatureEditor'), + FeatureEditorPlugin: require('../plugins/FeatureEditor').default, FeaturedMaps: require('../plugins/FeaturedMaps'), FeedbackMaskPlugin: require('../plugins/FeedbackMask'), + FilterLayerPlugin: require('../plugins/FilterLayer').default, FloatingLegendPlugin: require('../plugins/FloatingLegend'), FullScreenPlugin: require('../plugins/FullScreen'), GeoStoryPlugin: require('../plugins/GeoStory').default, @@ -103,7 +104,7 @@ module.exports = { SharePlugin: require('../plugins/Share'), SnapshotPlugin: require('../plugins/Snapshot'), StyleEditorPlugin: require('../plugins/StyleEditor'), - TOCItemsSettingsPlugin: require('../plugins/TOCItemsSettings'), + TOCItemsSettingsPlugin: require('../plugins/TOCItemsSettings').default, TOCPlugin: require('../plugins/TOC'), ThematicLayerPlugin: require('../plugins/ThematicLayer'), ThemeSwitcherPlugin: require('../plugins/ThemeSwitcher'), @@ -112,9 +113,10 @@ module.exports = { TutorialPlugin: require('../plugins/Tutorial'), UndoPlugin: require('../plugins/History'), UserManagerPlugin: require('../plugins/manager/UserManager'), + UserExtensionsPlugin: require('../plugins/UserExtensions').default, VersionPlugin: require('../plugins/Version'), WFSDownloadPlugin: require('../plugins/WFSDownload'), - WidgetsBuilderPlugin: require('../plugins/WidgetsBuilder'), + WidgetsBuilderPlugin: require('../plugins/WidgetsBuilder').default, WidgetsPlugin: require('../plugins/Widgets'), WidgetsTrayPlugin: require('../plugins/WidgetsTray'), ZoomAllPlugin: require('../plugins/ZoomAll'), diff --git a/web/client/reducers/context.js b/web/client/reducers/context.js index f5a6a479b5..17213f8f6a 100644 --- a/web/client/reducers/context.js +++ b/web/client/reducers/context.js @@ -5,8 +5,9 @@ * 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_CURRENT_CONTEXT, LOADING, SET_RESOURCE, CLEAR_CONTEXT } from "../actions/context"; -import {set} from '../utils/ImmutableUtils'; +import { SET_CURRENT_CONTEXT, LOADING, SET_RESOURCE, CLEAR_CONTEXT, UPDATE_USER_PLUGIN } from "../actions/context"; +import { find, get } from 'lodash'; +import {set, arrayUpdate} from '../utils/ImmutableUtils'; /** * Reducers for context page and configs. @@ -45,6 +46,13 @@ export default (state = {}, action) => { "loading", action.value, state )); } + case UPDATE_USER_PLUGIN: { + const plugin = find(get(state, 'currentContext.userPlugins', []), {name: action.name}); + if (plugin) { + return arrayUpdate('currentContext.userPlugins', { ...plugin, ...action.values}, {name: action.name}, state); + } + return state; + } default: return state; } diff --git a/web/client/themes/default/less/user-extensions.less b/web/client/themes/default/less/user-extensions.less new file mode 100644 index 0000000000..42b0023a09 --- /dev/null +++ b/web/client/themes/default/less/user-extensions.less @@ -0,0 +1,3 @@ +.msSideGrid.user-extensions { + position: relative; // this resets the default absolute style of msSideGrid +} diff --git a/web/client/themes/default/ms2-theme.less b/web/client/themes/default/ms2-theme.less index ffd0a4cd03..edef4ba08e 100644 --- a/web/client/themes/default/ms2-theme.less +++ b/web/client/themes/default/ms2-theme.less @@ -64,3 +64,4 @@ @import "./less/timeline.less"; @import "./less/backgroundSelector.less"; @import "./less/map-editor.less"; +@import "./less/user-extensions.less"; diff --git a/web/client/translations/data.de-DE.json b/web/client/translations/data.de-DE.json index 5d6f94ae0f..39e909b106 100644 --- a/web/client/translations/data.de-DE.json +++ b/web/client/translations/data.de-DE.json @@ -2176,6 +2176,14 @@ "newContext": "Neuer Kontext", "editContextTooltip": "Kontext bearbeiten" }, + "userExtensions": { + "title": "Benutzererweiterungen", + "emptyTitle": "Kein Ergebnis", + "emptyDescription": "Eingegebenen Filter Namen oder die Beschreibung von jedem nicht von Erweiterungen entsprechen", + "filterPlaceholder": "Filterschichten", + "addExtension": "Erweiterung hinzufügen", + "removeExtension": "Erweiterung entfernen" + }, "tutorial": { "title": "Anleitung", "back": "zurück", diff --git a/web/client/translations/data.en-US.json b/web/client/translations/data.en-US.json index 78f025678d..12e211729f 100644 --- a/web/client/translations/data.en-US.json +++ b/web/client/translations/data.en-US.json @@ -2180,6 +2180,14 @@ "newContext": "New Context", "editContextTooltip": "Edit context" }, + "userExtensions": { + "title": "User Extensions", + "emptyTitle": "No Result", + "emptyDescription": "Entered filter does not match name or description of any of extensions", + "filterPlaceholder": "Filter...", + "addExtension": "Add Extension", + "removeExtension": "Remove Extension" + }, "tutorial": { "title": "Tutorial", "back": "Back", diff --git a/web/client/translations/data.es-ES.json b/web/client/translations/data.es-ES.json index 5426c6f75b..b2cbfeb7e6 100644 --- a/web/client/translations/data.es-ES.json +++ b/web/client/translations/data.es-ES.json @@ -2176,6 +2176,14 @@ "newContext": "Nuevo contexto", "editContextTooltip": "Editar contexto" }, + "userExtensions": { + "title": "Extensiones de usuarios", + "emptyTitle": "Sin resultados", + "emptyDescription": "Filtro introducido no coincide con el nombre o descripción de cualquiera de las extensiones", + "filterPlaceholder": "Filtrar...", + "addExtension": "Añadir Extensión", + "removeExtension": "Retire Extensión" + }, "tutorial": { "title": "Tutorial", "back": "Atrás", diff --git a/web/client/translations/data.fr-FR.json b/web/client/translations/data.fr-FR.json index 51c7795df3..be263b076a 100644 --- a/web/client/translations/data.fr-FR.json +++ b/web/client/translations/data.fr-FR.json @@ -2176,6 +2176,14 @@ "newContext": "Nouveau contexte", "editContextTooltip": "Modifier le contexte" }, + "userExtensions": { + "title": "Extensions de l'utilisateur", + "emptyTitle": "Pas de résultat", + "emptyDescription": "Filtre...", + "filterPlaceholder": "des couches de filtre", + "addExtension": "Ajouter une extension", + "removeExtension": "Supprimer l'extension" + }, "tutorial": { "title": "Tutoriel", "back": "Retour", diff --git a/web/client/translations/data.it-IT.json b/web/client/translations/data.it-IT.json index 4430136c8d..4509c7f026 100644 --- a/web/client/translations/data.it-IT.json +++ b/web/client/translations/data.it-IT.json @@ -2177,6 +2177,14 @@ "newContext": "Nuovo contesto", "editContextTooltip": "Modifica contesto" }, + "userExtensions": { + "title": "Estensioni utente", + "emptyTitle": "Nessun risultato", + "emptyDescription": "Il filtro inserito non ha prodotto risultati", + "filterPlaceholder": "filtro...", + "addExtension": "Aggiungi estensione", + "removeExtension": "Rimuovi Estensione" + }, "tutorial": { "title": "Tutorial", "back": "Indietro",