From e2571228d85a95db186b1221b0166ca27c4297a8 Mon Sep 17 00:00:00 2001 From: Matteo V Date: Mon, 27 Nov 2023 11:27:50 +0100 Subject: [PATCH] updating stable branch of genova (#9748) * Fix #9624 Point cloud shading options (#9666) * Fix #9666 Include pointCloudShading option to saved layer config (#9670) * #9606 Error with circle annotations + radius selection (#9607) (#9727) Co-authored-by: Diego Vargas * Fix #9295 added better handling of format in csw service (#9712) (#9732) * #9702: Fix - Background selector in contexts won't retain thumbnail in view mode (#9720) (#9744) * #9567: handle functionality of zoom to record in table widgets (#9608) * #9567: handle functionality of zoom to record in table widgets * Fix: Correct failing test cases for zoom records issue in table widgets (#9567) This commit addresses the failing test cases related to the issue of zoom records in table widgets. * #9567: implement the new approach in zoom to records in table widgets + writing unit tests * #9567: handle adding flag into config file to show/hide zoom icon for tblWidget * #9567: reset flag enableZoomInTblWidget to be true for dashboard and map viewer * #9567: resolve comments' review: - put flag of zoomInTblWidget as a default prop - add translations - edit zoomToExtent enhancer to use internal zoom - remove selector "getFlagOfShowingTblWidgetZoom " and use plugin prop instead * #9683: add Details Panel for MS dashboard (#9689) * #9683: add Details Panel for MS dashboard - The tool have the same options (eg. show as modal, show at startup etc.) - The tool is defined in the same way of the corresponding one for maps. - Edit the layout to put add widget & show/hide connection buttons to the sidebar menu * #9683: resolve the FE test Update DashboardEditor.jsx * #9683: resolve review comments * description: - remove all dashboard selectors and pieces of code in generic components like sidebar plugin component that relevant to dashboard. - add missing test for detailsLoaded action - create new selectors, details uri selector and details settings se;ector that are used in many places in code - move AddWidgetDashboard, MapConnedctionDashboard plugins to direct plugins folder - Put global spinner in details plugin and remove it from sidebar plugin - edit in handleSave enhancer file to make update attributes of details just implemented for Map and Dashboard - Add custom style in details.less as the lib of react-dock doesn't allow to override left css property - remove unused added style from panels.less * #9683: remove unused comments in dashboard-test file * #9683: edit in details epics and selectors to fix FE test * #9683: Resolve review comments Description: - Reolve unused loading property from DashoardEditor file - Add tooltip for save dashboard - Remove custom style in BorderLayout and leave it with generic style * #9683: resolve review comments Description: - edit navbar.less files to fix going language selector behind body panel - remove unused z-index in dashboard.less # Conflicts: # web/client/epics/__tests__/config-test.js # web/client/epics/config.js * #9683: resolve test comment (#9730) - Adding export, import, delete dashboard - Reorder shown plugins in sidebar for dashboard * #9683: add Details Panel for MS dashboard [Editing the detail panel tooltip and title] (#9740) * #9683: resolve test comment Description: - edit the detail panel tooltip and shown title and make it generic one - Add translations for the new tooltip * Update web/client/translations/data.it-IT.json --------- Co-authored-by: Matteo V * #9728 fix misalignement issue (#9731) (#9742) * Fix #9729 fixed formats in catalog used in dashboard (#9733) (#9747) --------- Co-authored-by: stefano bovio Co-authored-by: Diego Vargas Co-authored-by: Suren Co-authored-by: mahmoud adel <58145645+mahmoudadel54@users.noreply.github.com> --- web/client/actions/__tests__/details-test.js | 12 +- web/client/actions/catalog.js | 2 + web/client/actions/details.js | 8 +- .../TOC/fragments/settings/Display.jsx | 17 +- .../settings/PointCloudShadingSettings.jsx | 140 +++++++++++ .../settings/ThreeDTilesSettings.jsx | 88 +++++++ .../PointCloudShadingSettings-test.jsx | 205 ++++++++++++++++ .../__tests__/ThreeDTilesSettings-test.jsx | 111 +++++++++ .../catalog/CatalogServiceEditor.jsx | 2 + .../__tests__/CatalogServiceEditor-test.jsx | 9 +- .../editor/AdvancedSettings/CSWFilters.jsx | 9 +- .../CommonAdvancedSettings.jsx | 52 ++-- .../RasterAdvancedSettings.js | 192 +++++++-------- .../AdvancedSettings/TMSAdvancedEditor.jsx | 38 ++- .../AdvancedSettings/WMSDomainAliases.js | 30 +-- .../__tests__/RasterAdvancedSettings-test.js | 12 +- .../components/details/DetailsPanel.jsx | 8 +- web/client/components/layout/BorderLayout.jsx | 2 +- .../map/cesium/plugins/ThreeDTilesLayer.js | 38 ++- .../__tests__/onMapViewChanges-test.js | 22 ++ .../map/enhancers/onMapViewChanges.js | 5 +- web/client/components/map/openlayers/Map.jsx | 42 ++-- .../map/openlayers/__tests__/Map-test.jsx | 2 +- .../annotations/CoordinatesEditor.jsx | 2 +- .../__tests__/CoordinatesEditor-test.js | 2 +- .../misc/combobox/PagedCombobox.jsx | 16 +- .../misc/popover/DisposablePopover.jsx | 2 +- .../components/resources/ResourceGrid.jsx | 2 +- .../resources/modals/enhancers/handleSave.js | 11 +- web/client/components/styleeditor/Fields.jsx | 3 +- .../components/styleeditor/PropertyField.jsx | 9 +- .../components/styleeditor/config/blocks.js | 3 +- .../components/styleeditor/config/property.js | 5 +- .../builder/wizard/table/TableOptions.jsx | 1 - .../__tests__/dependenciesToExtent-test.jsx | 6 +- .../enhancers/__tests__/tableWidget-test.jsx | 94 +++++++- .../widgets/enhancers/dependenciesToExtent.js | 33 ++- .../widgets/enhancers/tableWidget.js | 68 +++++- .../components/widgets/widget/TableWidget.jsx | 4 +- .../widget/__tests__/TableWidget-test.jsx | 28 ++- web/client/configs/localConfig.json | 24 +- web/client/epics/__tests__/catalog-test.js | 33 --- web/client/epics/__tests__/config-test.js | 226 +++++++++++++++++- web/client/epics/__tests__/dashboard-test.js | 26 +- web/client/epics/__tests__/details-test.js | 111 ++++++++- web/client/epics/__tests__/tutorial-test.js | 48 +++- web/client/epics/backgroundselector.js | 26 -- web/client/epics/catalog.js | 39 +-- web/client/epics/config.js | 118 ++++++++- web/client/epics/dashboard.js | 47 +++- web/client/epics/details.js | 11 +- web/client/epics/tutorial.js | 11 +- web/client/plugins/AddWidgetDashboard.jsx | 77 ++++++ web/client/plugins/Dashboard.jsx | 34 ++- web/client/plugins/DashboardEditor.jsx | 64 +---- web/client/plugins/DashboardExport.jsx | 12 + web/client/plugins/DashboardImport.jsx | 12 + web/client/plugins/DashboardSave.jsx | 35 ++- web/client/plugins/DeleteDashboard.jsx | 16 ++ web/client/plugins/Details.jsx | 39 ++- web/client/plugins/MapConnectionDashboard.jsx | 75 ++++++ web/client/plugins/MetadataExplorer.jsx | 4 +- web/client/plugins/SidebarMenu.jsx | 20 +- web/client/plugins/Widgets.jsx | 15 +- .../plugins/__tests__/DashboardSave-test.jsx | 68 +++++- .../plugins/__tests__/SidebarMenu-test.jsx | 19 ++ web/client/plugins/featuregrid/gridTools.jsx | 5 +- .../plugins/sidebarmenu/sidebarmenu.less | 5 + .../enhancers/catalogEditorEnhancer.js | 15 ++ web/client/product/plugins.js | 4 +- web/client/reducers/__tests__/config-test.js | 17 +- web/client/reducers/catalog.js | 8 +- web/client/reducers/config.js | 14 +- web/client/reducers/details.js | 2 +- .../selectors/__tests__/dashboard-test.js | 30 ++- web/client/selectors/catalog.js | 5 +- web/client/selectors/dashboard.js | 5 + web/client/selectors/details.js | 19 ++ web/client/selectors/sidebarmenu.js | 6 + web/client/selectors/widgets.js | 5 + web/client/themes/default/less/catalog.less | 8 + web/client/themes/default/less/common.less | 38 +++ web/client/themes/default/less/dashboard.less | 1 - web/client/themes/default/less/details.less | 5 + web/client/themes/default/less/navbar.less | 2 +- .../themes/default/less/react-select.less | 4 + web/client/themes/default/less/wizard.less | 1 - web/client/translations/data.de-DE.json | 31 ++- web/client/translations/data.en-US.json | 28 ++- web/client/translations/data.es-ES.json | 28 ++- web/client/translations/data.fr-FR.json | 28 ++- web/client/translations/data.is-IS.json | 4 +- web/client/translations/data.it-IT.json | 28 ++- web/client/utils/LayersUtils.js | 3 +- web/client/utils/MapUtils.js | 5 +- .../utils/__tests__/LayersUtils-test.js | 14 ++ 96 files changed, 2358 insertions(+), 555 deletions(-) create mode 100644 web/client/components/TOC/fragments/settings/PointCloudShadingSettings.jsx create mode 100644 web/client/components/TOC/fragments/settings/ThreeDTilesSettings.jsx create mode 100644 web/client/components/TOC/fragments/settings/__tests__/PointCloudShadingSettings-test.jsx create mode 100644 web/client/components/TOC/fragments/settings/__tests__/ThreeDTilesSettings-test.jsx create mode 100644 web/client/plugins/AddWidgetDashboard.jsx create mode 100644 web/client/plugins/MapConnectionDashboard.jsx diff --git a/web/client/actions/__tests__/details-test.js b/web/client/actions/__tests__/details-test.js index a0474fa074..254b876ada 100644 --- a/web/client/actions/__tests__/details-test.js +++ b/web/client/actions/__tests__/details-test.js @@ -28,13 +28,21 @@ describe('details actions tests', () => { const a = closeDetailsPanel(); expect(a.type).toBe(CLOSE_DETAILS_PANEL); }); - it('detailsLoaded', () => { + it('detailsLoaded for map', () => { const mapId = 1; const detailsUri = "sada/da/"; const a = detailsLoaded(mapId, detailsUri); expect(a.type).toBe(DETAILS_LOADED); expect(a.detailsUri).toBe(detailsUri); - expect(a.mapId).toBe(mapId); + expect(a.id).toBe(mapId); + }); + it('detailsLoaded for dashboard', () => { + const dashboardId = 1; + const detailsUri = "sada/da/"; + const a = detailsLoaded(dashboardId, detailsUri); + expect(a.type).toBe(DETAILS_LOADED); + expect(a.detailsUri).toBe(detailsUri); + expect(a.id).toBe(dashboardId); }); it('updateDetails', () => { const a = updateDetails('text'); diff --git a/web/client/actions/catalog.js b/web/client/actions/catalog.js index fd41eeab71..41147acc91 100644 --- a/web/client/actions/catalog.js +++ b/web/client/actions/catalog.js @@ -45,6 +45,7 @@ export const SAVING_SERVICE = 'CATALOG:SAVING_SERVICE'; export const CATALOG_INITED = 'CATALOG:INIT'; export const GET_METADATA_RECORD_BY_ID = 'CATALOG:GET_METADATA_RECORD_BY_ID'; export const SET_LOADING = 'CATALOG:SET_LOADING'; +export const SHOW_FORMAT_ERROR = 'CATALOG:SHOW_FORMAT_ERROR'; export const TOGGLE_TEMPLATE = 'CATALOG:TOGGLE_TEMPLATE'; export const TOGGLE_THUMBNAIL = 'CATALOG:TOGGLE_THUMBNAIL'; export const TOGGLE_ADVANCED_SETTINGS = 'CATALOG:TOGGLE_ADVANCED_SETTINGS'; @@ -285,6 +286,7 @@ export const toggleThumbnail = () => ({type: TOGGLE_THUMBNAIL}); export const formatOptionsFetch = (url, force) => ({type: FORMAT_OPTIONS_FETCH, url, force}); export const formatsLoading = (loading) => ({type: FORMAT_OPTIONS_LOADING, loading}); export const setSupportedFormats = (formats, url) => ({type: SET_FORMAT_OPTIONS, formats, url}); +export const showFormatError = (status) => ({type: SHOW_FORMAT_ERROR, status}); import {error} from './notifications'; diff --git a/web/client/actions/details.js b/web/client/actions/details.js index a5f4c288da..83de607bb7 100644 --- a/web/client/actions/details.js +++ b/web/client/actions/details.js @@ -17,9 +17,9 @@ export const NO_DETAILS_AVAILABLE = "NO_DETAILS_AVAILABLE"; * @memberof actions.details * @return {action} type `UPDATE_DETAILS` */ -export const updateDetails = (detailsText) => ({ +export const updateDetails = (detailsText, resourceId) => ({ type: UPDATE_DETAILS, - detailsText + detailsText, id: resourceId }); /** @@ -27,9 +27,9 @@ export const updateDetails = (detailsText) => ({ * @memberof actions.details * @return {action} type `DETAILS_LOADED` */ -export const detailsLoaded = (mapId, detailsUri, detailsSettings) => ({ +export const detailsLoaded = (resourceId, detailsUri, detailsSettings) => ({ type: DETAILS_LOADED, - mapId, + id: resourceId, detailsUri, detailsSettings }); diff --git a/web/client/components/TOC/fragments/settings/Display.jsx b/web/client/components/TOC/fragments/settings/Display.jsx index d4f93e06e4..acea7b9a83 100644 --- a/web/client/components/TOC/fragments/settings/Display.jsx +++ b/web/client/components/TOC/fragments/settings/Display.jsx @@ -21,6 +21,7 @@ import { ServerTypes } from '../../../../utils/LayersUtils'; import Select from 'react-select'; import { getSupportedFormat } from '../../../../api/WMS'; import WMSCacheOptions from './WMSCacheOptions'; +import ThreeDTilesSettings from './ThreeDTilesSettings'; export default class extends React.Component { static propTypes = { opacityText: PropTypes.node, @@ -206,18 +207,10 @@ export default class extends React.Component { - {this.props.element.type === "3dtiles" && - - - - this.props.onChange("heightOffset", parseFloat(val))}/> - - - } + {this.props.element.type === "wms" && diff --git a/web/client/components/TOC/fragments/settings/PointCloudShadingSettings.jsx b/web/client/components/TOC/fragments/settings/PointCloudShadingSettings.jsx new file mode 100644 index 0000000000..20f3673ca2 --- /dev/null +++ b/web/client/components/TOC/fragments/settings/PointCloudShadingSettings.jsx @@ -0,0 +1,140 @@ +/* + * Copyright 2023, 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 { FormGroup, ControlLabel, InputGroup, Checkbox } from 'react-bootstrap'; +import DebouncedFormControl from '../../../misc/DebouncedFormControl'; +import Message from '../../../I18N/Message'; +import InfoPopover from '../../../widgets/widget/InfoPopover'; + +/** + * PointCloudShadingSettings. This component shows the point cloud shading options available + * @prop {object} layer the layer options + * @prop {object} onChange callback on every on change event + */ +function PointCloudShadingSettings({ + layer, + onChange +}) { + if (!(layer?.type === '3dtiles' && layer?.format === 'pnts')) { + return null; + } + const { pointCloudShading = {} } = layer || {}; + return ( + <> +
+ + onChange('pointCloudShading', { + ...pointCloudShading, + attenuation: event?.target?.checked, + maximumAttenuation: pointCloudShading?.maximumAttenuation ?? 4, + eyeDomeLighting: pointCloudShading?.eyeDomeLighting ?? true + })} + > + + {' '}} /> + + + + + + + + { + onChange('pointCloudShading', { + ...pointCloudShading, + maximumAttenuation: value !== undefined ? parseFloat(value) : undefined + }); + }} + /> + + px + + + + + onChange('pointCloudShading', { + ...pointCloudShading, + eyeDomeLighting: event?.target?.checked + })} + > + + {' '}} /> + + + + + + + + onChange('pointCloudShading', { + ...pointCloudShading, + eyeDomeLightingStrength: value !== undefined ? parseFloat(value) : undefined + })} + /> + + + + + + + + onChange('pointCloudShading', { + ...pointCloudShading, + eyeDomeLightingRadius: value !== undefined ? parseFloat(value) : undefined + })} + /> + + + + ); +} + +PointCloudShadingSettings.propTypes = { + layer: PropTypes.object, + onChange: PropTypes.func +}; + +PointCloudShadingSettings.defaultProps = { + layer: {}, + onChange: () => {} +}; + +export default PointCloudShadingSettings; diff --git a/web/client/components/TOC/fragments/settings/ThreeDTilesSettings.jsx b/web/client/components/TOC/fragments/settings/ThreeDTilesSettings.jsx new file mode 100644 index 0000000000..3f5aa9f99f --- /dev/null +++ b/web/client/components/TOC/fragments/settings/ThreeDTilesSettings.jsx @@ -0,0 +1,88 @@ +/* + * Copyright 2023, 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 { FormGroup, ControlLabel, InputGroup } from 'react-bootstrap'; +import DebouncedFormControl from '../../../misc/DebouncedFormControl'; +import Message from '../../../I18N/Message'; +import PointCloudShadingSettings from './PointCloudShadingSettings'; +import Select from 'react-select'; + +/** + * ThreeDTilesSettings. This component shows the 3d tiles options available + * @prop {object} layer the layer options + * @prop {object} onChange callback on every on change event + */ +function ThreeDTilesSettings({ + layer, + onChange +}) { + if (layer?.type !== '3dtiles') { + return null; + } + return ( +
+ + + + onFormatOptionsFetch(service.url)} - value={service && service.format} - clearable - noResultsText={props.formatsLoading - ? "catalog.format.loading" : "catalog.format.noOption"} - options={props.formatsLoading ? [] : formatOptions.map((format) => format?.value ? format : ({ value: format, label: format }))} - onChange={event => onChangeServiceFormat(event && event.value)} /> -
- - + + + + onFormatOptionsFetch(service.url)} - value={service && service.infoFormat} - clearable - options={props.formatsLoading ? [] : infoFormatOptions.map((format) => ({ value: format, label: format }))} - onChange={event => onChangeServiceProperty("infoFormat", event && event.value)} /> - +
+ + +
+ {showFormatError ? } + text={} /> : null} - +
+
+ + + + onFormatOptionsFetch(service.url)} + value={service && service.infoFormat} + clearable + options={props.formatsLoading ? [] : infoFormatOptions.map((format) => ({ value: format, label: format }))} + onChange={event => onChangeServiceProperty("infoFormat", event && event.value)} /> + + +
+ + + onChangeServiceProperty("layerOptions", { ...service.layerOptions, serverType: event?.value })} /> - + {!isNil(service.type) && service.type === "csw" && diff --git a/web/client/components/catalog/editor/AdvancedSettings/TMSAdvancedEditor.jsx b/web/client/components/catalog/editor/AdvancedSettings/TMSAdvancedEditor.jsx index 5cd8e2d625..f92fae0169 100644 --- a/web/client/components/catalog/editor/AdvancedSettings/TMSAdvancedEditor.jsx +++ b/web/client/components/catalog/editor/AdvancedSettings/TMSAdvancedEditor.jsx @@ -14,7 +14,7 @@ import JSONEditor from '../../../misc/codeEditors/JSONEditor'; import Message from "../../../I18N/Message"; import HTML from "../../../I18N/HTML"; -import { FormGroup, Checkbox, Col, ControlLabel } from "react-bootstrap"; +import { FormGroup, Checkbox, ControlLabel } from "react-bootstrap"; import InfoPopover from '../../../widgets/widget/InfoPopover'; // TODO: add variants @@ -64,29 +64,27 @@ export default ({ }; return (
- - {service.autoload !== undefined && onChangeServiceProperty("autoload", e.target.checked)} - checked={!isNil(service.autoload) ? service.autoload : false}> - - } - onToggleThumbnail()} - checked={!isNil(service.hideThumbnail) ? !service.hideThumbnail : true}> - + {service.autoload !== undefined && onChangeServiceProperty("autoload", e.target.checked)} + checked={!isNil(service.autoload) ? service.autoload : false}> + + } + onToggleThumbnail()} + checked={!isNil(service.hideThumbnail) ? !service.hideThumbnail : true}> + + + {service.provider === "tms" + ? onChangeServiceProperty("forceDefaultTileGrid", e.target.checked)} + checked={!isNil(service.forceDefaultTileGrid) ? service.forceDefaultTileGrid : false}> +  } /> - {service.provider === "tms" - ? onChangeServiceProperty("forceDefaultTileGrid", e.target.checked)} - checked={!isNil(service.forceDefaultTileGrid) ? service.forceDefaultTileGrid : false}> -  } /> - - : null} - + : null} {!service.provider || service.provider === "custom" - ? + ?
  } /> - +
