diff --git a/geonode_mapstore_client/client/js/actions/gnresource.js b/geonode_mapstore_client/client/js/actions/gnresource.js index fd58224fb3..3035b9dd52 100644 --- a/geonode_mapstore_client/client/js/actions/gnresource.js +++ b/geonode_mapstore_client/client/js/actions/gnresource.js @@ -14,6 +14,7 @@ export const SET_RESOURCE_TYPE = 'GEONODE:SET_RESOURCE_TYPE'; export const SET_NEW_RESOURCE = 'GEONODE:SET_NEW_RESOURCE'; export const SET_RESOURCE_ID = 'GEONODE:SET_RESOURCE_ID'; export const SET_RESOURCE_PERMISSIONS = 'GEONODE:SET_RESOURCE_PERMISSIONS'; +export const SET_SELECTED_LAYER_PERMISSIONS = "GEONODE:SET_SELECTED_LAYER_PERMISSIONS"; /** * Actions for GeoNode resource @@ -114,3 +115,18 @@ export function setResourcePermissions(permissions) { permissions }; } + +/** +* Set resource permissions +* @memberof actions.gnresource +* @param {object} permissions permissions info +* @param {bool} permissions.canEdit can edit permission +* @param {bool} permissions.canView can view permission +*/ + +export function setSelectedLayerPermissions(permissions) { + return { + type: SET_SELECTED_LAYER_PERMISSIONS, + permissions + }; +} diff --git a/geonode_mapstore_client/client/js/actions/gnsave.js b/geonode_mapstore_client/client/js/actions/gnsave.js index 4fbb3369b7..daa2aaad71 100644 --- a/geonode_mapstore_client/client/js/actions/gnsave.js +++ b/geonode_mapstore_client/client/js/actions/gnsave.js @@ -12,7 +12,7 @@ export const SAVE_ERROR = 'GEONODE:SAVE_ERROR'; export const CLEAR_SAVE = 'GEONODE:CLEAR_SAVE'; export const SAVE_CONTENT = 'GEONODE:SAVE_CONTENT'; export const UPDATE_RESOURCE_BEFORE_SAVE = 'GEONODE:UPDATE_RESOURCE_BEFORE_SAVE'; -export const SAVE_DIRECT_CONTENT = 'GEONODE:SAVE_DIRECT_CONTENT' +export const SAVE_DIRECT_CONTENT = 'GEONODE:SAVE_DIRECT_CONTENT'; /** * Actions for GeoNode save workflow @@ -102,4 +102,4 @@ export function saveDirectContent() { return { type: SAVE_DIRECT_CONTENT }; -} \ No newline at end of file +} 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 4624d788a5..72df65c074 100644 --- a/geonode_mapstore_client/client/js/api/geonode/v2/index.js +++ b/geonode_mapstore_client/client/js/api/geonode/v2/index.js @@ -331,6 +331,23 @@ export const getResourceTypes = ({}, filterKey = 'resource-types') => { }); }; +export const getLayerByName = name => { + const url = parseDevHostname(`${endpoints[LAYERS]}/?filter{alternate}=${name}`); + return axios.get(url) + .then(({data}) => data?.layers[0]); +}; + +export const getLayersByName = names => { + const url = parseDevHostname(endpoints[LAYERS]); + return axios.get(url, { + params: { + page_size: names.length, + 'filter{alternate.in}': names + } + }) + .then(({data}) => data?.layers); +}; + export const getResourcesTotalCount = () => { const params = { page_size: 1 diff --git a/geonode_mapstore_client/client/js/apps/gn-geostory.js b/geonode_mapstore_client/client/js/apps/gn-geostory.js index 06dab97573..2bf9824cbe 100644 --- a/geonode_mapstore_client/client/js/apps/gn-geostory.js +++ b/geonode_mapstore_client/client/js/apps/gn-geostory.js @@ -20,7 +20,7 @@ import { registerMediaAPI } from '@mapstore/framework/api/media'; import * as geoNodeMediaApi from '@js/observables/media/geonode'; import { getEndpoints, - getConfiguration,getAccountInfo + getConfiguration, getAccountInfo } from '@js/api/geonode/v2'; import { setResourceType, @@ -111,7 +111,7 @@ document.addEventListener('DOMContentLoaded', function() { Promise.all([ getConfiguration(), getAccountInfo(), - getEndpoints(), + getEndpoints() ]) .then(([localConfig, user]) => { const { diff --git a/geonode_mapstore_client/client/js/apps/gn-map.js b/geonode_mapstore_client/client/js/apps/gn-map.js index 7e86ec2d09..dfe1faceba 100644 --- a/geonode_mapstore_client/client/js/apps/gn-map.js +++ b/geonode_mapstore_client/client/js/apps/gn-map.js @@ -58,8 +58,8 @@ import { updateGeoNodeSettings } from '@js/actions/gnsettings'; import { updateMapLayoutEpic, - _setFeatureEditPermission, - _setStyleEditorPermission + gnCheckSelectedLayerPermissions, + setLayersPermissions } from '@js/epics'; import maplayout from '@mapstore/framework/reducers/maplayout'; import 'react-widgets/dist/css/react-widgets.css'; @@ -174,8 +174,8 @@ Promise.all([ ...standardEpics, ...configEpics, updateMapLayoutEpic, - _setFeatureEditPermission, - _setStyleEditorPermission, + gnCheckSelectedLayerPermissions, + setLayersPermissions, ...pluginsDefinition.epics }, geoNodeConfiguration, diff --git a/geonode_mapstore_client/client/js/epics/__tests__/gnsave-test.js b/geonode_mapstore_client/client/js/epics/__tests__/gnsave-test.js index 32e79e8bb6..0003d2769b 100644 --- a/geonode_mapstore_client/client/js/epics/__tests__/gnsave-test.js +++ b/geonode_mapstore_client/client/js/epics/__tests__/gnsave-test.js @@ -22,13 +22,21 @@ import { UPDATE_RESOURCE_PROPERTIES, RESOURCE_LOADING, SET_RESOURCE, - RESOURCE_ERROR + RESOURCE_ERROR, + SET_SELECTED_LAYER_PERMISSIONS } from '@js/actions/gnresource'; import { gnSaveContent, gnUpdateResource, gnSaveDirectContent } from '@js/epics/gnsave'; +import {gnCheckSelectedLayerPermissions, setLayersPermissions} from '@js/epics'; +import { SET_PERMISSION } from '@mapstore/framework/actions/featuregrid'; +import { SET_EDIT_PERMISSION } from '@mapstore/framework/actions/styleeditor'; +import { configureMap } from '@mapstore/framework/actions/config'; + +import { selectNode, addLayer } from '@mapstore/framework/actions/layers'; + let mockAxios; @@ -172,9 +180,54 @@ describe('gnsave epics', () => { ); }); + it("gnCheckSelectedLayerPermissions should trigger permission actions for style and edit", (done) => { + + const NUM_ACTIONS = 3; + testEpic(gnCheckSelectedLayerPermissions, + NUM_ACTIONS, selectNode(1, "layer"), (actions) => { + try { + expect(actions.map(({type}) => type)).toEqual([SET_PERMISSION, SET_SELECTED_LAYER_PERMISSIONS, SET_EDIT_PERMISSION]); + done(); + } catch (error) { + done(error); + } + }, {layers: {flat: [{name: "testLayer", id: "test_id", perms: ['download_resourcebase']}], selected: ["test_id"]}}); + + }); + + it('test setLayersPermissions trigger updateNode for MAP_CONFIG_LOADED', (done) => { + mockAxios.onGet().reply(() => [200, + {layers: [{perms: ['change_layer_style', 'change_layer_data'], alternate: "testLayer"}]}]); + const NUM_ACTIONS = 1; + testEpic(setLayersPermissions, NUM_ACTIONS, configureMap({map: {layers: [{name: "testLayer", id: "test_id"}]}}), (actions) => { + try { + expect(actions.map(({type}) => type)).toEqual(["UPDATE_NODE"]); + done(); + } catch (error) { + done(error); + } + }, + {layers: {flat: [{name: "testLayer", id: "test_id", perms: ['download_resourcebase']}], selected: ["test_id"]}}); + }); + + it('test setLayersPermissions trigger updateNode for ADD_LAYER', (done) => { + mockAxios.onGet().reply(() => [200, + {layers: [{perms: ['change_layer_style', 'change_layer_data'], alternate: "testLayer"}]}]); + const NUM_ACTIONS = 1; + testEpic(setLayersPermissions, NUM_ACTIONS, addLayer({name: "testLayer"}), (actions) => { + try { + expect(actions.map(({type}) => type)).toEqual(["UPDATE_NODE"]); + done(); + } catch (error) { + done(error); + } + }, + {layers: {flat: [{name: "testLayer", id: "test_id", perms: ['download_resourcebase']}], selected: ["test_id"]}}); + }); + it('should trigger saveResource (gnSaveDirectContent)', (done) => { const NUM_ACTIONS = 2; - const pk = 1 + const pk = 1; const resource = { 'id': pk, 'title': 'Map', @@ -182,7 +235,7 @@ describe('gnsave epics', () => { 'thumbnail_url': 'thumbnail.jpeg' }; mockAxios.onGet(new RegExp(`resources/${pk}`)) - .reply(200, resource); + .reply(200, resource); testEpic( gnSaveDirectContent, NUM_ACTIONS, diff --git a/geonode_mapstore_client/client/js/epics/gnsave.js b/geonode_mapstore_client/client/js/epics/gnsave.js index 6e295819a6..4d5260ac3e 100644 --- a/geonode_mapstore_client/client/js/epics/gnsave.js +++ b/geonode_mapstore_client/client/js/epics/gnsave.js @@ -7,7 +7,7 @@ */ import { Observable } from 'rxjs'; -import { mapSelector } from '@mapstore/framework/selectors/map'; +import { mapSelector, mapInfoSelector } from '@mapstore/framework/selectors/map'; import { layersSelector, groupsSelector } from '@mapstore/framework/selectors/layers'; import { backgroundListSelector } from '@mapstore/framework/selectors/backgroundselector'; import { mapOptionsToSaveSelector } from '@mapstore/framework/selectors/mapsave'; @@ -20,7 +20,7 @@ import { getConfigProp } from '@mapstore/framework/utils/ConfigUtils'; import { currentStorySelector } from '@mapstore/framework/selectors/geostory'; import { userSelector } from '@mapstore/framework/selectors/security'; import { error as errorNotification, success as successNotification } from '@mapstore/framework/actions/notifications'; -import { mapInfoSelector } from '@mapstore/framework/selectors/map'; + import { creatMapStoreMap, @@ -31,8 +31,8 @@ import { UPDATE_RESOURCE_BEFORE_SAVE, saveSuccess, saveError, - savingResource, - SAVE_DIRECT_CONTENT, + savingResource, + SAVE_DIRECT_CONTENT, saveContent } from '@js/actions/gnsave'; import { @@ -150,10 +150,10 @@ export const gnSaveContent = (action$, store) => return Observable.of( saveError(error.data || error.message), action.showNotifications && errorNotification({title: "map.mapError.errorTitle", message: "map.mapError.errorDefault"}) - ); - }) + ); + }); - }).startWith(savingResource());; + }).startWith(savingResource()); export const gnSaveDirectContent = (action$, store) => action$.ofType(SAVE_DIRECT_CONTENT) @@ -178,7 +178,7 @@ export const gnSaveDirectContent = (action$, store) => return Observable.of( saveError(error.data || error.message), errorNotification({title: "map.mapError.errorTitle", message: error.data || error.message || "map.mapError.errorDefault"}) - ); + ); }); }).startWith(savingResource()); diff --git a/geonode_mapstore_client/client/js/epics/index.js b/geonode_mapstore_client/client/js/epics/index.js index 516f9e97f1..182b49f79f 100644 --- a/geonode_mapstore_client/client/js/epics/index.js +++ b/geonode_mapstore_client/client/js/epics/index.js @@ -12,18 +12,17 @@ import Rx from "rxjs"; import { setEditPermissionStyleEditor, INIT_STYLE_SERVICE } from "@mapstore/framework/actions/styleeditor"; -import { layerEditPermissions, styleEditPermissions } from "@js/api/geonode"; -import { getSelectedLayer } from "@mapstore/framework/selectors/layers"; +import { getSelectedLayer, layersSelector } from "@mapstore/framework/selectors/layers"; import { getConfigProp } from "@mapstore/framework/utils/ConfigUtils"; - +import { getLayerByName, getLayersByName } from '@js/api/geonode/v2'; import { updateMapLayout } from '@mapstore/framework/actions/maplayout'; import { TOGGLE_CONTROL, SET_CONTROL_PROPERTY, SET_CONTROL_PROPERTIES } from '@mapstore/framework/actions/controls'; import { MAP_CONFIG_LOADED } from '@mapstore/framework/actions/config'; import { SIZE_CHANGE, CLOSE_FEATURE_GRID, OPEN_FEATURE_GRID, setPermission } from '@mapstore/framework/actions/featuregrid'; import { CLOSE_IDENTIFY, ERROR_FEATURE_INFO, TOGGLE_MAPINFO_STATE, LOAD_FEATURE_INFO, EXCEPTIONS_FEATURE_INFO, PURGE_MAPINFO_RESULTS } from '@mapstore/framework/actions/mapInfo'; -import { SHOW_SETTINGS, HIDE_SETTINGS, SELECT_NODE } from '@mapstore/framework/actions/layers'; +import { SHOW_SETTINGS, HIDE_SETTINGS, SELECT_NODE, updateNode, ADD_LAYER } from '@mapstore/framework/actions/layers'; import { isMapInfoOpen } from '@mapstore/framework/selectors/mapInfo'; - +import { setSelectedLayerPermissions } from '@js/actions/gnresource'; import { isFeatureGridOpen, getDockSize } from '@mapstore/framework/selectors/featuregrid'; import head from 'lodash/head'; import get from 'lodash/get'; @@ -34,34 +33,51 @@ import get from 'lodash/get'; import { showCoordinateEditorSelector } from '@mapstore/framework/selectors/controls'; /** - * When a user selects a layer, the app checks for layer editing permission. + * Handles checking and for permissions of a layer when its selected */ -export const _setFeatureEditPermission = (action$, { getState } = {}) => - action$.ofType(SELECT_NODE).filter(({ nodeType }) => nodeType === "layer" && !getConfigProp("disableCheckEditPermissions")) +export const gnCheckSelectedLayerPermissions = (action$, { getState } = {}) => + action$.ofType(SELECT_NODE, INIT_STYLE_SERVICE) + .filter(({ nodeType }) => nodeType && nodeType === "layer" && !getConfigProp("disableCheckEditPermissions") + || !nodeType && !getConfigProp("disableCheckEditPermissions")) .switchMap(() => { - const layer = getSelectedLayer(getState() || {}); - return layer ? layerEditPermissions(layer) - .map(permissions => setPermission(permissions)) - .startWith(setPermission({ canEdit: false })) - .catch(() => Rx.Observable.empty()) : Rx.Observable.of(setPermission({ canEdit: false })); + const state = getState() || {}; + const layer = getSelectedLayer(state); + const permissions = layer?.perms || []; + const canEditStyles = permissions.includes("change_layer_style"); + const canEdit = permissions.includes("change_layer_data"); + return layer ? Rx.Observable.of( + setPermission({canEdit}), + setEditPermissionStyleEditor(canEditStyles), + setSelectedLayerPermissions(permissions) + ) + .startWith(setPermission({canEdit: false}), setSelectedLayerPermissions([]), setEditPermissionStyleEditor(false)) + .catch(() => {Rx.Observable.empty();}) : Rx.Observable.of(setPermission({canEdit: false}), setEditPermissionStyleEditor(false), setSelectedLayerPermissions([])); }); + + /** - * When a user selects a layer, the app checks for style editing permission. - * INIT_STYLE_SERVICE si needed for map editing, it ensures an user has permission to edit style of a specific layer retrieved from catalog + * Checks the permissions for layers when a map is loaded and when a new layer is added + * to a map */ -export const _setStyleEditorPermission = (action$, { getState } = {}) => - action$.ofType(INIT_STYLE_SERVICE, SELECT_NODE) - .filter(({ nodeType }) => - nodeType && nodeType === "layer" && !getConfigProp("disableCheckEditPermissions") - || !nodeType && !getConfigProp("disableCheckEditPermissions")) +export const setLayersPermissions = (actions$, { getState = () => {}} = {}) => + actions$.ofType(MAP_CONFIG_LOADED, ADD_LAYER) .switchMap((action) => { - const layer = getSelectedLayer(getState() || {}); - return layer - ? styleEditPermissions(layer) - .map(({ canEdit }) => setEditPermissionStyleEditor(canEdit)) - .startWith(setEditPermissionStyleEditor(action.canEdit)) - .catch(() => Rx.Observable.empty()) - : Rx.Observable.of(setEditPermissionStyleEditor(false)); + if (action.type === MAP_CONFIG_LOADED) { + const layerNames = action.config?.map?.layers?.filter((l) => l?.group !== "background").map((l) => l.name); + return Rx.Observable.defer(() => getLayersByName(layerNames)) + .switchMap((layers = []) => { + const stateLayers = layers.map((l) => ({ + ...l, + id: layersSelector(getState())?.find((la) => la.name === l.alternate)?.id + })); + return Rx.Observable.of(...stateLayers.map((l) => updateNode(l.id, 'layer', {perms: l.perms || []}) )); + }); + } + return Rx.Observable.defer(() => getLayerByName(action.layer?.name)) + .switchMap((layer = {}) => { + const layerId = layersSelector(getState())?.find((la) => la.name === layer.alternate)?.id; + return Rx.Observable.of(updateNode(layerId, 'layer', {perms: layer.perms})); + }); }); // Modified to accept map-layout from Config diff less NO_QUERYABLE_LAYERS, SET_CONTROL_PROPERTIES more action$.ofType(PURGE_MAPINFO_RESULTS) @@ -135,7 +151,7 @@ export const updateMapLayoutEpic = (action$, store) => })); }); export default { - _setFeatureEditPermission, - _setStyleEditorPermission, - updateMapLayoutEpic + gnCheckSelectedLayerPermissions, + updateMapLayoutEpic, + setLayersPermissions }; diff --git a/geonode_mapstore_client/client/js/plugins/Save.jsx b/geonode_mapstore_client/client/js/plugins/Save.jsx index 1329775653..0086e89c79 100644 --- a/geonode_mapstore_client/client/js/plugins/Save.jsx +++ b/geonode_mapstore_client/client/js/plugins/Save.jsx @@ -49,10 +49,10 @@ import { saveDirectContent } from '@js/actions/gnsave'; */ function Save(props) { return props.saving ? (