From 601b15fa38a7017bc021234e12df749b48318ce9 Mon Sep 17 00:00:00 2001 From: Suren Date: Thu, 16 Nov 2023 18:56:16 +0530 Subject: [PATCH 01/15] #9590: COG download metadata by default with abort fetch (#9687) --- web/client/actions/__tests__/catalog-test.js | 7 ++ web/client/actions/catalog.js | 5 +- web/client/api/catalog/COG.js | 56 +++++++++--- .../catalog/CatalogServiceEditor.jsx | 30 +++++-- .../__tests__/CatalogServiceEditor-test.jsx | 90 +++++++++++++++++++ .../CommonAdvancedSettings.jsx | 2 +- web/client/epics/catalog.js | 4 +- web/client/translations/data.de-DE.json | 2 +- web/client/translations/data.en-US.json | 2 +- web/client/translations/data.es-ES.json | 2 +- web/client/translations/data.fr-FR.json | 2 +- web/client/translations/data.it-IT.json | 2 +- 12 files changed, 175 insertions(+), 29 deletions(-) diff --git a/web/client/actions/__tests__/catalog-test.js b/web/client/actions/__tests__/catalog-test.js index b50ae764b2..17fdf54de5 100644 --- a/web/client/actions/__tests__/catalog-test.js +++ b/web/client/actions/__tests__/catalog-test.js @@ -238,6 +238,13 @@ describe('Test correctness of the catalog actions', () => { expect(retval).toExist(); expect(retval.type).toBe(ADD_SERVICE); }); + it('addService with options', () => { + const options = {"test": "1"}; + var retval = addService(options); + expect(retval).toExist(); + expect(retval.type).toBe(ADD_SERVICE); + expect(retval.options).toEqual(options); + }); it('addCatalogService', () => { var retval = addCatalogService(service); diff --git a/web/client/actions/catalog.js b/web/client/actions/catalog.js index d4fd402bc2..fd41eeab71 100644 --- a/web/client/actions/catalog.js +++ b/web/client/actions/catalog.js @@ -170,9 +170,10 @@ export function changeUrl(url) { url }; } -export function addService() { +export function addService(options) { return { - type: ADD_SERVICE + type: ADD_SERVICE, + options }; } export function addCatalogService(service) { diff --git a/web/client/api/catalog/COG.js b/web/client/api/catalog/COG.js index a566aa4813..a7de6e4814 100644 --- a/web/client/api/catalog/COG.js +++ b/web/client/api/catalog/COG.js @@ -8,8 +8,9 @@ import get from 'lodash/get'; import isEmpty from 'lodash/isEmpty'; +import isNil from 'lodash/isNil'; import { Observable } from 'rxjs'; -import { fromUrl } from 'geotiff'; +import { fromUrl as fromGeotiffUrl } from 'geotiff'; import { isValidURL } from '../../utils/URLUtils'; import ConfigUtils from '../../utils/ConfigUtils'; @@ -57,8 +58,25 @@ export const getProjectionFromGeoKeys = (image) => { return null; }; +const abortError = (reject) => reject(new DOMException("Aborted", "AbortError")); +/** + * fromUrl with abort fetching of data and data slices + * Note: The abort action will not cancel data fetch request but just the promise, + * because of the issue in https://github.com/geotiffjs/geotiff.js/issues/408 + */ +const fromUrl = (url, signal) => { + if (signal?.aborted) { + return abortError(Promise.reject); + } + return new Promise((resolve, reject) => { + signal?.addEventListener("abort", () => abortError(reject)); + return fromGeotiffUrl(url) + .then((image)=> image.getImage()) // Fetch and read first image to get medatadata of the tif + .then((image) => resolve(image)) + .catch(()=> abortError(reject)); + }); +}; let capabilitiesCache = {}; - export const getRecords = (_url, startPosition, maxRecords, text, info = {}) => { const service = get(info, 'options.service'); let layers = []; @@ -73,29 +91,43 @@ export const getRecords = (_url, startPosition, maxRecords, text, info = {}) => sources: [{url}], options: service.options || {} }; - if (service.fetchMetadata) { + const controller = get(info, 'options.controller'); + const isSave = get(info, 'options.save', false); + // Fetch metadata only on saving the service (skip on search) + if ((isNil(service.fetchMetadata) || service.fetchMetadata) && isSave) { const cached = capabilitiesCache[url]; if (cached && new Date().getTime() < cached.timestamp + (ConfigUtils.getConfigProp('cacheExpire') || 60) * 1000) { return {...cached.data}; } - return fromUrl(url) - .then(geotiff => geotiff.getImage()) + return fromUrl(url, controller?.signal) .then(image => { const crs = getProjectionFromGeoKeys(image); const extent = image.getBoundingBox(); const isProjectionDefined = isProjectionAvailable(crs); layer = { ...layer, + sourceMetadata: { + crs, + extent: extent, + width: image.getWidth(), + height: image.getHeight(), + tileWidth: image.getTileWidth(), + tileHeight: image.getTileHeight(), + origin: image.getOrigin(), + resolution: image.getResolution() + }, // skip adding bbox when geokeys or extent is empty - ...(!isEmpty(extent) && !isEmpty(crs) && isProjectionDefined && { + ...(!isEmpty(extent) && !isEmpty(crs) && { bbox: { crs, - bounds: { - minx: extent[0], - miny: extent[1], - maxx: extent[2], - maxy: extent[3] - } + ...(isProjectionDefined && { + bounds: { + minx: extent[0], + miny: extent[1], + maxx: extent[2], + maxy: extent[3] + }} + ) } }) }; diff --git a/web/client/components/catalog/CatalogServiceEditor.jsx b/web/client/components/catalog/CatalogServiceEditor.jsx index b4e747e75b..5eb06219a5 100644 --- a/web/client/components/catalog/CatalogServiceEditor.jsx +++ b/web/client/components/catalog/CatalogServiceEditor.jsx @@ -17,7 +17,20 @@ import Message from "../I18N/Message"; import AdvancedSettings from './editor/AdvancedSettings'; import MainForm from './editor/MainForm'; -export default ({ +const withAbort = (Component) => { + return (props) => { + const [abortController, setAbortController] = useState(null); + const onSave = () => { + // Currently abort request on saving is applicable only for COG service + const controller = props.format === 'cog' ? new AbortController() : null; + setAbortController(controller); + return props.onAddService({save: true, controller}); + }; + const onCancel = () => abortController && props.saving ? abortController?.abort() : props.onChangeCatalogMode("view"); + return ; + }; +}; +const CatalogServiceEditor = ({ service = { title: "", type: "wms", @@ -39,9 +52,9 @@ export default ({ onChangeServiceProperty = () => {}, onToggleTemplate = () => {}, onToggleThumbnail = () => {}, - onAddService = () => {}, onDeleteService = () => {}, - onChangeCatalogMode = () => {}, + onCancel = () => {}, + onSaveService = () => {}, onFormatOptionsFetch = () => {}, selectedService, isLocalizedLayerStylesEnabled, @@ -50,7 +63,8 @@ export default ({ layerOptions = {}, infoFormatOptions, services, - autoSetVisibilityLimits = false + autoSetVisibilityLimits = false, + disabled }) => { const [valid, setValid] = useState(true); return ( - {service && !service.isNew - ? : null } - @@ -110,3 +124,5 @@ export default ({ ); }; + +export default withAbort(CatalogServiceEditor); diff --git a/web/client/components/catalog/__tests__/CatalogServiceEditor-test.jsx b/web/client/components/catalog/__tests__/CatalogServiceEditor-test.jsx index 05e2d96463..f2f16f1bee 100644 --- a/web/client/components/catalog/__tests__/CatalogServiceEditor-test.jsx +++ b/web/client/components/catalog/__tests__/CatalogServiceEditor-test.jsx @@ -8,6 +8,7 @@ import React from 'react'; import ReactDOM from 'react-dom'; import expect from 'expect'; +import TestUtils from 'react-dom/test-utils'; import CatalogServiceEditor from '../CatalogServiceEditor'; import {defaultPlaceholder} from "../editor/MainFormUtils"; @@ -149,4 +150,93 @@ describe('Test CatalogServiceEditor', () => { let placeholder = defaultPlaceholder(service); expect(placeholder).toBe("e.g. https://mydomain.com/geoserver/wms"); }); + it('test save and delete button when saving', () => { + ReactDOM.render(, document.getElementById("container")); + let buttons = document.querySelectorAll('.form-group button'); + let saveBtn; let deleteBtn; + buttons.forEach(btn => {if (btn.textContent === 'save') saveBtn = btn;}); + buttons.forEach(btn => {if (btn.textContent === 'catalog.delete') deleteBtn = btn;}); + expect(saveBtn).toBeTruthy(); + expect(deleteBtn).toBeTruthy(); + expect(saveBtn.classList.contains("disabled")).toBeTruthy(); + expect(deleteBtn.classList.contains("disabled")).toBeTruthy(); + }); + it('test saving service for COG type', () => { + const actions = { + onAddService: () => {} + }; + const spyOnAdd = expect.spyOn(actions, 'onAddService'); + ReactDOM.render(, document.getElementById("container")); + let buttons = document.querySelectorAll('.form-group button'); + let saveBtn; + buttons.forEach(btn => {if (btn.textContent === 'save') saveBtn = btn;}); + expect(saveBtn).toBeTruthy(); + TestUtils.Simulate.click(saveBtn); + expect(spyOnAdd).toHaveBeenCalled(); + let arg = spyOnAdd.calls[0].arguments[0]; + expect(arg.save).toBe(true); + expect(arg.controller).toBeTruthy(); + + ReactDOM.render(, document.getElementById("container")); + buttons = document.querySelectorAll('.form-group button'); + buttons.forEach(btn => {if (btn.textContent === 'save') saveBtn = btn;}); + expect(saveBtn).toBeTruthy(); + TestUtils.Simulate.click(saveBtn); + expect(spyOnAdd).toHaveBeenCalled(); + arg = spyOnAdd.calls[1].arguments[0]; + expect(arg.save).toBeTruthy(); + expect(arg.controller).toBeFalsy(); + }); + it('test cancel service', () => { + const actions = { + onChangeCatalogMode: () => {}, + onAddService: () => {} + }; + const spyOnCancel = expect.spyOn(actions, 'onChangeCatalogMode'); + ReactDOM.render(, document.getElementById("container")); + let buttons = document.querySelectorAll('.form-group button'); + let cancelBtn; + buttons.forEach(btn => {if (btn.textContent === 'cancel') cancelBtn = btn;}); + expect(cancelBtn).toBeTruthy(); + TestUtils.Simulate.click(cancelBtn); + expect(spyOnCancel).toHaveBeenCalled(); + let arg = spyOnCancel.calls[0].arguments[0]; + expect(arg).toBe('view'); + + const spyOnAdd = expect.spyOn(actions, 'onAddService'); + ReactDOM.render(, document.getElementById("container")); + buttons = document.querySelectorAll('.form-group button'); + let saveBtn; + buttons.forEach(btn => {if (btn.textContent === 'save') saveBtn = btn;}); + TestUtils.Simulate.click(saveBtn); + expect(spyOnAdd).toHaveBeenCalled(); + + ReactDOM.render(, document.getElementById("container")); + buttons = document.querySelectorAll('.form-group button'); + buttons.forEach(btn => {if (btn.textContent === 'cancel') cancelBtn = btn;}); + TestUtils.Simulate.click(cancelBtn); + expect(spyOnCancel.calls[1]).toBeFalsy(); + }); }); diff --git a/web/client/components/catalog/editor/AdvancedSettings/CommonAdvancedSettings.jsx b/web/client/components/catalog/editor/AdvancedSettings/CommonAdvancedSettings.jsx index 048fd385ae..45900f3d36 100644 --- a/web/client/components/catalog/editor/AdvancedSettings/CommonAdvancedSettings.jsx +++ b/web/client/components/catalog/editor/AdvancedSettings/CommonAdvancedSettings.jsx @@ -58,7 +58,7 @@ export default ({ onChangeServiceProperty("fetchMetadata", e.target.checked)} - checked={!isNil(service.fetchMetadata) ? service.fetchMetadata : false}> + checked={!isNil(service.fetchMetadata) ? service.fetchMetadata : true}>  } /> diff --git a/web/client/epics/catalog.js b/web/client/epics/catalog.js index 3545be647d..294b54b8a1 100644 --- a/web/client/epics/catalog.js +++ b/web/client/epics/catalog.js @@ -292,7 +292,7 @@ export default (API) => ({ */ newCatalogServiceAdded: (action$, store) => action$.ofType(ADD_SERVICE) - .switchMap(() => { + .switchMap(({options} = {}) => { const state = store.getState(); const newService = newServiceSelector(state); const maxRecords = pageSizeSelector(state); @@ -310,7 +310,7 @@ export default (API) => ({ startPosition: 1, maxRecords, text: "", - options: {service, isNewService: true} + options: {service, isNewService: true, ...options} }) ); }) diff --git a/web/client/translations/data.de-DE.json b/web/client/translations/data.de-DE.json index 19a14b14a3..628718cb5b 100644 --- a/web/client/translations/data.de-DE.json +++ b/web/client/translations/data.de-DE.json @@ -1554,7 +1554,7 @@ "tooltip": "Fügen Sie der Karte Ebenen hinzu", "autoload": "Suche in Dienstauswahl", "fetchMetadata": { - "label": "Laden Sie Dateimetadaten bei der Suche herunter", + "label": "Dateimetadaten beim Speichern herunterladen", "tooltip": "Diese Option ruft Metadaten ab, um das Zoomen auf Ebene zu unterstützen. Es kann den Suchvorgang verlangsamen, wenn die Bilder zu groß oder zu viele sind." }, "clearValueText": "Auswahl aufheben", diff --git a/web/client/translations/data.en-US.json b/web/client/translations/data.en-US.json index c29dbae326..12e6ebd20d 100644 --- a/web/client/translations/data.en-US.json +++ b/web/client/translations/data.en-US.json @@ -1515,7 +1515,7 @@ "tooltip": "Add layers to the map", "autoload": "Search on service selection", "fetchMetadata": { - "label": "Download file metadata on search", + "label": "Download file metadata on save", "tooltip": "This option will fetch metadata to support the zoom to layer. It may slow down the search operation if the images are too big or too many." }, "clearValueText": "Clear selection", diff --git a/web/client/translations/data.es-ES.json b/web/client/translations/data.es-ES.json index ca119fa1f5..e11065dd2d 100644 --- a/web/client/translations/data.es-ES.json +++ b/web/client/translations/data.es-ES.json @@ -1516,7 +1516,7 @@ "tooltip": "agregar capas al mapa", "autoload": "Buscar en la selección de servicios", "fetchMetadata": { - "label": "Descargar metadatos de archivos en la búsqueda", + "label": "Descargar metadatos del archivo al guardar", "tooltip": "Esta opción recuperará metadatos para admitir el zoom a la capa. Puede ralentizar la operación de búsqueda si las imágenes son demasiado grandes o demasiadas." }, "clearValueText": "Borrar selección", diff --git a/web/client/translations/data.fr-FR.json b/web/client/translations/data.fr-FR.json index 4cc43dd1a4..8ad8ddb452 100644 --- a/web/client/translations/data.fr-FR.json +++ b/web/client/translations/data.fr-FR.json @@ -1516,7 +1516,7 @@ "tooltip": "Ajouter des couches à la carte", "autoload": "Recherche sur la sélection du service", "fetchMetadata": { - "label": "Télécharger les métadonnées du fichier lors de la recherche", + "label": "Télécharger les métadonnées du fichier lors de l'enregistrement", "tooltip": "Cette option récupérera les métadonnées pour prendre en charge le zoom sur la couche. Cela peut ralentir l'opération de recherche si les images sont trop grandes ou trop nombreuses." }, "clearValueText": "Effacer la sélection", diff --git a/web/client/translations/data.it-IT.json b/web/client/translations/data.it-IT.json index 9986f88e88..0d0f8036db 100644 --- a/web/client/translations/data.it-IT.json +++ b/web/client/translations/data.it-IT.json @@ -1514,7 +1514,7 @@ "title": "Catalogo", "autoload": "Ricerca alla selezione del servizio", "fetchMetadata": { - "label": "Scarica i metadati dei file durante la ricerca", + "label": "Scarica i metadati del file al salvataggio", "tooltip": "Questa opzione recupererà i metadati per supportare lo zoom a livello. Potrebbe rallentare l'operazione di ricerca se le immagini sono troppo grandi o troppe." }, "clearValueText": "Cancella selezione", From b34aaab064cdde6beebe358e283a06e821821ee1 Mon Sep 17 00:00:00 2001 From: Suren Date: Thu, 16 Nov 2023 18:58:26 +0530 Subject: [PATCH 02/15] #9600 - Link release version in MapStore Documentation in home page (#9705) --- .github/ISSUE_TEMPLATE/release_steps.md | 1 + web/client/translations/data.de-DE.json | 2 +- web/client/translations/data.en-US.json | 2 +- web/client/translations/data.es-ES.json | 2 +- web/client/translations/data.fr-FR.json | 2 +- web/client/translations/data.it-IT.json | 2 +- 6 files changed, 6 insertions(+), 5 deletions(-) diff --git a/.github/ISSUE_TEMPLATE/release_steps.md b/.github/ISSUE_TEMPLATE/release_steps.md index f6a2208c43..e2b5519299 100644 --- a/.github/ISSUE_TEMPLATE/release_steps.md +++ b/.github/ISSUE_TEMPLATE/release_steps.md @@ -88,6 +88,7 @@ If stable release (YYYY.XX.00) follow these sub-steps: - [ ] `mapstore-printing.zip` on github release - [ ] Publish the release - [ ] create on [ReadTheDocs](https://readthedocs.org/projects/mapstore/) project the version build for `vYYYY.XX.mm` (click on "Versions" and activate the version of the tag, created when release was published) +- [ ] Update `Default version` to point the release version in the `Advanced Settings` menu of the [ReadTheDocs](https://readthedocs.org/dashboard/mapstore/advanced/) admin panel ## Build and publish MapStoreExtension release diff --git a/web/client/translations/data.de-DE.json b/web/client/translations/data.de-DE.json index 628718cb5b..122b5b4d7e 100644 --- a/web/client/translations/data.de-DE.json +++ b/web/client/translations/data.de-DE.json @@ -350,7 +350,7 @@ }, "home":{ "open": "Öffnen", - "shortDescription": "Modern webmapping mit OpenLayers, Leaflet und React
besuchen sie die dokumentationsseite", + "shortDescription": "Modern webmapping mit OpenLayers, Leaflet und React
besuchen sie die dokumentationsseite", "forkMeOnGitHub": "Fork me on GitHub", "description": "MapStore wurde entwickelt, um auf einfache und intuitive Weise Karten und Mashups zu erstellen, zu speichern und zu teilen die auf Inhalten von bekannten Quellen wie Google Maps und OpenStreetMap oder von Diensten die auf offenen Protokollen wie OGC WMS, WFS, WMTS or TMS und so weiter basieren.
Besuche die Homepage für mehr Details.", "Applications": "Anwendungen", diff --git a/web/client/translations/data.en-US.json b/web/client/translations/data.en-US.json index 12e6ebd20d..b6456e9a4a 100644 --- a/web/client/translations/data.en-US.json +++ b/web/client/translations/data.en-US.json @@ -350,7 +350,7 @@ }, "home":{ "open": "Open", - "shortDescription": "Modern webmapping with OpenLayers, Leaflet and ReactJS.
Visit the documentation page", + "shortDescription": "Modern webmapping with OpenLayers, Leaflet and ReactJS.
Visit the documentation page", "forkMeOnGitHub": "Fork me on GitHub", "description": "MapStore has been developed to create, save and share in a simple and intuitive way maps and mashups created selecting contents coming from well-known sources like OpenStreetMap, Google Maps or from services provided by organizations using open protocols like OGC WMS, WFS, WMTS or TMS and so on...
Visit the home page for more details.", "Applications": "Applications", diff --git a/web/client/translations/data.es-ES.json b/web/client/translations/data.es-ES.json index e11065dd2d..8afc13b2e5 100644 --- a/web/client/translations/data.es-ES.json +++ b/web/client/translations/data.es-ES.json @@ -350,7 +350,7 @@ }, "home":{ "open": "Abrir", - "shortDescription": "Webmapping moderno con OpenLayers, Leaflet y React
visite la página de documentación", + "shortDescription": "Webmapping moderno con OpenLayers, Leaflet y React
visite la página de documentación", "forkMeOnGitHub": "Fork me on GitHub", "description": "MapStore está hecho para crear, guardar y compartir de forma simple e intuitiva mapas y composiciones creados a partir de contenidos de servidores tales como OpenStreetMap, Google Maps, MapQuest o cualquier otro servidor que proporcione protocolos estándar tales como OGC WMS, WFS, WMTS o TMS y otros.
Visite nuestra página principal para más detalles.", "Applications": "Aplicaciones", diff --git a/web/client/translations/data.fr-FR.json b/web/client/translations/data.fr-FR.json index 8ad8ddb452..038d9a9790 100644 --- a/web/client/translations/data.fr-FR.json +++ b/web/client/translations/data.fr-FR.json @@ -350,7 +350,7 @@ }, "home": { "open": "Ouvrir", - "shortDescription": "Application cartographique en ligne moderne Modern propulsée par OpenLayers, Leaflet et ReactJS.
Consultez la page de documentation ", + "shortDescription": "Application cartographique en ligne moderne Modern propulsée par OpenLayers, Leaflet et ReactJS.
Consultez la page de documentation ", "forkMeOnGitHub": "Fork me on GitHub", "description": "MapStore a été développé pour créer, sauvegarder et partager de façon simple et intuitive des cartes simple ou élaboréess créés à partir de services comme OpenStreetMap, Google Maps ou tout autres services fournis par des organisations utilisant des protocoles ouverts comme OGC WMS, WFS, WMTS ou TMS et bien d'autres...
Visitez notre page d'accueil pour plus de détails.", "Applications": "Applications", diff --git a/web/client/translations/data.it-IT.json b/web/client/translations/data.it-IT.json index 0d0f8036db..96fff0a44c 100644 --- a/web/client/translations/data.it-IT.json +++ b/web/client/translations/data.it-IT.json @@ -350,7 +350,7 @@ }, "home":{ "open": "Apri", - "shortDescription": "Modern webmapping con OpenLayers, Leaflet e React
visita la pagina di documentazione", + "shortDescription": "Modern webmapping con OpenLayers, Leaflet e React
visita la pagina di documentazione", "forkMeOnGitHub": "Fork me on GitHub", "description": "MapStore è sviluppato per creare, salvare e condividere in modo semplice ed intuitivo mappe e mashup creati selezionando contenuti da server come Google Maps, OpenStreetMap, MapQuest o da server specifici forniti dalla propria organizzazione o da terzi.
Visita la home page per maggiori dettagli.", "Applications": "Applicazioni", From 1e51fd9fa86b6aca7cbb88bcb143cd7bed6d6677 Mon Sep 17 00:00:00 2001 From: mahmoud adel <58145645+mahmoudadel54@users.noreply.github.com> Date: Fri, 17 Nov 2023 13:17:36 +0200 Subject: [PATCH 03/15] #9510: fix testers reviews (#9715) ** description: - fix css issue of white space below dropdown lists if user selects one item from them in rules manager page - fix issue in filtering roles dropdown --- web/client/api/geofence/RuleService.js | 2 ++ .../rulesmanager/rulesgrid/filterRenderers/RolesFilter.jsx | 4 ++-- web/client/reducers/rulesmanager.js | 2 +- web/client/selectors/__tests__/rulesmanager-test.js | 2 +- web/client/selectors/rulesmanager.js | 2 +- web/client/themes/default/less/autocomplete.less | 3 ++- 6 files changed, 9 insertions(+), 6 deletions(-) diff --git a/web/client/api/geofence/RuleService.js b/web/client/api/geofence/RuleService.js index 5238140f09..a376f0a42d 100644 --- a/web/client/api/geofence/RuleService.js +++ b/web/client/api/geofence/RuleService.js @@ -43,6 +43,8 @@ const normalizeKey = (key) => { return 'userName'; case 'rolename': return 'groupName'; + case 'roleAny': + return 'groupAny'; default: return key; } diff --git a/web/client/components/manager/rulesmanager/rulesgrid/filterRenderers/RolesFilter.jsx b/web/client/components/manager/rulesmanager/rulesgrid/filterRenderers/RolesFilter.jsx index dfe53cb6be..6be9c2d098 100644 --- a/web/client/components/manager/rulesmanager/rulesgrid/filterRenderers/RolesFilter.jsx +++ b/web/client/components/manager/rulesmanager/rulesgrid/filterRenderers/RolesFilter.jsx @@ -17,7 +17,7 @@ import { error } from '../../../../../actions/notifications'; import { filterSelector } from '../../../../../selectors/rulesmanager'; const selector = createSelector(filterSelector, (filter) => ({ selected: filter.rolename, - anyFieldVal: filter.groupAny + anyFieldVal: filter.roleAny })); export default compose( @@ -36,7 +36,7 @@ export default compose( title: "rulesmanager.errorTitle", message: "rulesmanager.errorLoadingRoles" }, - anyFilterRuleMode: 'groupAny' + anyFilterRuleMode: 'roleAny' }), withHandlers({ onValueSelected: ({column = {}, onFilterChange = () => {}}) => filterTerm => { diff --git a/web/client/reducers/rulesmanager.js b/web/client/reducers/rulesmanager.js index ef52c4fbe5..0522c9185d 100644 --- a/web/client/reducers/rulesmanager.js +++ b/web/client/reducers/rulesmanager.js @@ -108,7 +108,7 @@ function rulesmanager(state = defaultState, action) { const {key, value, isResetField} = action; if (isResetField) { if (key === "rolename") { - return assign({}, state, {filters: {...state.filters, [key]: value, ['groupAny']: undefined}}); + return assign({}, state, {filters: {...state.filters, [key]: value, ['roleAny']: undefined}}); } else if (key === "username") { return assign({}, state, {filters: {...state.filters, [key]: value, ['userAny']: undefined}}); } diff --git a/web/client/selectors/__tests__/rulesmanager-test.js b/web/client/selectors/__tests__/rulesmanager-test.js index 0002890cea..3b51ffe222 100644 --- a/web/client/selectors/__tests__/rulesmanager-test.js +++ b/web/client/selectors/__tests__/rulesmanager-test.js @@ -31,7 +31,7 @@ describe('test rules manager selectors', () => { id: 'rules1', priority: 1, roleName: 'role1', - groupAny: '*', + roleAny: '*', userName: '*', userAny: '*', service: '*', diff --git a/web/client/selectors/rulesmanager.js b/web/client/selectors/rulesmanager.js index 209fdf0be0..48a4c4ce22 100644 --- a/web/client/selectors/rulesmanager.js +++ b/web/client/selectors/rulesmanager.js @@ -21,7 +21,7 @@ export const rulesSelector = (state) => { assign(formattedRule, {'id': rule.id}); assign(formattedRule, {'priority': rule.priority}); assign(formattedRule, {'roleName': rule.roleName ? rule.roleName : '*'}); - assign(formattedRule, {'groupAny': rule.groupAny ? rule.groupAny : '*'}); + assign(formattedRule, {'roleAny': rule.roleAny ? rule.roleAny : '*'}); assign(formattedRule, {'userName': rule.userName ? rule.userName : '*'}); assign(formattedRule, {'userAny': rule.userAny ? rule.userAny : '*'}); assign(formattedRule, {'service': rule.service ? rule.service : '*'}); diff --git a/web/client/themes/default/less/autocomplete.less b/web/client/themes/default/less/autocomplete.less index 31eb94ac66..d8b496477e 100644 --- a/web/client/themes/default/less/autocomplete.less +++ b/web/client/themes/default/less/autocomplete.less @@ -38,6 +38,7 @@ .rw-combobox .rw-btn { overflow: hidden; } -.d-flex{ +.autocompleteField.d-flex{ display: flex; + height: 100%; } From 76207ab454f82ebea58f10e08771140cc0467962 Mon Sep 17 00:00:00 2001 From: Suren Date: Mon, 20 Nov 2023 14:38:28 +0530 Subject: [PATCH 04/15] #9589: Notify data projection not compatible / available for COG (#9690) --- web/client/components/TOC/DefaultLayer.jsx | 31 ++++++++++++--- .../TOC/__tests__/DefaultLayer-test.jsx | 39 +++++++++++++++++++ .../components/TOC/fragments/LayersTool.jsx | 3 +- web/client/components/catalog/RecordItem.jsx | 16 ++++++-- .../catalog/__tests__/RecordItem-test.jsx | 18 ++++++++- .../map/openlayers/plugins/COGLayer.js | 4 ++ web/client/translations/data.de-DE.json | 1 + web/client/translations/data.en-US.json | 1 + web/client/translations/data.es-ES.json | 1 + web/client/translations/data.fr-FR.json | 1 + web/client/translations/data.it-IT.json | 1 + 11 files changed, 104 insertions(+), 12 deletions(-) diff --git a/web/client/components/TOC/DefaultLayer.jsx b/web/client/components/TOC/DefaultLayer.jsx index 254d2fe511..2fb8f61ada 100644 --- a/web/client/components/TOC/DefaultLayer.jsx +++ b/web/client/components/TOC/DefaultLayer.jsx @@ -10,7 +10,7 @@ import React from 'react'; import PropTypes from 'prop-types'; import Node from './Node'; -import { isObject, castArray, find } from 'lodash'; +import { isObject, castArray, find, isNil } from 'lodash'; import { Grid, Row, Col, Glyphicon } from 'react-bootstrap'; import draggableComponent from './enhancers/draggableComponent'; import VisibilityCheck from './fragments/VisibilityCheck'; @@ -23,6 +23,7 @@ import tooltip from '../misc/enhancers/tooltip'; import localizedProps from '../misc/enhancers/localizedProps'; import { isInsideResolutionsLimits, getLayerTypeGlyph } from '../../utils/LayersUtils'; import StyleBasedLegend from './fragments/StyleBasedLegend'; +import { isSRSAllowed } from '../../utils/CoordinatesUtils'; const GlyphIndicator = localizedProps('tooltip')(tooltip(Glyphicon)); @@ -106,6 +107,17 @@ class DefaultLayer extends React.Component { : 'toc.notVisibleZoomOut'; }; + getErrorTooltipParams = () => { + if (!this.isCRSCompatible()) { + return { + tooltip: "toc.sourceCRSNotCompatible", + msgParams: {sourceCRS: this.getSourceCRS()} + }; + } + return { tooltip: "toc.loadingerror" }; + } + getSourceCRS = () => this.props.node?.bbox?.crs || this.props.node?.sourceMetadata?.crs; + renderOpacitySlider = (hideOpacityTooltip) => { return (this.props.activateOpacityTool && this.props.node?.type !== '3dtiles') ? ( { - return this.props.node.loadingError === 'Error' ? + return this.isLayerError() ? () : ( { - return this.props.node.loadingError === 'Error' || isEmpty ? + return this.isLayerError() || isEmpty ? null : ( {other.isDraggable && !isDummy ? this.props.connectDragPreview(head) : head} - {isDummy || !this.props.activateOpacityTool || this.props.node.expanded || !this.props.node.visibility || this.props.node.loadingError === 'Error' ? null : this.renderOpacitySlider(this.props.hideOpacityTooltip)} + {isDummy || !this.props.activateOpacityTool || this.props.node.expanded || !this.props.node.visibility || this.isLayerError() ? null : this.renderOpacitySlider(this.props.hideOpacityTooltip)} {isDummy || isEmpty ? null : this.renderCollapsible()} ); @@ -212,7 +224,7 @@ class DefaultLayer extends React.Component { let {children, propertiesChangeHandler, onToggle, connectDragSource, connectDropTarget, ...other } = this.props; const hide = !this.props.node.visibility || this.props.node.invalid || this.props.node.exclusiveMapType || !isInsideResolutionsLimits(this.props.node, this.props.resolution) ? ' visibility' : ''; const selected = this.props.selectedNodes.filter((s) => s === this.props.node.id).length > 0 ? ' selected' : ''; - const error = this.props.node.loadingError === 'Error' ? ' layer-error' : ''; + const error = this.isLayerError() ? ' layer-error' : ''; const warning = this.props.node.loadingError === 'Warning' ? ' layer-warning' : ''; const grab = other.isDraggable ? : ; const isDummy = !!this.props.node.dummy; @@ -234,6 +246,13 @@ class DefaultLayer extends React.Component { return (title || '').toLowerCase().indexOf(this.props.filterText.toLowerCase()) !== -1; }; + isLayerError = () => this.props.node.loadingError === 'Error' || !this.isCRSCompatible(); + + isCRSCompatible = () => { + const CRS = this.getSourceCRS(); + // Check if source crs is compatible + return !isNil(CRS) ? isSRSAllowed(CRS) : true; + } } export default draggableComponent('LayerOrGroup', DefaultLayer); diff --git a/web/client/components/TOC/__tests__/DefaultLayer-test.jsx b/web/client/components/TOC/__tests__/DefaultLayer-test.jsx index f68aace8dd..2c9ca943d1 100644 --- a/web/client/components/TOC/__tests__/DefaultLayer-test.jsx +++ b/web/client/components/TOC/__tests__/DefaultLayer-test.jsx @@ -466,4 +466,43 @@ describe('test DefaultLayer module component', () => { expect(button.length).toBe(1); } }); + it('test with layer source crs', () => { + // Invalid CRS + let node = { + name: 'layer00', + title: 'Layer', + visibility: false, + storeIndex: 9, + opacity: 0.5, + bbox: { + crs: "EPSG:3946" + } + }; + + let comp = ReactDOM.render(, document.getElementById("container")); + expect(ReactDOM.findDOMNode(comp)).toBeTruthy(); + let layerNode = document.querySelector('.toc-default-layer.layer-error'); + let errorTooltip = document.querySelector('.toc-layer-tool.toc-error'); + expect(layerNode).toBeTruthy(); + expect(errorTooltip).toBeTruthy(); + + // Valid CRS + node = { + name: 'layer00', + title: 'Layer', + visibility: false, + storeIndex: 9, + opacity: 0.5, + bbox: { + crs: "EPSG:4326" + } + }; + + comp = ReactDOM.render(, document.getElementById("container")); + expect(ReactDOM.findDOMNode(comp)).toBeTruthy(); + layerNode = document.querySelector('.toc-default-layer.layer-error'); + errorTooltip = document.querySelector('.toc-layer-tool.toc-error'); + expect(layerNode).toBeFalsy(); + expect(errorTooltip).toBeFalsy(); + }); }); diff --git a/web/client/components/TOC/fragments/LayersTool.jsx b/web/client/components/TOC/fragments/LayersTool.jsx index 2f81933b45..b2b8116d68 100644 --- a/web/client/components/TOC/fragments/LayersTool.jsx +++ b/web/client/components/TOC/fragments/LayersTool.jsx @@ -21,6 +21,7 @@ class LayersTool extends React.Component { style: PropTypes.object, glyph: PropTypes.string, tooltip: PropTypes.string, + msgParams: PropTypes.object, className: PropTypes.string }; @@ -34,7 +35,7 @@ class LayersTool extends React.Component { glyph={this.props.glyph} onClick={() => this.props.onClick(this.props.node)}/>); return this.props.tooltip ? - )}> + )}> {tool} : tool; diff --git a/web/client/components/catalog/RecordItem.jsx b/web/client/components/catalog/RecordItem.jsx index 7fa87dc45b..b290d35047 100644 --- a/web/client/components/catalog/RecordItem.jsx +++ b/web/client/components/catalog/RecordItem.jsx @@ -14,7 +14,7 @@ import { buildSRSMap, getRecordLinks } from '../../utils/CatalogUtils'; -import {isAllowedSRS} from '../../utils/CoordinatesUtils'; +import { isAllowedSRS, isSRSAllowed } from '../../utils/CoordinatesUtils'; import HtmlRenderer from '../misc/HtmlRenderer'; import {parseCustomTemplate} from '../../utils/TemplateUtils'; import {getMessageById} from '../../utils/LocaleUtils'; @@ -125,6 +125,16 @@ class RecordItem extends React.Component { }; + isSRSNotAllowed = (record) => { + if (record.serviceType !== 'cog') { + const ogcReferences = record.ogcReferences || { SRS: [] }; + const allowedSRS = ogcReferences?.SRS?.length > 0 && buildSRSMap(ogcReferences.SRS); + return allowedSRS && !isAllowedSRS(this.props.crs, allowedSRS); + } + const crs = record?.sourceMetadata?.crs; + return crs && !isSRSAllowed(crs); + } + getButtons = (record) => { const links = this.props.showGetCapLinks ? getRecordLinks(record) : []; return [ @@ -136,9 +146,7 @@ class RecordItem extends React.Component { loading: this.state.loading, glyph: 'plus', onClick: () => { - const ogcReferences = record.ogcReferences || { SRS: [] }; - const allowedSRS = ogcReferences?.SRS?.length > 0 && buildSRSMap(ogcReferences.SRS); - if (allowedSRS && !isAllowedSRS(this.props.crs, allowedSRS)) { + if (this.isSRSNotAllowed(record)) { return this.props.onError('catalog.srs_not_allowed'); } this.setState({ loading: true }); diff --git a/web/client/components/catalog/__tests__/RecordItem-test.jsx b/web/client/components/catalog/__tests__/RecordItem-test.jsx index d4933ce90b..16b8bbd2c0 100644 --- a/web/client/components/catalog/__tests__/RecordItem-test.jsx +++ b/web/client/components/catalog/__tests__/RecordItem-test.jsx @@ -605,7 +605,7 @@ describe('This test for RecordItem', () => { } }; let actionsSpy = expect.spyOn(actions, "onError"); - const item = ReactDOM.render(, document.getElementById("container")); @@ -617,6 +617,22 @@ describe('This test for RecordItem', () => { expect(button).toBeTruthy(); button.click(); expect(actionsSpy.calls.length).toBe(1); + + // With source metadata + const record = {...sampleRecord2, serviceType: "cog", sourceMetadata: {crs: "EPSG:3946"}}; + item = ReactDOM.render(, document.getElementById("container")); + expect(item).toBeTruthy(); + + button = TestUtils.findRenderedDOMComponentWithTag( + item, 'button' + ); + expect(button).toBeTruthy(); + button.click(); + expect(actionsSpy.calls.length).toBe(2); + }); it('check add layer with bounding box', (done) => { let actions = { diff --git a/web/client/components/map/openlayers/plugins/COGLayer.js b/web/client/components/map/openlayers/plugins/COGLayer.js index 5941f94d64..591361b86b 100644 --- a/web/client/components/map/openlayers/plugins/COGLayer.js +++ b/web/client/components/map/openlayers/plugins/COGLayer.js @@ -10,6 +10,7 @@ import Layers from '../../../../utils/openlayers/Layers'; import GeoTIFF from 'ol/source/GeoTIFF.js'; import TileLayer from 'ol/layer/WebGLTile.js'; +import { isProjectionAvailable } from '../../../../utils/ProjectionUtils'; function create(options) { return new TileLayer({ @@ -41,5 +42,8 @@ Layers.registerType('cog', { layer.setMaxResolution(newOptions.maxResolution === undefined ? Infinity : newOptions.maxResolution); } return null; + }, + isCompatible: (layer) => { + return isProjectionAvailable(layer?.sourceMetadata?.crs); } }); diff --git a/web/client/translations/data.de-DE.json b/web/client/translations/data.de-DE.json index 122b5b4d7e..3790d2c5b1 100644 --- a/web/client/translations/data.de-DE.json +++ b/web/client/translations/data.de-DE.json @@ -627,6 +627,7 @@ "browseData": "Attributtabelle der ausgewählten Ebene öffnen", "removeLayer": "Ebene entfernen", "loadingerror": "Diese Ebene wurde nicht korrekt geladen oder ist nicht verfügbar", + "sourceCRSNotCompatible": "Die Quelldatenprojektionsdefinition {sourceCRS} des Layers ist in der Anwendung nicht für die Neuprojektion der Daten in der aktuellen Karte verfügbar", "measure": "Messen", "backgroundSwitcher": "Hintergrund", "layers": "Ebenen", diff --git a/web/client/translations/data.en-US.json b/web/client/translations/data.en-US.json index b6456e9a4a..d98c73cd32 100644 --- a/web/client/translations/data.en-US.json +++ b/web/client/translations/data.en-US.json @@ -589,6 +589,7 @@ "browseData": "Open Attribute Table", "removeLayer": "Remove layer", "loadingerror": "The layer has not been loaded correctly or not available.", + "sourceCRSNotCompatible": "The layer's source data projection definition {sourceCRS} is not available in the application for reprojecting the data in the current map", "measure": "Measure", "layers": "Layers", "drawerButton": "Layers", diff --git a/web/client/translations/data.es-ES.json b/web/client/translations/data.es-ES.json index 8afc13b2e5..7fdff34ec1 100644 --- a/web/client/translations/data.es-ES.json +++ b/web/client/translations/data.es-ES.json @@ -589,6 +589,7 @@ "browseData": "Abrir la tabla de atributos", "removeLayer": "Borrar la capa", "loadingerror": "La capa no se ha cargado correctamente o no está disponible", + "sourceCRSNotCompatible": "La definición de proyección de datos de origen de la capa {sourceCRS} no está disponible en la aplicación para reproyectar los datos en el mapa actual", "measure": "Medir", "layers": "Capas", "drawerButton": "Capas", diff --git a/web/client/translations/data.fr-FR.json b/web/client/translations/data.fr-FR.json index 038d9a9790..47e1a55f78 100644 --- a/web/client/translations/data.fr-FR.json +++ b/web/client/translations/data.fr-FR.json @@ -589,6 +589,7 @@ "browseData": "Ouvrir la table d'attribut", "removeLayer": "Supprimer la couche", "loadingerror": "La couche n'a pas été chargée correctement ou n'est pas disponible.", + "sourceCRSNotCompatible": "La définition de projection des données sources de la couche {sourceCRS} n'est pas disponible dans l'application de reprojection des données dans la carte actuelle", "measure": "Mesurer", "layers": "Couches", "drawerButton": "Couches", diff --git a/web/client/translations/data.it-IT.json b/web/client/translations/data.it-IT.json index 96fff0a44c..661d8df4a6 100644 --- a/web/client/translations/data.it-IT.json +++ b/web/client/translations/data.it-IT.json @@ -589,6 +589,7 @@ "browseData": "Apri tabella degli attributi", "removeLayer": "Rimuovi livello", "loadingerror": "Il livello non è stato caricato correttamente o non disponible.", + "sourceCRSNotCompatible": "La definizione di proiezione dei dati di origine del layer {sourceCRS} non è disponibile nell'applicazione per riproiettare i dati nella mappa corrente", "measure": "Misure", "layers": "Livelli", "drawerButton": "Livelli", From 071623420c08af0b521bed6bb9eb96326e4a0dfb Mon Sep 17 00:00:00 2001 From: mahmoud adel <58145645+mahmoudadel54@users.noreply.github.com> Date: Tue, 21 Nov 2023 10:42:06 +0200 Subject: [PATCH 05/15] #9553: Improving readability of long attribute values in attribute table and table widgets (#9701) * #9553: handle showing tooltip on attr table cells once user hovers on cell * #9553: create a separate enhnacer for handleLongText and use it for formatter table cell * #9553: add copyright for the created handleLongTextEnhancer * #9553: handle test cases for handleLongTextEnhancer * #9553: add unit tests for handleLongTextEnhancer * #9553: fix unit test failure for featureTypeToGridColumns formatters * #9553:reset tests.webpack file * Update web/client/components/misc/enhancers/handleLongTextEnhancer.jsx --------- Co-authored-by: Matteo V --- .../data/featuregrid/formatters/index.js | 2 +- .../__tests__/handleLongTextEnhancer-test.jsx | 76 +++++++++++++++++++ .../misc/enhancers/handleLongTextEnhancer.jsx | 50 ++++++++++++ web/client/utils/FeatureGridUtils.js | 4 +- .../utils/__tests__/FeatureGridUtils-test.js | 35 +++++++++ 5 files changed, 165 insertions(+), 2 deletions(-) create mode 100644 web/client/components/misc/enhancers/__tests__/handleLongTextEnhancer-test.jsx create mode 100644 web/client/components/misc/enhancers/handleLongTextEnhancer.jsx diff --git a/web/client/components/data/featuregrid/formatters/index.js b/web/client/components/data/featuregrid/formatters/index.js index 28f2d339a6..5f2e4104e4 100644 --- a/web/client/components/data/featuregrid/formatters/index.js +++ b/web/client/components/data/featuregrid/formatters/index.js @@ -15,7 +15,7 @@ import NumberFormat from '../../../I18N/Number'; import { dateFormats as defaultDateFormats } from "../../../../utils/FeatureGridUtils"; const BooleanFormatter = ({value} = {}) => !isNil(value) ? {value.toString()} : null; -const StringFormatter = ({value} = {}) => !isNil(value) ? reactStringReplace(value, /(https?:\/\/\S+)/g, (match, i) => ( +export const StringFormatter = ({value} = {}) => !isNil(value) ? reactStringReplace(value, /(https?:\/\/\S+)/g, (match, i) => ( {match} )) : null; const NumberFormatter = ({value} = {}) => !isNil(value) ? : null; diff --git a/web/client/components/misc/enhancers/__tests__/handleLongTextEnhancer-test.jsx b/web/client/components/misc/enhancers/__tests__/handleLongTextEnhancer-test.jsx new file mode 100644 index 0000000000..24faa3e6ea --- /dev/null +++ b/web/client/components/misc/enhancers/__tests__/handleLongTextEnhancer-test.jsx @@ -0,0 +1,76 @@ +/** + * 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 expect from 'expect'; + +import React from 'react'; +import ReactDOM from 'react-dom'; +import { handleLongTextEnhancer } from '../handleLongTextEnhancer'; +import { StringFormatter } from '../../../data/featuregrid/formatters'; + +describe("handleLongTextEnhancer enhancer", () => { + beforeEach((done) => { + document.body.innerHTML = '
'; + setTimeout(done); + }); + + afterEach((done) => { + ReactDOM.unmountComponentAtNode(document.getElementById("container")); + document.body.innerHTML = ''; + setTimeout(done); + }); + + it('handleLongTextEnhancer by passing formatter as wrapper', () => { + const EnhancerWithFormatter = ()=> handleLongTextEnhancer(StringFormatter)({ value: "test12334567899999" }); + ReactDOM.render( + , + document.getElementById("container") + ); + expect(document.getElementById("container").innerHTML).toExist(); + expect(document.getElementsByTagName('span').length).toEqual(2); + expect(document.getElementsByTagName('span')[1].innerHTML).toExist(); + }); + + it('handleLongTextEnhancer with by passing td as wrapper', () => { + const wrapper = () => (15234568965); + const EnhancerWithFormatter = ()=> handleLongTextEnhancer(wrapper)({ value: "15234568965" }); + ReactDOM.render( + , + document.getElementById("container") + ); + expect(document.getElementById("container").innerHTML).toExist(); + expect(document.getElementsByTagName('span').length).toEqual(2); + expect(document.getElementsByTagName('span')[1].innerHTML).toExist(); + }); + + + it('handleLongTextEnhancer with by passing span as wrapper', () => { + const wrapper = () => (15234568965); + const EnhancerWithFormatter = ()=> handleLongTextEnhancer(wrapper)({ value: "15234568965" }); + ReactDOM.render( + , + document.getElementById("container") + ); + expect(document.getElementById("container").innerHTML).toExist(); + expect(document.getElementsByTagName('span').length).toEqual(3); + expect(document.getElementsByTagName('span')[1].innerHTML).toExist(); + }); + + + it('handleLongTextEnhancer with by passing td div wrapper', () => { + const wrapper = () => (
test
); + const EnhancerWithFormatter = ()=> handleLongTextEnhancer(wrapper)({ value: "test" }); + ReactDOM.render( + , + document.getElementById("container") + ); + expect(document.getElementById("container").innerHTML).toExist(); + expect(document.getElementsByTagName('span').length).toEqual(2); + expect(document.getElementsByTagName('span')[1].innerHTML).toExist(); + }); +}); diff --git a/web/client/components/misc/enhancers/handleLongTextEnhancer.jsx b/web/client/components/misc/enhancers/handleLongTextEnhancer.jsx new file mode 100644 index 0000000000..7b75d8a321 --- /dev/null +++ b/web/client/components/misc/enhancers/handleLongTextEnhancer.jsx @@ -0,0 +1,50 @@ +/* +* 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 OverlayTrigger from "../OverlayTrigger"; +import { Tooltip } from "react-bootstrap"; +/** + * handleLongTextEnhancer enhancer. Enhances a long text content by adding a tooltip. + * @type {function} + * @name handleLongTextEnhancer + * @memberof components.misc.enhancers + * Wraps [wrapped component with content] to add tooltip for long content if shown content less than the main content + * @param {Component} Wrapped the component wrapped with a tooltip when its content is too long + * @param {object} props the props that contains value content + * @return {Component} the rendered component that renders the content with the tooltip if the content is long or renders the content with no tooltip if not long + * @example + * const wrapper = () = > testtttttttttt + * const Component = ()=> handleLongTextEnhancer(wrapper)(props); + * render (){ + * return + * } + * + */ +export const handleLongTextEnhancer = (Wrapped) => (props) => { + const cellRef = React.useRef(null); + const contentRef = React.useRef(null); + const [isContentOverflowing, setIsContentOverflowing] = React.useState(false); + + const handleMouseEnter = () => { + const cellWidth = cellRef.current.offsetWidth; + const contentWidth = contentRef.current.offsetWidth; + setIsContentOverflowing(contentWidth > cellWidth); + }; + + return ({} : <>} + > +
+ + {} + +
+
); +}; diff --git a/web/client/utils/FeatureGridUtils.js b/web/client/utils/FeatureGridUtils.js index 19e41a2643..190f62cb91 100644 --- a/web/client/utils/FeatureGridUtils.js +++ b/web/client/utils/FeatureGridUtils.js @@ -18,6 +18,7 @@ import { } from './ogc/WFS/base'; import { applyDefaultToLocalizedString } from '../components/I18N/LocalizedString'; +import { handleLongTextEnhancer } from '../components/misc/enhancers/handleLongTextEnhancer'; const getGeometryName = (describe) => get(findGeometryProperty(describe), "name"); const getPropertyName = (name, describe) => name === "geometry" ? getGeometryName(describe) : name; @@ -115,6 +116,7 @@ export const getCurrentPaginationOptions = ({ startPage, endPage }, oldPages, si return { startIndex: nPs[0] * size, maxFeatures: needPages * size }; }; + /** * Utility function to get from a describeFeatureType response the columns to use in the react-data-grid * @param {object} describe describeFeatureType response @@ -146,7 +148,7 @@ export const featureTypeToGridColumns = ( editable, filterable, editor: getEditor(desc, field), - formatter: getFormatter(desc, field), + formatter: handleLongTextEnhancer(getFormatter(desc, field)), filterRenderer: getFilterRenderer(desc, field) }; }); diff --git a/web/client/utils/__tests__/FeatureGridUtils-test.js b/web/client/utils/__tests__/FeatureGridUtils-test.js index 68d081aca3..aafbbac3ea 100644 --- a/web/client/utils/__tests__/FeatureGridUtils-test.js +++ b/web/client/utils/__tests__/FeatureGridUtils-test.js @@ -5,7 +5,10 @@ * 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 ReactDOM from "react-dom"; import expect from 'expect'; + import { updatePages, gridUpdateToQueryUpdate, @@ -18,6 +21,18 @@ import { describe('FeatureGridUtils', () => { + beforeEach((done) => { + document.body.innerHTML = '
'; + setTimeout(done); + }); + + afterEach((done) => { + ReactDOM.unmountComponentAtNode( + document.getElementById("container") + ); + document.body.innerHTML = ""; + setTimeout(done); + }); it('Test updatePages when needPages * size is less then features', () => { const oldFeatures = Array(350); const features = Array(60); @@ -332,6 +347,26 @@ describe('FeatureGridUtils', () => { // test localized alias with empty default expect(featureTypeToGridColumns(describe, columnSettings, [{name: "Test1", alias: {"default": ""}}])[0].title.default).toEqual('Test1'); + }); + it('featureTypeToGridColumns formatters', () => { + const DUMMY = () => {}; + const formatterWrapper = () => (
testtttt
); + const describe = {featureTypes: [{properties: [{name: 'Test1', type: "xsd:number"}, {name: 'Test2', type: "xsd:number"}]}]}; + const columnSettings = {name: 'Test1', hide: false}; + const options = [{name: 'Test1', title: 'Some title', description: 'Some description'}]; + const featureGridColumns = featureTypeToGridColumns(describe, columnSettings, [], {options}, {getHeaderRenderer: () => DUMMY, getFilterRenderer: () => DUMMY, getFormatter: () => formatterWrapper, getEditor: () => DUMMY}); + expect(featureGridColumns.length).toBe(2); + featureGridColumns.forEach((fgColumns)=>{ + const Formatter = fgColumns.formatter; + ReactDOM.render( + , + document.getElementById("container") + ); + expect(document.getElementById("container").innerHTML).toExist(); + expect(document.getElementsByTagName('span').length).toEqual(2); + expect(document.getElementsByTagName('span')[1].innerHTML).toExist(); + }); + }); describe("supportsFeatureEditing", () => { it('test supportsFeatureEditing with valid layer type', () => { From d40233dd4e5782603ecc545a2b1644aaa674a9e6 Mon Sep 17 00:00:00 2001 From: mahmoud adel <58145645+mahmoudadel54@users.noreply.github.com> Date: Tue, 21 Nov 2023 11:14:12 +0200 Subject: [PATCH 06/15] #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 --- web/client/actions/__tests__/details-test.js | 12 +- web/client/actions/details.js | 8 +- .../components/details/DetailsPanel.jsx | 8 +- web/client/components/layout/BorderLayout.jsx | 2 +- .../components/resources/ResourceGrid.jsx | 2 +- .../resources/modals/enhancers/handleSave.js | 11 +- web/client/configs/localConfig.json | 10 +- web/client/epics/__tests__/config-test.js | 107 ++++++++++++++++- 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/config.js | 31 +++++ 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 | 18 ++- web/client/plugins/DashboardEditor.jsx | 63 +--------- web/client/plugins/DashboardSave.jsx | 35 +++++- web/client/plugins/Details.jsx | 39 ++++-- web/client/plugins/MapConnectionDashboard.jsx | 75 ++++++++++++ web/client/plugins/SidebarMenu.jsx | 20 ++-- .../plugins/__tests__/DashboardSave-test.jsx | 68 ++++++++++- .../plugins/__tests__/SidebarMenu-test.jsx | 19 +++ .../plugins/sidebarmenu/sidebarmenu.less | 5 + web/client/product/plugins.js | 4 +- web/client/reducers/__tests__/config-test.js | 17 ++- web/client/reducers/config.js | 14 ++- web/client/reducers/details.js | 2 +- .../selectors/__tests__/dashboard-test.js | 30 ++++- web/client/selectors/dashboard.js | 5 + web/client/selectors/details.js | 19 +++ web/client/selectors/sidebarmenu.js | 6 + web/client/themes/default/less/dashboard.less | 1 - web/client/themes/default/less/details.less | 5 + web/client/themes/default/less/navbar.less | 2 +- 36 files changed, 818 insertions(+), 151 deletions(-) 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/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/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" }) => (
{ .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.categoryName)) ? + 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/configs/localConfig.json b/web/client/configs/localConfig.json index 51a4ee8638..dadcf5f379 100644 --- a/web/client/configs/localConfig.json +++ b/web/client/configs/localConfig.json @@ -744,7 +744,15 @@ "FeedbackMask" ], "dashboard": [ - "BurgerMenu", + "Details", + "AddWidgetDashboard", + "MapConnectionDashboard", + { + "name": "SidebarMenu", + "cfg" : { + "containerPosition": "columns" + } + }, { "name": "Dashboard" }, diff --git a/web/client/epics/__tests__/config-test.js b/web/client/epics/__tests__/config-test.js index 92c1fe121a..3aa9a63d34 100644 --- a/web/client/epics/__tests__/config-test.js +++ b/web/client/epics/__tests__/config-test.js @@ -8,7 +8,7 @@ import expect from 'expect'; import {head} from 'lodash'; -import {loadMapConfigAndConfigureMap, loadMapInfoEpic, storeDetailsInfoEpic} from '../config'; +import {loadMapConfigAndConfigureMap, loadMapInfoEpic, storeDetailsInfoDashboardEpic, storeDetailsInfoEpic} from '../config'; import {LOAD_USER_SESSION} from '../../actions/usersession'; import { loadMapConfig, @@ -30,6 +30,7 @@ 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'; const api = { getResource: () => Promise.resolve({mapId: 1234}) @@ -345,7 +346,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 +380,107 @@ describe('config epics', () => { }}); }); }); + + 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/config.js b/web/client/epics/config.js index a9fb45ad8a..37dc56b1b6 100644 --- a/web/client/epics/config.js +++ b/web/client/epics/config.js @@ -27,6 +27,8 @@ 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"; @@ -212,3 +214,32 @@ export const storeDetailsInfoEpic = (action$, store) => ); }); }); +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()] : []) + ); + }); + }); 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..c70413f611 --- /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: 10, + tool: ConnectedAddWidget, + priority: 0 + } + } +}); diff --git a/web/client/plugins/Dashboard.jsx b/web/client/plugins/Dashboard.jsx index e8e6964edd..04d30e5f87 100644 --- a/web/client/plugins/Dashboard.jsx +++ b/web/client/plugins/Dashboard.jsx @@ -47,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( @@ -183,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 c5649d1cf6..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, isDashboardAvailable } 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; } } @@ -153,7 +98,7 @@ const Plugin = connect( isDashboardEditing, isDashboardLoading, isDashboardAvailable, - (editing, loading, isDashboardOpened) => ({ editing, loading, isDashboardOpened }) + (editing, isDashboardOpened) => ({ editing, isDashboardOpened }) ), { setEditing, onMount: () => setEditorAvailable(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/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..86da06303b --- /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: 10, + priority: 0 + } + } +}); 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/__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/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/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 81d88c60db..f843c6cb60 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/config.js b/web/client/reducers/config.js index ef022e6353..74a16041cd 100644 --- a/web/client/reducers/config.js +++ b/web/client/reducers/config.js @@ -98,8 +98,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, { @@ -108,6 +109,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/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/themes/default/less/dashboard.less b/web/client/themes/default/less/dashboard.less index 4fdcee055b..8758eb0ea6 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; From 08ca6a959728cd394f7a13a6322c5abf06e728cf Mon Sep 17 00:00:00 2001 From: Muhammad Umair Date: Tue, 21 Nov 2023 20:37:52 +0500 Subject: [PATCH 07/15] Fix #9627 replace quill editor with draftjs editor in dashboard text widget (#9696) --------- Co-authored-by: allyoucanmap --- web/client/plugins/WidgetsBuilder.jsx | 1 + web/client/themes/default/less/common.less | 12 +++++++++++- web/client/themes/default/less/wizard.less | 5 +++++ 3 files changed, 17 insertions(+), 1 deletion(-) diff --git a/web/client/plugins/WidgetsBuilder.jsx b/web/client/plugins/WidgetsBuilder.jsx index cf7f00030b..fb3ae176e8 100644 --- a/web/client/plugins/WidgetsBuilder.jsx +++ b/web/client/plugins/WidgetsBuilder.jsx @@ -81,6 +81,7 @@ class SideBarComponent extends React.Component { size={this.props.dockSize} zIndex={this.props.zIndex} position={this.props.position} + className="widgets-builder" bsStyle="primary" hideHeader style={{...this.props.layout, background: "white"}}> diff --git a/web/client/themes/default/less/common.less b/web/client/themes/default/less/common.less index 08eb07bf8c..8fff71902c 100644 --- a/web/client/themes/default/less/common.less +++ b/web/client/themes/default/less/common.less @@ -408,7 +408,17 @@ div#sync-popover.popover { position: absolute; transform: translate(-50%, 0px); } - } +} + +// forced to avoid conflicts with no-image display for not found at link +.rdw-link-decorator-icon +{ + width: 15px !important; + min-width: 10px !important; + min-height: 15px !important; + position: static !important; + vertical-align: text-top; +} .form-control.intl-numeric { float: unset; diff --git a/web/client/themes/default/less/wizard.less b/web/client/themes/default/less/wizard.less index 2fd71f6de8..51aa62ee01 100644 --- a/web/client/themes/default/less/wizard.less +++ b/web/client/themes/default/less/wizard.less @@ -168,3 +168,8 @@ flex-direction: row-reverse; } } + +.widgets-builder .ms2-border-layout-content { + position: relative; + overflow: auto; +} From 44b8131bc3066b2a7ebf662775f3250f6289d30a Mon Sep 17 00:00:00 2001 From: Matteo V Date: Wed, 22 Nov 2023 14:53:32 +0100 Subject: [PATCH 08/15] Fix #9295 added better handling of format in csw service (#9712) * Fix #9295 added better handling of format in csw service --- web/client/actions/catalog.js | 2 + .../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 +- .../misc/popover/DisposablePopover.jsx | 2 +- web/client/epics/__tests__/catalog-test.js | 6 +- web/client/epics/catalog.js | 16 +- web/client/plugins/MetadataExplorer.jsx | 4 +- web/client/reducers/catalog.js | 8 +- web/client/selectors/catalog.js | 1 + web/client/themes/default/less/catalog.less | 8 + web/client/themes/default/less/common.less | 7 +- web/client/translations/data.de-DE.json | 6 +- web/client/translations/data.en-US.json | 3 + web/client/translations/data.es-ES.json | 3 + web/client/translations/data.fr-FR.json | 3 + web/client/translations/data.it-IT.json | 3 + 22 files changed, 219 insertions(+), 197 deletions(-) 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/components/catalog/CatalogServiceEditor.jsx b/web/client/components/catalog/CatalogServiceEditor.jsx index 5eb06219a5..351000fd6d 100644 --- a/web/client/components/catalog/CatalogServiceEditor.jsx +++ b/web/client/components/catalog/CatalogServiceEditor.jsx @@ -46,6 +46,7 @@ const CatalogServiceEditor = ({ formatOptions, buttonStyle, saving, + showFormatError, onChangeServiceFormat = () => {}, onChangeMetadataTemplate = () => {}, onToggleAdvancedSettings = () => { }, @@ -100,6 +101,7 @@ const CatalogServiceEditor = ({ currentWMSCatalogLayerSize={layerOptions.tileSize ? layerOptions.tileSize : 256} selectedService={selectedService} onFormatOptionsFetch={onFormatOptionsFetch} + showFormatError={showFormatError} formatsLoading={formatsLoading} infoFormatOptions={infoFormatOptions} autoSetVisibilityLimits={autoSetVisibilityLimits} diff --git a/web/client/components/catalog/__tests__/CatalogServiceEditor-test.jsx b/web/client/components/catalog/__tests__/CatalogServiceEditor-test.jsx index f2f16f1bee..90865ca442 100644 --- a/web/client/components/catalog/__tests__/CatalogServiceEditor-test.jsx +++ b/web/client/components/catalog/__tests__/CatalogServiceEditor-test.jsx @@ -58,12 +58,9 @@ describe('Test CatalogServiceEditor', () => { layerOptions={{tileSize: 256}} />, document.getElementById("container")); - const formatFormGroups = [...document.querySelectorAll('.form-group')].filter(fg => { - const labels = [...fg.querySelectorAll('label')]; - return labels.length === 1 && labels[0].textContent === 'layerProperties.format.title'; - }); - expect(formatFormGroups.length).toBe(1); - const formatSelect = formatFormGroups[0].querySelector('.Select-value-label'); + const formatFormGroups = [...document.querySelectorAll('.form-group-flex')]; + expect(formatFormGroups.length).toBe(5); + const formatSelect = formatFormGroups[2].querySelector('.Select-value-label'); expect(formatSelect).toExist(); expect(formatSelect.textContent).toBe('image/png8'); // expect(formatSelect.props.options).toEqual(formatOptions); TODO: test properties are passed to select diff --git a/web/client/components/catalog/editor/AdvancedSettings/CSWFilters.jsx b/web/client/components/catalog/editor/AdvancedSettings/CSWFilters.jsx index 96bf34715b..29e440f891 100644 --- a/web/client/components/catalog/editor/AdvancedSettings/CSWFilters.jsx +++ b/web/client/components/catalog/editor/AdvancedSettings/CSWFilters.jsx @@ -1,6 +1,5 @@ import React, { useState, useEffect } from "react"; import { - Col, ControlLabel, FormGroup, Glyphicon, @@ -59,14 +58,14 @@ const FilterCode = ({ type, code, setCode, error }) => { const filterProp = `${type}Filter`; return ( - +
{error[type] && renderError} - - +
+
{ }} /> {type === 'dynamic' && renderHelpText} - +
); }; diff --git a/web/client/components/catalog/editor/AdvancedSettings/CommonAdvancedSettings.jsx b/web/client/components/catalog/editor/AdvancedSettings/CommonAdvancedSettings.jsx index 45900f3d36..5368fe49b9 100644 --- a/web/client/components/catalog/editor/AdvancedSettings/CommonAdvancedSettings.jsx +++ b/web/client/components/catalog/editor/AdvancedSettings/CommonAdvancedSettings.jsx @@ -7,7 +7,7 @@ */ import React from 'react'; import { isNil } from 'lodash'; -import { FormGroup, Checkbox, Col } from "react-bootstrap"; +import { FormGroup, Checkbox } from "react-bootstrap"; import Message from "../../../I18N/Message"; import InfoPopover from '../../../widgets/widget/InfoPopover'; @@ -25,44 +25,36 @@ export default ({ onChangeServiceProperty = () => { }, onToggleThumbnail = () => { } }) => ( -
+ <> - - {service.autoload !== undefined && onChangeServiceProperty("autoload", e.target.checked)} - checked={!isNil(service.autoload) ? service.autoload : false}> - - } - + {service.autoload !== undefined && onChangeServiceProperty("autoload", e.target.checked)} + checked={!isNil(service.autoload) ? service.autoload : false}> + + } - - onToggleThumbnail()} - checked={!isNil(service.hideThumbnail) ? !service.hideThumbnail : true}> - - - + onToggleThumbnail()} + checked={!isNil(service.hideThumbnail) ? !service.hideThumbnail : true}> + + {!isNil(service.type) && service.type === "wfs" && - - onChangeServiceProperty("allowUnsecureLayers", e.target.checked)} - checked={!isNil(service.allowUnsecureLayers) ? service.allowUnsecureLayers : false}> -  } /> - - + onChangeServiceProperty("allowUnsecureLayers", e.target.checked)} + checked={!isNil(service.allowUnsecureLayers) ? service.allowUnsecureLayers : false}> +  } /> + } {!isNil(service.type) && service.type === "cog" && - - onChangeServiceProperty("fetchMetadata", e.target.checked)} - checked={!isNil(service.fetchMetadata) ? service.fetchMetadata : true}> -  } /> - - + onChangeServiceProperty("fetchMetadata", e.target.checked)} + checked={!isNil(service.fetchMetadata) ? service.fetchMetadata : true}> +  } /> + } {children} -
+ ); diff --git a/web/client/components/catalog/editor/AdvancedSettings/RasterAdvancedSettings.js b/web/client/components/catalog/editor/AdvancedSettings/RasterAdvancedSettings.js index b5abdf7f04..42a7909dac 100644 --- a/web/client/components/catalog/editor/AdvancedSettings/RasterAdvancedSettings.js +++ b/web/client/components/catalog/editor/AdvancedSettings/RasterAdvancedSettings.js @@ -6,22 +6,22 @@ * LICENSE file in the root directory of this source tree. */ import React, {useEffect} from 'react'; -import {FormGroup, Col, ControlLabel, Checkbox, Button as ButtonRB, Glyphicon } from "react-bootstrap"; +import {FormGroup, ControlLabel, Checkbox, Button as ButtonRB, Glyphicon, InputGroup } from "react-bootstrap"; import RS from 'react-select'; -import localizedProps from '../../../misc/enhancers/localizedProps'; -const Select = localizedProps('noResultsText')(RS); +import {isNil, camelCase} from "lodash"; +import localizedProps from '../../../misc/enhancers/localizedProps'; import CommonAdvancedSettings from './CommonAdvancedSettings'; -import {isNil, camelCase} from "lodash"; import ReactQuill from '../../../../libs/quill/react-quill-suspense'; import { ServerTypes } from '../../../../utils/LayersUtils'; - import InfoPopover from '../../../widgets/widget/InfoPopover'; import CSWFilters from "./CSWFilters"; import Message from "../../../I18N/Message"; import WMSDomainAliases from "./WMSDomainAliases"; import tooltip from '../../../misc/enhancers/buttonTooltip'; + const Button = tooltip(ButtonRB); +const Select = localizedProps('noResultsText')(RS); /** * Generates an array of options in the form e.g. [{value: "256", label: "256x256"}] @@ -58,6 +58,7 @@ const getServerTypeOptions = () => { * */ export default ({ + showFormatError, service, formatOptions = [], infoFormatOptions = [], @@ -82,52 +83,42 @@ export default ({ const serverTypeOptions = getServerTypeOptions(); return ( {(isLocalizedLayerStylesEnabled && !isNil(service.type) ? service.type === "wms" : false) && ( - - onChangeServiceProperty("localizedLayerStyles", e.target.checked)} - checked={!isNil(service.localizedLayerStyles) ? service.localizedLayerStyles : false}> -  } /> - - + onChangeServiceProperty("localizedLayerStyles", e.target.checked)} + checked={!isNil(service.localizedLayerStyles) ? service.localizedLayerStyles : false}> +  } /> + )} - - onChangeServiceProperty("autoSetVisibilityLimits", e.target.checked)} - checked={!isNil(service.autoSetVisibilityLimits) ? service.autoSetVisibilityLimits : false}> -  } /> - - + onChangeServiceProperty("autoSetVisibilityLimits", e.target.checked)} + checked={!isNil(service.autoSetVisibilityLimits) ? service.autoSetVisibilityLimits : false}> +  } /> + {!isNil(service.type) && service.type === "wms" && - - onChangeServiceProperty("layerOptions", { ...service.layerOptions, singleTile: e.target.checked })} - checked={!isNil(service?.layerOptions?.singleTile) ? service.layerOptions.singleTile : false}> -  } /> - - + onChangeServiceProperty("layerOptions", { ...service.layerOptions, singleTile: e.target.checked })} + checked={!isNil(service?.layerOptions?.singleTile) ? service.layerOptions.singleTile : false}> +  } /> + } {!isNil(service.type) && service.type === "wms" && - - onChangeServiceProperty("allowUnsecureLayers", e.target.checked)} - checked={!isNil(service.allowUnsecureLayers) ? service.allowUnsecureLayers : false}> -  } /> - - + onChangeServiceProperty("allowUnsecureLayers", e.target.checked)} + checked={!isNil(service.allowUnsecureLayers) ? service.allowUnsecureLayers : false}> +  } /> + } {(!isNil(service.type) ? (service.type === "csw" && !service.excludeShowTemplate) : false) && ( - - onToggleTemplate()} - checked={service && service.showTemplate}> - - -
- + onToggleTemplate()} + checked={service && service.showTemplate}> + + +
{service && service.showTemplate && - ( + (

@@ -138,8 +129,8 @@ export default ({ {" ${ description }"} - )} - +

)} +
{service && service.showTemplate && } - +
)} - - - - - -
- onChangeServiceProperty("layerOptions", { ...service.layerOptions, serverType: event?.value })} /> + - - - - - -
- 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)} /> + - - - - - + + + + onChangeServiceProperty("layerOptions", { ...service.layerOptions, tileSize: event && event.value })} /> - - - - - - - -