: null}
); diff --git a/web/client/components/catalog/editor/AdvancedSettings/WMSDomainAliases.js b/web/client/components/catalog/editor/AdvancedSettings/WMSDomainAliases.js index 9e6bf23620..1122d63df1 100644 --- a/web/client/components/catalog/editor/AdvancedSettings/WMSDomainAliases.js +++ b/web/client/components/catalog/editor/AdvancedSettings/WMSDomainAliases.js @@ -5,7 +5,7 @@ * This source code is licensed under the BSD-style license found in the * LICENSE file in the root directory of this source tree. */ -import {FormControl, FormGroup, Col, ControlLabel, Glyphicon, Button} from "react-bootstrap"; +import {FormControl, FormGroup, ControlLabel, Glyphicon, Button} from "react-bootstrap"; import {debounce, size, map, omit, toInteger} from "lodash"; import Message from "../../../I18N/Message"; import InfoPopover from "../../../widgets/widget/InfoPopover"; @@ -45,7 +45,7 @@ export default ({ }; const elements = map(aliases, (el, k) => ( - +
{toInteger(k) !== 0 && } - +
)); return ( - - +   - } /> - + } /> {elements} - - } - tooltipid="add-alias-button" - tooltipPosition="right" - onClick={onCreateAlias}> - - - + } + tooltipid="add-alias-button" + tooltipPosition="right" + onClick={onCreateAlias}> + + ); }; diff --git a/web/client/components/catalog/editor/AdvancedSettings/__tests__/RasterAdvancedSettings-test.js b/web/client/components/catalog/editor/AdvancedSettings/__tests__/RasterAdvancedSettings-test.js index 36ac6af914..066792f28e 100644 --- a/web/client/components/catalog/editor/AdvancedSettings/__tests__/RasterAdvancedSettings-test.js +++ b/web/client/components/catalog/editor/AdvancedSettings/__tests__/RasterAdvancedSettings-test.js @@ -33,7 +33,7 @@ describe('Test Raster advanced settings', () => { const advancedSettingPanel = document.getElementsByClassName("mapstore-switch-panel"); expect(advancedSettingPanel).toBeTruthy(); const fields = document.querySelectorAll(".form-group"); - expect(fields.length).toBe(12); + expect(fields.length).toBe(13); }); it('test csw advanced options', () => { ReactDOM.render(, document.getElementById("container")); @@ -41,7 +41,7 @@ describe('Test Raster advanced settings', () => { expect(advancedSettingPanel).toBeTruthy(); const fields = document.querySelectorAll(".form-group"); const cswFilters = document.getElementsByClassName("catalog-csw-filters"); - expect(fields.length).toBe(10); + expect(fields.length).toBe(11); expect(cswFilters).toBeTruthy(); }); it('test component onChangeServiceProperty autoload', () => { @@ -159,7 +159,7 @@ describe('Test Raster advanced settings', () => { service={{type: "wms"}}/>, document.getElementById("container")); const advancedSettingsPanel = document.getElementsByClassName("mapstore-switch-panel"); expect(advancedSettingsPanel).toBeTruthy(); - const format = document.querySelectorAll('input[role="combobox"]')[0]; + const format = document.querySelectorAll('input[role="combobox"]')[1]; expect(format).toBeTruthy(); TestUtils.Simulate.change(format, { target: { value: 'image/png' } }); TestUtils.Simulate.keyDown(format, { keyCode: 9, key: 'Tab' }); @@ -176,7 +176,7 @@ describe('Test Raster advanced settings', () => { service={{type: "wms", layerOptions: {tileSize: 256}}}/>, document.getElementById("container")); const advancedSettingsPanel = document.getElementsByClassName("mapstore-switch-panel"); expect(advancedSettingsPanel).toBeTruthy(); - const layerOption = document.querySelectorAll('input[role="combobox"]')[2]; + const layerOption = document.querySelectorAll('input[role="combobox"]')[3]; expect(layerOption).toBeTruthy(); TestUtils.Simulate.change(layerOption, { target: { value: "512" }}); TestUtils.Simulate.keyDown(layerOption, { keyCode: 9, key: 'Tab' }); @@ -237,7 +237,7 @@ describe('Test Raster advanced settings', () => { />, document.getElementById("container")); const advancedSettingsPanel = document.getElementsByClassName("mapstore-switch-panel"); expect(advancedSettingsPanel).toBeTruthy(); - const serverTypeOption = document.querySelectorAll('input[role="combobox"]')[3]; + const serverTypeOption = document.querySelectorAll('input[role="combobox"]')[0]; expect(serverTypeOption).toBeTruthy(); TestUtils.Simulate.change(serverTypeOption, { target: { value: "geoserver" }}); TestUtils.Simulate.keyDown(serverTypeOption, { keyCode: 9, key: 'Tab' }); @@ -255,7 +255,7 @@ describe('Test Raster advanced settings', () => { service={{ type: "wms" }}/>, document.getElementById("container")); const advancedSettingsPanel = document.getElementsByClassName("mapstore-switch-panel"); expect(advancedSettingsPanel).toBeTruthy(); - const infoFormatOption = document.querySelectorAll('input[role="combobox"]')[1]; + const infoFormatOption = document.querySelectorAll('input[role="combobox"]')[2]; expect(infoFormatOption).toBeTruthy(); TestUtils.Simulate.change(infoFormatOption, { target: { value: "application/json" }}); TestUtils.Simulate.keyDown(infoFormatOption, { keyCode: 9, key: 'Tab' }); diff --git a/web/client/components/details/DetailsPanel.jsx b/web/client/components/details/DetailsPanel.jsx index 90309fefb7..254d24d707 100644 --- a/web/client/components/details/DetailsPanel.jsx +++ b/web/client/components/details/DetailsPanel.jsx @@ -22,7 +22,8 @@ class DetailsPanel extends React.Component { panelClassName: PropTypes.string, style: PropTypes.object, onClose: PropTypes.func, - width: PropTypes.number + width: PropTypes.number, + isDashboard: PropTypes.bool }; static contextTypes = { @@ -41,14 +42,15 @@ class DetailsPanel extends React.Component { }, active: false, panelClassName: "details-panel", - width: 550 + width: 550, + isDashboard: false }; render() { return ( * */ -export default ({id, children, header, footer, columns, height, style = {}, className, bodyClassName = "ms2-border-layout-body"}) => +export default ({id, children, header, footer, columns, height, style = {}, className, bodyClassName = "ms2-border-layout-body" }) => (
{ + if (style && options?.pointCloudShading?.attenuation) { + // remove pointSize if attenuation are applied + const { pointSize, ...others } = style; + return others; + } + return style; +}; + +function getStyle({ style, pointCloudShading = {} }) { const { format, body } = style || {}; if (!format || !body) { return Promise.resolve(null); } if (format === '3dtiles') { - return Promise.resolve(body); + return Promise.resolve(cleanStyle(body, { pointCloudShading })); } if (format === 'geostyler') { return getStyleParser('3dtiles') - .then((parser) => parser.writeStyle(body)); + .then((parser) => parser.writeStyle(body)) + .then((parsedStyle) => cleanStyle(parsedStyle, { pointCloudShading })); } return Promise.all([ getStyleParser(format), @@ -38,6 +48,7 @@ function getStyle({ style }) { parser .readStyle(body) .then(parsedStyle => threeDTilesParser.writeStyle(parsedStyle)) + .then((parsedStyle) => cleanStyle(parsedStyle, { pointCloudShading })) ); } @@ -106,6 +117,16 @@ function updateGooglePhotorealistic3DTilesBrandLogo(map, options, tileSet) { return null; } +function updateShading(tileSet, options, map) { + // point cloud + tileSet.pointCloudShading.attenuation = !!options?.pointCloudShading?.attenuation; + tileSet.pointCloudShading.maximumAttenuation = options?.pointCloudShading?.maximumAttenuation ?? 4; + tileSet.pointCloudShading.eyeDomeLighting = !!options?.pointCloudShading?.eyeDomeLighting; + tileSet.pointCloudShading.eyeDomeLightingStrength = options?.pointCloudShading?.eyeDomeLightingStrength ?? 1.0; + tileSet.pointCloudShading.eyeDomeLightingRadius = options?.pointCloudShading?.eyeDomeLightingRadius ?? 1.0; + setTimeout(() => map.scene.requestRender()); +} + Layers.registerType('3dtiles', { create: (options, map) => { if (options.visibility && options.url) { @@ -133,6 +154,7 @@ Layers.registerType('3dtiles', { ensureReady(tileSet, () => { updateModelMatrix(tileSet, options); clip3DTiles(tileSet, options, map); + updateShading(tileSet, options, map); getStyle(options) .then((style) => { if (style) { @@ -184,7 +206,10 @@ Layers.registerType('3dtiles', { clip3DTiles(tileSet, newOptions, map); }); } - if (!isEqual(newOptions.style, oldOptions.style) && tileSet) { + if (( + !isEqual(newOptions.style, oldOptions.style) + || newOptions?.pointCloudShading?.attenuation !== oldOptions?.pointCloudShading?.attenuation + ) && tileSet) { ensureReady(tileSet, () => { getStyle(newOptions) .then((style) => { @@ -195,6 +220,11 @@ Layers.registerType('3dtiles', { }); }); } + if (!isEqual(newOptions.pointCloudShading, oldOptions.pointCloudShading) && tileSet) { + ensureReady(tileSet, () => { + updateShading(tileSet, newOptions, map); + }); + } if (tileSet && newOptions.heightOffset !== oldOptions.heightOffset) { ensureReady(tileSet, () => { updateModelMatrix(tileSet, newOptions); diff --git a/web/client/components/map/enhancers/__tests__/onMapViewChanges-test.js b/web/client/components/map/enhancers/__tests__/onMapViewChanges-test.js index e3648253fa..8dde240dea 100644 --- a/web/client/components/map/enhancers/__tests__/onMapViewChanges-test.js +++ b/web/client/components/map/enhancers/__tests__/onMapViewChanges-test.js @@ -44,5 +44,27 @@ describe('onMapViewChanges enhancer', () => { expect(map.mapStateSource).toExist(); expect(map.projection).toExist(); }); + it('onMapViewChanges rendering with zoomToExtentHandler', () => { + const Sink = onMapViewChanges(createSink( props => { + expect(props.eventHandlers.onMapViewChanges).toExist(); + setTimeout(props.eventHandlers.onMapViewChanges("CENTER", "ZOOM", { bbox: { x: 2 } }, "SIZE", "mapStateSource", "projection", {}, "RESOLUTION", "ORINATE", () => {})); + + })); + const actions = { + onMapViewChanges: () => {} + }; + const spy = expect.spyOn(actions, 'onMapViewChanges'); + ReactDOM.render(, document.getElementById("container")); + expect(spy).toHaveBeenCalled(); + const map = spy.calls[0].arguments[0]; + expect(map).toExist(); + expect(map.center).toExist(); + expect(map.zoom).toExist(); + expect(map.bbox).toExist(); + expect(map.size).toExist(); + expect(map.mapStateSource).toExist(); + expect(map.projection).toExist(); + expect(map.zoomToExtentHandler).toExist(); + }); }); diff --git a/web/client/components/map/enhancers/onMapViewChanges.js b/web/client/components/map/enhancers/onMapViewChanges.js index 27238cb940..b1d6ffc9d3 100644 --- a/web/client/components/map/enhancers/onMapViewChanges.js +++ b/web/client/components/map/enhancers/onMapViewChanges.js @@ -14,7 +14,7 @@ import { compose, withHandlers, withPropsOnChange } from 'recompose'; */ export default compose( withHandlers({ - onMapViewChanges: ({ map = {}, onMapViewChanges = () => {}}) => (center, zoom, bbox, size, mapStateSource, projection, viewerOptions, resolution, orientate) => { + onMapViewChanges: ({ map = {}, onMapViewChanges = () => {}}) => (center, zoom, bbox, size, mapStateSource, projection, viewerOptions, resolution, orientate, zoomToExtentHandler) => { onMapViewChanges({ ...map, center, @@ -27,7 +27,8 @@ export default compose( mapStateSource, projection, resolution, - orientate + orientate, + zoomToExtentHandler }); } }), diff --git a/web/client/components/map/openlayers/Map.jsx b/web/client/components/map/openlayers/Map.jsx index ed9ac85a62..88925688cc 100644 --- a/web/client/components/map/openlayers/Map.jsx +++ b/web/client/components/map/openlayers/Map.jsx @@ -552,6 +552,26 @@ class OpenlayersMap extends React.Component { } }; + zoomToExtentHandler = (extent, { padding, crs, maxZoom: zoomLevel, duration, nearest} = {})=> { + let bounds = reprojectBbox(extent, crs, this.props.projection); + // TODO: improve this to manage all degenerated bounding boxes. + if (bounds && bounds[0] === bounds[2] && bounds[1] === bounds[3] && + crs === "EPSG:4326" && isArray(extent) && extent[0] === -180 && extent[1] === -90) { + bounds = this.map.getView().getProjection().getExtent(); + } + let maxZoom = zoomLevel; + if (bounds && bounds[0] === bounds[2] && bounds[1] === bounds[3] && isNil(maxZoom)) { + maxZoom = 21; // TODO: allow to this maxZoom to be customizable + } + this.map.getView().fit(bounds, { + size: this.map.getSize(), + padding: padding && [padding.top || 0, padding.right || 0, padding.bottom || 0, padding.left || 0], + maxZoom, + duration, + nearest + }); + } + registerHooks = () => { this.props.hookRegister.registerHook(mapUtils.RESOLUTIONS_HOOK, (srs) => { return this.getResolutions(srs); @@ -581,27 +601,7 @@ class OpenlayersMap extends React.Component { this.props.hookRegister.registerHook(mapUtils.GET_COORDINATES_FROM_PIXEL_HOOK, (pixel) => { return this.map.getCoordinateFromPixel(pixel); }); - this.props.hookRegister.registerHook(mapUtils.ZOOM_TO_EXTENT_HOOK, (extent, { padding, crs, maxZoom: zoomLevel, duration, nearest} = {}) => { - let bounds = reprojectBbox(extent, crs, this.props.projection); - // if EPSG:4326 with max extent (-180, -90, 180, 90) bounds are 0,0,0,0. In this case zoom to max extent - // TODO: improve this to manage all degenerated bounding boxes. - if (bounds && bounds[0] === bounds[2] && bounds[1] === bounds[3] && - crs === "EPSG:4326" && isArray(extent) && extent[0] === -180 && extent[1] === -90) { - bounds = this.map.getView().getProjection().getExtent(); - } - let maxZoom = zoomLevel; - if (bounds && bounds[0] === bounds[2] && bounds[1] === bounds[3] && isNil(maxZoom)) { - maxZoom = 21; // TODO: allow to this maxZoom to be customizable - } - - this.map.getView().fit(bounds, { - size: this.map.getSize(), - padding: padding && [padding.top || 0, padding.right || 0, padding.bottom || 0, padding.left || 0], - maxZoom, - duration, - nearest - }); - }); + this.props.hookRegister.registerHook(mapUtils.ZOOM_TO_EXTENT_HOOK, this.zoomToExtentHandler); }; } diff --git a/web/client/components/map/openlayers/__tests__/Map-test.jsx b/web/client/components/map/openlayers/__tests__/Map-test.jsx index b9900912f7..15ec0c0bae 100644 --- a/web/client/components/map/openlayers/__tests__/Map-test.jsx +++ b/web/client/components/map/openlayers/__tests__/Map-test.jsx @@ -1463,7 +1463,7 @@ describe('OpenlayersMap', () => { expect(MapUtils.getHook(MapUtils.ZOOM_TO_EXTENT_HOOK)).toBeTruthy(); }); it("with custom hookRegister", () => { - const customHooRegister = MapUtils.createRegisterHooks(); + const customHooRegister = MapUtils.createRegisterHooks("mymap"); const map = ReactDOM.render(, document.getElementById("map")); expect(map).toBeTruthy(); expect(ReactDOM.findDOMNode(map).id).toBe('mymap'); diff --git a/web/client/components/mapcontrols/annotations/CoordinatesEditor.jsx b/web/client/components/mapcontrols/annotations/CoordinatesEditor.jsx index 2257dd530f..89d2fe519d 100644 --- a/web/client/components/mapcontrols/annotations/CoordinatesEditor.jsx +++ b/web/client/components/mapcontrols/annotations/CoordinatesEditor.jsx @@ -147,7 +147,7 @@ class CoordinatesEditor extends React.Component { if (this.isValid(this.props.components, radius )) { this.props.onChangeRadius(parseFloat(radius), this.props.components.map(coordToArray), uom); } else if (radius !== "") { - this.props.onChangeRadius(parseFloat(radius), [], uom); + this.props.onChangeRadius(parseFloat(radius), [[0, 0]], uom); } else { this.props.onChangeRadius(null, this.props.components.map(coordToArray), uom); this.props.onSetInvalidSelected("radius", this.props.components.map(coordToArray)); diff --git a/web/client/components/mapcontrols/annotations/__tests__/CoordinatesEditor-test.js b/web/client/components/mapcontrols/annotations/__tests__/CoordinatesEditor-test.js index 655d39bec1..3cfc75cdbd 100644 --- a/web/client/components/mapcontrols/annotations/__tests__/CoordinatesEditor-test.js +++ b/web/client/components/mapcontrols/annotations/__tests__/CoordinatesEditor-test.js @@ -551,7 +551,7 @@ describe("test the CoordinatesEditor Panel", () => { inputRadius.value = 10000; TestUtils.Simulate.change(inputRadius); expect(spyOnChangeRadius).toHaveBeenCalled(); - expect(spyOnChangeRadius).toHaveBeenCalledWith(10000, [], mapProjection); + expect(spyOnChangeRadius).toHaveBeenCalledWith(10000, [[0, 0]], mapProjection); expect(spyOnSetInvalidSelected).toNotHaveBeenCalled(); }); diff --git a/web/client/components/misc/combobox/PagedCombobox.jsx b/web/client/components/misc/combobox/PagedCombobox.jsx index 496905556a..f16f3e2978 100644 --- a/web/client/components/misc/combobox/PagedCombobox.jsx +++ b/web/client/components/misc/combobox/PagedCombobox.jsx @@ -212,6 +212,7 @@ class PagedCombobox extends React.Component { let label = l ? () : (); // TODO change "the else case" value with null ? return (
+ {label} {clearable ? (
{this.renderField()} @@ -226,12 +227,15 @@ class PagedCombobox extends React.Component {
) : this.renderField() } -   -
- {label} - { this.props.anyFilterRuleMode ? - this.renderTooltipCheckbox() : null} -
+ { this.props.anyFilterRuleMode ? + <> +   +
+ { + this.renderTooltipCheckbox() + } +
+ : null}
); } } diff --git a/web/client/components/misc/popover/DisposablePopover.jsx b/web/client/components/misc/popover/DisposablePopover.jsx index 37c453cf1c..d57936a507 100644 --- a/web/client/components/misc/popover/DisposablePopover.jsx +++ b/web/client/components/misc/popover/DisposablePopover.jsx @@ -40,7 +40,7 @@ export default function DisposablePopover({ glyph = "question-sign", popoverClassName }) { - const [show, setShow] = useState(showOnRender); + const [show, setShow] = useState(() => showOnRender); let target = useRef(null); return ( <> diff --git a/web/client/components/resources/ResourceGrid.jsx b/web/client/components/resources/ResourceGrid.jsx index a72e9c7ea6..5d62be3d74 100644 --- a/web/client/components/resources/ResourceGrid.jsx +++ b/web/client/components/resources/ResourceGrid.jsx @@ -113,7 +113,7 @@ export default ({ edit, nameFieldFilter, category: categoryName, - enableDetails: categoryName === 'MAP', + enableDetails: categoryName === 'MAP' || categoryName === 'DASHBOARD', onResourceLoad // resource })} diff --git a/web/client/components/resources/modals/enhancers/handleSave.js b/web/client/components/resources/modals/enhancers/handleSave.js index 23cbc3ed54..0fe33578e8 100644 --- a/web/client/components/resources/modals/enhancers/handleSave.js +++ b/web/client/components/resources/modals/enhancers/handleSave.js @@ -18,11 +18,12 @@ export default mapPropsStream(props$ => { .withLatestFrom(props$) .switchMap(([resource, props]) => updateResource(resource) - .flatMap(rid => resource.category === 'MAP' ? updateResourceAttribute({ - id: rid, - name: 'detailsSettings', - value: JSON.stringify(resource.attributes?.detailsSettings || {}) - }) : Rx.Observable.of(rid)) + .flatMap(rid => (['MAP', 'DASHBOARD'].includes(resource.category)) ? + updateResourceAttribute({ + id: rid, + name: 'detailsSettings', + value: JSON.stringify(resource.attributes?.detailsSettings || {}) + }) : Rx.Observable.of(rid)) .do(() => { if (props) { if (props.onClose) { diff --git a/web/client/components/styleeditor/Fields.jsx b/web/client/components/styleeditor/Fields.jsx index f04f075829..8150d3689e 100644 --- a/web/client/components/styleeditor/Fields.jsx +++ b/web/client/components/styleeditor/Fields.jsx @@ -113,7 +113,8 @@ export const fields = { return ( + disabled={disabled} + infoMessageId={config.infoMessageId}>
; @@ -20,9 +21,13 @@ function PropertyField({ children, label, tools, divider, invalid, warning, disa return (
-
+
+ + {infoMessageId ? <> }/> : null} +
{ event.stopPropagation(); diff --git a/web/client/components/styleeditor/config/blocks.js b/web/client/components/styleeditor/config/blocks.js index 1b1d2ad8ef..02c134b2d2 100644 --- a/web/client/components/styleeditor/config/blocks.js +++ b/web/client/components/styleeditor/config/blocks.js @@ -380,7 +380,8 @@ const getBlocks = ({ range: { min: 1, max: 10 - } + }, + infoMessageId: 'styleeditor.pointCloudSizeInfo' }) }, defaultProperties: { diff --git a/web/client/components/styleeditor/config/property.js b/web/client/components/styleeditor/config/property.js index 2ff391a4a6..8711d5f1f9 100644 --- a/web/client/components/styleeditor/config/property.js +++ b/web/client/components/styleeditor/config/property.js @@ -192,7 +192,7 @@ const property = { }, isDisabled }), - size: ({ key = 'radius', label = 'Radius', range, fallbackValue = 1 }) => ({ + size: ({ key = 'radius', label = 'Radius', range, fallbackValue = 1, infoMessageId }) => ({ type: 'input', label, config: { @@ -201,7 +201,8 @@ const property = { maxWidth: 105, uom: 'px', min: range?.min ?? 0, - max: range?.max + max: range?.max, + infoMessageId }, setValue: (value = 1) => { return value === undefined ? fallbackValue : parseFloat(value); diff --git a/web/client/components/widgets/builder/wizard/table/TableOptions.jsx b/web/client/components/widgets/builder/wizard/table/TableOptions.jsx index 7beadf1435..0ceff37894 100644 --- a/web/client/components/widgets/builder/wizard/table/TableOptions.jsx +++ b/web/client/components/widgets/builder/wizard/table/TableOptions.jsx @@ -9,7 +9,6 @@ import React from 'react'; import { Col, Form, Row } from 'react-bootstrap'; import {compose, withProps} from 'recompose'; - import { isGeometryType } from '../../../../../utils/ogc/WFS/base'; import AttributeTable from '../../../../data/featuregrid/AttributeTable'; import Message from '../../../../I18N/Message'; diff --git a/web/client/components/widgets/enhancers/__tests__/dependenciesToExtent-test.jsx b/web/client/components/widgets/enhancers/__tests__/dependenciesToExtent-test.jsx index 5eb8927268..9e9ca65f33 100644 --- a/web/client/components/widgets/enhancers/__tests__/dependenciesToExtent-test.jsx +++ b/web/client/components/widgets/enhancers/__tests__/dependenciesToExtent-test.jsx @@ -36,7 +36,7 @@ describe('widgets dependenciesToExtent enhancer', () => { it('dependenciesToExtent default', (done) => { const Sink = dependenciesToExtent(createSink( props => { expect(props).toExist(); - expect(props).toEqual({}); + expect(props.hookRegister).toEqual(null); done(); })); ReactDOM.render(, document.getElementById("container")); @@ -55,7 +55,7 @@ describe('widgets dependenciesToExtent enhancer', () => { '-124.731422 24.955967-66.969849 49.371735' ); - const hookRegister = MapUtils.createRegisterHooks(); + const hookRegister = MapUtils.createRegisterHooks("id"); hookRegister.registerHook(MapUtils.ZOOM_TO_EXTENT_HOOK, {hookName: MapUtils.ZOOM_TO_EXTENT_HOOK}); ReactDOM.render( { '-124.731422 24.955967-66.969849 49.371735' ); - const hookRegister = MapUtils.createRegisterHooks(); + const hookRegister = MapUtils.createRegisterHooks("id"); hookRegister.registerHook(MapUtils.ZOOM_TO_EXTENT_HOOK, {hookName: MapUtils.ZOOM_TO_EXTENT_HOOK}); ReactDOM.render( { + let store; beforeEach((done) => { + store = mockStore(); document.body.innerHTML = '
'; setTimeout(done); }); @@ -34,9 +40,93 @@ describe('widgets tableWidget enhancer', () => { props.gridEvents.onAddFilter(someFilter); done(); })); - ReactDOM.render( { + ReactDOM.render( { expect(path).toBe("quickFilters.state"); expect(filter).toBe(someFilter); - }}/>, document.getElementById("container")); + }}/>, document.getElementById("container")); + }); + + it('tableWidget with gridTools including zoom icon for dashboard viewer [enable zoom in config]', (done) => { + const Sink = tableWidget(createSink( props => { + expect(props).toExist(); + expect(props.gridTools.length).toEqual(1); + props.gridTools[0].events.onClick( + { + bbox: [-10, 0, 0, -10] + }, {}, "", { crs: "", maxZoom: null } + ); + done(); + })); + ReactDOM.render( { + expect(path).toBe("dependencies.extentObj"); + expect(id).toBe("123456"); + expect(value).toEqual({ + bbox: [-10, 0, 0, -10] + }, {}, "", { crs: "EPSG:4326", maxZoom: null }); + }}/>, document.getElementById("container")); + const container = document.getElementById('container'); + expect(container).toExist(); + + }); + it('tableWidget with gridTools including zoom icon for dashboard viewer in case of just table is added [No maps added]', (done) => { + const Sink = tableWidget(createSink( props => { + expect(props).toExist(); + expect(props.gridTools.length).toEqual(0); + done(); + })); + ReactDOM.render( { + expect(path).toBe("dependencies.extentObj"); + expect(id).toBe("123456"); + expect(value).toEqual({ + bbox: [-10, 0, 0, -10] + }, {}, "", { crs: "EPSG:4326", maxZoom: null }); + }}/>, document.getElementById("container")); + const container = document.getElementById('container'); + expect(container).toExist(); + + }); + it('tableWidget with gridTools including zoom icon for dashboard viewer [not enable zoom in config]', (done) => { + const Sink = tableWidget(createSink( props => { + expect(props).toExist(); + expect(props.gridTools.length).toEqual(0); + done(); + })); + ReactDOM.render( { + expect(path).toBe("dependencies.extentObj"); + expect(id).toBe("123456"); + expect(value).toEqual({ + bbox: [-10, 0, 0, -10] + }, {}, "", { crs: "EPSG:4326", maxZoom: null }); + }}/>, document.getElementById("container")); + const container = document.getElementById('container'); + expect(container).toExist(); + + }); + it('tableWidget with gridTools including zoom icon for mapViewer [enable zoom in config]', (done) => { + const Sink = tableWidget(createSink( props => { + expect(props).toExist(); + expect(props.gridTools.length).toEqual(1); + props.gridTools[0].events.onClick( + { + bbox: [-10, 0, 0, -10] + }, {}, "", { crs: "", maxZoom: null } + ); + done(); + })); + ReactDOM.render( , document.getElementById("container")); + const container = document.getElementById('container'); + expect(container).toExist(); + + }); + it('tableWidget with gridTools including zoom icon for mapViewer [not enable zoom in config]', (done) => { + const Sink = tableWidget(createSink( props => { + expect(props).toExist(); + expect(props.gridTools.length).toEqual(0); + done(); + })); + ReactDOM.render( , document.getElementById("container")); + const container = document.getElementById('container'); + expect(container).toExist(); + }); }); diff --git a/web/client/components/widgets/enhancers/dependenciesToExtent.js b/web/client/components/widgets/enhancers/dependenciesToExtent.js index d1b57c29f4..a848597e2b 100644 --- a/web/client/components/widgets/enhancers/dependenciesToExtent.js +++ b/web/client/components/widgets/enhancers/dependenciesToExtent.js @@ -22,15 +22,42 @@ import { createRegisterHooks, ZOOM_TO_EXTENT_HOOK } from '../../../utils/MapUtil * @returns {object} the map with center and zoom updated */ export default compose( - + withPropsOnChange(["id"], + ({hookRegister = null, id}) => ({ + hookRegister: hookRegister?.id !== id ? createRegisterHooks(id) : hookRegister + })), + compose( + withPropsOnChange((props = {}, nextProps = {}) => { + const currentExtentObj = props.widgets?.find(i=>i?.dependencies?.extentObj); + const nextExtentObj = nextProps.widgets?.find(i=>i?.dependencies?.extentObj); + return !(isEqual(currentExtentObj, nextExtentObj)) && nextExtentObj; + }, + ({ id, widgets, updateProperty, hookRegister })=>{ + const tblWidgetWithExtentObj = widgets?.find(i=>i?.dependencies?.extentObj); + const extentObj = tblWidgetWithExtentObj?.dependencies?.extentObj; + const mapWidgetID = id; + const mapWidget = widgets?.find(i=>tblWidgetWithExtentObj?.dependenciesMap?.mapSync.includes(i.id) && i.id === mapWidgetID); + const connectedMap = mapWidget?.maps.find(i=>i.mapStateSource === mapWidget.id); + const hook = hookRegister?.getHook("ZOOM_TO_EXTENT_HOOK"); + if (hook && hookRegister?.id === id && connectedMap) { // a condition to detect which connected map with its id to zoom within + // trigger "internal" zoom to extent + hook(extentObj.extent, { + crs: extentObj.crs, maxZoom: extentObj.maxZoom + }); + // remove extentObj from state + updateProperty(tblWidgetWithExtentObj.id, `dependencies.extentObj`, undefined); + } + return {}; + }) + ), branch( ({mapSync, dependencies} = {}) => { return mapSync && (!isEmpty(dependencies.quickFilters) || !isEmpty(dependencies.filter)); }, compose( withPropsOnChange(["id"], - ({hookRegister = null}) => ({ - hookRegister: hookRegister || createRegisterHooks() + ({hookRegister = null, id}) => ({ + hookRegister: hookRegister?.id !== id ? createRegisterHooks(id) : hookRegister })), mapPropsStream(props$ => { return props$ diff --git a/web/client/components/widgets/enhancers/tableWidget.js b/web/client/components/widgets/enhancers/tableWidget.js index ffd813c839..4dc968c048 100644 --- a/web/client/components/widgets/enhancers/tableWidget.js +++ b/web/client/components/widgets/enhancers/tableWidget.js @@ -5,13 +5,19 @@ * 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 { get } from 'lodash'; +import {connect} from 'react-redux'; import { compose, withPropsOnChange } from 'recompose'; import debounce from 'lodash/debounce'; +import bbox from '@turf/bbox'; import deleteWidget from './deleteWidget'; +import LoadingSpinner from '../../misc/LoadingSpinner'; import { defaultIcons, editableWidget, withHeaderTools } from './tools'; - +import { zoomToExtent } from '../../../actions/map'; +import {error} from '../../../actions/notifications'; +import { gridTools } from '../../../plugins/featuregrid/index'; +import { getFeature } from '../../../api/WFS'; const withSorting = () => withPropsOnChange(["gridEvents"], ({ gridEvents = {}, updateProperty = () => { }, id } = {}) => ({ gridEvents: { ...gridEvents, @@ -23,6 +29,64 @@ const withSorting = () => withPropsOnChange(["gridEvents"], ({ gridEvents = {}, * Moreover enhances it to allow delete. */ export default compose( + compose(connect(null, (dispatch, ownProps)=>{ + let isTblDashboard = ownProps?.enableZoomInTblWidget && ownProps?.widgetType === 'table' && ownProps?.isDashboardOpened; + let isTblWidgetInMapViewer = ownProps?.widgetType === 'table' && !isTblDashboard && ownProps?.enableZoomInTblWidget; + let isTblSyncWithMap = ownProps?.mapSync; + return { + gridTools: (isTblSyncWithMap && isTblDashboard) || (isTblWidgetInMapViewer) ? gridTools.map((t) => ({ + ...t, + events: { + onClick: async(p, opts, describe, {crs, maxZoom} = {}) => { + if (ownProps?.recordZoomLoading) return; + try { + // fetch feature with geometry and zoom to it if geometry not exist + if (!p?.bbox) { + ownProps?.updateProperty(ownProps.id, `dependencies.zoomLoader`, true); // show loader instead of zoom icon + let { data: featureData } = await getFeature(ownProps?.layer?.search?.url, ownProps?.layer?.name, { + outputFormat: "application/json", + srsname: 'EPSG:4326', + featureId: p.id, + propertyName: ownProps?.geomProp || "the_geom" // fetch only the geometry + }); + p.geometry = featureData?.features[0].geometry; // set geometry to feature for the future hit + p.bbox = bbox(featureData?.features[0]); // set geometry to feature for the future hit + if (isTblDashboard) { // in case of table widget in dashboard view set extent to widget dependencies + ownProps?.updateProperty(ownProps.id, `dependencies.extentObj`, { + extent: p.bbox, + crs: crs || "EPSG:4326", maxZoom + }); + } else { // in case of table widget within the map viewer zoom to the feature + dispatch(zoomToExtent(p.bbox, crs || "EPSG:4326", maxZoom)); + } + ownProps?.updateProperty(ownProps.id, `dependencies.zoomLoader`, false); // stop zoom loader + } else { // in case the geometry is already existing --> zoom to feature directly without fetching + if (isTblDashboard) { + ownProps?.updateProperty(ownProps.id, `dependencies.extentObj`, { + extent: p.bbox, + crs: crs || "EPSG:4326", maxZoom + }); + } else { + dispatch(zoomToExtent(p.bbox, crs || "EPSG:4326", maxZoom)); + } + } + } catch (err) { + dispatch(error({ + title: "notification.warning", + message: "notification.errorLoadingGF", + action: { + label: "notification.warning" + }, + autoDismiss: 3, + position: "tc" + })); + ownProps?.updateProperty(ownProps.id, `dependencies.zoomLoader`, false); // stop zoom loader + } + } + }, formatter: ownProps?.recordZoomLoading ? : t.tableWidgetFormatter + })) : [] + }; + })), withPropsOnChange(["gridEvents"], ({ gridEvents = {}, updateProperty = () => {}, id } = {}) => { const _debounceOnAddFilter = debounce((...args) => updateProperty(...args), 500); return { diff --git a/web/client/components/widgets/widget/TableWidget.jsx b/web/client/components/widgets/widget/TableWidget.jsx index 7b51ae9394..7ffbe8414f 100644 --- a/web/client/components/widgets/widget/TableWidget.jsx +++ b/web/client/components/widgets/widget/TableWidget.jsx @@ -52,7 +52,8 @@ export default getWidgetFilterRenderers(({ virtualScroll = true, gridOpts = defaultGridOpts, options = {}, - dateFormats + dateFormats, + gridTools }) => ( diff --git a/web/client/components/widgets/widget/__tests__/TableWidget-test.jsx b/web/client/components/widgets/widget/__tests__/TableWidget-test.jsx index ed6ece6b98..723cde5b7c 100644 --- a/web/client/components/widgets/widget/__tests__/TableWidget-test.jsx +++ b/web/client/components/widgets/widget/__tests__/TableWidget-test.jsx @@ -12,6 +12,8 @@ import ReactDOM from 'react-dom'; import ReactTestUtils from 'react-dom/test-utils'; import { compose, defaultProps } from 'recompose'; import { waitFor } from '@testing-library/react'; +import {Provider} from 'react-redux'; +import configureMockStore from 'redux-mock-store'; import describePois from '../../../../test-resources/wfs/describe-pois.json'; import museam from '../../../../test-resources/wfs/museam.json'; @@ -23,8 +25,12 @@ const TableWidget = compose( tableWidget )(TableWidgetComp); +const mockStore = configureMockStore(); + describe('TableWidget component', () => { + let store; beforeEach((done) => { + store = mockStore(); document.body.innerHTML = '
'; setTimeout(done); }); @@ -34,7 +40,7 @@ describe('TableWidget component', () => { setTimeout(done); }); it('TableWidget rendering with defaults', () => { - ReactDOM.render(, document.getElementById("container")); + ReactDOM.render(, document.getElementById("container")); const container = document.getElementById('container'); const el = container.querySelector('.mapstore-widget-card'); expect(el).toExist(); @@ -42,7 +48,7 @@ describe('TableWidget component', () => { expect(container.querySelector('.glyphicon-trash')).toExist(); }); it('view only mode', () => { - ReactDOM.render(, document.getElementById("container")); + ReactDOM.render(, document.getElementById("container")); const container = document.getElementById('container'); expect(container.querySelector('.glyphicon-pencil')).toNotExist(); expect(container.querySelector('.glyphicon-trash')).toNotExist(); @@ -52,27 +58,27 @@ describe('TableWidget component', () => { onEdit: () => { } }; const spyonEdit = expect.spyOn(actions, 'onEdit'); - ReactDOM.render(, document.getElementById("container")); + ReactDOM.render(, document.getElementById("container")); const container = document.getElementById('container'); const el = container.querySelector('.glyphicon-pencil'); ReactTestUtils.Simulate.click(el); // <-- trigger event callback expect(spyonEdit).toHaveBeenCalled(); }); it('TableWidget loading', () => { - ReactDOM.render(, document.getElementById("container")); + ReactDOM.render(, document.getElementById("container")); const container = document.getElementById('container'); const el = container.querySelector('.loader-container'); expect(el).toExist(); }); it('TableWidget empty', (done) => { - ReactDOM.render(, document.getElementById("container")); + ReactDOM.render(, document.getElementById("container")); const container = document.getElementById('container'); waitFor(() =>expect( container.querySelector('.react-grid-Empty')).toBeTruthy()) .then(() => done()); }); it('TableWidget with default gridOpts', () => { - ReactDOM.render(, document.getElementById("container")); + ReactDOM.render(, document.getElementById("container")); const gridHeaderRows = document.getElementsByClassName('react-grid-HeaderRow'); expect(gridHeaderRows.length).toBe(2); const headerRowHeight = gridHeaderRows[0]?.getAttribute('height'); @@ -81,9 +87,9 @@ describe('TableWidget component', () => { expect(Number(headerFiltersHeight)).toBe(28); }); it('TableWidget with custom gridOpts', () => { - ReactDOM.render(, document.getElementById("container")); + virtualScroll={false} describeFeatureType={describePois} enableColumnFilters features={museam.features}/>, document.getElementById("container")); const gridHeaderRows = document.getElementsByClassName('react-grid-HeaderRow'); expect(gridHeaderRows.length).toBe(2); const headerRowHeight = gridHeaderRows[0]?.getAttribute('height'); @@ -101,7 +107,7 @@ describe('TableWidget component', () => { "localType": "number" }]}]}; ReactTestUtils.act(()=>{ - ReactDOM.render({ + ReactDOM.render({ try { expect(id).toBe(1); expect(path).toBe("quickFilters.FLOAT"); @@ -113,7 +119,7 @@ describe('TableWidget component', () => { done(e); } done(); - }} describeFeatureType={_d} features={[]} />, document.getElementById("container")); + }} describeFeatureType={_d} features={[]} />, document.getElementById("container")); }); const container = document.getElementById('container'); const filterFields = container.querySelectorAll("input"); diff --git a/web/client/configs/localConfig.json b/web/client/configs/localConfig.json index 4c32239878..18c1ed324a 100644 --- a/web/client/configs/localConfig.json +++ b/web/client/configs/localConfig.json @@ -485,7 +485,12 @@ "declineUrl" : "http://www.google.com" } }, - "OmniBar", "Login", "Save", "SaveAs", "Expander", "Undo", "Redo", "FullScreen", "GlobeViewSwitcher", "SearchServicesConfig", "SearchByBookmark", "WidgetsBuilder", "Widgets", + "OmniBar", "Login", "Save", "SaveAs", "Expander", "Undo", "Redo", "FullScreen", "GlobeViewSwitcher", "SearchServicesConfig", "SearchByBookmark", "WidgetsBuilder", { + "name": "Widgets", + "cfg": { + "enableZoomInTblWidget": true + } + }, "WidgetsTray", { "name": "Timeline", @@ -671,8 +676,21 @@ "FeedbackMask" ], "dashboard": [ - "BurgerMenu", - "Dashboard", + "Details", + "AddWidgetDashboard", + "MapConnectionDashboard", + { + "name": "SidebarMenu", + "cfg" : { + "containerPosition": "columns" + } + }, + { + "name": "Dashboard", + "cfg": { + "enableZoomInTblWidget": true + } + }, "Notifications", "Login", { diff --git a/web/client/epics/__tests__/catalog-test.js b/web/client/epics/__tests__/catalog-test.js index 85d730c2ee..c5ac64bd84 100644 --- a/web/client/epics/__tests__/catalog-test.js +++ b/web/client/epics/__tests__/catalog-test.js @@ -16,7 +16,6 @@ const { autoSearchEpic, openCatalogEpic, recordSearchEpic, - getSupportedFormatsEpic, updateGroupSelectedMetadataExplorerEpic, newCatalogServiceAdded } = catalog(API); @@ -34,9 +33,6 @@ import { RECORD_LIST_LOADED, RECORD_LIST_LOAD_ERROR, SET_LOADING, - formatOptionsFetch, - FORMAT_OPTIONS_LOADING, - SET_FORMAT_OPTIONS, ADD_LAYER_AND_DESCRIBE, addLayerAndDescribe, DESCRIBE_ERROR, @@ -747,35 +743,6 @@ describe('catalog Epics', () => { } }); }); - it('getSupportedFormatsEpic wms', (done) => { - const NUM_ACTIONS = 3; - const url = "base/web/client/test-resources/wms/GetCapabilities-1.1.1.xml"; - testEpic(addTimeoutEpic(getSupportedFormatsEpic, 0), NUM_ACTIONS, formatOptionsFetch(url), (actions) => { - expect(actions.length).toBe(NUM_ACTIONS); - try { - actions.map((action) => { - switch (action.type) { - case SET_FORMAT_OPTIONS: - expect(action.formats).toBeTruthy(); - expect(action.formats.imageFormats).toEqual(['image/png', 'image/gif', 'image/jpeg', 'image/png8', 'image/png; mode=8bit', 'image/vnd.jpeg-png']); - expect(action.formats.infoFormats).toEqual(['text/plain', 'text/html', 'application/json']); - break; - case FORMAT_OPTIONS_LOADING: - break; - case TEST_TIMEOUT: - break; - default: - expect(true).toBe(false); - } - }); - } catch (e) { - done(e); - } - done(); - }, { - catalog: {} - }); - }); it('updateGroupSelectedMetadataExplorerEpic allows clicking on group to set destination to current group', done => { const state = { diff --git a/web/client/epics/__tests__/config-test.js b/web/client/epics/__tests__/config-test.js index 92c1fe121a..658b99416d 100644 --- a/web/client/epics/__tests__/config-test.js +++ b/web/client/epics/__tests__/config-test.js @@ -8,7 +8,8 @@ import expect from 'expect'; import {head} from 'lodash'; -import {loadMapConfigAndConfigureMap, loadMapInfoEpic, storeDetailsInfoEpic} from '../config'; +import {loadMapConfigAndConfigureMap, loadMapInfoEpic, storeDetailsInfoDashboardEpic, storeDetailsInfoEpic, backgroundsListInitEpic, getSupportedFormatsEpic} from '../config'; + import {LOAD_USER_SESSION} from '../../actions/usersession'; import { loadMapConfig, @@ -18,7 +19,8 @@ import { MAP_INFO_LOADED, MAP_INFO_LOAD_START, loadMapInfo, - mapInfoLoaded + mapInfoLoaded, + configureMap } from '../../actions/config'; import { TEST_TIMEOUT, addTimeoutEpic, testEpic } from './epicTestUtils'; @@ -30,13 +32,52 @@ import testConfigEPSG31468 from "raw-loader!../../test-resources/testConfigEPSG3 import ConfigUtils from "../../utils/ConfigUtils"; import { DETAILS_LOADED } from '../../actions/details'; import { EMPTY_RESOURCE_VALUE } from '../../utils/MapInfoUtils'; +import { dashboardLoaded } from '../../actions/dashboard'; +import { + formatOptionsFetch, + FORMAT_OPTIONS_LOADING, + SET_FORMAT_OPTIONS, + SHOW_FORMAT_ERROR +} from '../../actions/catalog'; const api = { getResource: () => Promise.resolve({mapId: 1234}) }; let mockAxios; describe('config epics', () => { + it('getSupportedFormatsEpic wms', (done) => { + const NUM_ACTIONS = 4; + const url = "base/web/client/test-resources/wms/GetCapabilities-1.1.1.xml"; + testEpic(addTimeoutEpic(getSupportedFormatsEpic, 0), NUM_ACTIONS, formatOptionsFetch(url), (actions) => { + expect(actions.length).toBe(NUM_ACTIONS); + try { + actions.map((action) => { + switch (action.type) { + case SET_FORMAT_OPTIONS: + expect(action.formats).toBeTruthy(); + expect(action.formats.imageFormats).toEqual(['image/png', 'image/gif', 'image/jpeg', 'image/png8', 'image/png; mode=8bit', 'image/vnd.jpeg-png']); + expect(action.formats.infoFormats).toEqual(['text/plain', 'text/html', 'application/json']); + break; + case SHOW_FORMAT_ERROR: + expect(action.status).toBeFalsy(); + break; + case FORMAT_OPTIONS_LOADING: + break; + case TEST_TIMEOUT: + break; + default: + expect(true).toBe(false); + } + }); + } catch (e) { + done(e); + } + done(); + }, { + catalog: {} + }); + }); describe('loadMapConfigAndConfigureMap', () => { beforeEach(done => { ConfigUtils.setConfigProp("userSessions", { @@ -345,7 +386,7 @@ describe('config epics', () => { switch (action.type) { case DETAILS_LOADED: - expect(action.mapId).toBe(mapId); + expect(action.id).toBe(mapId); expect(action.detailsUri).toBe("rest/geostore/data/1/raw?decode=datauri"); break; default: @@ -379,5 +420,184 @@ describe('config epics', () => { }}); }); }); + describe("backgroundsListInitEpic", () => { + const base64 = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAIAQMAAAD+wSzIAAAABlBMVEX///+/v7+jQ3Y5AAAADklEQVQI12P4AIX8EAgALgAD/aNpbtEAAAAASUVORK5CYII"; + it('test layer update with thumbnail on background init', (done) => { + testEpic(addTimeoutEpic(backgroundsListInitEpic), 3, configureMap({ + map: { + backgrounds: [{id: "1", thumbnail: base64}], + layers: [{id: "1", group: "background", name: "layer_1", visibility: true}] + } + }), actions => { + expect(actions.length).toBe(3); + actions.map((action) => { + switch (action.type) { + case "CHANGE_LAYER_PROPERTIES": + expect(action.newProperties.thumbURL).toBeTruthy(); + expect(action.layer).toBe("1"); + break; + case "BACKGROUND_SELECTOR:CREATE_BACKGROUNDS_LIST": + expect(action.backgrounds.length).toBe(1); + expect(action.backgrounds[0].id).toBe("1"); + expect(action.backgrounds[0].thumbnail).toBe(base64); + break; + case "BACKGROUND_SELECTOR:SET_CURRENT_BACKGROUND_LAYER": + expect(action.layerId).toBe("1"); + break; + default: + expect(true).toBe(false); + } + }); + done(); + }); + }); + it('test layer update with thumbnail with layer not visible', (done) => { + testEpic(addTimeoutEpic(backgroundsListInitEpic), 2, configureMap({ + map: { + backgrounds: [{id: "1", thumbnail: base64}], + layers: [{id: "1", group: "background", name: "layer_1", visibility: false}] + } + }), actions => { + expect(actions.length).toBe(2); + actions.map((action) => { + switch (action.type) { + case "CHANGE_LAYER_PROPERTIES": + expect(action.newProperties.thumbURL).toBeTruthy(); + expect(action.layer).toBe("1"); + break; + case "BACKGROUND_SELECTOR:CREATE_BACKGROUNDS_LIST": + expect(action.backgrounds.length).toBe(1); + expect(action.backgrounds[0].id).toBe("1"); + expect(action.backgrounds[0].thumbnail).toBe(base64); + break; + default: + expect(true).toBe(false); + } + }); + done(); + }, {}); + }); + it('test backgroundsListInitEpic with no background layer', (done) => { + testEpic(addTimeoutEpic(backgroundsListInitEpic), 1, configureMap({ + map: { + layers: [{id: "1", name: "layer_1", visibility: false}] + } + }), actions => { + expect(actions.length).toBe(1); + actions.map((action) => { + switch (action.type) { + case "BACKGROUND_SELECTOR:CREATE_BACKGROUNDS_LIST": + expect(action.backgrounds.length).toBe(0); + break; + default: + expect(true).toBe(false); + } + }); + done(); + }, {}); + }); + }); + + describe("storeDetailsInfoDashboardEpic", () => { + beforeEach(done => { + mockAxios = new MockAdapter(axios); + setTimeout(done); + }); + + afterEach(done => { + mockAxios.restore(); + setTimeout(done); + }); + const dashboardId = 1; + const dashboardAttributesEmptyDetails = { + "AttributeList": { + "Attribute": [ + { + "name": "details", + "type": "STRING", + "value": EMPTY_RESOURCE_VALUE + } + ] + } + }; + + const dashboardAttributesWithoutDetails = { + "AttributeList": { + "Attribute": [] + } + }; + + const dashboardAttributesWithDetails = { + AttributeList: { + Attribute: [ + { + name: 'details', + type: 'STRING', + value: 'rest\/geostore\/data\/1\/raw?decode=datauri' + }, + { + name: "thumbnail", + type: "STRING", + value: 'rest\/geostore\/data\/1\/raw?decode=datauri' + }, + { + name: 'owner', + type: 'STRING', + value: 'admin' + } + ] + } + }; + it('test storeDetailsInfoDashboardEpic', (done) => { + mockAxios.onGet().reply(200, dashboardAttributesWithDetails); + testEpic(addTimeoutEpic(storeDetailsInfoDashboardEpic), 1, dashboardLoaded("RES", "DATA"), actions => { + expect(actions.length).toBe(1); + actions.map((action) => { + + switch (action.type) { + case DETAILS_LOADED: + expect(action.id).toBe(dashboardId); + expect(action.detailsUri).toBe("rest/geostore/data/1/raw?decode=datauri"); + break; + default: + expect(true).toBe(false); + } + }); + done(); + }, {dashboard: { + resource: { + id: dashboardId, + attributes: {} + } + }}); + }); + it('test storeDetailsInfoDashboardEpic when api returns NODATA value', (done) => { + // const mock = new MockAdapter(axios); + mockAxios.onGet().reply(200, dashboardAttributesWithoutDetails); + testEpic(addTimeoutEpic(storeDetailsInfoDashboardEpic), 1, dashboardLoaded("RES", "DATA"), actions => { + expect(actions.length).toBe(1); + actions.map((action) => expect(action.type).toBe(TEST_TIMEOUT)); + done(); + }, {dashboard: { + resource: { + id: dashboardId, + attributes: {} + } + }}); + }); + it('test storeDetailsInfoDashboardEpic when api doesnt return details', (done) => { + mockAxios.onGet().reply(200, dashboardAttributesEmptyDetails); + testEpic(addTimeoutEpic(storeDetailsInfoDashboardEpic), 1, dashboardLoaded("RES", "DATA"), actions => { + expect(actions.length).toBe(1); + actions.map((action) => expect(action.type).toBe(TEST_TIMEOUT)); + done(); + }, {dashboard: { + resource: { + id: dashboardId, + attributes: {} + } + }}); + }); + }); }); diff --git a/web/client/epics/__tests__/dashboard-test.js b/web/client/epics/__tests__/dashboard-test.js index 1d67d3beb8..4c224e0efb 100644 --- a/web/client/epics/__tests__/dashboard-test.js +++ b/web/client/epics/__tests__/dashboard-test.js @@ -338,16 +338,14 @@ describe('saveDashboard', () => { const startActions = [saveDashboard(RESOURCE)]; testEpic(saveDashboardMethod, actionsCount, startActions, actions => { - expect(actions.length).toBe(actionsCount); - expect(actions[0].type).toBe(DASHBOARD_LOADING); - expect(actions[0].value).toBe(true); - expect(actions[1].type).toBe(SAVE_ERROR); + expect(actions.length).toBe(2); + expect(actions[0].type).toBe(SAVE_ERROR); expect( - actions[1].error.status === 403 - || actions[1].error.status === 404 + actions[0].error.status === 403 + || actions[0].error.status === 404 ).toBeTruthy(); - expect(actions[2].type).toBe(DASHBOARD_LOADING); - expect(actions[2].value).toBe(false); + expect(actions[1].type).toBe(DASHBOARD_LOADING); + expect(actions[1].value).toBe(false); }, BASE_STATE, done); }); @@ -364,13 +362,11 @@ describe('saveDashboard', () => { const startActions = [saveDashboard(withoutMetadata)]; testEpic(saveDashboardMethod, actionsCount, startActions, actions => { - expect(actions.length).toBe(3); - expect(actions[0].type).toBe(DASHBOARD_LOADING); - expect(actions[0].value).toBe(true); - expect(actions[1].type).toBe(SAVE_ERROR); - expect(typeof(actions[1].error) === 'string').toBeTruthy(); - expect(actions[2].type).toBe(DASHBOARD_LOADING); - expect(actions[2].value).toBe(false); + expect(actions.length).toBe(2); + expect(actions[0].type).toBe(SAVE_ERROR); + expect(typeof(actions[0].error) === 'string').toBeTruthy(); + expect(actions[1].type).toBe(DASHBOARD_LOADING); + expect(actions[1].value).toBe(false); }, BASE_STATE, done); }); }); diff --git a/web/client/epics/__tests__/details-test.js b/web/client/epics/__tests__/details-test.js index 7c6ff90a85..29cd03de96 100644 --- a/web/client/epics/__tests__/details-test.js +++ b/web/client/epics/__tests__/details-test.js @@ -35,7 +35,7 @@ let map1 = { id: mapId, name: "name" }; -const testState = { +const mapTestState = { mapInitialConfig: { mapId }, @@ -48,11 +48,116 @@ const testState = { }, details: {} }; +const dashboardTestState = { + dashboard: { + resource: { + id: "123", + attributes: { + details: encodeURIComponent(detailsUri) + + } + } + } +}; + const rootEpic = combineEpics(closeDetailsPanelEpic); const epicMiddleware = createEpicMiddleware(rootEpic); const mockStore = configureMockStore([epicMiddleware]); -describe('details epics tests', () => { +describe('details epics tests for map', () => { + const oldGetDefaults = ConfigUtils.getDefaults; + let store; + + beforeEach(() => { + store = mockStore(); + ConfigUtils.getDefaults = () => ({ + geoStoreUrl: baseUrl + }); + }); + + afterEach(() => { + epicMiddleware.replaceEpic(rootEpic); + ConfigUtils.getDefaults = oldGetDefaults; + }); + + it('test closeDetailsPanel', (done) => { + + store.dispatch(closeDetailsPanel()); + + setTimeout( () => { + try { + const actions = store.getActions(); + expect(actions.length).toBe(2); + expect(actions[0].type).toBe(CLOSE_DETAILS_PANEL); + expect(actions[1].type).toBe(SET_CONTROL_PROPERTY); + } catch (e) { + done(e); + } + done(); + }, 50); + + }); + it('test fetchDataForDetailsPanel', (done) => { + map1.details = encodeURIComponent(detailsUri); + testEpic(addTimeoutEpic(fetchDataForDetailsPanel), 2, openDetailsPanel(), actions => { + expect(actions.length).toBe(2); + actions.map((action) => { + switch (action.type) { + case TOGGLE_CONTROL: + expect(action.control).toBe("details"); + expect(action.property).toBe("enabled"); + break; + case UPDATE_DETAILS: + expect(action.detailsText.indexOf(detailsText)).toNotBe(-1); + break; + default: + expect(true).toBe(false); + } + }); + done(); + }, mapTestState); + }); + it('test fetchDataForDetailsPanel with Error', (done) => { + testEpic(addTimeoutEpic(fetchDataForDetailsPanel), 2, openDetailsPanel(), actions => { + expect(actions.length).toBe(2); + actions.map((action) => { + switch (action.type) { + case TOGGLE_CONTROL: + expect(action.control).toBe("details"); + expect(action.property).toBe("enabled"); + break; + case SHOW_NOTIFICATION: + expect(action.message).toBe("maps.feedback.errorFetchingDetailsOfMap"); + break; + default: + expect(true).toBe(false); + } + }); + done(); + }, { + locale: { + messages: { + maps: { + feedback: { + errorFetchingDetailsOfMap: "maps.feedback.errorFetchingDetailsOfMap" + } + } + } + }, + mapInitialConfig: { + mapId + }, + map: { + present: { + info: {} + } + } + }); + }); +}); + + +describe('details epics tests for dashbaord', () => { const oldGetDefaults = ConfigUtils.getDefaults; let store; @@ -103,7 +208,7 @@ describe('details epics tests', () => { } }); done(); - }, testState); + }, dashboardTestState); }); it('test fetchDataForDetailsPanel with Error', (done) => { testEpic(addTimeoutEpic(fetchDataForDetailsPanel), 2, openDetailsPanel(), actions => { diff --git a/web/client/epics/__tests__/tutorial-test.js b/web/client/epics/__tests__/tutorial-test.js index c3a026f54f..37bfe5c976 100644 --- a/web/client/epics/__tests__/tutorial-test.js +++ b/web/client/epics/__tests__/tutorial-test.js @@ -584,7 +584,7 @@ describe('tutorial Epics', () => { }); }); describe('openDetailsPanelEpic tests', () => { - it('should open the details panel if it has showAtStartup set to true', (done) => { + it('should open the details panel if it is a (Map) and it has showAtStartup set to true', (done) => { const NUM_ACTIONS = 1; testEpic(openDetailsPanelEpic, NUM_ACTIONS, closeTutorial(), (actions) => { @@ -595,6 +595,7 @@ describe('tutorial Epics', () => { }, { map: { present: { + mapId: "123", info: { detailsSettings: { showAtStartup: true @@ -604,7 +605,28 @@ describe('tutorial Epics', () => { } }); }); - it('should open the details panel if it has showAtStartup set to false', (done) => { + it('should open the details panel if it is a (Dashboard) and it has showAtStartup set to true', (done) => { + const NUM_ACTIONS = 1; + + testEpic(openDetailsPanelEpic, NUM_ACTIONS, closeTutorial(), (actions) => { + expect(actions.length).toBe(NUM_ACTIONS); + const [action] = actions; + expect(action.type).toBe(OPEN_DETAILS_PANEL); + done(); + }, { + dashboard: { + resource: { + id: "123", + attributes: { + detailsSettings: { + showAtStartup: true + } + } + } + } + }); + }); + it('should open the details panel if it is a(Map) and it has showAtStartup set to false', (done) => { const NUM_ACTIONS = 1; testEpic(addTimeoutEpic(openDetailsPanelEpic, 100), NUM_ACTIONS, closeTutorial(), (actions) => { @@ -615,6 +637,7 @@ describe('tutorial Epics', () => { }, { map: { present: { + mapId: "123", info: { detailsSettings: { showAtStartup: false @@ -624,5 +647,26 @@ describe('tutorial Epics', () => { } }); }); + it('should open the details panel if it is a (Dashboard) and it has showAtStartup set to false', (done) => { + const NUM_ACTIONS = 1; + + testEpic(addTimeoutEpic(openDetailsPanelEpic, 100), NUM_ACTIONS, closeTutorial(), (actions) => { + expect(actions.length).toBe(NUM_ACTIONS); + const [action] = actions; + expect(action.type).toBe(TEST_TIMEOUT); + done(); + }, { + dashboard: { + resource: { + id: "123", + attributes: { + detailsSettings: { + showAtStartup: false + } + } + } + } + }); + }); }); }); diff --git a/web/client/epics/backgroundselector.js b/web/client/epics/backgroundselector.js index 2a133afe98..41cf887381 100644 --- a/web/client/epics/backgroundselector.js +++ b/web/client/epics/backgroundselector.js @@ -18,7 +18,6 @@ import { BACKGROUND_EDITED, REMOVE_BACKGROUND, SYNC_CURRENT_BACKGROUND_LAYER, - createBackgroundsList, clearModalParameters, setBackgroundModalParams, setCurrentBackgroundLayer, @@ -27,7 +26,6 @@ import { } from '../actions/backgroundselector'; import { setControlProperty } from '../actions/controls'; -import { MAP_CONFIG_LOADED } from '../actions/config'; import { changeSelectedService } from '../actions/catalog'; import {ADD_LAYER, changeLayerProperties, removeNode} from '../actions/layers'; import { getLayerFromId, currentBackgroundSelector } from '../selectors/layers'; @@ -58,29 +56,6 @@ const addBackgroundPropertiesEpic = (action$) => : defaultAction; }); -const backgroundsListInit = (action$) => - action$.ofType(MAP_CONFIG_LOADED) - .switchMap(({config}) => { - const backgrounds = config.map && config.map.backgrounds || []; - const backgroundLayers = (config.map && config.map.layers || []).filter(layer => layer.group === 'background'); - const layerUpdateActions = backgrounds.filter(background => !!background.thumbnail).map(background => { - const toBlob = (data) => { - const bytes = atob(data.split(',')[1]); - const mimeType = data.split(',')[0].split(':')[1].split(';')[0]; - let buffer = new ArrayBuffer(bytes.length); - let byteArray = new Uint8Array(buffer); - for (let i = 0; i < bytes.length; ++i) { - byteArray[i] = bytes.charCodeAt(i); - } - return URL.createObjectURL(new Blob([buffer], {type: mimeType})); - }; - return changeLayerProperties(background.id, {thumbURL: toBlob(background.thumbnail)}); - }); - const currentBackground = head(backgroundLayers.filter(layer => layer.visibility)); - return Rx.Observable.of(...layerUpdateActions.concat(createBackgroundsList(backgrounds)), - ...(currentBackground ? [setCurrentBackgroundLayer(currentBackground.id)] : [])); - }); - const setCurrentBackgroundLayerEpic = (action$, store) => action$.ofType(SET_CURRENT_BACKGROUND_LAYER) .switchMap(({layerId}) => { @@ -151,7 +126,6 @@ const syncSelectedBackgroundEpic = (action$) => export default { accessMetadataExplorer, addBackgroundPropertiesEpic, - backgroundsListInit, setCurrentBackgroundLayerEpic, backgroundAddedEpic, backgroundEditedEpic, diff --git a/web/client/epics/catalog.js b/web/client/epics/catalog.js index 294b54b8a1..13d3cc685d 100644 --- a/web/client/epics/catalog.js +++ b/web/client/epics/catalog.js @@ -11,6 +11,7 @@ import axios from 'axios'; import xpathlib from 'xpath'; import { DOMParser } from 'xmldom'; import {head, get, find, isArray, isString, isObject, keys, toPairs, merge, castArray} from 'lodash'; + import { ADD_SERVICE, ADD_LAYERS_FROM_CATALOGS, @@ -19,7 +20,6 @@ import { GET_METADATA_RECORD_BY_ID, TEXT_SEARCH, CATALOG_CLOSE, - FORMAT_OPTIONS_FETCH, addCatalogService, setLoading, deleteCatalogService, @@ -31,8 +31,6 @@ import { resetCatalog, textSearch, changeSelectedService, - formatsLoading, - setSupportedFormats, ADD_LAYER_AND_DESCRIBE, describeError, addLayer, @@ -53,7 +51,6 @@ import { selectedCatalogSelector, searchOptionsSelector, catalogSearchInfoSelector, - getFormatUrlUsedSelector, isActiveSelector, servicesSelectorWithBackgrounds } from '../selectors/catalog'; import { metadataSourceSelector } from '../selectors/backgroundselector'; @@ -64,11 +61,11 @@ import { buildSRSMap, extractOGCServicesReferences } from '../utils/CatalogUtils'; -import { getSupportedFormat, getCapabilities, describeLayers, flatLayers } from '../api/WMS'; +import { getCapabilities, describeLayers, flatLayers } from '../api/WMS'; import CoordinatesUtils from '../utils/CoordinatesUtils'; import ConfigUtils from '../utils/ConfigUtils'; import {getCapabilitiesUrl, getLayerId, getLayerUrl, removeWorkspace} from '../utils/LayersUtils'; -import { wrapStartStop } from '../observables/epics'; + import {zoomToExtent} from "../actions/map"; import CSW from '../api/CSW'; import { projectionSelector } from '../selectors/map'; @@ -500,37 +497,9 @@ export default (API) => ({ [changeSelectedService(head(keys(services))), allowBackgroundsDeletion(true)] : []))); }), - /** - * Fetch all supported formats of a WMS service configured (infoFormats and imageFormats) - * Dispatches an action that sets the supported formats of the service. - * @param {Observable} action$ the actions triggered - * @param {object} getState store object - * @memberof epics.catalog - * @return {external:Observable} - */ - getSupportedFormatsEpic: (action$, {getState = ()=> {}} = {}) => - action$.ofType(FORMAT_OPTIONS_FETCH) - .filter((action)=> action.force || getFormatUrlUsedSelector(getState()) !== action?.url) - .switchMap(({url = ''} = {})=> { - return Rx.Observable.defer(() => getSupportedFormat(url, true)) - .switchMap((supportedFormats) => Rx.Observable.of(setSupportedFormats(supportedFormats, url))) - .let( - wrapStartStop( - formatsLoading(true), - formatsLoading(false), - () => { - return Rx.Observable.of( - error({ title: "layerProperties.format.error.title", message: 'layerProperties.format.error.message' }), - formatsLoading(false) - ); - } - ) - ); - }), - /** * Sets control property to currently selected group when catalogue is open - * Sets the currently selected group as the detination of new layers in catalogue + * Sets the currently selected group as the destination of new layers in catalogue * if a layer instead of a group is selected it resets the groupId to Default * Action performed: setControlProperty (only if catalogue is open) * @memberof epics.layers diff --git a/web/client/epics/config.js b/web/client/epics/config.js index a9fb45ad8a..7d9e0d4d2d 100644 --- a/web/client/epics/config.js +++ b/web/client/epics/config.js @@ -7,7 +7,7 @@ */ import { Observable } from 'rxjs'; import axios from '../libs/ajax'; -import { get, merge, isNaN, find } from 'lodash'; +import { get, merge, isNaN, find, head } from 'lodash'; import { LOAD_NEW_MAP, LOAD_MAP_CONFIG, @@ -27,11 +27,27 @@ import Persistence from '../api/persistence'; import GeoStoreApi from '../api/GeoStoreDAO'; import { isLoggedIn, userSelector } from '../selectors/security'; import { mapIdSelector, projectionDefsSelector } from '../selectors/map'; +import { getDashboardId } from '../selectors/dashboard'; +import { DASHBOARD_LOADED } from '../actions/dashboard'; import {loadUserSession, USER_SESSION_LOADED, userSessionStartSaving, saveMapConfig} from '../actions/usersession'; import { detailsLoaded, openDetailsPanel } from '../actions/details'; import {userSessionEnabledSelector, buildSessionName} from "../selectors/usersession"; import {getRequestParameterValue} from "../utils/QueryParamsUtils"; import { EMPTY_RESOURCE_VALUE } from '../utils/MapInfoUtils'; +import { changeLayerProperties } from '../actions/layers'; +import { createBackgroundsList, setCurrentBackgroundLayer } from '../actions/backgroundselector'; +import { + FORMAT_OPTIONS_FETCH, + formatsLoading, + showFormatError, + setSupportedFormats +} from '../actions/catalog'; +import { + getFormatUrlUsedSelector +} from '../selectors/catalog'; +import { getSupportedFormat } from '../api/WMS'; +import { wrapStartStop } from '../observables/epics'; +import { error } from '../actions/notifications'; const prepareMapConfiguration = (data, override, state) => { const queryParamsMap = getRequestParameterValue('map', state); @@ -212,3 +228,103 @@ export const storeDetailsInfoEpic = (action$, store) => ); }); }); +/** + * Intercept MAP_CONFIG_LOADED and update background layers thumbnail + * Epic is placed here to better intercept and update background layers thumbnail info, + * when loading context with map and to avoid race condition + * when loading plugins and map configuration + * @memberof epics.config + * @param {Observable} action$ stream of actions + * @param {object} store redux store + * @return {external:Observable} + */ +export const backgroundsListInitEpic = (action$) => + action$.ofType(MAP_CONFIG_LOADED) + .switchMap(({config}) => { + const backgrounds = config.map && config.map.backgrounds || []; + const backgroundLayers = (config.map && config.map.layers || []).filter(layer => layer.group === 'background'); + const layerUpdateActions = backgrounds.filter(background => !!background.thumbnail).map(background => { + const toBlob = (data) => { + const bytes = atob(data.split(',')[1]); + const mimeType = data.split(',')[0].split(':')[1].split(';')[0]; + let buffer = new ArrayBuffer(bytes.length); + let byteArray = new Uint8Array(buffer); + for (let i = 0; i < bytes.length; ++i) { + byteArray[i] = bytes.charCodeAt(i); + } + return URL.createObjectURL(new Blob([buffer], {type: mimeType})); + }; + return changeLayerProperties(background.id, {thumbURL: toBlob(background.thumbnail)}); + }); + const currentBackground = head(backgroundLayers.filter(layer => layer.visibility)); + return Observable.of( + ...layerUpdateActions.concat(createBackgroundsList(backgrounds)), + ...(currentBackground ? [setCurrentBackgroundLayer(currentBackground.id)] : []) + ); + }); + +export const storeDetailsInfoDashboardEpic = (action$, store) => + action$.ofType(DASHBOARD_LOADED) + .switchMap(() => { + const dashboardId = getDashboardId(store.getState()); + const isTutorialRunning = store.getState()?.tutorial?.run; + return !dashboardId + ? Observable.empty() + : Observable.fromPromise( + GeoStoreApi.getResourceAttributes(dashboardId) + ).switchMap((attributes) => { + let details = find(attributes, {name: 'details'}); + const detailsSettingsAttribute = find(attributes, {name: 'detailsSettings'}); + let detailsSettings = {}; + if (!details || details.value === EMPTY_RESOURCE_VALUE) { + return Observable.empty(); + } + + try { + detailsSettings = JSON.parse(detailsSettingsAttribute.value); + } catch (e) { + detailsSettings = {}; + } + + return Observable.of( + detailsLoaded(dashboardId, details.value, detailsSettings), + ...(detailsSettings.showAtStartup && !isTutorialRunning ? [openDetailsPanel()] : []) + ); + }); + }); +/** + * this epic is moved here because it needs to work also in dashboards + * Fetch all supported formats of a WMS service configured (infoFormats and imageFormats) + * Dispatches an action that sets the supported formats of the service. + * @param {Observable} action$ the actions triggered + * @param {object} getState store object + * @memberof epics.catalog + * @return {external:Observable} + */ +export const getSupportedFormatsEpic = (action$, {getState = ()=> {}} = {}) => + action$.ofType(FORMAT_OPTIONS_FETCH) + .filter((action)=> action.force || getFormatUrlUsedSelector(getState()) !== action?.url) + .switchMap(({url = ''} = {})=> { + return Observable.defer(() => getSupportedFormat(url, true)) + .switchMap((supportedFormats) => { + return Observable.of( + setSupportedFormats(supportedFormats, url), + showFormatError(supportedFormats.imageFormats.length === 0 && supportedFormats.infoFormats.length === 0) + ); + }) + .let( + wrapStartStop( + formatsLoading(true), + formatsLoading(false), + () => { + return Observable.of( + error({ + title: "layerProperties.format.error.title", + message: 'layerProperties.format.error.message' + }), + formatsLoading(false) + ); + } + ) + ); + }); diff --git a/web/client/epics/dashboard.js b/web/client/epics/dashboard.js index eaf304b836..5c5a85636d 100644 --- a/web/client/epics/dashboard.js +++ b/web/client/epics/dashboard.js @@ -6,6 +6,7 @@ * LICENSE file in the root directory of this source tree. */ import Rx from 'rxjs'; +import { mapValues, isObject, keys, isNil } from 'lodash'; import { NEW, INSERT, EDIT, OPEN_FILTER_EDITOR, editNewWidget, onEditorChange} from '../actions/widgets'; @@ -35,7 +36,7 @@ import { isLoggedIn } from '../selectors/security'; import { getEditingWidgetLayer, getEditingWidgetFilter, getWidgetFilterKey } from '../selectors/widgets'; import { pathnameSelector } from '../selectors/router'; import { download, readJson } from '../utils/FileUtils'; -import { createResource, updateResource, getResource } from '../api/persistence'; +import { createResource, updateResource, getResource, updateResourceAttribute } from '../api/persistence'; import { wrapStartStop } from '../observables/epics'; import { LOCATION_CHANGE, push } from 'connected-react-router'; import { convertDependenciesMappingForCompatibility } from "../utils/WidgetsUtils"; @@ -161,9 +162,32 @@ export const reloadDashboardOnLoginLogout = (action$) => // saving dashboard flow (both creation and update) export const saveDashboard = action$ => action$ .ofType(SAVE_DASHBOARD) - .exhaustMap(({resource} = {}) => - (!resource.id ? createResource(resource) : updateResource(resource)) - .switchMap(rid => Rx.Observable.of( + .exhaustMap(({resource} = {}) =>{ + // convert to json if attribute is an object + const attributesFixed = mapValues(resource.attributes, attr => { + if (isObject(attr)) { + let json = null; + try { + json = JSON.stringify(attr); + } catch (e) { + json = null; + } + return json; + } + return attr; + }); + // filter out invalid attributes + // thumbnails and details are handled separately(linked resources) + const validAttributesNames = keys(attributesFixed) + .filter(attrName => attrName !== 'thumbnail' && attrName !== 'details' && !isNil(attributesFixed[attrName])); + return Rx.Observable.forkJoin( + (!resource.id ? createResource(resource) : updateResource(resource))) + .switchMap(([rid]) => (validAttributesNames.length > 0 ? + Rx.Observable.forkJoin(validAttributesNames.map(attrName => updateResourceAttribute({ + id: rid, + name: attrName, + value: attributesFixed[attrName] + }))) : Rx.Observable.of([])) .switchMap(() => Rx.Observable.of( dashboardSaved(rid), resource.id ? triggerSave(false) : triggerSaveAs(false), !resource.id @@ -175,15 +199,14 @@ export const saveDashboard = action$ => action$ title: "saveDialog.saveSuccessTitle", message: "saveDialog.saveSuccessMessage" })).delay(!resource.id ? 1000 : 0) // delay to allow loading - ) - ) - .let(wrapStartStop( - dashboardLoading(true, "saving"), - dashboardLoading(false, "saving") )) - .catch( - ({ status, statusText, data, message, ...other } = {}) => Rx.Observable.of(dashboardSaveError(status ? { status, statusText, data } : message || other), dashboardLoading(false, "saving")) - ) + .let(wrapStartStop( + dashboardLoading(true, "saving"), + dashboardLoading(false, "saving") + ) + )); + }).catch( + ({ status, statusText, data, message, ...other } = {}) => Rx.Observable.of(dashboardSaveError(status ? { status, statusText, data } : message || other), dashboardLoading(false, "saving")) ); export const exportDashboard = action$ => action$ diff --git a/web/client/epics/details.js b/web/client/epics/details.js index 2e9e246f12..48ef7e7f4b 100644 --- a/web/client/epics/details.js +++ b/web/client/epics/details.js @@ -18,26 +18,31 @@ import { import { toggleControl, setControlProperty } from '../actions/controls'; import { - mapInfoDetailsUriFromIdSelector + mapIdSelector } from '../selectors/map'; +import { getDashboardId } from '../selectors/dashboard'; import GeoStoreApi from '../api/GeoStoreDAO'; import { getIdFromUri } from '../utils/MapUtils'; import { basicError } from '../utils/NotificationUtils'; import { VISUALIZATION_MODE_CHANGED } from '../actions/maptype'; +import { detailsUriSelector } from '../selectors/details'; export const fetchDataForDetailsPanel = (action$, store) => action$.ofType(OPEN_DETAILS_PANEL) .switchMap(() => { const state = store.getState(); - const detailsUri = mapInfoDetailsUriFromIdSelector(state); + const mapId = mapIdSelector(state); + const dashboardId = getDashboardId(state); + const detailsUri = detailsUriSelector(state); const detailsId = getIdFromUri(detailsUri); + const resourceId = dashboardId || mapId; return Rx.Observable.fromPromise(GeoStoreApi.getData(detailsId) .then(data => data)) .switchMap((details) => { return Rx.Observable.of( - updateDetails(details) + updateDetails(details, resourceId) ); }).startWith(toggleControl("details", "enabled")) .catch(() => { diff --git a/web/client/epics/tutorial.js b/web/client/epics/tutorial.js index 7563764dc6..48c046e408 100644 --- a/web/client/epics/tutorial.js +++ b/web/client/epics/tutorial.js @@ -27,9 +27,9 @@ import { CONTEXT_TUTORIALS } from '../actions/contextcreator'; import { LOCATION_CHANGE } from 'connected-react-router'; import { isEmpty, isArray, isObject } from 'lodash'; import { getApi } from '../api/userPersistedStorage'; -import { mapSelector } from '../selectors/map'; import {REDUCERS_LOADED} from "../actions/storemanager"; import { VISUALIZATION_MODE_CHANGED } from '../actions/maptype'; +import { detailsSettingsSelector } from '../selectors/details'; const findTutorialId = path => path.match(/\/(viewer)\/(\w+)\/(\d+)/) && path.replace(/\/(viewer)\/(\w+)\/(\d+)/, "$2") || path.match(/\/(\w+)\/(\d+)/) && path.replace(/\/(\w+)\/(\d+)/, "$1") @@ -168,7 +168,14 @@ export const getActionsFromStepEpic = (action$) => export const openDetailsPanelEpic = (action$, store) => action$.ofType(CLOSE_TUTORIAL) - .filter(() => mapSelector(store.getState())?.info?.detailsSettings?.showAtStartup ) + .filter(() => { + const state = store.getState(); + let detailsSettings = detailsSettingsSelector(state); + if (detailsSettings && typeof detailsSettings === 'string') { + detailsSettings = JSON.parse(detailsSettings); + } + return detailsSettings?.showAtStartup; + }) .switchMap( () => { return Rx.Observable.of(openDetailsPanel()); }); diff --git a/web/client/plugins/AddWidgetDashboard.jsx b/web/client/plugins/AddWidgetDashboard.jsx new file mode 100644 index 0000000000..ada8b63621 --- /dev/null +++ b/web/client/plugins/AddWidgetDashboard.jsx @@ -0,0 +1,77 @@ +/* + * Copyright 2023, 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 { createSelector } from 'reselect'; +import { connect } from 'react-redux'; +import ToolbarButton from '../components/misc/toolbar/ToolbarButton'; +import { buttonCanEdit, isDashboardEditing } from '../selectors/dashboard'; +import { setEditing } from '../actions/dashboard'; +import { createWidget } from '../actions/widgets'; +import { createPlugin } from '../utils/PluginsUtils'; + +class AddWidgetDashboard extends React.Component { + static propTypes = { + canEdit: PropTypes.bool, + editing: PropTypes.bool, + onAddWidget: PropTypes.func, + setEditing: PropTypes.func + } + + static defaultProps = { + editing: false, + canEdit: false + } + + render() { + if (!this.props.canEdit && !this.props.editing) return false; + return ( { + if (this.props.editing) this.props.setEditing(false); + else { + this.props.onAddWidget(); + } + }} + id={'ms-add-card-dashboard'} + tooltipPosition={'left'} + btnDefaultProps={{ tooltipPosition: 'right', className: 'square-button-md', bsStyle: this.props.editing ? 'primary' : 'tray' }}/>); + } +} + +const ConnectedAddWidget = connect( + createSelector( + buttonCanEdit, + isDashboardEditing, + ( edit, editing ) => ({ + canEdit: edit, + editing + }) + ), + { + onAddWidget: createWidget, + setEditing: setEditing + } +)(AddWidgetDashboard); + +export default createPlugin('AddWidgetDashboard', { + component: () => null, + containers: { + SidebarMenu: { + name: "AddWidgetDashboard", + position: 3, + tool: ConnectedAddWidget, + priority: 0 + } + } +}); diff --git a/web/client/plugins/Dashboard.jsx b/web/client/plugins/Dashboard.jsx index 6af04b1949..04d30e5f87 100644 --- a/web/client/plugins/Dashboard.jsx +++ b/web/client/plugins/Dashboard.jsx @@ -30,7 +30,8 @@ import { dashboardResource, isBrowserMobile, isDashboardLoading, - showConnectionsSelector + showConnectionsSelector, + isDashboardAvailable } from '../selectors/dashboard'; import { currentLocaleLanguageSelector, currentLocaleSelector } from '../selectors/locale'; import { isLocalizedLayerStylesEnabledSelector, localizedLayerStylesEnvSelector } from '../selectors/localizedLayerStyles'; @@ -46,6 +47,8 @@ import { import dashboardReducers from '../reducers/dashboard'; import dashboardEpics from '../epics/dashboard'; import widgetsEpics from '../epics/widgets'; +import GlobalSpinner from '../components/misc/spinners/GlobalSpinner/GlobalSpinner'; +import { createPlugin } from '../utils/PluginsUtils'; const WidgetsView = compose( connect( @@ -65,8 +68,9 @@ const WidgetsView = compose( localizedLayerStylesEnvSelector, getMaximizedState, currentLocaleSelector, + isDashboardAvailable, (resource, widgets, layouts, dependencies, selectionActive, editingWidget, groups, showGroupColor, loading, isMobile, currentLocaleLanguage, isLocalizedLayerStylesEnabled, - env, maximized, currentLocale) => ({ + env, maximized, currentLocale, isDashboardOpened) => ({ resource, loading, canEdit: isMobile ? !isMobile : resource && !!resource.canEdit, @@ -80,7 +84,8 @@ const WidgetsView = compose( language: isLocalizedLayerStylesEnabled ? currentLocaleLanguage : null, env, maximized, - currentLocale + currentLocale, + isDashboardOpened }) ), { editWidget, @@ -156,11 +161,13 @@ class DashboardPlugin extends React.Component { rowHeight: PropTypes.number, cols: PropTypes.object, minLayoutWidth: PropTypes.number, - widgetOpts: PropTypes.object + widgetOpts: PropTypes.object, + enableZoomInTblWidget: PropTypes.bool }; static defaultProps = { enabled: true, - minLayoutWidth: 480 + minLayoutWidth: 480, + enableZoomInTblWidget: true }; render() { return this.props.enabled @@ -170,6 +177,7 @@ class DashboardPlugin extends React.Component { rowHeight={this.props.rowHeight} cols={this.props.cols} minLayoutWidth={this.props.minLayoutWidth} + enableZoomInTblWidget={this.props.enableZoomInTblWidget} widgetOpts={this.props.widgetOpts} /> : null; @@ -177,14 +185,24 @@ class DashboardPlugin extends React.Component { } } -export default { - DashboardPlugin: withResizeDetector(DashboardPlugin), +export default createPlugin("Dashboard", { + component: withResizeDetector(DashboardPlugin), reducers: { dashboard: dashboardReducers, widgets: widgetsReducers }, + containers: { + SidebarMenu: { + name: "Dashboard-spinner", + alwaysVisible: true, + position: 2000, + tool: connect((state) => ({ + loading: isDashboardLoading(state) + }))(GlobalSpinner) + } + }, epics: { ...dashboardEpics, ...widgetsEpics } -}; +}); diff --git a/web/client/plugins/DashboardEditor.jsx b/web/client/plugins/DashboardEditor.jsx index e1238de7a9..cfd3391b90 100644 --- a/web/client/plugins/DashboardEditor.jsx +++ b/web/client/plugins/DashboardEditor.jsx @@ -12,22 +12,17 @@ import { createSelector } from 'reselect'; import { connect } from 'react-redux'; import PropTypes from 'prop-types'; import { createPlugin } from '../utils/PluginsUtils'; - - -import { dashboardHasWidgets, getWidgetsDependenciesGroups } from '../selectors/widgets'; -import { isDashboardEditing, showConnectionsSelector, isDashboardLoading, buttonCanEdit } from '../selectors/dashboard'; +import { isDashboardEditing, isDashboardLoading, isDashboardAvailable } from '../selectors/dashboard'; import { dashboardSelector, dashboardsLocalizedSelector } from './widgetbuilder/commons'; -import { createWidget, toggleConnection } from '../actions/widgets'; +import { toggleConnection } from '../actions/widgets'; import { setEditing, setEditorAvailable, triggerShowConnections } from '../actions/dashboard'; import withDashboardExitButton from './widgetbuilder/enhancers/withDashboardExitButton'; -import LoadingSpinner from '../components/misc/LoadingSpinner'; import WidgetTypeBuilder from './widgetbuilder/WidgetTypeBuilder'; import epics from '../epics/dashboard'; import dashboard from '../reducers/dashboard'; -import Toolbar from '../components/misc/toolbar/Toolbar'; const Builder = compose( @@ -38,51 +33,6 @@ const Builder = })), withDashboardExitButton )(WidgetTypeBuilder); -const EditorToolbar = compose( - connect( - createSelector( - showConnectionsSelector, - dashboardHasWidgets, - buttonCanEdit, - getWidgetsDependenciesGroups, - (showConnections, hasWidgets, edit, groups = []) => ({ - showConnections, - hasConnections: groups.length > 0, - hasWidgets, - canEdit: edit - }) - ), - { - onShowConnections: triggerShowConnections, - onAddWidget: createWidget - } - ), - withProps(({ - onAddWidget = () => { }, - hasWidgets, - canEdit, - hasConnections, - showConnections, - onShowConnections = () => { } - }) => ({ - buttons: [{ - glyph: 'plus', - tooltipId: 'dashboard.editor.addACardToTheDashboard', - bsStyle: 'primary', - visible: canEdit, - id: 'ms-add-card-dashboard', - onClick: () => onAddWidget() - }, - { - glyph: showConnections ? 'bulb-on' : 'bulb-off', - tooltipId: showConnections ? 'dashboard.editor.hideConnections' : 'dashboard.editor.showConnections', - bsStyle: showConnections ? 'success' : 'primary', - visible: !!hasWidgets && !!hasConnections || !canEdit, - onClick: () => onShowConnections(!showConnections) - }] - })) -)(Toolbar); - /** * Side toolbar that allows to edit dashboard widgets. @@ -98,7 +48,6 @@ class DashboardEditorComponent extends React.Component { static propTypes = { id: PropTypes.string, editing: PropTypes.bool, - loading: PropTypes.bool, limitDockHeight: PropTypes.bool, fluid: PropTypes.bool, zIndex: PropTypes.number, @@ -118,7 +67,6 @@ class DashboardEditorComponent extends React.Component { id: "dashboard-editor", editing: false, dockSize: 500, - loading: true, limitDockHeight: true, zIndex: 10000, fluid: false, @@ -141,10 +89,7 @@ class DashboardEditorComponent extends React.Component { return this.props.editing ?
this.props.setEditing(false)} catalog={this.props.catalog} />
- : (
- - {this.props.loading ? : null} -
); + : false; } } @@ -152,7 +97,8 @@ const Plugin = connect( createSelector( isDashboardEditing, isDashboardLoading, - (editing, loading) => ({ editing, loading }) + isDashboardAvailable, + (editing, isDashboardOpened) => ({ editing, isDashboardOpened }) ), { setEditing, onMount: () => setEditorAvailable(true), diff --git a/web/client/plugins/DashboardExport.jsx b/web/client/plugins/DashboardExport.jsx index 212dc5bc0f..6e8fd898fe 100644 --- a/web/client/plugins/DashboardExport.jsx +++ b/web/client/plugins/DashboardExport.jsx @@ -72,6 +72,18 @@ const DashboardExportPlugin = createPlugin('DashboardExport', { toggle: true, doNotHide: true }; + }, SidebarMenu: () => { + return { + name: "export", + position: 4, + text: , + tooltip: "mapExport.title", + icon: , + action: () => toggleControl('export'), + priority: 2, + toggle: true, + doNotHide: true + }; } } }); diff --git a/web/client/plugins/DashboardImport.jsx b/web/client/plugins/DashboardImport.jsx index 8fdc2304ae..17fcb6a81a 100644 --- a/web/client/plugins/DashboardImport.jsx +++ b/web/client/plugins/DashboardImport.jsx @@ -84,6 +84,18 @@ const DashboardImportPlugin = createPlugin('DashboardImport', { toggle: true, doNotHide: true }; + }, SidebarMenu: () => { + return { + name: "import", + position: 4, + tooltip: "mapImport.title", + text: , + icon: , + action: () => toggleControl('import'), + priority: 2, + toggle: true, + doNotHide: true + }; } } }); diff --git a/web/client/plugins/DashboardSave.jsx b/web/client/plugins/DashboardSave.jsx index 25cdf61230..4afffdfe94 100644 --- a/web/client/plugins/DashboardSave.jsx +++ b/web/client/plugins/DashboardSave.jsx @@ -37,7 +37,8 @@ const SaveBaseDialog = compose( onSave: saveDashboard }), withProps({ - category: "DASHBOARD" + category: "DASHBOARD", + enableDetails: true // to enable details in dashboard }), handleSaveModal )(Save); @@ -68,6 +69,23 @@ export const DashboardSave = createPlugin('DashboardSave', { text: , icon: , action: triggerSave.bind(null, true), + tooltip: "saveDialog.saveTooltip", + // display the BurgerMenu button only if the map can be edited + selector: createSelector( + isLoggedIn, + dashboardResource, + (loggedIn, {canEdit, id} = {}) => ({ + style: loggedIn && id && canEdit ? {} : { display: "none" }// the resource is new (no resource) or if present, is editable + }) + ) + }, + SidebarMenu: { + name: 'dashboardSave', + position: 30, + text: , + icon: , + action: triggerSave.bind(null, true), + tooltip: "saveDialog.saveTooltip", // display the BurgerMenu button only if the map can be edited selector: createSelector( isLoggedIn, @@ -120,6 +138,21 @@ export const DashboardSaveAs = createPlugin('DashboardSaveAs', { style: loggedIn ? {} : { display: "none" }// the resource is new (no resource) or if present, is editable }) ) + }, + SidebarMenu: { + name: 'dashboardSaveAs', + position: 31, + tooltip: "saveAs", + text: , + icon: , + action: triggerSaveAs.bind(null, true), + // always display on the BurgerMenu button if logged in + selector: createSelector( + isLoggedIn, + (loggedIn) => ({ + style: loggedIn ? {} : { display: "none" }// the resource is new (no resource) or if present, is editable + }) + ) } } }); diff --git a/web/client/plugins/DeleteDashboard.jsx b/web/client/plugins/DeleteDashboard.jsx index 7753b04523..929d2504ab 100644 --- a/web/client/plugins/DeleteDashboard.jsx +++ b/web/client/plugins/DeleteDashboard.jsx @@ -90,6 +90,22 @@ export default createPlugin('DeleteDashboard', { ), priority: 1, doNotHide: true + }, SidebarMenu: { + name: 'dashboardDelete', + tooltip: "dashboard.delete", + position: 300, + text: , + icon: , + action: setControl.bind(null, Controls.SHOW_DELETE, true), + selector: createSelector( + isLoggedIn, + dashboardResource, + (loggedIn, {canEdit, id} = {}) => ({ + style: loggedIn && (id && canEdit) ? {} : { display: "none" } // save is present only if the resource already exists and you can save + }) + ), + priority: 1, + doNotHide: true } } }); diff --git a/web/client/plugins/Details.jsx b/web/client/plugins/Details.jsx index 113d89dd7b..90d80beb09 100644 --- a/web/client/plugins/Details.jsx +++ b/web/client/plugins/Details.jsx @@ -17,8 +17,7 @@ import { NO_DETAILS_AVAILABLE } from "../actions/details"; -import { mapIdSelector, mapInfoDetailsUriFromIdSelector, mapInfoDetailsSettingsFromIdSelector } from '../selectors/map'; -import { detailsTextSelector } from '../selectors/details'; +import { detailsTextSelector, detailsUriSelector, detailsSettingsSelector } from '../selectors/details'; import { mapLayoutValuesSelector } from '../selectors/maplayout'; import DetailsViewer from '../components/resources/modals/fragments/DetailsViewer'; @@ -32,6 +31,7 @@ import { createPlugin } from '../utils/PluginsUtils'; import details from '../reducers/details'; import * as epics from '../epics/details'; import {createStructuredSelector} from "reselect"; +import { getDashboardId } from '../selectors/dashboard'; /** * Allow to show details for the map. @@ -51,7 +51,8 @@ const DetailsPlugin = ({ dockStyle, detailsText, showAsModal = false, - onClose = () => {} + onClose = () => {}, + isDashboard }) => { const viewer = (); - return showAsModal ? + return showAsModal && active ? } onClose={onClose}> {viewer} - : + : active && { + return getDashboardId(state) ? true : false; + }, active: state => get(state, "controls.details.enabled"), - dockStyle: state => mapLayoutValuesSelector(state, { height: true, right: true }, true), + dockStyle: state => { + const isDashbaord = getDashboardId(state); + let layoutValues = mapLayoutValuesSelector(state, { height: true, right: true }, true); + if (isDashbaord) { + layoutValues = { ...layoutValues, right: 0, height: '100%'}; + } + return layoutValues; + }, detailsText: detailsTextSelector, - showAsModal: state => mapInfoDetailsSettingsFromIdSelector(state)?.showAsModal + showAsModal: state => { + let detailsSettings = detailsSettingsSelector(state); + if (detailsSettings && typeof detailsSettings === 'string') detailsSettings = JSON.parse(detailsSettings); + return detailsSettings?.showAsModal; + } }), { onClose: closeDetailsPanel })(DetailsPlugin), @@ -99,8 +115,7 @@ export default createPlugin('Details', { icon: , action: openDetailsPanel, selector: (state) => { - const mapId = mapIdSelector(state); - const detailsUri = mapId && mapInfoDetailsUriFromIdSelector(state, mapId); + const detailsUri = detailsUriSelector(state); if (detailsUri) { return {}; } @@ -117,8 +132,7 @@ export default createPlugin('Details', { icon: , action: openDetailsPanel, selector: (state) => { - const mapId = mapIdSelector(state); - const detailsUri = mapId && mapInfoDetailsUriFromIdSelector(state, mapId); + const detailsUri = detailsUriSelector(state); if (detailsUri) { return {}; } @@ -134,8 +148,7 @@ export default createPlugin('Details', { icon: , action: openDetailsPanel, selector: (state) => { - const mapId = mapIdSelector(state); - const detailsUri = mapId && mapInfoDetailsUriFromIdSelector(state, mapId); + const detailsUri = detailsUriSelector(state); if (detailsUri) { return { bsStyle: state.controls.details && state.controls.details.enabled ? 'primary' : 'tray', diff --git a/web/client/plugins/MapConnectionDashboard.jsx b/web/client/plugins/MapConnectionDashboard.jsx new file mode 100644 index 0000000000..d1132ec1a5 --- /dev/null +++ b/web/client/plugins/MapConnectionDashboard.jsx @@ -0,0 +1,75 @@ +/* + * Copyright 2023, 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 { createSelector } from 'reselect'; +import { connect } from 'react-redux'; +import ToolbarButton from '../components/misc/toolbar/ToolbarButton'; +import { buttonCanEdit, showConnectionsSelector } from '../selectors/dashboard'; +import { dashboardHasWidgets, getWidgetsDependenciesGroups } from '../selectors/widgets'; +import { triggerShowConnections } from '../actions/dashboard'; +import { createPlugin } from '../utils/PluginsUtils'; + +class MapConnectionDashboard extends React.Component { + static propTypes = { + showConnections: PropTypes.bool, + canEdit: PropTypes.bool, + hasWidgets: PropTypes.bool, + hasConnections: PropTypes.bool, + onShowConnections: PropTypes.func + } + + static defaultProps = { + onShowConnections: () => {} + } + + render() { + const { showConnections, canEdit, hasConnections, hasWidgets, onShowConnections } = this.props; + if (!(!!hasWidgets && !!hasConnections || !canEdit)) return false; + return (onShowConnections(!showConnections)} + tooltipPosition={'left'} + id={'ms-map-connection-card-dashboard'} + btnDefaultProps={{ tooltipPosition: 'bottom', className: 'square-button-md', bsStyle: 'primary' }}/>); + } +} + +const ConnectedMapAddWidget = connect( + createSelector( + showConnectionsSelector, + dashboardHasWidgets, + buttonCanEdit, + getWidgetsDependenciesGroups, + (showConnections, hasWidgets, edit, groups = []) => ({ + showConnections, + hasConnections: groups.length > 0, + hasWidgets, + canEdit: edit + }) + ), + { + onShowConnections: triggerShowConnections + } +)(MapConnectionDashboard); + +export default createPlugin('MapConnectionDashboard', { + component: () => null, + containers: { + SidebarMenu: { + name: "MapConnectionDashboard", + tool: ConnectedMapAddWidget, + position: 3, + priority: 0 + } + } +}); diff --git a/web/client/plugins/MetadataExplorer.jsx b/web/client/plugins/MetadataExplorer.jsx index b296c0070d..84194d13af 100644 --- a/web/client/plugins/MetadataExplorer.jsx +++ b/web/client/plugins/MetadataExplorer.jsx @@ -73,7 +73,8 @@ import { formatsLoadingSelector, getSupportedFormatsSelector, getSupportedGFIFormatsSelector, - getNewServiceStatusSelector + getNewServiceStatusSelector, + showFormatErrorSelector } from '../selectors/catalog'; import { layersSelector } from '../selectors/layers'; import { currentLocaleSelector, currentMessagesSelector } from '../selectors/locale'; @@ -87,6 +88,7 @@ export const DEFAULT_ALLOWED_PROVIDERS = ["OpenStreetMap", "OpenSeaMap", "Stamen const metadataExplorerSelector = createStructuredSelector({ searchOptions: searchOptionsSelector, + showFormatError: showFormatErrorSelector, result: resultSelector, loadingError: loadingErrorSelector, selectedService: selectedServiceSelector, diff --git a/web/client/plugins/SidebarMenu.jsx b/web/client/plugins/SidebarMenu.jsx index 538b93b823..32bb6dcdf6 100644 --- a/web/client/plugins/SidebarMenu.jsx +++ b/web/client/plugins/SidebarMenu.jsx @@ -23,7 +23,7 @@ import {createPlugin} from "../utils/PluginsUtils"; import sidebarMenuReducer from "../reducers/sidebarmenu"; import './sidebarmenu/sidebarmenu.less'; -import {lastActiveToolSelector, sidebarIsActiveSelector} from "../selectors/sidebarmenu"; +import {lastActiveToolSelector, sidebarIsActiveSelector, isSidebarWithFullHeight} from "../selectors/sidebarmenu"; import {setLastActiveItem} from "../actions/sidebarmenu"; import Message from "../components/I18N/Message"; @@ -40,7 +40,7 @@ class SidebarMenu extends React.Component { sidebarWidth: PropTypes.number, state: PropTypes.object, setLastActiveItem: PropTypes.func, - lastActiveTool: PropTypes.oneOfType([PropTypes.string, PropTypes.bool]) + isSidebarFullHeight: PropTypes.bool }; static contextTypes = { @@ -60,7 +60,8 @@ class SidebarMenu extends React.Component { stateSelector: 'sidebarMenu', tool: SidebarElement, toolCfg: {}, - sidebarWidth: 40 + sidebarWidth: 40, + isSidebarFullHeight: false }; constructor() { @@ -90,7 +91,8 @@ class SidebarMenu extends React.Component { } return prev; }, []).length > 0 : false; - return newSize || newItems || newVisibleItems || newHeight || burgerMenuState || markedAsInactive; + const nextIsSideBarFullHeight = !this.props.isSidebarFullHeight && nextProps.isSidebarFullHeight; + return newSize || newItems || newVisibleItems || newHeight || burgerMenuState || markedAsInactive || nextIsSideBarFullHeight; } componentDidUpdate(prevProps) { @@ -229,7 +231,7 @@ class SidebarMenu extends React.Component { render() { return this.state.hidden ? false : ( -
+
{ ({ height }) => state, state => lastActiveToolSelector(state), state => mapLayoutValuesSelector(state, {dockSize: true, bottom: true, height: true}), - sidebarIsActiveSelector -], (state, lastActiveTool, style, isActive) => ({ + sidebarIsActiveSelector, + isSidebarWithFullHeight +], (state, lastActiveTool, style, isActive, isSidebarFullHeight ) => ({ style, lastActiveTool, state, - isActive + isActive, + isSidebarFullHeight })); /** diff --git a/web/client/plugins/Widgets.jsx b/web/client/plugins/Widgets.jsx index 27dc3e3549..e69c8015eb 100644 --- a/web/client/plugins/Widgets.jsx +++ b/web/client/plugins/Widgets.jsx @@ -22,7 +22,8 @@ import { getFloatingWidgetsLayout, getMaximizedState, getVisibleFloatingWidgets, - isTrayEnabled + isTrayEnabled, + getTblWidgetZoomLoader } from '../selectors/widgets'; import { changeLayout, @@ -56,7 +57,8 @@ compose( (state) => mapLayoutValuesSelector(state, { right: true}), state => state.browser && state.browser.mobile, getFloatingWidgets, - (id, widgets, layouts, maximized, dependencies, mapLayout, isMobileAgent, dropdownWidgets) => ({ + getTblWidgetZoomLoader, + (id, widgets, layouts, maximized, dependencies, mapLayout, isMobileAgent, dropdownWidgets, recordZoomLoading) => ({ id, widgets, layouts, @@ -64,7 +66,8 @@ compose( dependencies, mapLayout, isMobileAgent, - dropdownWidgets + dropdownWidgets, + recordZoomLoading }) ), { editWidget, @@ -273,10 +276,12 @@ compose( class Widgets extends React.Component { static propTypes = { - enabled: PropTypes.bool + enabled: PropTypes.bool, + enableZoomInTblWidget: PropTypes.bool }; static defaultProps = { - enabled: true + enabled: true, + enableZoomInTblWidget: true }; render() { return this.props.enabled ? : null; diff --git a/web/client/plugins/__tests__/DashboardSave-test.jsx b/web/client/plugins/__tests__/DashboardSave-test.jsx index 3b147745b1..fa3ceaa9be 100644 --- a/web/client/plugins/__tests__/DashboardSave-test.jsx +++ b/web/client/plugins/__tests__/DashboardSave-test.jsx @@ -29,7 +29,7 @@ describe('DashboardSave Plugins (DashboardSave, DashboardSaveAs)', () => { document.body.innerHTML = ''; setTimeout(done); }); - describe('DashboardSave', () => { + describe('DashboardSave within burger menu', () => { const DUMMY_ACTION = { type: "DUMMY_ACTION" }; it('hidden by default, visibility of the button', () => { const { Plugin, containers } = getPluginForTest(DashboardSave, stateMocker(DUMMY_ACTION), { @@ -54,7 +54,32 @@ describe('DashboardSave Plugins (DashboardSave, DashboardSaveAs)', () => { expect(document.getElementsByClassName('modal-fixed').length).toBe(1); }); }); - describe('DashboardSaveAs', () => { + describe('DashboardSave within sidebar menu', () => { + const DUMMY_ACTION = { type: "DUMMY_ACTION" }; + it('hidden by default, visibility of the button', () => { + const { Plugin, containers } = getPluginForTest(DashboardSave, stateMocker(DUMMY_ACTION), { + SidebarMenuPlugin: {} + }); + // check container for burger menu + expect(Object.keys(containers)).toContain('SidebarMenu'); + ReactDOM.render(, document.getElementById("container")); + expect(document.getElementsByClassName('modal-fixed').length).toBe(0); + // check log-in logout properties selector for button in burger menu + // hide when not logged in + expect(containers.SidebarMenu.selector({ security: {} }).style.display).toBe("none"); + // hide when logged in but without resource selected + expect(containers.SidebarMenu.selector({security: {user: {}}}).style.display).toBe("none"); + // hide if you don't have permissions + expect(containers.SidebarMenu.selector({ security: { user: {} }, dashboard: { resource: { id: 1234, canEdit: false } } }).style.display ).toBe("none"); + }); + it('show when control is set to "save"', () => { + const storeState = stateMocker(DUMMY_ACTION, triggerSave(true)); + const { Plugin } = getPluginForTest(DashboardSave, storeState); + ReactDOM.render(, document.getElementById("container")); + expect(document.getElementsByClassName('modal-fixed').length).toBe(1); + }); + }); + describe('DashboardSaveAs within burger menu', () => { const DUMMY_ACTION = { type: "DUMMY_ACTION" }; it('hidden by default, visibility of the button', () => { const { Plugin, containers } = getPluginForTest(DashboardSaveAs, stateMocker(DUMMY_ACTION), { @@ -92,4 +117,43 @@ describe('DashboardSave Plugins (DashboardSave, DashboardSaveAs)', () => { expect(inputEl.value).toBe('f'); }); }); + + describe('DashboardSaveAs within sidebar menu', () => { + const DUMMY_ACTION = { type: "DUMMY_ACTION" }; + it('hidden by default, visibility of the button', () => { + const { Plugin, containers } = getPluginForTest(DashboardSaveAs, stateMocker(DUMMY_ACTION), { + SidebarMenuPlugin: {} + }); + // check container for burger menu + expect(Object.keys(containers)).toContain('SidebarMenu'); + ReactDOM.render(, document.getElementById("container")); + expect(document.getElementsByClassName('modal-fixed').length).toBe(0); + // check log-in logout properties selector for button in burger menu + // hide when not logged in + expect(containers.SidebarMenu.selector({ security: {} }).style.display).toBe("none"); + // always show when user logged in + expect(containers.SidebarMenu.selector({ security: { user: {} } }).style.display).toNotExist(); + // show if resource is available for clone + expect(containers.SidebarMenu.selector({ + security: { user: {} }, + geostory: { resource: { id: 1234, canEdit: false } } + }).style.display).toNotExist(); + }); + it('show when control is set to "saveAs"', () => { + const { Plugin } = getPluginForTest(DashboardSaveAs, stateMocker(DUMMY_ACTION, triggerSaveAs(true))); + ReactDOM.render(, document.getElementById("container")); + expect(document.getElementsByClassName('modal-fixed').length).toBe(1); + }); + it('title is editable', () => { + const { Plugin } = getPluginForTest(DashboardSaveAs, stateMocker(DUMMY_ACTION, triggerSaveAs(true))); + ReactDOM.render(, document.getElementById("container")); + const modal = document.getElementsByClassName('modal-fixed')[0]; + expect(modal).toExist(); + const inputEl = modal.getElementsByTagName('input')[1]; + expect(inputEl).toExist(); + inputEl.value = 'f'; + TestUtils.Simulate.change(inputEl); + expect(inputEl.value).toBe('f'); + }); + }); }); diff --git a/web/client/plugins/__tests__/SidebarMenu-test.jsx b/web/client/plugins/__tests__/SidebarMenu-test.jsx index 331c04d3a2..f0b4c7f295 100644 --- a/web/client/plugins/__tests__/SidebarMenu-test.jsx +++ b/web/client/plugins/__tests__/SidebarMenu-test.jsx @@ -39,4 +39,23 @@ describe('SidebarMenu Plugin', () => { const elements = document.querySelectorAll('#mapstore-sidebar-menu > button, #mapstore-sidebar-menu #extra-items + .dropdown-menu li'); expect(elements.length).toBe(2); }); + + it('sidebar menu with full height', () => { + 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/featuregrid/gridTools.jsx b/web/client/plugins/featuregrid/gridTools.jsx index f440c9a2f7..6ce1314b8d 100644 --- a/web/client/plugins/featuregrid/gridTools.jsx +++ b/web/client/plugins/featuregrid/gridTools.jsx @@ -21,5 +21,8 @@ export default [{ : }> - + , + tableWidgetFormatter: }> + + }]; diff --git a/web/client/plugins/sidebarmenu/sidebarmenu.less b/web/client/plugins/sidebarmenu/sidebarmenu.less index 04fdf249a6..f9aabcc145 100644 --- a/web/client/plugins/sidebarmenu/sidebarmenu.less +++ b/web/client/plugins/sidebarmenu/sidebarmenu.less @@ -35,3 +35,8 @@ } } } + +#mapstore-sidebar-menu-container.fullHeightSideBar{ + position: relative; + max-height: 100% !important; +} \ No newline at end of file diff --git a/web/client/plugins/widgetbuilder/enhancers/catalogEditorEnhancer.js b/web/client/plugins/widgetbuilder/enhancers/catalogEditorEnhancer.js index 5c0c56e7a6..d36da4f0f7 100644 --- a/web/client/plugins/widgetbuilder/enhancers/catalogEditorEnhancer.js +++ b/web/client/plugins/widgetbuilder/enhancers/catalogEditorEnhancer.js @@ -12,6 +12,16 @@ import { dashboardSetSelectedService, setDashboardCatalogMode, dashboardDeleteSe import { dashboardServicesSelector, selectedDashboardServiceSelector, dashboardCatalogModeSelector, dashboardIsNewServiceSelector } from '../../../selectors/dashboard'; import { error } from '../../../actions/notifications'; +import { + getSupportedFormatsInNewServiceSelector, + getSupportedGFIFormatsSelector, + formatsLoadingSelector, + showFormatErrorSelector +} from '../../../selectors/catalog'; +import { + formatOptionsFetch +} from '../../../actions/catalog'; + import CatalogServiceEditor from '../CatalogServiceEditor'; export const catalogEditorEnhancer = compose( @@ -20,12 +30,17 @@ export const catalogEditorEnhancer = compose( marginRight: "5px"}}), connect((state) => ({ mode: dashboardCatalogModeSelector(state), + showFormatError: showFormatErrorSelector(state), dashboardServices: dashboardServicesSelector(state), + formatsLoading: formatsLoadingSelector(state), + infoFormatOptions: getSupportedGFIFormatsSelector(state), + formatOptions: getSupportedFormatsInNewServiceSelector(state), dashboardSelectedService: selectedDashboardServiceSelector(state), isNew: dashboardIsNewServiceSelector(state) }), { onChangeSelectedService: dashboardSetSelectedService, onChangeCatalogMode: setDashboardCatalogMode, + onFormatOptionsFetch: formatOptionsFetch, onDeleteService: dashboardDeleteService, onAddService: updateDashboardService, error: error diff --git a/web/client/product/plugins.js b/web/client/product/plugins.js index ceef399b86..367494a474 100644 --- a/web/client/product/plugins.js +++ b/web/client/product/plugins.js @@ -158,7 +158,9 @@ export const plugins = { WidgetsTrayPlugin: toModulePlugin('WidgetsTray', () => import(/* webpackChunkName: 'plugins/widgetsTray' */ '../plugins/WidgetsTray')), ZoomAllPlugin: toModulePlugin('ZoomAll', () => import(/* webpackChunkName: 'plugins/zoomAll' */ '../plugins/ZoomAll')), ZoomInPlugin: toModulePlugin('ZoomIn', () => import(/* webpackChunkName: 'plugins/zoomIn' */ '../plugins/ZoomIn')), - ZoomOutPlugin: toModulePlugin('ZoomOut', () => import(/* webpackChunkName: 'plugins/zoomOut' */ '../plugins/ZoomOut')) + ZoomOutPlugin: toModulePlugin('ZoomOut', () => import(/* webpackChunkName: 'plugins/zoomOut' */ '../plugins/ZoomOut')), + AddWidgetDashboardPlugin: toModulePlugin('AddWidgetDashboard', () => import(/* webpackChunkName: 'plugins/AddWidgetDashboard' */ '../plugins/AddWidgetDashboard')), + MapConnectionDashboardPlugin: toModulePlugin('MapConnectionDashboard', () => import(/* webpackChunkName: 'plugins/MapConnectionDashboard' */ '../plugins/MapConnectionDashboard')) }; const pluginsDefinition = { diff --git a/web/client/reducers/__tests__/config-test.js b/web/client/reducers/__tests__/config-test.js index 4b0ab8e061..785337cb53 100644 --- a/web/client/reducers/__tests__/config-test.js +++ b/web/client/reducers/__tests__/config-test.js @@ -75,7 +75,7 @@ describe('Test the mapConfig reducer', () => { expect(state.map.info).toExist(); expect(state.map.info.canEdit).toBe(true); }); - it('DETAILS_LOADED', () => { + it('DETAILS_LOADED Map', () => { const detailsUri = "details/uri"; var state = mapConfig({ map: { @@ -83,11 +83,24 @@ describe('Test the mapConfig reducer', () => { mapId: 1 } } - }, {type: DETAILS_LOADED, mapId: 1, detailsUri}); + }, {type: DETAILS_LOADED, id: 1, detailsUri}); expect(state.map).toExist(); expect(state.map.info).toExist(); expect(state.map.info.details).toBe(detailsUri); }); + it('DETAILS_LOADED Dahboard', () => { + const detailsUri = "details/uri"; + var state = mapConfig({ + dashboard: { + resource: { + id: "1", attributes: {} + } + } + }, {type: DETAILS_LOADED, id: "1", detailsUri}); + expect(state.dashboard).toExist(); + expect(state.dashboard.resource).toExist(); + expect(state.dashboard.resource.attributes.details).toBe(detailsUri); + }); it('map created', () => { expect(mapConfig({ diff --git a/web/client/reducers/catalog.js b/web/client/reducers/catalog.js index bc6b2c4129..ccb842e725 100644 --- a/web/client/reducers/catalog.js +++ b/web/client/reducers/catalog.js @@ -30,6 +30,7 @@ import { TOGGLE_TEMPLATE, TOGGLE_ADVANCED_SETTINGS, FORMAT_OPTIONS_LOADING, + SHOW_FORMAT_ERROR, SET_FORMAT_OPTIONS, NEW_SERVICE_STATUS } from '../actions/catalog'; @@ -60,6 +61,7 @@ function catalog(state = { }, delayAutoSearch: 1000, loading: false, + showFormatError: false, pageSize: 4, services: {}, selectedService: "", @@ -112,6 +114,7 @@ function catalog(state = { newService: action.isNew ? emptyService : assign({}, state.services && state.services[state.selectedService || ""] || {}, {oldService: state.selectedService || ""}), mode: action.mode, result: null, + showFormatError: false, loadingError: null, layerError: null}); case MAP_CONFIG_LOADED: { @@ -133,7 +136,7 @@ function catalog(state = { case CHANGE_TITLE: return set("newService.title", action.title, state); case CHANGE_URL: - return set("newService.url", action.url, state); + return set("newService.url", action.url, set("showFormatError", false, state)); case CHANGE_SERVICE_FORMAT: return set("newService.format", action.format, state); case CHANGE_TYPE: { @@ -211,6 +214,9 @@ function catalog(state = { case FORMAT_OPTIONS_LOADING: { return set("formatsLoading", action.loading, state); } + case SHOW_FORMAT_ERROR: { + return set("showFormatError", action.status, state); + } case SET_FORMAT_OPTIONS: { return set("newService.supportedFormats", action.formats, set("newService.formatUrlUsed", action.url, state)); } diff --git a/web/client/reducers/config.js b/web/client/reducers/config.js index 8119591743..d8b648ae84 100644 --- a/web/client/reducers/config.js +++ b/web/client/reducers/config.js @@ -110,8 +110,9 @@ function mapConfig(state = null, action) { } return state; case DETAILS_LOADED: + let dashboardResource = state.dashboard?.resource; map = state && state.map && state.map.present ? state.map.present : state && state.map; - if (map && map.mapId.toString() === action.mapId.toString()) { + if (map && map.mapId.toString() === action.id.toString()) { map = assign({}, map, { info: assign({}, map.info, { @@ -120,6 +121,17 @@ function mapConfig(state = null, action) { }) }); return assign({}, state, {map: map}); + } else if (dashboardResource && dashboardResource.id === action.id.toString()) { + dashboardResource = assign({}, dashboardResource, { + attributes: + assign({}, dashboardResource.attributes, { + details: action.detailsUri, + detailsSettings: action.detailsSettings + }) + }); + return assign({}, state, {dashboard: { + ...state.dashboard, resource: dashboardResource + }}); } return state; case MAP_CREATED: { diff --git a/web/client/reducers/details.js b/web/client/reducers/details.js index 630b355bf8..a071f9b721 100644 --- a/web/client/reducers/details.js +++ b/web/client/reducers/details.js @@ -13,7 +13,7 @@ import { const details = (state = {}, action) => { switch (action.type) { case UPDATE_DETAILS: { - return {...state, detailsText: action.detailsText}; + return {...state, detailsText: action.detailsText, id: action.id}; } default: return state; diff --git a/web/client/selectors/__tests__/dashboard-test.js b/web/client/selectors/__tests__/dashboard-test.js index 1c7c2a7f11..8e9048c1d3 100644 --- a/web/client/selectors/__tests__/dashboard-test.js +++ b/web/client/selectors/__tests__/dashboard-test.js @@ -22,7 +22,10 @@ import { selectedDashboardServiceSelector, dashboardCatalogModeSelector, dashboardIsNewServiceSelector, - dashboardSaveServiceSelector + dashboardSaveServiceSelector, + dashboardResourceInfoSelector, + dashbaordInfoDetailsUriFromIdSelector, + dashboardInfoDetailsSettingsFromIdSelector } from '../dashboard'; describe('dashboard selectors', () => { @@ -122,5 +125,28 @@ describe('dashboard selectors', () => { it("getDashboardId should return undefined in case resource does not exists", () => { expect(getDashboardId({dashboard: {resource: {}}})).toBe(undefined); }); - + it("test dashboardResourceInfoSelector", () => { + const resource = {}; + expect(dashboardResourceInfoSelector({dashboard: { + resource: resource + }})).toBe(resource); + }); + it("test dashbaordInfoDetailsUriFromIdSelector", () => { + expect(dashbaordInfoDetailsUriFromIdSelector({dashboard: { + resource: { + attributes: { + details: "Details" + } + } + }})).toBe("Details"); + }); + it("test dashboardInfoDetailsSettingsFromIdSelector", () => { + expect(dashboardInfoDetailsSettingsFromIdSelector({dashboard: { + resource: { + attributes: { + detailsSettings: "detailsSettings" + } + } + }})).toBe("detailsSettings"); + }); }); diff --git a/web/client/selectors/catalog.js b/web/client/selectors/catalog.js index 09d3f954b9..4f38c24f93 100644 --- a/web/client/selectors/catalog.js +++ b/web/client/selectors/catalog.js @@ -56,9 +56,12 @@ export const catalogSearchInfoSelector = createStructuredSelector({ projection: projectionSelector }); export const formatsLoadingSelector = (state) => get(state, "catalog.formatsLoading", false); +export const getSupportedFormatsInNewServiceSelector = (state) => get(state, "catalog.newService.supportedFormats.imageFormats", []); export const getSupportedFormatsSelector = (state) => modeSelector(state) === 'edit' - ? get(state, "catalog.newService.supportedFormats.imageFormats", []) + ? getSupportedFormatsInNewServiceSelector(state) : selectedCatalogSelector(state)?.supportedFormats?.imageFormats || []; + export const getSupportedGFIFormatsSelector = (state) => get(state, "catalog.newService.supportedFormats.infoFormats", getDefaultSupportedGetFeatureInfoFormats()); export const getFormatUrlUsedSelector = (state) => get(state, "catalog.newService.formatUrlUsed", ''); export const getNewServiceStatusSelector = (state) => get(state, "catalog.isNewServiceAdded", false); +export const showFormatErrorSelector = (state) => get(state, "catalog.showFormatError", false); diff --git a/web/client/selectors/dashboard.js b/web/client/selectors/dashboard.js index 967c64e883..5f565ce8be 100644 --- a/web/client/selectors/dashboard.js +++ b/web/client/selectors/dashboard.js @@ -6,6 +6,7 @@ * LICENSE file in the root directory of this source tree. */ import { createSelector } from 'reselect'; +import {get} from 'lodash'; import { pathnameSelector } from './router'; export const getDashboardId = state => state?.dashboard?.resource?.id; @@ -27,3 +28,7 @@ export const selectedDashboardServiceSelector = state => state && state.dashboar export const dashboardCatalogModeSelector = state => state && state.dashboard && state.dashboard.mode || "view"; export const dashboardIsNewServiceSelector = state => state.dashboard?.isNew || false; export const dashboardSaveServiceSelector = state => state.dashboard?.saveServiceLoading || false; +export const dashboardResourceInfoSelector = state => get(state, "dashboard.resource"); +export const dashbaordInfoDetailsUriFromIdSelector = state => state?.dashboard?.resource?.attributes?.details; +export const dashboardInfoDetailsSettingsFromIdSelector = state => get(dashboardResource(state), "attributes.detailsSettings"); + diff --git a/web/client/selectors/details.js b/web/client/selectors/details.js index 68241b5c71..98d51ac963 100644 --- a/web/client/selectors/details.js +++ b/web/client/selectors/details.js @@ -6,4 +6,23 @@ * LICENSE file in the root directory of this source tree. */ +import { dashboardInfoDetailsSettingsFromIdSelector, getDashboardId, dashbaordInfoDetailsUriFromIdSelector } from "./dashboard"; +import { mapIdSelector, mapInfoDetailsSettingsFromIdSelector, mapInfoDetailsUriFromIdSelector } from "./map"; + export const detailsTextSelector = state => state?.details?.detailsText; + +export const detailsUriSelector = state => { + const mapId = mapIdSelector(state); + const dashboardId = getDashboardId(state); + // todo: this is now for map and dashboard only, in the future if something else needs to use this like geostory, an additional contional should be added + let detailsUri = dashboardId && dashbaordInfoDetailsUriFromIdSelector(state, dashboardId) || mapId && mapInfoDetailsUriFromIdSelector(state, mapId); + return detailsUri; +}; + +export const detailsSettingsSelector = state => { + const mapId = mapIdSelector(state); + const dashboardId = getDashboardId(state); + // todo: this is now for map and dashboard only, in the future if something else needs to use this like geostory, an additional contional should be added + let detailsSettings = dashboardId && dashboardInfoDetailsSettingsFromIdSelector(state, dashboardId) || mapId && mapInfoDetailsSettingsFromIdSelector(state, mapId); + return detailsSettings; +}; diff --git a/web/client/selectors/sidebarmenu.js b/web/client/selectors/sidebarmenu.js index f93cf8488e..a0bdc08951 100644 --- a/web/client/selectors/sidebarmenu.js +++ b/web/client/selectors/sidebarmenu.js @@ -1,5 +1,11 @@ import {get} from "lodash"; +import { isDashboardAvailable } from "./dashboard"; export const lastActiveToolSelector = (state) => get(state, "sidebarmenu.lastActiveItem", false); export const sidebarIsActiveSelector = (state) => get(state, 'controls.sidebarMenu.enabled', false); +export const isSidebarWithFullHeight = (state) =>{ + // here It is just for dashboard, but in the future if sidebar with full height is needed for anythinf else put here + return isDashboardAvailable(state); +}; + diff --git a/web/client/selectors/widgets.js b/web/client/selectors/widgets.js index d4bba4b428..97f44fc81e 100644 --- a/web/client/selectors/widgets.js +++ b/web/client/selectors/widgets.js @@ -185,3 +185,8 @@ export const getWidgetFilterKey = (state) => { // Set chart key if editor widget type is chart return selectedChartId ? `charts[${selectedChartId}].filter` : "filter"; }; + +export const getTblWidgetZoomLoader = state => { + let tableWidgets = (getFloatingWidgets(state) || []).filter(({ widgetType } = {}) => widgetType === "table"); + return tableWidgets?.find(t=>t.dependencies?.zoomLoader) ? true : false; +}; diff --git a/web/client/themes/default/less/catalog.less b/web/client/themes/default/less/catalog.less index 44975d0b4c..06c4a539c8 100644 --- a/web/client/themes/default/less/catalog.less +++ b/web/client/themes/default/less/catalog.less @@ -15,10 +15,18 @@ // ************** // Layout // ************** +.ms2-border-layout-body.catalog { + .form-group-flex { + padding: 0; + } +} .catalog-panel { position: relative; width: 100%; height: 100%; + .form-group-flex { + padding: 0; + } .panel-body { padding: 0; position: absolute; diff --git a/web/client/themes/default/less/common.less b/web/client/themes/default/less/common.less index 41a1512f0d..92893c09a4 100644 --- a/web/client/themes/default/less/common.less +++ b/web/client/themes/default/less/common.less @@ -76,6 +76,23 @@ .background-color-var(@theme-vars[main-bg]); } } + .form-group-flex { + &.has-success { + .Select-control { + .border-color-var(@theme-vars[success], true); + } + } + &.has-error { + .Select-control { + .border-color-var(@theme-vars[danger], true); + } + } + &.has-warning { + .Select-control { + .border-color-var(@theme-vars[warning], true); + } + } + } } // ************** @@ -396,3 +413,24 @@ div#sync-popover.popover { float: unset; } + +.form-group-flex { + display: flex; + align-items: center; + padding: 0 8px; + .checkbox, + .control-label { + flex: 1; + font-weight: normal; + &.strong{ + font-weight: bold; + } + } + .input-group { + flex: 1; + } + input { + z-index: 0; + } + margin-bottom: 8px; +} diff --git a/web/client/themes/default/less/dashboard.less b/web/client/themes/default/less/dashboard.less index e35a1ff7f3..1ebe1ab335 100644 --- a/web/client/themes/default/less/dashboard.less +++ b/web/client/themes/default/less/dashboard.less @@ -28,7 +28,6 @@ #mapstore-navbar-container { margin-bottom: 0; - z-index: 100; } .ms2-border-layout-content { diff --git a/web/client/themes/default/less/details.less b/web/client/themes/default/less/details.less index 9223bb2738..adb0ab86d6 100644 --- a/web/client/themes/default/less/details.less +++ b/web/client/themes/default/less/details.less @@ -89,3 +89,8 @@ text-align: inherit; } } + +// issue in react-dock fixed left with a fixed value and it can't be overrided +#details-container.leftZeroPanel> div.ms-side-panel> div> div{ + left: auto !important; +} diff --git a/web/client/themes/default/less/navbar.less b/web/client/themes/default/less/navbar.less index 055be3c0ce..66f597ca3f 100644 --- a/web/client/themes/default/less/navbar.less +++ b/web/client/themes/default/less/navbar.less @@ -59,7 +59,7 @@ ol { #mapstore-navbar-container { height: @square-btn-size; - + z-index: 1032; .nav { &.pull-left { display: flex; diff --git a/web/client/themes/default/less/react-select.less b/web/client/themes/default/less/react-select.less index d1787121d4..c4d1de2c1e 100644 --- a/web/client/themes/default/less/react-select.less +++ b/web/client/themes/default/less/react-select.less @@ -195,3 +195,7 @@ margin-top: 3px; } } + +.Select.is-open { + z-index: 20; +} \ No newline at end of file diff --git a/web/client/themes/default/less/wizard.less b/web/client/themes/default/less/wizard.less index df81921d05..a51e44545d 100644 --- a/web/client/themes/default/less/wizard.less +++ b/web/client/themes/default/less/wizard.less @@ -179,4 +179,3 @@ } } } - diff --git a/web/client/translations/data.de-DE.json b/web/client/translations/data.de-DE.json index 4358063988..ccc7eeccb5 100644 --- a/web/client/translations/data.de-DE.json +++ b/web/client/translations/data.de-DE.json @@ -57,8 +57,8 @@ "404": "Übersetzungsdatei nicht gefunden" }, "details": { - "title": "Infos zu dieser Karte", - "tooltip": "Infos zu dieser Karte" + "title": "Infos zu dieser Inhalt", + "tooltip": "Infos zu dieser Inhalt" }, "showEmptyMessageGFI": "Nachricht mit leeren Ergebnissen anzeigen", "remove": "Löschen", @@ -116,6 +116,7 @@ "heightOffset": "Höhenversatz (m)", "wmsLayerTileSize": "Kachelgröße (WMS)", "serverType": "Servertyp", + "formatError": "Es war nicht möglich, Format und Informationsblattformat vom konfigurierten Dienst abzurufen. Wahrscheinlich verwenden Sie einen No-Vendor-Dienst und dieser wird nicht unterstützt (z. B. GeoNetwork).", "serverTypeOption": { "noVendor": "No Vendor", "geoserver": "GeoServer" @@ -156,6 +157,21 @@ "scale": "Maßstab", "resolution": "Auflösung" }, + "3dTiles": { + "format": "Format", + "3dModel": "3D-Modell", + "pointCloud": "Punktwolke", + "pointCloudShading": { + "title": "Visualisierungsoptionen", + "attenuationInfo": "Punktabschwächung basierend auf der Entfernung vom aktuellen Blickpunkt durchführen", + "attenuation": "Dämpfung aktivieren", + "maximumAttenuation": "Maximale Dämpfung", + "eyeDomeLightingInfo": "Dies ermöglicht eine Augenkuppelbeleuchtung, um die Visualisierung der Punktwolke zu verbessern. Dies stellt nicht das echte Sonnenlicht dar", + "eyeDomeLighting": "Beleuchtung aktivieren", + "eyeDomeLightingStrength": "Lichtstärke", + "eyeDomeLightingRadius": "Beleuchtungsradius" + } + }, "tooltip": { "label": "QuickInfo", "title": "Titel", @@ -182,6 +198,8 @@ }, "format": { "title": "Format", + "tile": "Fliese", + "information": "Infoblatt", "refresh": "Unterstützte Formate abrufen", "loading": "Wird geladen...", "noOption": "Keine Option", @@ -222,7 +240,8 @@ "warningSaveUpdatedMap": "Einige Ebenen wurden nicht korrekt aktualisiert", "saveUpdatedMap": "Alle Ebenen wurden erfolgreich aktualisiert", "incompatibleBackgroundAndProjection":"Die von Ihnen ausgewählte Projektion ist nicht mit der Hintergrundkarte kompatibel. Wechseln Sie zu einer kompatiblen oder leeren Hintergrundkarte und wählen Sie danach diese Projektion aus!", - "incompatibleDataAndProjection":"Die aktuellen Ebenen und die Kartenprojektion sind nicht vollständig kompatibel. Teile oder alle Daten werden möglicherweise nicht auf der Karte angezeigt" + "incompatibleDataAndProjection":"Die aktuellen Ebenen und die Kartenprojektion sind nicht vollständig kompatibel. Teile oder alle Daten werden möglicherweise nicht auf der Karte angezeigt", + "errorLoadingGF":"Fehler laden GF GEOM" }, "dock": { "row": "{rowsSelected}. Zeile ausgewählt", @@ -1571,7 +1590,8 @@ "urlTemplate": "URL-Vorlage", "urlTemplateHint": "

Vorlagen-URL für Ihren benutzerdefinierten TMS-Dienst mit der Möglichkeit, Variablen einzufügen (ausgedrückt in geschweiften Klammern, z. B. {var_name} ), die von der Anwendung ersetzt werden.
. Die Variablen {x} , {y} und {z} sind für Kachelgitterkoordinatenwerte reserviert. Sie können auch Variablen verwenden {s} für Subdomains. Jede andere in der URL angegebene Variable muss auch in den Optionen unter \"Benutzerdefinierte TMS-Konfiguration\" (Erweiterte Einstellungen) angegeben werden.

", "customTMSConfiguration": "Benutzerdefinierte TMS-Konfiguration", - "customTMSConfigurationHint": "

Optionen können Folgendes enthalten:

    • \"subdomains\": ['a', 'b'] : Wird für mehrere Domains verwendet. Ersetzen des {s} in der URL-Vorlage
    • \"maxNativeZoom\": 20 : Maximaler Zoom vom Server

    ", "forceDefaultTileGrid": "Standard-Kachelraster erzwingen", + "customTMSConfigurationHint": "

    Optionen können Folgendes enthalten:

      • \"subdomains\": ['a', 'b'] : Wird für mehrere Domains verwendet. Ersetzen des {s} in der URL-Vorlage
      • \"maxNativeZoom\": 20 : Maximaler Zoom vom Server

      ", + "forceDefaultTileGrid": "Standard-Kachelraster erzwingen", "forceDefaultTileGridDescription": "Verwenden Sie das Kachelraster der globalen Projektion anstelle des vom Server bereitgestellten Ursprungs und der vom Server bereitgestellten Auflösungen. Dies ist nützlich für einige TMS-Dienste, die falsche Ursprünge oder Ergebnisse anzeigen." }, "tileprovider": { @@ -2544,7 +2564,8 @@ "startPoint": "Startpunkt", "endPoint": "Endpunkt", "line": "Linie", - "geodesicLine": "Geodätische Linie" + "geodesicLine": "Geodätische Linie", + "pointCloudSizeInfo": "Der Punktwolkenradius wird nur angewendet, wenn die Dämpfungsoptionen deaktiviert sind. Die Dämpfungsoption hat Vorrang vor dieser Eigenschaft." }, "playback": { "settings": { diff --git a/web/client/translations/data.en-US.json b/web/client/translations/data.en-US.json index 9995418617..c19d70a83d 100644 --- a/web/client/translations/data.en-US.json +++ b/web/client/translations/data.en-US.json @@ -57,8 +57,8 @@ "404": "Translation file not found" }, "details": { - "title": "About this map", - "tooltip": "About this map" + "title": "About this content", + "tooltip": "About this content" }, "showEmptyMessageGFI": "Show empty results message in GetFeatureInfo panel", "remove": "Delete", @@ -116,6 +116,7 @@ "heightOffset": "Height offset (m)", "wmsLayerTileSize": "Tile size (WMS)", "serverType": "Server Type", + "formatError": "It was not possible to fetch format and information sheet format from the service configured. Probably you are using a No Vendor service and this is not supported (e.g. GeoNetwork)", "serverTypeOption": { "noVendor": "No Vendor", "geoserver": "GeoServer" @@ -156,6 +157,21 @@ "scale": "Scale", "resolution": "Resolution" }, + "3dTiles": { + "format": "Format", + "3dModel": "3D Model", + "pointCloud": "Point Cloud", + "pointCloudShading": { + "title": "Visualization options", + "attenuationInfo": "Perform point attenuation based on the distance from the current view point", + "attenuation": "Enable attenuation", + "maximumAttenuation": "Maximum attenuation", + "eyeDomeLightingInfo": "This enable an eye dome lighting to improve visualization of the point cloud. This does not represent the real sun lighting", + "eyeDomeLighting": "Enable lighting", + "eyeDomeLightingStrength": "Lighting strength", + "eyeDomeLightingRadius": "Lighting radius" + } + }, "tooltip": { "label": "Tooltip", "title": "Title", @@ -182,6 +198,8 @@ }, "format": { "title": "Format", + "tile": "Tile", + "information": "Information sheet", "refresh": "Fetch supported formats", "loading": "Loading...", "noOption": "No option", @@ -222,7 +240,8 @@ "warningSaveUpdatedMap": "Some layers haven't been updated correctly.", "saveUpdatedMap": "All the layers have been successfully updated.", "incompatibleBackgroundAndProjection":"The Projection you selected is not compatible with background. Switch to a compatible -or an empty- background, then select this projection.", - "incompatibleDataAndProjection":"The current layer(s) and map projection are not completely compatible. Parts or all of the data might not appear in the map." + "incompatibleDataAndProjection":"The current layer(s) and map projection are not completely compatible. Parts or all of the data might not appear in the map.", + "errorLoadingGF":"Error loading GF Geom" }, "dock": { "row": "{rowsSelected} row selected", @@ -2517,7 +2536,8 @@ "startPoint": "Start point", "endPoint": "End point", "line": "Line", - "geodesicLine": "Geodesic line" + "geodesicLine": "Geodesic line", + "pointCloudSizeInfo": "The point cloud radius is applied only when the attenuation options is disabled. The attenuation option takes precedence over this property." }, "playback": { "settings": { diff --git a/web/client/translations/data.es-ES.json b/web/client/translations/data.es-ES.json index 0b6d36d50f..d44ef196df 100644 --- a/web/client/translations/data.es-ES.json +++ b/web/client/translations/data.es-ES.json @@ -57,8 +57,8 @@ "404": "Archivo de traducción no encontrado" }, "details": { - "title": "Información en este mapa", - "tooltip": "Información en este mapa" + "title": "Información en este contenido", + "tooltip": "Información en este contenido" }, "showEmptyMessageGFI": "Mostrar mensaje de resultados vacío en el panel de GFI", "remove": "Borrar", @@ -113,6 +113,7 @@ "heightOffset": "Desplazamiento de altura (m)", "wmsLayerTileSize": "Tamaño del mosaico (WMS)", "serverType": "Tipo de servidor", + "formatError": "No fue posible recuperar el formato y el formato de la hoja de información del servicio configurado. Probablemente esté utilizando un servicio sin proveedor y no es compatible (por ejemplo, GeoNetwork)", "serverTypeOption": { "noVendor": "No Vendor", "geoserver": "GeoServer" @@ -153,6 +154,21 @@ "scale": "Escala", "resolution": "Resolución" }, + "3dTiles": { + "format": "Formato", + "3dModel": "Modelo 3D", + "pointCloud": "Nube de puntos", + "pointCloudShading": { + "title": "Opciones de visualización", + "attenuationInfo": "Realiza atenuación de puntos según la distancia desde el punto de vista actual", + "attenuation": "Habilitar atenuación", + "maximumAttenuation": "Atenuación máxima", + "eyeDomeLightingInfo": "Esto habilita una iluminación de domo ocular para mejorar la visualización de la nube de puntos. Esto no representa la iluminación solar real", + "eyeDomeLighting": "Habilitar iluminación", + "eyeDomeLightingStrength": "Intensidad de iluminación", + "eyeDomeLightingRadius": "Radio de iluminación" + } + }, "tooltip": { "label": "Información sobre herramientas", "title": "Título", @@ -182,6 +198,8 @@ }, "format": { "title": "Formato", + "tile": "Teja", + "information": "Identificar el formato de respuesta", "refresh": "Obtener formatos admitidos", "loading": "Cargando...", "noOption": "Sin opcion", @@ -222,7 +240,8 @@ "warningSaveUpdatedMap": "Algunas capas no han sido actualizadas correctamente", "saveUpdatedMap": "Todas las capas han sido actulizadas correctamente", "incompatibleBackgroundAndProjection":"La proyección que seleccionó no es compatible con el fondo, cambie a un fondo compatible -o un vacío-, luego seleccione esta proyección!", - "incompatibleDataAndProjection":"la (s) capa (s) actual (es) y la proyección del mapa no son completamente compatibles. Partes o todos los datos podrían no aparecer en el mapa" + "incompatibleDataAndProjection":"la (s) capa (s) actual (es) y la proyección del mapa no son completamente compatibles. Partes o todos los datos podrían no aparecer en el mapa", + "errorLoadingGF":"Error de carga de GF Geometry" }, "dock": { "row": "{rowsSelected} fila seleccionada", @@ -2507,7 +2526,8 @@ "startPoint": "Punto de inicio", "endPoint": "Punto final", "line": "Línea", - "geodesicLine": "Línea geodésica" + "geodesicLine": "Línea geodésica", + "pointCloudSizeInfo": "El radio de la nube de puntos se aplica sólo cuando las opciones de atenuación están deshabilitadas. La opción de atenuación tiene prioridad sobre esta propiedad." }, "playback": { "settings": { diff --git a/web/client/translations/data.fr-FR.json b/web/client/translations/data.fr-FR.json index bca0accddb..602b4826fe 100644 --- a/web/client/translations/data.fr-FR.json +++ b/web/client/translations/data.fr-FR.json @@ -57,8 +57,8 @@ "404": "Fichier de traduction introuvable" }, "details": { - "title": "À propos de cette carte", - "tooltip": "À propos de cette carte" + "title": "À propos de cette contenu", + "tooltip": "À propos de cette contenu" }, "showEmptyMessageGFI": "Afficher un message de résultat vide dans le panneau GetFeatureInfo", "remove": "Supprimer", @@ -116,6 +116,7 @@ "heightOffset": "Décalage en hauteur (m)", "wmsLayerTileSize": "Taille de la tuile (WMS)", "serverType": "Type de serveur", + "formatError": "Il n'a pas été possible de récupérer le format et le format de la fiche d'information à partir du service configuré. Vous utilisez probablement un service No Vendor et celui-ci n'est pas pris en charge (par exemple GeoNetwork)", "serverTypeOption": { "noVendor": "No Vendor", "geoserver": "GeoServer" @@ -156,6 +157,21 @@ "scale": "Échelle", "resolution": "Résolution" }, + "3dTiles": { + "format": "Formater", + "3dModel": "Modèle 3D", + "pointCloud": "Nuage de points", + "pointCloudShading": { + "title": "Options de visualisation", + "attenuationInfo": "Effectuer une atténuation de point en fonction de la distance du point de vue actuel", + "attenuation": "Activer l'atténuation", + "maximumAttenuation": "Atténuation maximale", + "eyeDomeLightingInfo": "Cela permet à un éclairage du dôme oculaire d'améliorer la visualisation du nuage de points. Cela ne représente pas l'éclairage réel du soleil", + "eyeDomeLighting": "Activer l'éclairage", + "eyeDomeLightingStrength": "Force d'éclairage", + "eyeDomeLightingRadius": "Rayon d'éclairage" + } + }, "tooltip": { "label": "Infobulle", "title": "Titre", @@ -182,6 +198,8 @@ }, "format": { "title": "Format", + "tile": "Tile", + "information": "Fiche d'information", "refresh": "Récupérer les formats pris en charge", "loading": "Chargement...", "noOption": "Pas d'option", @@ -222,7 +240,8 @@ "warningSaveUpdatedMap": "Certaines couches n'ont pas été mises à jour correctement", "saveUpdatedMap": "Toutes les couches ont bien été mises à jour", "incompatibleBackgroundAndProjection": "La projection que vous avez choisie n'est pas compatible avec le fond de plan. Changez pour un fond de plan compatible ou retirez le fond de plan, puis sélectionnez à nouveau la projection.", - "incompatibleDataAndProjection": "La/les couche(s) actuelle(s) et la projection cartographique ne sont pas totalement compatibles. Certaines données voire toutes les données risquent de ne pas apparaître sur la carte." + "incompatibleDataAndProjection": "La/les couche(s) actuelle(s) et la projection cartographique ne sont pas totalement compatibles. Certaines données voire toutes les données risquent de ne pas apparaître sur la carte.", + "errorLoadingGF":"Erreur Chargement GF Geom" }, "dock": { "row": "{rowsSelected} ligne sélectionnée", @@ -2507,7 +2526,8 @@ "startPoint": "Point de départ", "endPoint": "Point final", "line": "Ligne", - "geodesicLine": "Ligne géodésique" + "geodesicLine": "Ligne géodésique", + "pointCloudSizeInfo": "Le rayon du nuage de points est appliqué uniquement lorsque les options d'atténuation sont désactivées. L'option d'atténuation est prioritaire sur cette propriété." }, "playback": { "settings": { diff --git a/web/client/translations/data.is-IS.json b/web/client/translations/data.is-IS.json index 7466070478..67a233c29b 100644 --- a/web/client/translations/data.is-IS.json +++ b/web/client/translations/data.is-IS.json @@ -57,8 +57,8 @@ "404": "Translation file not found" }, "details": { - "title": "About this map", - "tooltip": "About this map" + "title": "About this content", + "tooltip": "About this content" }, "showEmptyMessageGFI": "Show empty results message in GetFeatureInfo panel", "remove": "Delete", diff --git a/web/client/translations/data.it-IT.json b/web/client/translations/data.it-IT.json index f978182b98..0a49ddffb8 100644 --- a/web/client/translations/data.it-IT.json +++ b/web/client/translations/data.it-IT.json @@ -57,8 +57,8 @@ "404": "File di traduzione non trovato" }, "details": { - "title": "Info su questa mappa", - "tooltip": "Info su questa mappa" + "title": "Info su questo contenuto", + "tooltip": "Info su questo contenuto" }, "showEmptyMessageGFI": "Mostra il messaggio: Nessuna feature per gli altri layer", "remove": "Rimuovi", @@ -116,6 +116,7 @@ "heightOffset": "Spostamento in altezza (m)", "wmsLayerTileSize": "Dimensione tile (WMS)", "serverType": "Tipo di Server", + "formatError": "Non è stato possibile recuperare le informazioni sui formati dal servizio configurato. Probabilmente stai usando un servizio \"No Vendor\" e questo non è supportato (es. GeoNetwork)", "serverTypeOption": { "noVendor": "No Vendor", "geoserver": "GeoServer" @@ -156,6 +157,21 @@ "scale": "Scala", "resolution": "Risoluzione" }, + "3dTiles": { + "format": "Formato", + "3dModel": "Modello 3D", + "pointCloud": "Nuvola di punti", + "pointCloudShading": { + "title": "Opzioni di visualizzazione", + "attenuationInfo": "Applica una attenuazione della nuvola di punti basata sul punto di vista corrente", + "attenuation": "Abilita attenuazione", + "maximumAttenuation": "Massima attenuazione", + "eyeDomeLightingInfo": "Abilita l'illuminazione di tipo eye dome per migliorare la visualizzazione della nuvola di punti. Questa non rappresenta l'attuale illuminazione del sole", + "eyeDomeLighting": "Abilita illuminazione", + "eyeDomeLightingStrength": "Intensità luminosa", + "eyeDomeLightingRadius": "Raggio di illuminazione" + } + }, "tooltip": { "label": "Tooltip", "title": "Titolo", @@ -182,6 +198,8 @@ }, "format": { "title": "Formato", + "tile": "Tile", + "information": "Risposte interrogazioni su mappa", "refresh": "Recupera i formati supportati", "loading": "Caricamento in corso...", "noOption": "Nessuna opzione", @@ -222,7 +240,8 @@ "warningSaveUpdatedMap": "Alcuni layer non sono stati aggiornati correttamente", "saveUpdatedMap": "Tutti i layer sono stati aggiornati con successo", "incompatibleBackgroundAndProjection":"The Projection you selected is not compatible with background, switch to a compatible -or an empty- background, then select this projection!", - "incompatibleDataAndProjection":"Uno o più livelli non sono completamente compatibili con la proiezione selezionata. I dati potrebbero essere (tutti o in parte) non visibili in mappa" + "incompatibleDataAndProjection":"Uno o più livelli non sono completamente compatibili con la proiezione selezionata. I dati potrebbero essere (tutti o in parte) non visibili in mappa", + "errorLoadingGF":"Errore di caricamento della geometria" }, "dock": { "row": "{rowsSelected} riga selezionata", @@ -2508,7 +2527,8 @@ "startPoint": "Punto iniziale", "endPoint": "Punto finale", "line": "Linea", - "geodesicLine": "Linea geodesica" + "geodesicLine": "Linea geodesica", + "pointCloudSizeInfo": "Il raggio della nuvola di punti viene applicato solo quando le opzioni di attenuazione sono disabilitate. L'opzione di attenuazione ha la precedenza su questa proprietà." }, "playback": { "settings": { diff --git a/web/client/utils/LayersUtils.js b/web/client/utils/LayersUtils.js index e57e396a5c..8cfca18d72 100644 --- a/web/client/utils/LayersUtils.js +++ b/web/client/utils/LayersUtils.js @@ -655,7 +655,8 @@ export const saveLayer = (layer) => { layer.tileGridStrategy ? { tileGridStrategy: layer.tileGridStrategy } : {}, layer.tileGridCacheSupport ? { tileGridCacheSupport: layer.tileGridCacheSupport } : {}, !isNil(layer.forceProxy) ? { forceProxy: layer.forceProxy } : {}, - !isNil(layer.disableFeaturesEditing) ? { disableFeaturesEditing: layer.disableFeaturesEditing } : {}); + !isNil(layer.disableFeaturesEditing) ? { disableFeaturesEditing: layer.disableFeaturesEditing } : {}, + layer.pointCloudShading ? { pointCloudShading: layer.pointCloudShading } : {}); }; /** diff --git a/web/client/utils/MapUtils.js b/web/client/utils/MapUtils.js index 380817b304..9ca76e86a2 100644 --- a/web/client/utils/MapUtils.js +++ b/web/client/utils/MapUtils.js @@ -842,7 +842,7 @@ export const compareMapChanges = (map1 = {}, map2 = {}) => { * used to override default ones in order to have a local hooks object * one for each map widget */ -export const createRegisterHooks = () => { +export const createRegisterHooks = (id) => { let hooksCustom = {}; return { registerHook: (name, hook) => { @@ -858,7 +858,8 @@ export const createRegisterHooks = () => { return dontExistCallback(); } return null; - } + }, + id }; }; diff --git a/web/client/utils/__tests__/LayersUtils-test.js b/web/client/utils/__tests__/LayersUtils-test.js index 7e628fc827..4354ed5394 100644 --- a/web/client/utils/__tests__/LayersUtils-test.js +++ b/web/client/utils/__tests__/LayersUtils-test.js @@ -1246,6 +1246,20 @@ describe('LayersUtils', () => { l => { expect(l.disableFeaturesEditing).toBeTruthy(); } + ], + [ + { + pointCloudShading: { + attenuation: true, + maximumAttenuation: 4, + eyeDomeLighting: true, + eyeDomeLightingStrength: 1, + eyeDomeLightingRadius: 1 + } + }, + l => { + expect(l.pointCloudShading).toBeTruthy(); + } ] ]; layers.map(([layer, test]) => test(LayersUtils.saveLayer(layer)) );