diff --git a/geonode_mapstore_client/client/js/actions/gnresource.js b/geonode_mapstore_client/client/js/actions/gnresource.js index c0ef88d8c6..d56191bc4e 100644 --- a/geonode_mapstore_client/client/js/actions/gnresource.js +++ b/geonode_mapstore_client/client/js/actions/gnresource.js @@ -42,6 +42,8 @@ export const SET_MAP_VIEWER_LINKED_RESOURCE = 'GEONODE:SET_MAP_VIEWER_LINKED_RES export const MANAGE_LINKED_RESOURCE = 'GEONODE:MANAGE_LINKED_RESOURCE'; export const SET_DEFAULT_VIEWER_PLUGINS = 'GEONODE:SET_DEFAULT_VIEWER_PLUGINS'; export const SET_SELECTED_LAYER = 'GEONODE:SET_SELECTED_LAYER'; +export const UPDATE_LAYER_DATASET = 'GEONODE:UPDATE_LAYER_DATASET'; +export const SET_SELECTED_LAYER_DATASET = 'GEONODE:SET_SELECTED_LAYER_DATASET'; /** * Actions for GeoNode resource @@ -398,3 +400,23 @@ export function setSelectedLayer(layer) { layer }; } + +/** + * Update layer dataset + */ +export function updateLayerDataset(layer) { + return { + type: UPDATE_LAYER_DATASET, + layer + }; +} + +/** + * Get layer dataset + */ +export function setLayerDataset(layerId) { + return { + type: SET_SELECTED_LAYER_DATASET, + layerId + }; +} diff --git a/geonode_mapstore_client/client/js/apps/gn-catalogue.js b/geonode_mapstore_client/client/js/apps/gn-catalogue.js index bce72e3513..4ee9d289b0 100644 --- a/geonode_mapstore_client/client/js/apps/gn-catalogue.js +++ b/geonode_mapstore_client/client/js/apps/gn-catalogue.js @@ -64,6 +64,7 @@ import { import { CATALOGUE_ROUTES, appRouteComponentTypes } from '@js/utils/AppRoutesUtils'; import { updateGeoNodeSettings } from '@js/actions/gnsettings'; import { + gnFetchMissingLayerData, gnCheckSelectedDatasetPermissions, gnSetDatasetsPermissions, // to make the current layout work we need this epic @@ -140,6 +141,7 @@ getEndpoints() const appEpics = cleanEpics({ ...standardEpics, ...configEpics, + gnFetchMissingLayerData, gnCheckSelectedDatasetPermissions, gnSetDatasetsPermissions, ...pluginsDefinition.epics, diff --git a/geonode_mapstore_client/client/js/components/DetailsPanel/DetailsPanel.jsx b/geonode_mapstore_client/client/js/components/DetailsPanel/DetailsPanel.jsx index d7fedc93b5..aa86d83ee0 100644 --- a/geonode_mapstore_client/client/js/components/DetailsPanel/DetailsPanel.jsx +++ b/geonode_mapstore_client/client/js/components/DetailsPanel/DetailsPanel.jsx @@ -38,7 +38,7 @@ const EditTitle = ({ title, onEdit, disabled }) => { setTextValue(evt.target.value); onEdit(evt.target.value); }} - value={textValue} + value={onEdit ? textValue : title} disabled={disabled} /> ); diff --git a/geonode_mapstore_client/client/js/epics/index.js b/geonode_mapstore_client/client/js/epics/index.js index 3d86091a85..9e34081f65 100644 --- a/geonode_mapstore_client/client/js/epics/index.js +++ b/geonode_mapstore_client/client/js/epics/index.js @@ -14,12 +14,13 @@ import Rx from "rxjs"; import { setEditPermissionStyleEditor, INIT_STYLE_SERVICE } from "@mapstore/framework/actions/styleeditor"; import { getSelectedLayer, layersSelector } from "@mapstore/framework/selectors/layers"; import { getConfigProp } from "@mapstore/framework/utils/ConfigUtils"; -import { getDatasetByName, getDatasetsByName, getResourceByTypeAndByPk } from '@js/api/geonode/v2'; +import { getDatasetByName, getDatasetsByName, getDatasetByPk } from '@js/api/geonode/v2'; import { MAP_CONFIG_LOADED } from '@mapstore/framework/actions/config'; import { setPermission } from '@mapstore/framework/actions/featuregrid'; import { SELECT_NODE, updateNode, ADD_LAYER } from '@mapstore/framework/actions/layers'; -import { setSelectedDatasetPermissions, setSelectedLayer } from '@js/actions/gnresource'; +import { setSelectedDatasetPermissions, setSelectedLayer, updateLayerDataset, setLayerDataset } from '@js/actions/gnresource'; import { updateMapLayoutEpic as msUpdateMapLayoutEpic } from '@mapstore/framework/epics/maplayout'; +import isEmpty from 'lodash/isEmpty'; // We need to include missing epics. The plugins that normally include this epic is not used. @@ -56,6 +57,37 @@ export const gnCheckSelectedDatasetPermissions = (action$, { getState } = {}) => ); }); +/** + * Fetches missing values for selected layers + */ +export const gnFetchMissingLayerData = (action$, { getState } = {}) => + action$.ofType(SELECT_NODE) + .filter(({ nodeType }) => nodeType && nodeType === "layer") + .switchMap(() => { + const state = getState() || {}; + const layer = getSelectedLayer(state); + const layerResourceId = layer?.extendedParams?.pk; + const layerResourceDataset = state.gnresource.data?.maplayers?.find(mapLayer => mapLayer.dataset?.pk === parseInt(layerResourceId, 10))?.dataset; + return layerResourceDataset + ? isEmpty(layerResourceDataset?.linkedResources) + ? Rx.Observable.defer(() => + getDatasetByPk(layerResourceId) + .then((layerDataset) => layerDataset) + .catch(() => []) + ).switchMap((layerDataset) => + Rx.Observable.of( + updateLayerDataset(layerDataset), + setLayerDataset(layerResourceId) + ) + ) + : Rx.Observable.of( + setLayerDataset(layerResourceId) + ) + : Rx.Observable.of( + setLayerDataset() + ) + }); + /** * Checks the permissions for layers when a map is loaded and when a new layer is added diff --git a/geonode_mapstore_client/client/js/plugins/LayerDetailViewer.jsx b/geonode_mapstore_client/client/js/plugins/LayerDetailViewer.jsx index 247f88f5c3..8cca68e8a2 100644 --- a/geonode_mapstore_client/client/js/plugins/LayerDetailViewer.jsx +++ b/geonode_mapstore_client/client/js/plugins/LayerDetailViewer.jsx @@ -12,102 +12,65 @@ import { connect } from 'react-redux'; import { createSelector } from 'reselect'; import { getConfigProp } from '@mapstore/framework/utils/ConfigUtils'; import DetailsPanel from '@js/components/DetailsPanel'; -import { userSelector } from '@mapstore/framework/selectors/security'; -import { - editTitleResource, - editAbstractResource, - editThumbnailResource, - setFavoriteResource, - setMapThumbnail, - setResourceThumbnail, - enableMapThumbnailViewer, - downloadResource -} from '@js/actions/gnresource'; -import { processingDownload } from '@js/selectors/resourceservice'; +import { enableMapThumbnailViewer } from '@js/actions/gnresource'; import FaIcon from '@js/components/FaIcon/FaIcon'; import controls from '@mapstore/framework/reducers/controls'; import { setControlProperty } from '@mapstore/framework/actions/controls'; import gnresource from '@js/reducers/gnresource'; -import gnsearch from '@js/reducers/gnsearch'; -import { - canEditResource, - getResourceId, - isThumbnailChanged, - updatingThumbnailResource -} from '@js/selectors/resource'; -import { - getUpdatedLayer -} from '@mapstore/framework/selectors/styleeditor'; +import { getSelectedLayerDataset } from '@js/selectors/resource'; import GNButton from '@js/components/Button'; import useDetectClickOut from '@js/hooks/useDetectClickOut'; import OverlayContainer from '@js/components/OverlayContainer'; import { withRouter } from 'react-router'; import { hashLocationToHref } from '@js/utils/SearchUtils'; -import Message from '@mapstore/framework/components/I18N/Message'; import { mapSelector } from '@mapstore/framework/selectors/map'; -import { resourceHasPermission } from '@js/utils/ResourceUtils'; import { parsePluginConfigExpressions } from '@js/utils/MenuUtils'; import tooltip from '@mapstore/framework/components/misc/enhancers/tooltip'; -import layerDetailViewerEpics from '@js/epics/layerdetailviewer'; +import tabComponents from '@js/plugins/detailviewer/tabComponents'; const Button = tooltip(GNButton); const ConnectedDetailsPanel = connect( createSelector([ - state => state?.gnresource?.layerDataset || null, + state => state?.gnresource?.selectedLayerDataset || null, state => state?.gnresource?.loading || false, - state => state?.gnresource?.layerDataset?.favorite || false, - state => state?.gnsave?.savingThumbnailMap || false, - isThumbnailChanged, - updatingThumbnailResource, mapSelector, - state => state?.gnresource?.showMapThumbnail || false, - processingDownload - ], (resource, loading, favorite, savingThumbnailMap, thumbnailChanged, resourceThumbnailUpdating, mapData, showMapThumbnail, downloading) => ({ + state => state?.gnresource?.showMapThumbnail || false + ], (resource, loading, mapData, showMapThumbnail) => ({ resource, loading, - savingThumbnailMap, - favorite: favorite, - isThumbnailChanged: thumbnailChanged, - resourceThumbnailUpdating, initialBbox: mapData?.bbox, enableMapViewer: showMapThumbnail, - downloading, - canDownload: resourceHasPermission(resource, 'download_resourcebase'), - resourceId: resource.pk + resourceId: resource?.pk, + tabComponents })), { closePanel: setControlProperty.bind(null, 'rightOverlay', 'enabled', false), - onFavorite: setFavoriteResource, - onMapThumbnail: setMapThumbnail, - onResourceThumbnail: setResourceThumbnail, - onClose: enableMapThumbnailViewer, - onAction: downloadResource + onClose: enableMapThumbnailViewer } )(DetailsPanel); -const ButtonViewer = ({ onClick, layer, size, status, showMessage }) => { - const layerResourceId = layer?.extendedParams?.pk; +const ButtonViewer = ({ onClick, layer, size, status }) => { + const layerResourceId = layer?.pk; const handleClickButton = () => { onClick(); }; - return layerResourceId && status === 'LAYER' ? ( ) : null; }; const ConnectedButton = connect( - createSelector([getUpdatedLayer], (layer) => ({ - layer: layer + createSelector([ + getSelectedLayerDataset + ], (layer) => ({ + layer })), { onClick: setControlProperty.bind( @@ -122,11 +85,6 @@ const ConnectedButton = connect( function LayerDetailViewer({ location, enabled, - onEditResource, - onEditAbstractResource, - onEditThumbnail, - canEdit, - user, onClose, monitoredState, queryPathname = '/', @@ -134,17 +92,6 @@ function LayerDetailViewer({ }) { const parsedConfig = parsePluginConfigExpressions(monitoredState, { tabs }); - const handleTitleValue = (val) => { - onEditResource(val); - }; - - const handleAbstractValue = (val) => { - onEditAbstractResource(val); - }; - const handleEditThumbnail = (val) => { - onEditThumbnail(val, true); - }; - const node = useDetectClickOut({ disabled: !enabled, onClickOut: () => { @@ -166,11 +113,11 @@ function LayerDetailViewer({ className="gn-overlay-wrapper" > {}} + activeEditMode={false} + enableFavorite={false} formatHref={handleFormatHref} tabs={parsedConfig.tabs} pathname={queryPathname} @@ -184,24 +131,16 @@ const LayerDetailViewerPlugin = connect( [ (state) => state?.controls?.rightOverlay?.enabled === 'LayerDetailViewer', - canEditResource, - getUpdatedLayer, - getResourceId, - userSelector, + getSelectedLayerDataset, state => getMonitoredState(state, getConfigProp('monitorState')) ], - (enabled, canEdit, layer, user, monitoredState) => ({ + (enabled, layer, monitoredState) => ({ enabled, - canEdit, layer, - user, monitoredState }) ), { - onEditResource: editTitleResource, - onEditAbstractResource: editAbstractResource, - onEditThumbnail: editThumbnailResource, onClose: setControlProperty.bind(null, 'rightOverlay', 'enabled', false) } )(withRouter(LayerDetailViewer)); @@ -211,13 +150,12 @@ export default createPlugin('LayerDetailViewer', { containers: { TOC: { target: 'toolbar', + name: 'LayerDetailViewerButton', Component: ConnectedButton } }, - epics: layerDetailViewerEpics, reducers: { gnresource, - gnsearch, controls } -}); +}); \ No newline at end of file diff --git a/geonode_mapstore_client/client/js/plugins/detailviewer/tabComponents.js b/geonode_mapstore_client/client/js/plugins/detailviewer/tabComponents.js new file mode 100644 index 0000000000..0343b7fcfe --- /dev/null +++ b/geonode_mapstore_client/client/js/plugins/detailviewer/tabComponents.js @@ -0,0 +1,25 @@ +/* + * Copyright 2024, 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 { connect } from 'react-redux'; +import DetailsLocations from '@js/components/DetailsPanel/DetailsLocations'; +import DetailsAssets from '@js/components/DetailsPanel/DetailsAssets'; +import DetailsAttributeTable from '@js/components/DetailsPanel/DetailsAttributeTable'; +import DetailsLinkedResources from '@js/components/DetailsPanel/DetailsLinkedResources'; +import DetailsSettings from '@js/components/DetailsPanel/DetailsSettings'; +import { updateResourceProperties } from '@js/actions/gnresource'; + +const tabComponents = { + 'attribute-table': DetailsAttributeTable, + 'linked-resources': DetailsLinkedResources, + 'locations': DetailsLocations, + 'assets': DetailsAssets, + 'settings': connect(() => ({}), { onChange: updateResourceProperties })(DetailsSettings) +}; + +export default tabComponents; \ No newline at end of file diff --git a/geonode_mapstore_client/client/js/plugins/index.js b/geonode_mapstore_client/client/js/plugins/index.js index 969674687b..ffebecae4d 100644 --- a/geonode_mapstore_client/client/js/plugins/index.js +++ b/geonode_mapstore_client/client/js/plugins/index.js @@ -376,6 +376,10 @@ export const plugins = { 'VisualStyleEditor', () => import(/* webpackChunkName: 'plugins/visual-style-editor-plugin' */ '@js/plugins/VisualStyleEditor') ), + LayerDetailViewerPlugin: toModulePlugin( + 'LayerDetailViewer', + () => import(/* webpackChunkName: 'plugins/detail-viewer-plugin' */ '@js/plugins/LayerDetailViewer') + ), LegendPlugin: toModulePlugin( 'Legend', () => import(/* webpackChunkName: 'plugins/legend-plugin' */ '@js/plugins/Legend') @@ -436,10 +440,6 @@ export const plugins = { 'Settings', () => import(/* webpackChunkName: 'plugins/settings' */ '@mapstore/framework/plugins/Settings') ), - LayerDetailViewerPlugin: toModulePlugin( - 'LayerDetailViewer', - () => import(/* webpackChunkName: 'plugins/detail-viewer-plugin' */ '@js/plugins/LayerDetailViewer') - ), DataCiteDownloadPlugin: toModulePlugin( 'DataCiteDownload', () => import('@js/plugins/downloads/DataCiteDownload') diff --git a/geonode_mapstore_client/client/js/reducers/gnresource.js b/geonode_mapstore_client/client/js/reducers/gnresource.js index cb1d71a2b0..29f020425d 100644 --- a/geonode_mapstore_client/client/js/reducers/gnresource.js +++ b/geonode_mapstore_client/client/js/reducers/gnresource.js @@ -37,7 +37,9 @@ import { SET_RESOURCE_PATH_PARAMETERS, SET_MAP_VIEWER_LINKED_RESOURCE, SET_DEFAULT_VIEWER_PLUGINS, - SET_SELECTED_LAYER + SET_SELECTED_LAYER, + UPDATE_LAYER_DATASET, + SET_SELECTED_LAYER_DATASET } from '@js/actions/gnresource'; import { cleanCompactPermissions, @@ -279,6 +281,38 @@ function gnresource(state = defaultState, action) { ...state, selectedLayer: action.layer }; + case SET_SELECTED_LAYER_DATASET: + return { + ...state, + selectedLayerDataset: state.data?.maplayers?.find(layer => layer.dataset?.pk === parseInt(action.layerId, 10))?.dataset + }; + case UPDATE_LAYER_DATASET: + const { pk, ...newData } = action.layer; + let linkedResources = action.layer.linked_resources ?? {}; + if (!isEmpty(linkedResources)) { + const linkedTo = linkedResources.linked_to ?? []; + const linkedBy = linkedResources.linked_by ?? []; + linkedResources = isEmpty(linkedTo) && isEmpty(linkedBy) ? {} : ({ linkedTo, linkedBy }); + } + return { + ...state, + data: { + ...state.data, + maplayers: state.data?.maplayers?.map(layer => { + if (layer.dataset?.pk === parseInt(pk, 10)) { + return { + ...layer, + dataset: { + ...layer.dataset, + ...newData, + linkedResources + } + }; + } + return layer; + }) + } + }; default: return state; } diff --git a/geonode_mapstore_client/client/js/selectors/resource.js b/geonode_mapstore_client/client/js/selectors/resource.js index 9fbb411306..a034cbd45c 100644 --- a/geonode_mapstore_client/client/js/selectors/resource.js +++ b/geonode_mapstore_client/client/js/selectors/resource.js @@ -92,6 +92,14 @@ export const getLayerResourceData = (state) => { return state?.gnresource?.layerDataset; }; +export const getSelectedLayer = (state) => { + return state?.gnresource?.selectedLayer; +}; + +export const getSelectedLayerDataset = (state) => { + return state?.gnresource?.selectedLayerDataset; +}; + export const getCompactPermissions = (state) => { const compactPermissions = state?.gnresource?.compactPermissions || {}; return compactPermissions; diff --git a/geonode_mapstore_client/static/mapstore/configs/localConfig.json b/geonode_mapstore_client/static/mapstore/configs/localConfig.json index 17df102923..8e0fef2847 100644 --- a/geonode_mapstore_client/static/mapstore/configs/localConfig.json +++ b/geonode_mapstore_client/static/mapstore/configs/localConfig.json @@ -89,8 +89,8 @@ "path": "gnresource.viewerLinkedResource" }, { - "name": "selectedLayer", - "path": "gnresource.selectedLayer" + "name": "gnResourceSelectedLayerDataset", + "path": "gnresource.selectedLayerDataset" } ], "projectionDefs": [], @@ -2795,6 +2795,151 @@ "cfg": { "wrap": true } + }, + { + "mandatory": true, + "name": "LayerDetailViewer", + "cfg": { + "containerPosition": "rightOverlay", + "tabs": [ + { + "type": "tab", + "id": "info", + "labelId": "gnviewer.info", + "items": [ + { + "type": "text", + "labelId": "gnviewer.title", + "value": "{context.get(state('gnResourceSelectedLayerDataset'), 'title')}" + }, + { + "type": "link", + "labelId": "gnviewer.owner", + "href": "{'/people/profile/' + context.get(state('gnResourceSelectedLayerDataset'), 'owner.username')}", + "value": "{context.getUserResourceName(context.get(state('gnResourceSelectedLayerDataset'), 'owner'))}", + "disableIf": "{!context.get(state('gnResourceSelectedLayerDataset'), 'owner.username')}" + }, + { + "type": "date", + "format": "YYYY-MM-DD HH:mm", + "labelId": "{'gnviewer.'+context.get(state('gnResourceSelectedLayerDataset'), 'date_type')}", + "value": "{context.get(state('gnResourceSelectedLayerDataset'), 'date')}" + }, + { + "type": "date", + "format": "YYYY-MM-DD HH:mm", + "labelId": "gnviewer.created", + "value": "{context.get(state('gnResourceSelectedLayerDataset'), 'created')}" + }, + { + "type": "date", + "format": "YYYY-MM-DD HH:mm", + "labelId": "gnviewer.lastModified", + "value": "{context.get(state('gnResourceSelectedLayerDataset'), 'last_updated')}" + }, + { + "type": "query", + "labelId": "gnviewer.resourceType", + "value": "{context.get(state('gnResourceSelectedLayerDataset'), 'resource_type')}", + "pathname": "/", + "query": { + "f": "{context.get(state('gnResourceSelectedLayerDataset'), 'resource_type')}" + } + }, + { + "type": "{context.isDocumentExternalSource(state('gnResourceSelectedLayerDataset')) ? 'link' : 'text'}", + "labelId": "gnviewer.sourceType", + "value": "{context.get(state('gnResourceSelectedLayerDataset'), 'sourcetype', '').toLowerCase()}", + "href": "{context.get(state('gnResourceSelectedLayerDataset'), 'href')}" + }, + { + "type": "query", + "labelId": "gnviewer.category", + "value": "{context.get(state('gnResourceSelectedLayerDataset'), 'category.gn_description')}", + "pathname": "/", + "query": { + "filter{category.identifier}": "{context.get(state('gnResourceSelectedLayerDataset'), 'category.identifier')}" + } + }, + { + "type": "link", + "labelId": "gnviewer.pointOfContact", + "value": "{context.getUserResourceNames(context.get(state('gnResourceSelectedLayerDataset'), 'poc'))}", + "disableIf": "{!context.get(state('gnResourceSelectedLayerDataset'), 'poc')}" + }, + { + "type": "query", + "labelId": "gnviewer.keywords", + "value": "{context.get(state('gnResourceSelectedLayerDataset'), 'keywords')}", + "valueKey": "name", + "pathname": "/", + "queryTemplate": { + "filter{keywords.slug.in}": "${slug}" + } + }, + { + "type": "query", + "labelId": "gnviewer.regions", + "value": "{context.get(state('gnResourceSelectedLayerDataset'), 'regions')}", + "valueKey": "code", + "pathname": "/", + "queryTemplate": { + "filter{regions.code.in}": "${code}" + } + }, + { + "type": "text", + "labelId": "gnviewer.attribution", + "value": "{context.get(state('gnResourceSelectedLayerDataset'), 'attribution')}" + }, + { + "type": "text", + "labelId": "gnviewer.language", + "value": "{context.get(state('gnResourceSelectedLayerDataset'), 'language')}" + }, + { + "type": "html", + "labelId": "gnviewer.supplementalInformation", + "value": "{context.get(state('gnResourceSelectedLayerDataset'), 'supplemental_information')}" + }, + { + "type": "date", + "format": "YYYY-MM-DD HH:mm", + "labelId": "gnviewer.temporalExtent", + "value": { + "start": "{context.get(state('gnResourceSelectedLayerDataset'), 'temporal_extent_start')}", + "end": "{context.get(state('gnResourceSelectedLayerDataset'), 'temporal_extent_end')}" + } + }, + { + "type": "link", + "style": "label", + "labelId": "gnviewer.viewFullMetadata", + "href": "{context.getMetadataDetailUrl(state('gnResourceSelectedLayerDataset'))}", + "disableIf": "{!context.getMetadataDetailUrl(state('gnResourceSelectedLayerDataset'))}" + } + ] + }, + { + "type": "locations", + "id": "locations", + "labelId": "gnviewer.locations", + "items": "{({extent: context.get(state('gnResourceSelectedLayerDataset'), 'extent')})}" + }, + { + "type": "linked-resources", + "id": "related", + "labelId": "gnviewer.linkedResources.label", + "items": "{context.get(state('gnResourceSelectedLayerDataset'), 'linkedResources')}" + }, + { + "type": "assets", + "id": "assets", + "labelId": "gnviewer.assets", + "items": "{context.get(state('gnResourceSelectedLayerDataset'), 'assets')}" + } + ] + } } ], "geostory_viewer": [