From d4e4073d222c21f1fd6bd094695b3151027d413b Mon Sep 17 00:00:00 2001 From: allyoucanmap Date: Tue, 12 May 2026 17:29:29 +0200 Subject: [PATCH 1/2] Fix #2532 Replace of DatasetCatalog with the new Catalog --- geonode_mapstore_client/client/MapStore2 | 2 +- .../client/js/api/geonode/v2/facets.js | 198 ----------- .../client/js/api/geonode/v2/index.js | 34 -- .../js/epics/__tests__/gnresource-test.js | 7 +- .../client/js/epics/datasetscatalog.js | 52 --- .../client/js/epics/gnresource.js | 85 ++--- .../js/observables/persistence/index.js | 2 +- .../client/js/plugins/DatasetsCatalog.jsx | 138 -------- .../client/js/plugins/MapViewersCatalog.jsx | 4 +- .../client/js/plugins/index.js | 8 +- .../client/js/utils/LocaleUtils.js | 62 ---- .../client/js/utils/ResourceUtils.js | 318 +----------------- .../js/utils/__tests__/LocaleUtils-test.js | 26 -- .../js/utils/__tests__/ResourceUtils-test.js | 222 ------------ geonode_mapstore_client/context_processors.py | 20 +- .../static/mapstore/configs/localConfig.json | 81 +++-- .../mapstore/configs/pluginsConfig.json | 24 +- tutorials/01-settings-variables.md | 8 + 18 files changed, 163 insertions(+), 1128 deletions(-) delete mode 100644 geonode_mapstore_client/client/js/api/geonode/v2/facets.js delete mode 100644 geonode_mapstore_client/client/js/epics/datasetscatalog.js delete mode 100644 geonode_mapstore_client/client/js/plugins/DatasetsCatalog.jsx delete mode 100644 geonode_mapstore_client/client/js/utils/LocaleUtils.js delete mode 100644 geonode_mapstore_client/client/js/utils/__tests__/LocaleUtils-test.js diff --git a/geonode_mapstore_client/client/MapStore2 b/geonode_mapstore_client/client/MapStore2 index 277de13970..82ac01e300 160000 --- a/geonode_mapstore_client/client/MapStore2 +++ b/geonode_mapstore_client/client/MapStore2 @@ -1 +1 @@ -Subproject commit 277de13970b9f3c61465f7a0c859eca9848f6eae +Subproject commit 82ac01e30011d6b60f9ee003e2221aa433537d2b diff --git a/geonode_mapstore_client/client/js/api/geonode/v2/facets.js b/geonode_mapstore_client/client/js/api/geonode/v2/facets.js deleted file mode 100644 index ceb8111b2a..0000000000 --- a/geonode_mapstore_client/client/js/api/geonode/v2/facets.js +++ /dev/null @@ -1,198 +0,0 @@ -/* - * Copyright 2025, 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 { castArray, isEmpty } from 'lodash'; -import axios from '@mapstore/framework/libs/ajax'; -import { - paramsSerializer -} from '@js/utils/APIUtils'; -import { - FACETS, - getEndpointUrl, - getQueryParams -} from './constants'; -import { parseIcon } from '@js/utils/SearchUtils'; -import { addFilters, getFilterByField } from '@mapstore/framework/utils/ResourcesFiltersUtils'; -import { getCustomMenuFilters } from '@js/selectors/config'; - -const parseTopicsItems = (items = [], { facet, style }) => { - return items.map((item) => { - const value = String(item.key); - return { - type: "filter", - // TODO remove when api send isLocalized for all facets response - ...(item.is_localized ? { labelId: item.label } : { label: item.label }), - count: item.count ?? 0, - filterKey: facet.filter, - filterValue: value, - value, - style, - facetName: facet.name, - icon: parseIcon(item.fa_class), - image: item.image - }; - }); -}; - -const applyFacetToFields = (fields, facets = [], { customFilters }) => { - return fields.map((field) => { - if (field.facet) { - const filteredFacetsByType = facets - .filter(f => f.type === field.facet) - .filter(f => field.include - ? field.include?.includes(f.name) - : field.exclude - ? !field.exclude?.includes(f.name) - : true); - if (!filteredFacetsByType.length) { - return null; - } - - return filteredFacetsByType.map((facet) => { - const facetConfig = facet.config || {}; - const style = facetConfig.style || field.style; - const type = facetConfig.type || field.type; - const order = facetConfig.order || field.order; - const isLocalized = !!facet.is_localized; - const label = facet.label; - return { - id: facet.name, - name: facet.name, - type, - style, - order, - facet: field.facet, - ...(isLocalized ? { labelId: label } : { label }), - key: facet.filter, - loadItems: ({ params, config }) => { - const { q, ...updatedParams } = getQueryParams(params, customFilters); - return axios.get(getEndpointUrl(FACETS, `/${facet.name}`), { - ...config, - params: { - ...(q && { topic_contains: q }), - ...updatedParams - }, - ...paramsSerializer() - }) - .then(({ data }) => { - const topics = data?.topics ?? {}; - const pageSize = topics?.page_size; - const page = Number(topics.page); - const total = topics?.total; - const isNextPageAvailable = (Math.ceil(Number(total) / Number(pageSize)) - (page + 1)) !== 0; - const items = parseTopicsItems(topics.items, { facet, style }); - const filterField = { key: facet.filter, style, name: facet.name }; - // if the items are empty and items are selected - // we should still see them with count equal 0 - // to allow user to deselect the filter - // TODO: review if possible to move this control in accordion - const facetQuery = updatedParams[facet.filter]; - if (!isEmpty(facetQuery) && items.length === 0) { - - const appliedFilters = castArray(facetQuery) - .map((val) => getFilterByField(filterField, val)) - .filter(Boolean) - .map(appliedFilter => ({ ...appliedFilter, count: 0 })); - // store all filters information - addFilters(filterField, appliedFilters); - - return { - isNextPageAvailable, - items: appliedFilters - }; - } - - // store all filters information - addFilters(filterField, items); - - return { - isNextPageAvailable, - items - }; - }); - } - }; - }); - } - if (field.items) { - return { - ...field, - items: applyFacetToFields(field.items, facets, { customFilters }) - }; - } - return field; - }).flat().filter(val => val).sort((a, b) => a.order - b.order); -}; - -let facetsCache; - -const findFacetField = (facet, fields) => { - return fields.filter((field) => field.facet && field.id === facet.name - ? true - : field.items - ? findFacetField(facet, field.items) - : false).flat()[0]; -}; - -const updateFacets = (fields, facets = [], query = {}) => { - const queryFacets = facets.filter(facet => query[facet.filter]); - if (queryFacets.length) { - return axios.all(queryFacets.map((queryFacet) => { - const field = findFacetField(queryFacet, fields); - if (!field) { - return Promise.resolve({}); - } - const keys = castArray(query[queryFacet.filter]); - const style = queryFacet?.config?.style || field.style; - return keys.map((key) => { - const { q, ...params } = query; - return axios.get(getEndpointUrl(FACETS, `/${queryFacet.name}`), { - params: { - ...params, - ...(q && { topic_contains: q }), - include_topics: true, - key - }, - ...paramsSerializer() - }) - .then(({ data } = {}) => { - const filterField = { key: queryFacet.filter, style, name: queryFacet.name }; - const topics = data?.topics ?? {}; - const items = parseTopicsItems(topics.items, { facet: queryFacet, style }); - // store all filters information - addFilters(filterField, items); - return {}; - }) - .catch(() => ({})); - }); - }).flat()); - } - return Promise.resolve({}); -}; - -export const getFacetItems = ({ - fields, - query, - monitoredState -}) => { - const customFilters = getCustomMenuFilters(monitoredState); - const updatedParams = getQueryParams(query, customFilters); - return ( - !facetsCache - ? axios.get(getEndpointUrl(FACETS), { params: { include_config: true }}) - .then(({ data } = {}) => { - facetsCache = data?.facets; - return { fields: applyFacetToFields(fields, facetsCache, { customFilters }) }; - }) - .catch(() => ({ fields: applyFacetToFields(fields, [], { customFilters }) })) - : Promise.resolve({ fields: applyFacetToFields(fields, facetsCache, { customFilters }) }) - ).then((payload) => { - // update information of the current selected facet filters - return updateFacets(payload.fields, facetsCache, updatedParams).then(() => payload); - }); -}; diff --git a/geonode_mapstore_client/client/js/api/geonode/v2/index.js b/geonode_mapstore_client/client/js/api/geonode/v2/index.js index 0c8a8355d5..0e8e02fc82 100644 --- a/geonode_mapstore_client/client/js/api/geonode/v2/index.js +++ b/geonode_mapstore_client/client/js/api/geonode/v2/index.js @@ -134,39 +134,6 @@ export const getMaps = ({ }); }; -export const getDatasets = ({ - q, - pageSize = 20, - page = 1, - sort -}) => { - return axios - .get( - getEndpointUrl(RESOURCES), { - // axios will format query params array to `key[]=value1&key[]=value2` - params: { - 'filter{resource_type.in}': 'dataset', - 'filter{metadata_only}': false, - ...(q && { - search: q, - search_index: getResourcesSearchIndex() - }), - ...(sort && { sort: isArray(sort) ? sort : [ sort ]}), - page, - page_size: pageSize, - api_preset: API_PRESET.CATALOGS - }, - ...paramsSerializer() - }) - .then(({ data }) => { - return { - totalCount: data.total, - isNextPageAvailable: !!data.links.next, - resources: (data.resources || []) - }; - }); -}; - export const getDocuments = ({ q, pageSize = 10, @@ -843,7 +810,6 @@ export default { deleteResource, copyResource, downloadResource, - getDatasets, deleteExecutionRequest, getResourceByTypeAndByPk, createDataset, diff --git a/geonode_mapstore_client/client/js/epics/__tests__/gnresource-test.js b/geonode_mapstore_client/client/js/epics/__tests__/gnresource-test.js index 8c56571d16..c199c0f304 100644 --- a/geonode_mapstore_client/client/js/epics/__tests__/gnresource-test.js +++ b/geonode_mapstore_client/client/js/epics/__tests__/gnresource-test.js @@ -29,6 +29,7 @@ import { } from '@js/actions/gnresource'; import { clickOnMap } from '@mapstore/framework/actions/map'; import { SET_CONTROL_PROPERTY } from '@mapstore/framework/actions/controls'; +import { CATALOG_CLOSE } from '@mapstore/framework/actions/catalog'; import { SHOW_NOTIFICATION } from '@mapstore/framework/actions/notifications'; @@ -194,7 +195,7 @@ describe('gnresource epics', () => { requests: ["something"] }, controls: { - datasetsCatalog: { + metadataexplorer: { enabled: true } } @@ -206,9 +207,7 @@ describe('gnresource epics', () => { (actions) => { try { expect(actions.length).toBe(1); - expect(actions[0].type).toBe(SET_CONTROL_PROPERTY); - expect(actions[0].control).toBe("datasetsCatalog"); - expect(actions[0].value).toBe(false); + expect(actions[0].type).toBe(CATALOG_CLOSE); } catch (e) { done(e); } diff --git a/geonode_mapstore_client/client/js/epics/datasetscatalog.js b/geonode_mapstore_client/client/js/epics/datasetscatalog.js deleted file mode 100644 index 3f24a6b06d..0000000000 --- a/geonode_mapstore_client/client/js/epics/datasetscatalog.js +++ /dev/null @@ -1,52 +0,0 @@ -/* - * Copyright 2021, GeoSolutions Sas. - * All rights reserved. - * - * This source code is licensed under the BSD-style license found in the - * LICENSE file in the root directory of this source tree. - */ - -import { SET_CONTROL_PROPERTY } from '@mapstore/framework/actions/controls'; -import { updateMapLayout, UPDATE_MAP_LAYOUT } from '@mapstore/framework/actions/maplayout'; -import { mapLayoutSelector, boundingSidebarRectSelector } from '@mapstore/framework/selectors/maplayout'; -import { getConfigProp } from "@mapstore/framework/utils/ConfigUtils"; -import { LayoutSections } from "@js/utils/LayoutUtils"; - -/** -* @module epics/datasetcatalog -*/ - -/** - * Override the layout to get the correct right offset when the data catalog is open - */ -export const gnUpdateDatasetsCatalogMapLayout = (action$, store) => - action$.ofType(UPDATE_MAP_LAYOUT, SET_CONTROL_PROPERTY) - .filter(() => store.getState()?.controls?.datasetsCatalog?.enabled) - .filter(({ source }) => { - return source !== LayoutSections.PANEL; - }) - .map(({ layout }) => { - const mapLayout = getConfigProp('mapLayout') || { left: { sm: 300, md: 500, lg: 600 }, right: { md: 658 }, bottom: { sm: 30 } }; - const boundingSidebarRect = boundingSidebarRectSelector(store.getState()); - const left = !!store.getState()?.controls?.drawer?.enabled ? mapLayout.left.sm : null; - const action = updateMapLayout({ - ...mapLayoutSelector(store.getState()), - ...layout, - right: mapLayout.right.md, - ...(left && {left}), - boundingMapRect: { - ...(layout?.boundingMapRect || {}), - right: mapLayout.right.md, - ...(left && {left}) - }, - boundingSidebarRect: { - ...boundingSidebarRect, - ...layout?.boundingSidebarRect - } - }); - return { ...action, source: LayoutSections.PANEL }; // add an argument to avoid infinite loop. - }); - -export default { - gnUpdateDatasetsCatalogMapLayout -}; diff --git a/geonode_mapstore_client/client/js/epics/gnresource.js b/geonode_mapstore_client/client/js/epics/gnresource.js index 239a3489a6..dd6dbaaf19 100644 --- a/geonode_mapstore_client/client/js/epics/gnresource.js +++ b/geonode_mapstore_client/client/js/epics/gnresource.js @@ -138,7 +138,7 @@ import { REDUCERS_LOADED } from '@mapstore/framework/actions/storemanager'; import { wrapStartStop } from '@mapstore/framework/observables/epics'; import { parseDevHostname } from '@js/utils/APIUtils'; import { ProcessTypes } from '@js/utils/ResourceServiceUtils'; -import { catalogClose } from '@mapstore/framework/actions/catalog'; +import { catalogClose, addLayerAndDescribe } from '@mapstore/framework/actions/catalog'; import { VisualizationModes } from '@mapstore/framework/utils/MapTypeUtils'; import { forceUpdateMapLayout } from '@mapstore/framework/actions/maplayout'; import { getShowDetails } from '@mapstore/framework/plugins/ResourcesCatalog/selectors/resources'; @@ -206,6 +206,27 @@ const resourceTypes = { const extent = newLayer?.bbox?.bounds && [minx, miny, maxx, maxy ]; const hasNoGeometry = gnLayer?.subtype === 'tabular'; const hasDownloadPermission = gnLayer?.perms?.includes('download_resourcebase'); + const isNewLoad = !options?.isSamePreviousResource; + + // page-specific layer actions shared between the two paths + const pageSpecificActions = [ + selectNode(newLayer.id, 'layer', false), + ...((hasNoGeometry || page === 'dataset_edit_data_viewer') + ? [ + browseData(newLayer), + ...(hasDownloadPermission ? [] : [setDatasetEditPermissionsError('gnviewer.noEditPermissions')]) + ] + : []), + ...(page === 'dataset_edit_layer_settings' + ? [ + showSettings(newLayer.id, "layers", {opacity: newLayer.opacity ?? 1}), + setControlProperty("layersettings", "activeTab", query.tab ?? "general"), + updateAdditionalLayer(newLayer.id, STYLE_OWNER_NAME, 'override', {}), + resizeMap() + ] + : []) + ]; + return Observable.concat( Observable.of( configureMap({ @@ -215,13 +236,11 @@ const resourceTypes = { ...mapLayerData?.mapConfig?.map, zoom: 20, // start zoomed in to mitigate initial tile blurring before the deferred fit lands visualizationMode: ['3dtiles'].includes(subtype) ? VisualizationModes._3D : VisualizationModes._2D, + // on same-resource navigation the layer is already described; + // on new loads it is added via addLayerAndDescribe below layers: [ ...mapConfig.map.layers, - { - ...newLayer, - isDataset: true, - _v_: Date.now() - } + ...(!isNewLoad ? [{ ...newLayer, isDataset: true, _v_: Date.now() }] : []) ] } }), @@ -231,23 +250,15 @@ const resourceTypes = { setProjectionsConfig(mapLayerData?.mapConfig?.crsSelector), setControlProperty('toolbar', 'expanded', false), forceUpdateMapLayout(), - selectNode(newLayer.id, 'layer', false), - ...(!options?.isSamePreviousResource ? [setResource({...gnLayer, hasNoGeometry})] : []), + // on new loads: configureMap (= MAP_CONFIG_LOADED) has already updated + // the layers state, so addLayerAndDescribe safely appends + describes + ...(isNewLoad + ? [addLayerAndDescribe({ ...newLayer, isDataset: true, _v_: Date.now() }, { zoomToLayer: false })] + : [] + ), + ...pageSpecificActions, + ...(isNewLoad ? [setResource({...gnLayer, hasNoGeometry})] : []), setResourceId(pk), - ...((hasNoGeometry || page === 'dataset_edit_data_viewer') - ? [ - browseData(newLayer), - ...(hasDownloadPermission ? [] : [setDatasetEditPermissionsError('gnviewer.noEditPermissions')]) - ] - : []), - ...(page === 'dataset_edit_layer_settings' - ? [ - showSettings(newLayer.id, "layers", {opacity: newLayer.opacity ?? 1}), - setControlProperty("layersettings", "activeTab", query.tab ?? "general"), - updateAdditionalLayer(newLayer.id, STYLE_OWNER_NAME, 'override', {}), - resizeMap() - ] - : []), ...(newLayer?.bboxError ? [warningNotification({ title: "gnviewer.invalidBbox", message: "gnviewer.invalidBboxMsg" })] : []), @@ -338,13 +349,13 @@ const resourceTypes = { ? VisualizationModes._3D : VisualizationModes._2D }), - layers: [ - ...(mapConfig?.map?.layers || []), - newLayer - ] + // configureMap (= MAP_CONFIG_LOADED) updates the layers state + // synchronously, so addLayerAndDescribe below safely appends + describes + layers: [...(mapConfig?.map?.layers || [])] } } : mapConfig), + ...(newLayer ? [addLayerAndDescribe(newLayer, { zoomToLayer: false })] : []), setControlProperty('toolbar', 'expanded', false), // unblock the Viewer route so the Map plugin actually mounts; // see fitBoundsAfterMapReady comment. @@ -651,7 +662,7 @@ export const closeInfoPanelOnMapClick = (action$, store) => action$.ofType(CLICK .switchMap(() => Observable.of(setControlProperty('rightOverlay', 'enabled', false))); -// Check which control is enabled between annotations, datasetsCatalog and documentsCatalog +// Check which control is enabled between annotations, metadataexplorer and documentsCatalog const oneOfTheOther = (control) => { if (control === 'rightOverlay') return null; @@ -659,10 +670,10 @@ const oneOfTheOther = (control) => { if (control === 'annotations') { return { control, - alternates: ['datasetsCatalog', 'documentsCatalog'] + alternates: ['metadataexplorer', 'documentsCatalog'] }; } - if (control === 'datasetsCatalog') { + if (control === 'metadataexplorer') { return { control, alternates: ['annotations', 'documentsCatalog'] @@ -671,7 +682,7 @@ const oneOfTheOther = (control) => { if (control === 'documentsCatalog') { return { control, - alternates: ['annotations', 'datasetsCatalog'] + alternates: ['annotations', 'metadataexplorer'] }; } @@ -693,16 +704,12 @@ export const closeOpenPanels = (action$, store) => action$.ofType(SET_CONTROL_PR if (isMapInfoOpen(state)) { setActions.push(purgeMapInfoResults(), closeIdentify()); } - const isDatasetCatalogPanelOpen = get(state, "controls.datasetsCatalog.enabled"); const isDocumentsCatalogPanelOpen = get(state, "controls.documentsCatalog.enabled"); const isCatalogOpen = get(state, "controls.metadataexplorer.enabled"); const isVisualStyleEditorOpen = get(state, "controls.visualStyleEditor.enabled"); - if ((isDatasetCatalogPanelOpen || isDocumentsCatalogPanelOpen || isVisualStyleEditorOpen) && isCatalogOpen) { + if (isVisualStyleEditorOpen && isCatalogOpen) { setActions.push(catalogClose()); } - if (isDatasetCatalogPanelOpen && isVisualStyleEditorOpen) { - setActions.push(setControlProperty('datasetsCatalog', 'enabled', false)); - } if (isDocumentsCatalogPanelOpen && isVisualStyleEditorOpen) { setActions.push(setControlProperty('documentsCatalog', 'enabled', false)); } @@ -730,21 +737,21 @@ export const closeOpenPanels = (action$, store) => action$.ofType(SET_CONTROL_PR }); /** - * Close dataset and documents panels on map info panel open + * Close catalog and documents panels on map info panel open */ export const closeDatasetCatalogPanel = (action$, store) => action$.ofType(NEW_MAPINFO_REQUEST) .filter(() => { const state = store.getState(); return isMapInfoOpen(state) && - (get(state, "controls.datasetsCatalog.enabled") || + (get(state, "controls.metadataexplorer.enabled") || get(state, "controls.documentsCatalog.enabled")); }) .switchMap(() => { const state = store.getState(); const actions = []; - if (get(state, "controls.datasetsCatalog.enabled")) { - actions.push(setControlProperty('datasetsCatalog', 'enabled', false)); + if (get(state, "controls.metadataexplorer.enabled")) { + actions.push(catalogClose()); } if (get(state, "controls.documentsCatalog.enabled")) { diff --git a/geonode_mapstore_client/client/js/observables/persistence/index.js b/geonode_mapstore_client/client/js/observables/persistence/index.js index 4c02820e46..cf63bad678 100644 --- a/geonode_mapstore_client/client/js/observables/persistence/index.js +++ b/geonode_mapstore_client/client/js/observables/persistence/index.js @@ -9,7 +9,7 @@ import { Observable } from 'rxjs'; import { addApi, setApi } from '@mapstore/framework/api/persistence'; import { getMaps, getMapByPk, getGeoAppByPk, getResources as gnGetResources } from '@js/api/geonode/v2'; -import { getFacetItems } from '@js/api/geonode/v2/facets'; +import { getFacetItems } from '@mapstore/framework/api/GeoNode'; import { parseCatalogResource, ResourceTypes } from '@js/utils/ResourceUtils'; import { getCustomMenuFilters } from '@js/selectors/config'; diff --git a/geonode_mapstore_client/client/js/plugins/DatasetsCatalog.jsx b/geonode_mapstore_client/client/js/plugins/DatasetsCatalog.jsx deleted file mode 100644 index b72aaf673d..0000000000 --- a/geonode_mapstore_client/client/js/plugins/DatasetsCatalog.jsx +++ /dev/null @@ -1,138 +0,0 @@ -/* - * Copyright 2021, GeoSolutions Sas. - * All rights reserved. - * - * This source code is licensed under the BSD-style license found in the - * LICENSE file in the root directory of this source tree. - */ - -import React, { useState } from 'react'; -import PropTypes from 'prop-types'; -import { createPlugin } from '@mapstore/framework/utils/PluginsUtils'; -import { connect } from 'react-redux'; -import { createSelector } from 'reselect'; -import Message from '@mapstore/framework/components/I18N/Message'; -import Button from '@mapstore/framework/components/layout/Button'; -import { getDatasets, getDatasetByPk, getResourceByPk } from '@js/api/geonode/v2'; -import { resourceToLayerConfig, isDefaultDatasetSubtype } from '@js/utils/ResourceUtils'; -import { addLayer } from '@mapstore/framework/actions/layers'; -import { zoomToExtent } from '@mapstore/framework/actions/map'; -import { setControlProperty } from '@mapstore/framework/actions/controls'; -import datasetscatalogEpics from '@js/epics/datasetscatalog'; -import { mapLayoutValuesSelector } from '@mapstore/framework/selectors/maplayout'; -import ResourcesCompactCatalog from '@js/components/ResourcesCompactCatalog'; -import useIsMounted from "@js/hooks/useIsMounted"; - -function DatasetsCatalog({ - onAdd, - onZoomTo, - ...props -}) { - const isMounted = useIsMounted(); - const [loading, setLoading] = useState(false); - - function handleSelectResource(entry) { - setLoading(true); - (isDefaultDatasetSubtype(entry.subtype) - ? getDatasetByPk(entry.pk) - : getResourceByPk(entry.pk)) - .then((dataset) => { - const layer = resourceToLayerConfig(dataset); - onAdd(layer); - const { minx, miny, maxx, maxy } = layer?.bbox?.bounds || {}; - const extent = layer?.bbox?.bounds && [minx, miny, maxx, maxy]; - if (extent) { - onZoomTo(extent, layer?.bbox?.crs); - } - }) - .finally(() => { - isMounted(() => setLoading(false)); - }); - } - - return (); -} - -DatasetsCatalog.propTypes = { - request: PropTypes.func, - responseToEntries: PropTypes.func, - pageSize: PropTypes.number, - onAdd: PropTypes.func, - placeholderId: PropTypes.string, - onClose: PropTypes.func, - onZoomTo: PropTypes.func -}; - -DatasetsCatalog.defaultProps = { - request: getDatasets, - responseToEntries: res => res.resources, - pageSize: 10, - onAdd: () => { }, - placeholderId: 'gnviewer.datasetsCatalogFilterPlaceholder', - titleId: 'gnviewer.datasetsCatalogTitle', - noResultId: 'gnviewer.datasetsCatalogEntriesNoResults', - onZoomTo: () => { }, - onClose: () => { } -}; - -function DatasetsCatalogPlugin({ enabled, ...props }) { - return enabled ? : null; -} - -const ConnectedDatasetsCatalogPlugin = connect( - createSelector([ - state => mapLayoutValuesSelector(state, { height: true }), - state => state?.controls?.datasetsCatalog?.enabled - ], (style, enabled) => ({ - style, - enabled - })), { - onAdd: addLayer, - onClose: setControlProperty.bind(null, 'datasetsCatalog', 'enabled', false), - onZoomTo: zoomToExtent - } -)(DatasetsCatalogPlugin); - -const DatasetsCatalogButton = ({ - onClick, - size, - variant -}) => { - - const handleClickButton = () => { - onClick(); - }; - - return ( - - ); -}; - -const ConnectedDatasetsCatalogButton = connect( - createSelector([], () => ({})), - { - onClick: setControlProperty.bind(null, 'datasetsCatalog', 'enabled', true) - } -)((DatasetsCatalogButton)); - -export default createPlugin('DatasetsCatalog', { - component: ConnectedDatasetsCatalogPlugin, - containers: { - ActionNavbar: { - name: 'DatasetsCatalog', - Component: ConnectedDatasetsCatalogButton - } - }, - epics: datasetscatalogEpics, - reducers: {} -}); diff --git a/geonode_mapstore_client/client/js/plugins/MapViewersCatalog.jsx b/geonode_mapstore_client/client/js/plugins/MapViewersCatalog.jsx index 1b5b28048f..79e2477cf9 100644 --- a/geonode_mapstore_client/client/js/plugins/MapViewersCatalog.jsx +++ b/geonode_mapstore_client/client/js/plugins/MapViewersCatalog.jsx @@ -18,7 +18,6 @@ import Button from '@mapstore/framework/components/layout/Button'; import { getGeoApps } from '@js/api/geonode/v2'; import { getDefaultPluginsConfig } from '@js/api/geonode/config'; import { setControlProperty } from '@mapstore/framework/actions/controls'; -import datasetscatalogEpics from '@js/epics/datasetscatalog'; import contextcreatorEpics from '@js/epics/contextcreator'; import ResourcesCompactCatalog from '@js/components/ResourcesCompactCatalog'; import ResizableModal from '@mapstore/framework/components/misc/ResizableModal'; @@ -35,7 +34,7 @@ function MapViewersCatalogPlugin({ match, resourcesParams, location, - defaultViewerPlugins = ['Annotations', 'TOC', 'BackgroundSelector', 'Identify', 'QueryPanel', 'Measure', 'Print', 'MousePosition', 'Search', 'ScaleBox', 'GlobeViewSwitcher', 'ZoomAll', 'ZoomIn', 'ZoomOut', 'Timeline', 'MetadataExplorer', 'Widgets'], + defaultViewerPlugins = ['Annotations', 'TOC', 'BackgroundSelector', 'Identify', 'QueryPanel', 'Measure', 'Print', 'MousePosition', 'Search', 'ScaleBox', 'GlobeViewSwitcher', 'ZoomAll', 'ZoomIn', 'ZoomOut', 'Timeline', 'Catalog', 'Widgets'], onReplaceLocation, onSetMapViewer, onManageLinkedResource, @@ -200,7 +199,6 @@ export default createPlugin('MapViewersCatalog', { } }, epics: { - ...datasetscatalogEpics, ...contextcreatorEpics }, reducers: {} diff --git a/geonode_mapstore_client/client/js/plugins/index.js b/geonode_mapstore_client/client/js/plugins/index.js index 34cbed2810..072cbc301b 100644 --- a/geonode_mapstore_client/client/js/plugins/index.js +++ b/geonode_mapstore_client/client/js/plugins/index.js @@ -22,9 +22,9 @@ import Isochrone from "@mapstore/framework/plugins/Isochrone"; import Itinerary from "@mapstore/framework/plugins/Itinerary"; import SecurityPopup from "@mapstore/framework/plugins/SecurityPopup"; import BackgroundSelector from '@mapstore/framework/plugins/BackgroundSelector'; -import MetadataExplorer from '@mapstore/framework/plugins/MetadataExplorer'; import CameraPosition from '@mapstore/framework/plugins/CameraPosition'; import CRSSelector from '@mapstore/framework/plugins/CRSSelector'; +import CatalogPlugin from '@mapstore/framework/plugins/Catalog'; import OperationPlugin from '@js/plugins/Operation'; import ExecutionTrackerPlugin from '@js/plugins/ExecutionTracker'; @@ -94,7 +94,6 @@ export const plugins = { ItineraryPlugin: Itinerary, SecurityPopupPlugin: SecurityPopup, BackgroundSelectorPlugin: BackgroundSelector, - MetadataExplorerPlugin: MetadataExplorer, CameraPositionPlugin: CameraPosition, CRSSelectorPlugin: CRSSelector, LayerDownloadPlugin: toModulePlugin( @@ -443,10 +442,7 @@ export const plugins = { 'DocumentsCatalog', () => import(/* webpackChunkName: 'plugins/documents-catalog' */ '@js/plugins/DocumentsCatalog') ), - DatasetsCatalogPlugin: toModulePlugin( - 'DatasetsCatalog', - () => import(/* webpackChunkName: 'plugins/dataset-catalog' */ '@js/plugins/DatasetsCatalog') - ), + CatalogPlugin, SyncPlugin: toModulePlugin( 'Sync', () => import(/* webpackChunkName: 'plugins/sync-plugin' */ '@js/plugins/Sync') diff --git a/geonode_mapstore_client/client/js/utils/LocaleUtils.js b/geonode_mapstore_client/client/js/utils/LocaleUtils.js deleted file mode 100644 index 8321f1f67d..0000000000 --- a/geonode_mapstore_client/client/js/utils/LocaleUtils.js +++ /dev/null @@ -1,62 +0,0 @@ -/* - * Copyright 2026, 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. - */ - - -/** -* @module utils/LocaleUtils -*/ - -/** - * Normalizes a locale code to a standard format (language-region). - * If the locale code has a region, it returns the language-region combination. - * Otherwise, it maximizes the locale to include region information. - * - * @param {string} code - The locale code to normalize (e.g., 'en', 'en-US', 'it') - * @returns {string} The normalized locale code in language-region format, or an empty string if normalization fails - * - * @example - * longLocale('en'); // Returns 'en-US' (or similar based on system locale) - * longLocale('en-GB'); // Returns 'en-GB' - * longLocale('invalid code'); // Returns '' - * - * shortLocale('en-US'); // Returns 'en' - * shortLocale('it-IT'); // Returns 'it' - * shortLocale('invalid code'); // Returns '' - */ - -/** - * return the normalized locale code in language-region format, 5 chars length e.g. 'en-US', 'it-IT' - * @param {*} code - * @returns {string} The normalized locale code in language-region format, or an empty string if normalization fails - */ -export function longLocale(code) { - if (!code) return ''; - try { - const loc = new Intl.Locale(code); - if (loc.region) return `${loc.language}-${loc.region}`; - const maximized = loc.maximize(); - return `${maximized.language}-${maximized.region}`; - } catch { - return ''; - } -} - -/** - * return the language component of a locale code, or an empty string if shortening fails, length is 2 chars e.g. 'en', 'it' - * @param {string} code - The locale code to shorten (e.g., 'en-US', 'it-IT') - * @returns {string} The language component of the locale code, or an empty string if shortening fails - */ -export function shortLocale(code) { - if (!code) return ''; - try { - const loc = new Intl.Locale(code); - return loc.language; - } catch { - return ''; - } -} diff --git a/geonode_mapstore_client/client/js/utils/ResourceUtils.js b/geonode_mapstore_client/client/js/utils/ResourceUtils.js index ed26abd109..9e91dcc415 100644 --- a/geonode_mapstore_client/client/js/utils/ResourceUtils.js +++ b/geonode_mapstore_client/client/js/utils/ResourceUtils.js @@ -6,64 +6,23 @@ * LICENSE file in the root directory of this source tree. */ -import uuid from 'uuid'; import url from 'url'; import { isEmpty, uniqBy, omit, orderBy, isString, isObject } from 'lodash'; -import { isImageServerUrl } from '@mapstore/framework/utils/ArcGISUtils'; import { getConfigProp, convertFromLegacy, normalizeConfig } from '@mapstore/framework/utils/ConfigUtils'; -import { excludeGoogleBackground, extractTileMatrixFromSources, ServerTypes } from '@mapstore/framework/utils/LayersUtils'; +import { excludeGoogleBackground, extractTileMatrixFromSources } from '@mapstore/framework/utils/LayersUtils'; +import { SOURCE_TYPES, FEATURE_INFO_FORMAT, GXP_PTYPES, ResourceTypes, isDefaultDatasetSubtype, getDimensions, resourceToLayers, resourceToLayerConfig } from '@mapstore/framework/utils/GeoNodeUtils'; import { getGeoNodeLocalConfig, parseDevHostname } from '@js/utils/APIUtils'; import { ProcessTypes, ProcessStatus } from '@js/utils/ResourceServiceUtils'; import { determineResourceType } from '@js/utils/FileUtils'; -import { createDefaultStyle } from '@mapstore/framework/utils/StyleUtils'; -import { getSupportedLocales } from '@mapstore/framework/utils/LocaleUtils'; -import { shortLocale } from '@js/utils/LocaleUtils'; +export { SOURCE_TYPES, FEATURE_INFO_FORMAT, GXP_PTYPES, ResourceTypes, isDefaultDatasetSubtype, getDimensions, resourceToLayers, resourceToLayerConfig }; /** * @module utils/ResourceUtils */ -function getExtentFromResource({ extent }) { - if (isEmpty(extent?.coords)) { - return null; - } - const [minx, miny, maxx, maxy] = extent.coords; - - // if the extent is greater than the max extent of the WGS84 return null - const WGS84_MAX_EXTENT = [-180, -90, 180, 90]; - if (minx < WGS84_MAX_EXTENT[0] || miny < WGS84_MAX_EXTENT[1] || maxx > WGS84_MAX_EXTENT[2] || maxy > WGS84_MAX_EXTENT[3]) { - return { - crs: 'EPSG:4326', - bounds: { - minx: WGS84_MAX_EXTENT[0], - miny: WGS84_MAX_EXTENT[1], - maxx: WGS84_MAX_EXTENT[2], - maxy: WGS84_MAX_EXTENT[3] - } - }; - } - const bbox = { - crs: 'EPSG:4326', - bounds: { minx, miny, maxx, maxy } - }; - return bbox; -} - -export const GXP_PTYPES = { - 'AUTO': 'gxp_wmscsource', - 'OWS': 'gxp_wmscsource', - 'WMS': 'gxp_wmscsource', - 'WFS': 'gxp_wmscsource', - 'WCS': 'gxp_wmscsource', - 'REST_MAP': 'gxp_arcrestsource', - 'REST_IMG': 'gxp_arcrestsource', - 'HGL': 'gxp_hglsource', - 'GN_WMS': 'gxp_geonodecataloguesource' -}; - const RESOURCE_PUBLISHING_PROPERTIES_BASE = { 'is_published': { labelId: 'gnviewer.publishResource', @@ -128,233 +87,6 @@ export const TIME_PRECISION_STEPS = ['years', 'months', 'days', 'hours', 'minute // Formats that support styling in GeoServer. export const STYLE_SUPPORTED_LAYER_TYPES = ['vector', 'raster', 'vector_time']; -export const isDefaultDatasetSubtype = (subtype) => !subtype || ['vector', 'raster', 'remote', 'vector_time'].includes(subtype); - -export const FEATURE_INFO_FORMAT = 'TEMPLATE'; - -export const SOURCE_TYPES = { - LOCAL: 'LOCAL', - REMOTE: 'REMOTE' -}; - -const datasetAttributeSetToFields = ({ attribute_set: attributeSet = [] }) => { - return attributeSet - .filter(({ attribute_type: type }) => !type.includes('gml:')) - .map(({ - attribute, - attribute_label: alias, - attribute_type: type - }) => { - return { - name: attribute, - alias: alias, - type: type - }; - }); -}; - -export const getDimensions = ({links, has_time: hasTime} = {}) => { - const { url: wmsUrl } = links?.find(({ link_type: linkType }) => linkType === 'OGC:WMS') || {}; - const { url: wmtsUrl } = links?.find(({ link_type: linkType }) => linkType === 'OGC:WMTS') || {}; - const dimensions = [ - ...(hasTime ? [{ - name: 'time', - source: { - type: 'multidim-extension', - url: wmtsUrl || (wmsUrl || '').split('/geoserver/')[0] + '/geoserver/gwc/service/wmts' - } - }] : []) - ]; - return dimensions; -}; - - -const getLocalizedValue = (resource, key, locale = '') => { - if (resource[`${key}_${locale}`]) { - return resource[`${key}_${locale}`]; - } - const languageCode = shortLocale(locale); - if (resource[`${key}_${languageCode}`]) { - return resource[`${key}_${languageCode}`]; - } - return null; -}; - -export const getLocalizedValues = (resource, key, defaultValue) => { - const supportedLocales = getSupportedLocales() || {}; - const translations = Object.values(supportedLocales) - .map(({ code }) => { - const value = getLocalizedValue(resource, key, code); - return value ? [code, value] : null; - }) - .filter(value => value !== null); - - if (translations.length) { - return { - ...Object.fromEntries(translations), - 'default': defaultValue - }; - } - return defaultValue; -}; - - -/** -* convert resource layer configuration to a mapstore layer object -* @param {object} resource geonode layer resource -* @return {object} -*/ -export const resourceToLayerConfig = (resource) => { - - const { - alternate, - attribute_set: attributeSet = [], - links = [], - featureinfo_custom_template: template, - title: defaultTitle, - perms, - pk, - default_style: defaultStyle, - ptype, - subtype, - sourcetype, - data - } = resource; - - const layerSettings = data?.layerSettings ?? data; - - const title = getLocalizedValues(resource, 'title', defaultTitle); - - - const bbox = getExtentFromResource(resource); - const defaultStyleParams = defaultStyle && { - defaultStyle: { - title: defaultStyle.sld_title, - name: defaultStyle.workspace ? `${defaultStyle.workspace}:${defaultStyle.name}` : defaultStyle.name - } - }; - - const extendedParams = { - pk, - alternate - }; - - if (subtype === '3dtiles') { - const { url: tilesetUrl } = links.find(({ extension }) => (extension === '3dtiles')) || {}; - return { - id: uuid(), - type: '3dtiles', - title, - url: parseDevHostname(tilesetUrl || ''), - ...(bbox && { bbox }), - visibility: true, - ...layerSettings, - extendedParams - }; - } - if (subtype === 'cog') { - const { url: cogUrl } = links.find(({ extension }) => (extension === 'cog')) || {}; - return { - perms, - id: uuid(), - type: 'cog', - title, - sources: [{ - url: parseDevHostname(cogUrl || '') - }], - ...(bbox && { bbox }), - visibility: true, - ...layerSettings, - extendedParams - }; - } - if (subtype === 'flatgeobuf') { - const defaultGeomType = 'GeometryCollection'; - const geometryType = attributeSet.find(attr => attr.attribute === 'geometryType')?.attribute_type || defaultGeomType; - - const { url: fgbUrl } = links.find(({ extension }) => (extension === 'flatgeobuf')) || {}; - return { - perms, - id: uuid(), - type: 'flatgeobuf', - title, - style: createDefaultStyle({ geometryType }), - url: parseDevHostname(fgbUrl || ''), - ...(bbox && { bbox }), - visibility: true, - ...layerSettings, - extendedParams - }; - } - switch (ptype) { - case GXP_PTYPES.REST_MAP: - case GXP_PTYPES.REST_IMG: { - const { url: arcgisUrl } = links.find(({ mime, link_type: linkType }) => (mime === 'text/html' && linkType === 'image')) || {}; - return { - perms, - id: uuid(), - pk, - type: 'arcgis', - ...(isImageServerUrl(arcgisUrl) - ? { queryable: false } - : { name: alternate.replace('remoteWorkspace:', '') }), - url: arcgisUrl, - ...(bbox && { bbox }), - title, - visibility: true, - ...layerSettings, - extendedParams - }; - } - default: - const { url: wfsUrl } = links.find(({ link_type: linkType }) => linkType === 'OGC:WFS') || {}; - const { url: wmsUrl } = links.find(({ link_type: linkType }) => linkType === 'OGC:WMS') || {}; - - const dimensions = getDimensions(resource); - - const params = wmsUrl && url.parse(wmsUrl, true).query; - const { - defaultLayerFormat = 'image/png', - defaultTileSize = 512 - } = getConfigProp('geoNodeSettings') || {}; - const fields = datasetAttributeSetToFields(resource); - return { - perms, - id: uuid(), - pk, - type: 'wms', - name: alternate, - url: wmsUrl || '', - format: defaultLayerFormat, - ...(wfsUrl && { - search: { - type: 'wfs', - url: wfsUrl - } - }), - ...(bbox ? { bbox } : { bboxError: true }), - ...(template && { - featureInfo: { - format: FEATURE_INFO_FORMAT, - template - } - }), - style: defaultStyleParams?.defaultStyle?.name || '', - title, - tileSize: defaultTileSize, - visibility: true, - ...(params && { params }), - ...(dimensions.length > 0 && ({ dimensions })), - ...(fields && { fields }), - ...(sourcetype === SOURCE_TYPES.REMOTE && !wmsUrl.includes('/geoserver/') && { - serverType: ServerTypes.NO_VENDOR - }), - ...layerSettings, - extendedParams - }; - } -}; - function updateUrlQueryParameter(requestUrl = '', query) { const parsedUrl = url.parse(requestUrl, true); return url.format({ @@ -465,15 +197,6 @@ export const resourceHasPermission = (resource, perm) => { }; -export const ResourceTypes = { - DATASET: 'dataset', - MAP: 'map', - DOCUMENT: 'document', - GEOSTORY: 'geostory', - DASHBOARD: 'dashboard', - VIEWER: 'mapviewer' -}; - export const isDocumentExternalSource = (resource) => { return resource && resource.resource_type === ResourceTypes.DOCUMENT && resource.sourcetype === SOURCE_TYPES.REMOTE; }; @@ -789,8 +512,21 @@ export function toMapStoreMapConfig(resource, baseConfig) { .filter(mLayer => !layers.find(layer => layer.id !== undefined && mLayer?.extra_params?.msId === layer.id)) .map(mLayer => resourceToLayerConfig(mLayer?.dataset)); + const { catalogueServices = {}, catalogueSelectedService = '' } = getConfigProp('geoNodeSettings') || {}; + const existingServices = data?.catalogServices?.services || {}; + const missingServices = Object.fromEntries( + Object.entries(catalogueServices).filter(([key]) => !existingServices[key]) + ); + const catalogServices = Object.keys(missingServices).length > 0 + ? { + services: { ...missingServices, ...existingServices }, + selectedService: data?.catalogServices?.selectedService || catalogueSelectedService + } + : data?.catalogServices; + return { ...data, + ...(catalogServices && { catalogServices }), map: { ...data?.map, layers: [ @@ -1077,28 +813,6 @@ export const parseCatalogResource = (resource, user) => { }; }; -export const resourceToLayers = (resource) => { - if (resource?.resource_type === ResourceTypes.DATASET) { - return [{...resourceToLayerConfig(resource), isDataset: true}]; - } - if (resource.maplayers && resource?.resource_type === ResourceTypes.MAP) { - return resource.maplayers - .map(maplayer => { - maplayer.dataset ? resourceToLayerConfig(maplayer.dataset) : null; - if (maplayer.dataset) { - const layer = resourceToLayerConfig(maplayer.dataset); - return { - ...layer, - style: maplayer.current_style - }; - } - return null; - }) - .filter(value => value); - } - return []; -}; - export const canManageResourcePublishing = (resource) => { const { perms } = resource || {}; const settingsPerms = ['feature_resourcebase', 'change_resourcebase', 'publish_resourcebase']; diff --git a/geonode_mapstore_client/client/js/utils/__tests__/LocaleUtils-test.js b/geonode_mapstore_client/client/js/utils/__tests__/LocaleUtils-test.js deleted file mode 100644 index 8279d12f68..0000000000 --- a/geonode_mapstore_client/client/js/utils/__tests__/LocaleUtils-test.js +++ /dev/null @@ -1,26 +0,0 @@ -/* - * Copyright 2026, 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 { - longLocale, - shortLocale -} from '@js/utils/LocaleUtils'; - -describe('LocaleUtils', () => { - it('longLocale should return language-region format', () => { - expect(longLocale('en')).toMatch(/en-[A-Z]{2}/); - expect(longLocale('en-GB')).toBe('en-GB'); - expect(longLocale('invalid code')).toBe(''); - }); - it('shortLocale should return the language component of a locale code', () => { - expect(shortLocale('en-US')).toBe('en'); - expect(shortLocale('it-IT')).toBe('it'); - expect(shortLocale('invalid code')).toBe(''); - }); -}); diff --git a/geonode_mapstore_client/client/js/utils/__tests__/ResourceUtils-test.js b/geonode_mapstore_client/client/js/utils/__tests__/ResourceUtils-test.js index 39cd3e1da9..2029d317c9 100644 --- a/geonode_mapstore_client/client/js/utils/__tests__/ResourceUtils-test.js +++ b/geonode_mapstore_client/client/js/utils/__tests__/ResourceUtils-test.js @@ -11,7 +11,6 @@ import expect from 'expect'; import get from 'lodash/get'; import set from 'lodash/set'; import { - resourceToLayerConfig, getResourcePermissions, permissionsCompactToLists, availableResourceTypes, @@ -33,7 +32,6 @@ import { getCataloguePath, getResourceWithLinkedResources, getResourceAdditionalProperties, - getDimensions, canManageResourcePublishing, canManageResourceOptions, canManageResourceSettings, @@ -42,63 +40,7 @@ import { canEditMap } from '../ResourceUtils'; -import {setSupportedLocales} from '@mapstore/framework/utils/LocaleUtils'; - describe('Test Resource Utils', () => { - it('should keep the wms params from the url if available', () => { - const newLayer = resourceToLayerConfig({ - alternate: 'geonode:layer_name', - links: [{ - extension: 'html', - link_type: 'OGC:WMS', - name: 'OGC WMS Service', - mime: 'text/html', - url: 'http://localhost:8080/geoserver/wms?map=name&map_resolution=91' - }], - title: 'Layer title', - perms: [], - pk: 1 - }); - expect(newLayer.params).toEqual({ map: 'name', map_resolution: '91' }); - }); - it('test resourceToLayerConfig with layer settings of the dataset', () => { - const newLayer = resourceToLayerConfig({ - alternate: 'geonode:layer_name', - links: [{ - extension: 'html', - link_type: 'OGC:WMS', - name: 'OGC WMS Service', - mime: 'text/html', - url: 'http://localhost:8080/geoserver/wms?map=name&map_resolution=91' - }], - title: 'Layer title', - perms: [], - pk: 1, - data: {opacity: 0.8} - }); - expect(newLayer.opacity).toBe(0.8); - }); - - it('should parse arcgis dataset', () => { - const newLayer = resourceToLayerConfig({ - alternate: 'remoteWorkspace:1', - title: 'Layer title', - perms: [], - links: [{ - extension: 'html', - link_type: 'image', - mime: 'text/html', - name: 'ArcGIS REST ImageServer', - url: 'http://localhost:8080/MapServer' - }], - pk: 1, - ptype: 'gxp_arcrestsource' - }); - expect(newLayer.type).toBe('arcgis'); - expect(newLayer.name).toBe('1'); - expect(newLayer.url).toBe('http://localhost:8080/MapServer'); - }); - it('should getViewedResourcePermissions', () => { const data = [{ name: "testType", @@ -1162,54 +1104,6 @@ describe('Test Resource Utils', () => { })) .toEqual({pk: 1, links: [{}], assets: [{_showEmptyState: true}]}); }); - describe('getDimensions', () => { - it('should return empty array if no links and has_time is false', () => { - const result = getDimensions(); - expect(result).toEqual([]); - }); - - it('should return dimensions with time if has_time is true and WMTS link is present', () => { - const links = [{ link_type: 'OGC:WMTS', url: 'http://example.com/wmts' }]; - const result = getDimensions({ links, has_time: true }); - expect(result).toEqual([{ - name: 'time', - source: { - type: 'multidim-extension', - url: 'http://example.com/wmts' - } - }]); - }); - - it('should return dimensions with time if has_time is true and only WMS link is present', () => { - const links = [{ link_type: 'OGC:WMS', url: 'http://example.com/geoserver/wms' }]; - const result = getDimensions({ links, has_time: true }); - expect(result).toEqual([{ - name: 'time', - source: { - type: 'multidim-extension', - url: 'http://example.com/geoserver/gwc/service/wmts' - } - }]); - }); - - it('should return empty array if has_time is false', () => { - const links = [{ link_type: 'OGC:WMTS', url: 'http://example.com/wmts' }]; - const result = getDimensions({ links, has_time: false }); - expect(result).toEqual([]); - }); - - it('should return default url if no matching link types are found', () => { - const links = [{ link_type: 'OGC:OTHER', url: 'http://example.com/other' }]; - const result = getDimensions({ links, has_time: true }); - expect(result).toEqual([{ - name: 'time', - source: { - type: 'multidim-extension', - url: '/geoserver/gwc/service/wmts' - } - }]); - }); - }); it('canManageResourcePublishing', () => { expect(canManageResourcePublishing({ perms: ['publish_resourcebase'] })).toBeTruthy(); @@ -1341,124 +1235,8 @@ describe('Test Resource Utils', () => { const result = canEditMap(gnresourceState, { isNewCheck: true, resourceTypes: [ResourceTypes.MAP, ResourceTypes.DATASET] }); expect(result).toBeTruthy(); }); - it('dataset with title in multilanguage', () => { - let supportedLocales = { - "en": { - code: "en-US", - description: "English" - }, - "it": { - code: "it-IT", - description: "Italiano" - }, - "fr": { - code: "fr-FR", - description: "Français" - } - }; - setSupportedLocales(supportedLocales); - const newLayer = resourceToLayerConfig({ - alternate: 'geonode:layer_numtilangue', - title: 'Default title', - title_en: 'Layer title', - title_it: 'Titolo del layer', - title_fr: 'Titre de la couche', - perms: [], - pk: 1 - }); - expect(newLayer.title).toEqual({ - 'en-US': 'Layer title', - 'it-IT': 'Titolo del layer', - 'fr-FR': 'Titre de la couche', - 'default': 'Default title' - }); - }); }); describe('alternate is propagated through extendedParams', () => { - // parseDevHostname references __DEVTOOLS__ (a webpack DefinePlugin global - // not declared in the karma test config); the 3dtiles/cog/flatgeobuf - // branches call it, so define it here to avoid ReferenceError - let prevDevtools; - before(() => { - prevDevtools = window.__DEVTOOLS__; - window.__DEVTOOLS__ = false; - }); - after(() => { - window.__DEVTOOLS__ = prevDevtools; - }); - - it('resourceToLayerConfig (default WMS) includes alternate in extendedParams', () => { - const newLayer = resourceToLayerConfig({ - alternate: 'geonode:layer_name', - links: [{ - extension: 'html', - link_type: 'OGC:WMS', - name: 'OGC WMS Service', - mime: 'text/html', - url: '/geoserver/wms' - }], - title: 'Layer title', - perms: [], - pk: 1 - }); - expect(newLayer.extendedParams).toEqual({ pk: 1, alternate: 'geonode:layer_name' }); - }); - - it('resourceToLayerConfig (3dtiles) includes alternate in extendedParams', () => { - const newLayer = resourceToLayerConfig({ - alternate: 'geonode:tileset', - subtype: '3dtiles', - links: [{ extension: '3dtiles', url: '/tileset.json' }], - title: 'Tileset', - perms: [], - pk: 2 - }); - expect(newLayer.extendedParams).toEqual({ pk: 2, alternate: 'geonode:tileset' }); - }); - - it('resourceToLayerConfig (cog) includes alternate in extendedParams', () => { - const newLayer = resourceToLayerConfig({ - alternate: 'geonode:cog_layer', - subtype: 'cog', - links: [{ extension: 'cog', url: '/raster.tif' }], - title: 'COG', - perms: [], - pk: 3 - }); - expect(newLayer.extendedParams).toEqual({ pk: 3, alternate: 'geonode:cog_layer' }); - }); - - it('resourceToLayerConfig (flatgeobuf) includes alternate in extendedParams', () => { - const newLayer = resourceToLayerConfig({ - alternate: 'geonode:fgb_layer', - subtype: 'flatgeobuf', - attribute_set: [], - links: [{ extension: 'flatgeobuf', url: '/data.fgb' }], - title: 'FGB', - perms: [], - pk: 4 - }); - expect(newLayer.extendedParams).toEqual({ pk: 4, alternate: 'geonode:fgb_layer' }); - }); - - it('resourceToLayerConfig (arcgis) includes alternate in extendedParams', () => { - const newLayer = resourceToLayerConfig({ - alternate: 'remoteWorkspace:1', - title: 'Layer title', - perms: [], - links: [{ - extension: 'html', - link_type: 'image', - mime: 'text/html', - name: 'ArcGIS REST ImageServer', - url: '/MapServer' - }], - pk: 5, - ptype: 'gxp_arcrestsource' - }); - expect(newLayer.extendedParams).toEqual({ pk: 5, alternate: 'remoteWorkspace:1' }); - }); - it('getGeoNodeMapLayers prefers extendedParams.alternate over layer.name', () => { const data = { map: { diff --git a/geonode_mapstore_client/context_processors.py b/geonode_mapstore_client/context_processors.py index 6430f0e070..528b38300f 100644 --- a/geonode_mapstore_client/context_processors.py +++ b/geonode_mapstore_client/context_processors.py @@ -17,17 +17,27 @@ def resource_urls(request): """Global values to pass to templates""" + SITE_URL = (getattr(settings, "SITEURL", "") or "").rstrip("/") + default_catalogue_selected_service = "GeoNode" + default_catalogue_services = { + "GeoNode": { + "type": "geonode", + "url": SITE_URL, + "autoload": True, + "title": "GeoNode" + } + } defaults = dict(GEOAPPS=["GeoStory", "GeoDashboard", "MapViewer"]) defaults["GEONODE_SETTINGS"] = { "MAP_BASELAYERS": getattr(settings, "MAPSTORE_BASELAYERS", []), "MAP_BASELAYERS_SOURCES": getattr(settings, "MAPSTORE_BASELAYERS_SOURCES", {}), - "CATALOGUE_SERVICES": getattr(settings, "MAPSTORE_CATALOGUE_SERVICES", {}), + "CATALOGUE_SERVICES": getattr(settings, "MAPSTORE_CATALOGUE_SERVICES", default_catalogue_services), "CATALOGUE_SELECTED_SERVICE": getattr( - settings, "MAPSTORE_CATALOGUE_SELECTED_SERVICE", None + settings, "MAPSTORE_CATALOGUE_SELECTED_SERVICE", default_catalogue_selected_service ), - "DASHBOARD_CATALOGUE_SERVICES": getattr(settings, "MAPSTORE_DASHBOARD_CATALOGUE_SERVICES", {}), + "DASHBOARD_CATALOGUE_SERVICES": getattr(settings, "MAPSTORE_DASHBOARD_CATALOGUE_SERVICES", default_catalogue_services), "DASHBOARD_CATALOGUE_SELECTED_SERVICE": getattr( - settings, "MAPSTORE_DASHBOARD_CATALOGUE_SELECTED_SERVICE", None + settings, "MAPSTORE_DASHBOARD_CATALOGUE_SELECTED_SERVICE", default_catalogue_selected_service ), "CREATE_LAYER": getattr(settings, "CREATE_LAYER", False), "DEFAULT_MAP_CENTER_X": getattr(settings, "DEFAULT_MAP_CENTER_X", 0), @@ -55,7 +65,7 @@ def resource_urls(request): "PROJECTION_DEFS_ENDPOINT": getattr( settings, "MAPSTORE_PROJECTION_DEFS_ENDPOINT", - (getattr(settings, "SITEURL", "") or "").rstrip("/") + "/geoserver", + SITE_URL + "/geoserver", ), "PLUGINS_CONFIG_PATCH_RULES": getattr( settings, "MAPSTORE_PLUGINS_CONFIG_PATCH_RULES", [] diff --git a/geonode_mapstore_client/static/mapstore/configs/localConfig.json b/geonode_mapstore_client/static/mapstore/configs/localConfig.json index e5907a0faa..77799d5a94 100644 --- a/geonode_mapstore_client/static/mapstore/configs/localConfig.json +++ b/geonode_mapstore_client/static/mapstore/configs/localConfig.json @@ -1959,15 +1959,6 @@ "type": "divider", "disableIf": "{not canEditResource(state('isNewResource'), state('gnResourceData'))}" }, - { - "type": "plugin", - "name": "DatasetsCatalog", - "disableIf": "{not canEditResource(state('isNewResource'), state('gnResourceData'))}" - }, - { - "type": "divider", - "disableIf": "{not canEditResource(state('isNewResource'), state('gnResourceData'))}" - }, { "type": "plugin", "name": "DocumentsCatalog", @@ -2441,10 +2432,6 @@ "symbolsPath": "/static/mapstore/symbols/" } }, - { - "mandatory": true, - "name": "DatasetsCatalog" - }, { "mandatory": true, "name": "DocumentsCatalog" @@ -2459,20 +2446,66 @@ "name": "SidebarMenu" }, { - "name": "MetadataExplorer", + "name": "Catalog", "cfg": { - "wrap": true, + "hideIdentifier": true, "serviceTypes": [ - { "name": "csw", "label": "CSW" }, - { "name": "wms", "label": "WMS" }, - { "name": "wmts", "label": "WMTS" }, - { "name": "tms", "label": "TMS", "allowedProviders": ["OpenStreetMap", "OpenSeaMap", "Stamen"] }, - { "name": "wfs", "label": "WFS" }, - { "name": "3dtiles", "label": "3D Tiles" }, - { "name": "model", "label": "IFC Model" }, - { "name": "arcgis", "label": "ArcGIS" }, + { "name": "csw", "label": "CSW" }, + { "name": "wms", "label": "WMS" }, + { "name": "wmts", "label": "WMTS" }, + { "name": "tms", "label": "TMS", "allowedProviders": ["OpenStreetMap", "OpenSeaMap", "Stamen"] }, + { "name": "wfs", "label": "WFS" }, + { "name": "3dtiles", "label": "3D Tiles" }, + { "name": "model", "label": "IFC Model" }, + { "name": "arcgis", "label": "ArcGIS" }, { "name": "cog", "label": "COG" }, - { "name": "flatgeobuf", "label": "FlatGeobuf" } + { "name": "flatgeobuf", "label": "FlatGeobuf" }, + { "name": "geonode", "label": "GeoNode" } + ], + "filterFormFields": [ + { + "type": "tabs", + "id": "catalog-filter-tabs", + "persistSelection": true, + "items": [ + { + "label": "Filters", + "items": [ + { "id": "category", "type": "select", "order": 5, "facet": "category", "label": "Category", "key": "filter{category.identifier.in}" }, + { "id": "keyword", "type": "select", "order": 6, "facet": "keyword", "label": "Keyword", "key": "filter{keywords.slug.in}" }, + { "id": "region", "type": "select", "order": 7, "facet": "place", "label": "Region", "key": "filter{regions.code.in}" }, + { "type": "date-range", "filterKey": "date", "labelId": "resourcesCatalog.creationFilter" }, + { "label": "Extent Filter", "type": "extent" } + ] + }, + { + "label": "Quick Filters", + "items": [ + { + "type": "accordion", + "id": "quick-categories", + "label": "Category", + "items": [ + { "type": "filter", "id": "boundaries", "label": "Boundaries", "filterKey": "filter{category.identifier.in}", "filterValue": "boundaries" }, + { "type": "filter", "id": "economy", "label": "Economy", "filterKey": "filter{category.identifier.in}", "filterValue": "economy" }, + { "type": "filter", "id": "elevation", "label": "Elevation", "filterKey": "filter{category.identifier.in}", "filterValue": "elevation" }, + { "type": "filter", "id": "environment", "label": "Environment", "filterKey": "filter{category.identifier.in}", "filterValue": "environment" } + ] + }, + { + "type": "accordion", + "id": "quick-keywords", + "label": "Keywords", + "items": [ + { "type": "filter", "id": "open-data", "label": "Open Data", "filterKey": "filter{keywords.slug.in}", "filterValue": "open-data" }, + { "type": "filter", "id": "climate", "label": "Climate", "filterKey": "filter{keywords.slug.in}", "filterValue": "climate" }, + { "type": "filter", "id": "urban", "label": "Urban", "filterKey": "filter{keywords.slug.in}", "filterValue": "urban" } + ] + } + ] + } + ] + } ] } }, diff --git a/geonode_mapstore_client/static/mapstore/configs/pluginsConfig.json b/geonode_mapstore_client/static/mapstore/configs/pluginsConfig.json index 17f05f571b..89a4e51bd5 100644 --- a/geonode_mapstore_client/static/mapstore/configs/pluginsConfig.json +++ b/geonode_mapstore_client/static/mapstore/configs/pluginsConfig.json @@ -547,7 +547,7 @@ } }, { - "name": "MetadataExplorer", + "name": "Catalog", "glyph": "folder-open", "title": "plugins.MetadataExplorer.title", "description": "plugins.MetadataExplorer.description", @@ -555,17 +555,19 @@ "SidebarMenu" ], "defaultConfig": { - "wrap": true, + "hideIdentifier": true, "serviceTypes": [ - { "name": "csw", "label": "CSW" }, - { "name": "wms", "label": "WMS" }, - { "name": "wmts", "label": "WMTS" }, - { "name": "tms", "label": "TMS", "allowedProviders": ["OpenStreetMap", "OpenSeaMap", "Stamen"] }, - { "name": "wfs", "label": "WFS" }, - { "name": "3dtiles", "label": "3D Tiles" }, - { "name": "model", "label": "IFC Model" }, - { "name": "arcgis", "label": "ArcGIS" }, - { "name": "cog", "label": "COG" } + { "name": "csw", "label": "CSW" }, + { "name": "wms", "label": "WMS" }, + { "name": "wmts", "label": "WMTS" }, + { "name": "tms", "label": "TMS", "allowedProviders": ["OpenStreetMap", "OpenSeaMap", "Stamen"] }, + { "name": "wfs", "label": "WFS" }, + { "name": "3dtiles", "label": "3D Tiles" }, + { "name": "model", "label": "IFC Model" }, + { "name": "arcgis", "label": "ArcGIS" }, + { "name": "cog", "label": "COG" }, + { "name": "flatgeobuf", "label": "FlatGeobuf" }, + { "name": "geonode", "label": "GeoNode" } ] } }, diff --git a/tutorials/01-settings-variables.md b/tutorials/01-settings-variables.md index 2f839f5040..180d0d38b4 100644 --- a/tutorials/01-settings-variables.md +++ b/tutorials/01-settings-variables.md @@ -5,12 +5,20 @@ name | description | default value --- | --- | --- MAPSTORE_BASELAYERS | list of base layer used in map and dataset viewers | [] MAPSTORE_BASELAYERS_SOURCES | this object defines tilematrix sets for wmts base layers | {} +MAPSTORE_CATALOGUE_SERVICES | catalog services available in the map viewer (keys are service names, values are service config objects with `type`, `url`, `title`, and optional `autoload`) | `{"GeoNode": {"type": "geonode", "url": SITEURL, "autoload": true, "title": "GeoNode"}}` +MAPSTORE_CATALOGUE_SELECTED_SERVICE | key of the catalog service selected by default in the map viewer | `"GeoNode"` +MAPSTORE_DASHBOARD_CATALOGUE_SERVICES | catalog services available in the dashboard viewer (same structure as `MAPSTORE_CATALOGUE_SERVICES`) | same as `MAPSTORE_CATALOGUE_SERVICES` default +MAPSTORE_DASHBOARD_CATALOGUE_SELECTED_SERVICE | key of the catalog service selected by default in the dashboard viewer | `"GeoNode"` +CREATE_LAYER | enables the create layer feature in the map viewer toolbar | False DEFAULT_MAP_CENTER_X | initial x center position of new map | 0 DEFAULT_MAP_CENTER_Y | initial y center position of new map | 0 DEFAULT_MAP_CRS | crs used by the map and dataset viewers | EPSG:3857 DEFAULT_MAP_ZOOM | initial zoom of new map | 0 DEFAULT_TILE_SIZE | tiles size used by map and dataset viewers by default | 512 DEFAULT_LAYER_FORMAT | tiles format used by map and dataset viewers by default | 'image/png' +THUMBNAIL_SIZE | default size of resource thumbnails | `{"width": 500, "height": 200}` +MAPSTORE_TRANSLATIONS_PATH | list of paths where the client looks for translation files | `["/static/mapstore/ms-translations", "/static/mapstore/gn-translations"]` +MAPSTORE_PROJECTION_DEFS | list of custom projection definitions to register in the client | [] MAPSTORE_PROJECTION_DEFS_ENDPOINT | base URL of a GeoServer instance. Enables the remote projection search feature | SITEURL + '/geoserver' (embedded GeoServer) From cfda0060b626c08f83dcdea76192d2e258f36dc2 Mon Sep 17 00:00:00 2001 From: allyoucanmap Date: Wed, 13 May 2026 09:58:11 +0200 Subject: [PATCH 2/2] remove test configuration --- .../static/mapstore/configs/localConfig.json | 45 ------------------- 1 file changed, 45 deletions(-) diff --git a/geonode_mapstore_client/static/mapstore/configs/localConfig.json b/geonode_mapstore_client/static/mapstore/configs/localConfig.json index 77799d5a94..e21c333e8d 100644 --- a/geonode_mapstore_client/static/mapstore/configs/localConfig.json +++ b/geonode_mapstore_client/static/mapstore/configs/localConfig.json @@ -2461,51 +2461,6 @@ { "name": "cog", "label": "COG" }, { "name": "flatgeobuf", "label": "FlatGeobuf" }, { "name": "geonode", "label": "GeoNode" } - ], - "filterFormFields": [ - { - "type": "tabs", - "id": "catalog-filter-tabs", - "persistSelection": true, - "items": [ - { - "label": "Filters", - "items": [ - { "id": "category", "type": "select", "order": 5, "facet": "category", "label": "Category", "key": "filter{category.identifier.in}" }, - { "id": "keyword", "type": "select", "order": 6, "facet": "keyword", "label": "Keyword", "key": "filter{keywords.slug.in}" }, - { "id": "region", "type": "select", "order": 7, "facet": "place", "label": "Region", "key": "filter{regions.code.in}" }, - { "type": "date-range", "filterKey": "date", "labelId": "resourcesCatalog.creationFilter" }, - { "label": "Extent Filter", "type": "extent" } - ] - }, - { - "label": "Quick Filters", - "items": [ - { - "type": "accordion", - "id": "quick-categories", - "label": "Category", - "items": [ - { "type": "filter", "id": "boundaries", "label": "Boundaries", "filterKey": "filter{category.identifier.in}", "filterValue": "boundaries" }, - { "type": "filter", "id": "economy", "label": "Economy", "filterKey": "filter{category.identifier.in}", "filterValue": "economy" }, - { "type": "filter", "id": "elevation", "label": "Elevation", "filterKey": "filter{category.identifier.in}", "filterValue": "elevation" }, - { "type": "filter", "id": "environment", "label": "Environment", "filterKey": "filter{category.identifier.in}", "filterValue": "environment" } - ] - }, - { - "type": "accordion", - "id": "quick-keywords", - "label": "Keywords", - "items": [ - { "type": "filter", "id": "open-data", "label": "Open Data", "filterKey": "filter{keywords.slug.in}", "filterValue": "open-data" }, - { "type": "filter", "id": "climate", "label": "Climate", "filterKey": "filter{keywords.slug.in}", "filterValue": "climate" }, - { "type": "filter", "id": "urban", "label": "Urban", "filterKey": "filter{keywords.slug.in}", "filterValue": "urban" } - ] - } - ] - } - ] - } ] } },