diff --git a/web/client/actions/__tests__/currentMap-test.js b/web/client/actions/__tests__/currentMap-test.js index 905f9759f8..7d615af6a2 100644 --- a/web/client/actions/__tests__/currentMap-test.js +++ b/web/client/actions/__tests__/currentMap-test.js @@ -10,7 +10,12 @@ var expect = require('expect'); var { EDIT_MAP, editMap, UPDATE_CURRENT_MAP, updateCurrentMap, - ERROR_CURRENT_MAP, errorCurrentMap + ERROR_CURRENT_MAP, errorCurrentMap, + REMOVE_THUMBNAIL, removeThumbnail, + UPDATE_CURRENT_MAP_PERMISSIONS, updateCurrentMapPermissions, + UPDATE_CURRENT_MAP_GROUPS, updateCurrentMapGroups, + RESET_CURRENT_MAP, resetCurrentMap, + ADD_CURRENT_MAP_PERMISSION, addCurrentMapPermission } = require('../currentMap'); @@ -32,12 +37,28 @@ describe('Test correctness of the maps actions', () => { }); it('updateCurrentMap', () => { - let files = []; + let thumbnailData = []; let thumbnail = "myThumnbnailUrl"; - var retval = updateCurrentMap(files, thumbnail); + var retval = updateCurrentMap(thumbnailData, thumbnail); expect(retval).toExist(); expect(retval.type).toBe(UPDATE_CURRENT_MAP); expect(retval.thumbnail).toBe(thumbnail); + expect(retval.thumbnailData).toBe(thumbnailData); + }); + it('updateCurrentMapGroups', () => { + let groups = { + groups: { + group: { + enabled: true, + groupName: 'everyone', + id: 3 + } + } + }; + var retval = updateCurrentMapGroups(groups); + expect(retval).toExist(); + expect(retval.type).toBe(UPDATE_CURRENT_MAP_GROUPS); + expect(retval.groups).toBe(groups); }); it('errorCurrentMap', () => { @@ -47,5 +68,48 @@ describe('Test correctness of the maps actions', () => { expect(retval.type).toBe(ERROR_CURRENT_MAP); expect(retval.errors).toBe(errors); }); + it('updateCurrentMapPermissions', () => { + let permissions = { + SecurityRuleList: { + SecurityRule: { + canRead: true, + canWrite: true, + user: { + id: 6, + name: 'admin' + } + } + } + }; + const retval = updateCurrentMapPermissions(permissions); + expect(retval).toExist(); + expect(retval.type).toBe(UPDATE_CURRENT_MAP_PERMISSIONS); + expect(retval.permissions).toBe(permissions); + }); + it('removeThumbnail', () => { + let resourceId = 1; + const retval = removeThumbnail(resourceId); + expect(retval).toExist(); + expect(retval.type).toBe(REMOVE_THUMBNAIL); + expect(retval.resourceId).toBe(resourceId); + }); + it('resetCurrentMap', () => { + const retval = resetCurrentMap(); + expect(retval).toExist(); + expect(retval.type).toBe(RESET_CURRENT_MAP); + }); + it('addCurrentMapPermission', () => { + const rule = { + canRead: true, + canWrite: true, + user: { + id: 6, + name: 'admin' + } + }; + const retval = addCurrentMapPermission(rule); + expect(retval).toExist(); + expect(retval.type).toBe(ADD_CURRENT_MAP_PERMISSION); + }); }); diff --git a/web/client/actions/__tests__/maps-test.js b/web/client/actions/__tests__/maps-test.js index a6ca24fe54..ca8ba4784e 100644 --- a/web/client/actions/__tests__/maps-test.js +++ b/web/client/actions/__tests__/maps-test.js @@ -8,16 +8,30 @@ var expect = require('expect'); const assign = require('object-assign'); -var { - // CREATE_THUMBNAIL, createThumbnail, +const { + toggleDetailsSheet, TOGGLE_DETAILS_SHEET, + toggleGroupProperties, TOGGLE_GROUP_PROPERTIES, + toggleUnsavedChanges, TOGGLE_UNSAVED_CHANGES, + deleteMap, DELETE_MAP, + updateDetails, UPDATE_DETAILS, + saveDetails, SAVE_DETAILS, + deleteDetails, DELETE_DETAILS, + setDetailsChanged, SET_DETAILS_CHANGED, + saveResourceDetails, SAVE_RESOURCE_DETAILS, + backDetails, BACK_DETAILS, + undoDetails, UNDO_DETAILS, + doNothing, DO_NOTHING, + setUnsavedChanged, SET_UNSAVED_CHANGES, + openDetailsPanel, OPEN_DETAILS_PANEL, + closeDetailsPanel, CLOSE_DETAILS_PANEL, MAP_UPDATING, mapUpdating, + DETAILS_LOADED, detailsLoaded, PERMISSIONS_UPDATED, permissionsUpdated, ATTRIBUTE_UPDATED, attributeUpdated, SAVE_MAP, saveMap, DISPLAY_METADATA_EDIT, onDisplayMetadataEdit, RESET_UPDATING, resetUpdating, THUMBNAIL_ERROR, thumbnailError, - RESET_CURRENT_MAP, resetCurrentMap, MAPS_SEARCH_TEXT_CHANGED, mapsSearchTextChanged, MAPS_LIST_LOAD_ERROR, loadError, MAP_ERROR, mapError, updatePermissions, @@ -25,6 +39,7 @@ var { METADATA_CHANGED, metadataChanged, updateAttribute, saveAll } = require('../maps'); + let GeoStoreDAO = require('../../api/GeoStoreDAO'); let oldAddBaseUri = GeoStoreDAO.addBaseUrl; @@ -73,6 +88,21 @@ describe('Test correctness of the maps actions', () => { const retFun = saveAll({}, {name: "name"}, null, null, null, resourceId, {}); expect(retFun).toExist(); let count = 0; + retFun((action) => { + switch (count) { + case 0: expect(action.type).toBe(MAP_UPDATING); break; + case 1: expect(action.type).toBe("NONE"); break; + default: done(); + } + count++; + }, () => {}); + }); + it('saveAll - without metadataMap, without thumbnail', (done) => { + const resourceId = 1; + // saveAll(map, metadataMap, nameThumbnail, dataThumbnail, categoryThumbnail, resourceIdMap, options) + const retFun = saveAll({}, null, null, null, null, resourceId, {}); + expect(retFun).toExist(); + let count = 0; retFun((action) => { switch (count) { case 0: expect(action.type).toBe(MAP_UPDATING); break; @@ -80,9 +110,9 @@ describe('Test correctness of the maps actions', () => { default: done(); } count++; - }); + }, () => {}); }); - it('saveAll - with metadataMap, without thumbnail', (done) => { + it('saveAll - without metadataMap, without thumbnail, detailsChanged', (done) => { const resourceId = 1; // saveAll(map, metadataMap, nameThumbnail, dataThumbnail, categoryThumbnail, resourceIdMap, options) const retFun = saveAll({}, null, null, null, null, resourceId, {}); @@ -92,12 +122,14 @@ describe('Test correctness of the maps actions', () => { switch (count) { case 0: expect(action.type).toBe(MAP_UPDATING); break; case 1: expect(action.type).toBe("NONE"); break; - case 2: expect(action.type).toBe(RESET_UPDATING); break; - case 3: expect(action.type).toBe(DISPLAY_METADATA_EDIT); break; + case 2: expect(action.type).toBe(SAVE_RESOURCE_DETAILS); done(); break; default: done(); } count++; - }); + }, () => ({currentMap: { + detailsChanged: true + }} + )); }); it('updatePermissions with securityRules list & without', (done) => { const securityRules = { @@ -206,10 +238,7 @@ describe('Test correctness of the maps actions', () => { expect(retval.resourceId).toBe(resourceId); expect(retval.map).toBe(map); }); - it('resetCurrentMap', () => { - const a = resetCurrentMap(); - expect(a.type).toBe(RESET_CURRENT_MAP); - }); + it('mapsSearchTextChanged', () => { const a = mapsSearchTextChanged("TEXT"); expect(a.type).toBe(MAPS_SEARCH_TEXT_CHANGED); @@ -243,4 +272,100 @@ describe('Test correctness of the maps actions', () => { expect(a.prop).toBe(prop); expect(a.value).toBe(value); }); + + it('toggleDetailsSheet', () => { + const detailsSheetReadOnly = true; + const a = toggleDetailsSheet(detailsSheetReadOnly); + expect(a.type).toBe(TOGGLE_DETAILS_SHEET); + expect(a.detailsSheetReadOnly).toBeTruthy(); + + }); + it('toggleGroupProperties', () => { + const a = toggleGroupProperties(); + expect(a.type).toBe(TOGGLE_GROUP_PROPERTIES); + }); + it('toggleUnsavedChanges', () => { + const a = toggleUnsavedChanges(); + expect(a.type).toBe(TOGGLE_UNSAVED_CHANGES); + }); + it('updateDetails', () => { + const detailsText = "
some value
"; + const originalDetails = "old value
"; + const doBackup = true; + const a = updateDetails(detailsText, doBackup, originalDetails); + expect(a.doBackup).toBeTruthy(); + expect(a.detailsText).toBe(detailsText); + expect(a.originalDetails).toBe(originalDetails); + expect(a.type).toBe(UPDATE_DETAILS); + }); + it('deleteMap', () => { + const resourceId = 1; + const someOpt = { + name: "name" + }; + const options = { + someOpt + }; + const a = deleteMap(resourceId, options); + expect(a.resourceId).toBe(resourceId); + expect(a.type).toBe(DELETE_MAP); + expect(a.options).toBe(options); + }); + it('saveDetails', () => { + const detailsText = "some detailsText
"; + const a = saveDetails(detailsText); + expect(a.type).toBe(SAVE_DETAILS); + expect(a.detailsText).toBe(detailsText); + }); + it('deleteDetails', () => { + const a = deleteDetails(); + expect(a.type).toBe(DELETE_DETAILS); + }); + it('setDetailsChanged', () => { + const detailsChanged = true; + const a = setDetailsChanged(detailsChanged); + expect(a.type).toBe(SET_DETAILS_CHANGED); + expect(a.detailsChanged).toBe(detailsChanged); + }); + it('backDetails', () => { + const backupDetails = true; + const a = backDetails(backupDetails); + expect(a.type).toBe(BACK_DETAILS); + expect(a.backupDetails).toBe(backupDetails); + }); + it('undoDetails', () => { + const a = undoDetails(); + expect(a.type).toBe(UNDO_DETAILS); + }); + it('setUnsavedChanged', () => { + const value = true; + const a = setUnsavedChanged(value); + expect(a.type).toBe(SET_UNSAVED_CHANGES); + expect(a.value).toBe(value); + }); + it('openDetailsPanel', () => { + const a = openDetailsPanel(); + expect(a.type).toBe(OPEN_DETAILS_PANEL); + }); + it('closeDetailsPanel', () => { + const a = closeDetailsPanel(); + expect(a.type).toBe(CLOSE_DETAILS_PANEL); + }); + it('doNothing', () => { + const a = doNothing(); + expect(a.type).toBe(DO_NOTHING); + }); + it('saveResourceDetails', () => { + const a = saveResourceDetails(); + expect(a.type).toBe(SAVE_RESOURCE_DETAILS); + }); + it('detailsLoaded', () => { + const mapId = 1; + const detailsUri = "sada/da/"; + const a = detailsLoaded(mapId, detailsUri); + expect(a.type).toBe(DETAILS_LOADED); + expect(a.detailsUri).toBe(detailsUri); + expect(a.mapId).toBe(mapId); + }); + }); diff --git a/web/client/actions/currentMap.js b/web/client/actions/currentMap.js index 3fd5212fe7..3eeb0dcb7a 100644 --- a/web/client/actions/currentMap.js +++ b/web/client/actions/currentMap.js @@ -15,19 +15,20 @@ const REMOVE_THUMBNAIL = 'REMOVE_THUMBNAIL'; const RESET_CURRENT_MAP = 'RESET_CURRENT_MAP'; const ADD_CURRENT_MAP_PERMISSION = 'ADD_CURRENT_MAP_PERMISSION'; -function editMap(map) { +function editMap(map, openModalProperties) { return { type: EDIT_MAP, - map + map, + openModalProperties }; } -// update the thumbnail and the files property of the currentMap -function updateCurrentMap(files, thumbnail) { +// update the thumbnail and the thumbnailData property of the currentMap +function updateCurrentMap(thumbnailData, thumbnail) { return { type: UPDATE_CURRENT_MAP, thumbnail, - files + thumbnailData }; } @@ -60,6 +61,11 @@ function removeThumbnail(resourceId) { }; } +/** + * reset current map , `RESET_CURRENT_MAP` + * @memberof actions.maps + * @return {action} of type `RESET_CURRENT_MAP` + */ function resetCurrentMap() { return { type: RESET_CURRENT_MAP diff --git a/web/client/actions/maps.js b/web/client/actions/maps.js index 92f96ab0c0..ea7069462d 100644 --- a/web/client/actions/maps.js +++ b/web/client/actions/maps.js @@ -9,8 +9,11 @@ const GeoStoreApi = require('../api/GeoStoreDAO'); const {updateCurrentMapPermissions, updateCurrentMapGroups} = require('./currentMap'); const ConfigUtils = require('../utils/ConfigUtils'); +const {userGroupSecuritySelector, userSelector} = require('../selectors/security'); +const {currentMapDetailsChangedSelector} = require('../selectors/currentmap'); +const {resetCurrentMap} = require('./currentMap'); const assign = require('object-assign'); -const {get, findIndex} = require('lodash'); +const {findIndex, isNil} = require('lodash'); const MAPS_LIST_LOADED = 'MAPS_LIST_LOADED'; const MAPS_LIST_LOADING = 'MAPS_LIST_LOADING'; @@ -32,18 +35,35 @@ const RESET_UPDATING = 'RESET_UPDATING'; const SAVE_MAP = 'SAVE_MAP'; const PERMISSIONS_LIST_LOADING = 'PERMISSIONS_LIST_LOADING'; const PERMISSIONS_LIST_LOADED = 'PERMISSIONS_LIST_LOADED'; -const RESET_CURRENT_MAP = 'RESET_CURRENT_MAP'; const MAPS_SEARCH_TEXT_CHANGED = 'MAPS_SEARCH_TEXT_CHANGED'; const METADATA_CHANGED = 'METADATA_CHANGED'; +const TOGGLE_DETAILS_SHEET = 'MAPS:TOGGLE_DETAILS_SHEET'; +const TOGGLE_GROUP_PROPERTIES = 'MAPS:TOGGLE_GROUP_PROPERTIES'; +const TOGGLE_UNSAVED_CHANGES = 'MAPS:TOGGLE_UNSAVED_CHANGES'; +const UPDATE_DETAILS = 'MAPS:UPDATE_DETAILS'; +const SAVE_DETAILS = 'MAPS:SAVE_DETAILS'; +const DELETE_DETAILS = 'MAPS:DELETE_DETAILS'; +const SET_DETAILS_CHANGED = 'MAPS:SET_DETAILS_CHANGED'; +const SAVE_RESOURCE_DETAILS = 'MAPS:SAVE_RESOURCE_DETAILS'; +const DO_NOTHING = 'MAPS:DO_NOTHING'; +const DELETE_MAP = 'MAPS:DELETE_MAP'; +const BACK_DETAILS = 'MAPS:BACK_DETAILS'; +const UNDO_DETAILS = 'MAPS:UNDO_DETAILS'; +const SET_UNSAVED_CHANGES = 'MAPS:SET_UNSAVED_CHANGES'; +const OPEN_DETAILS_PANEL = 'DETAILS:OPEN_DETAILS_PANEL'; +const CLOSE_DETAILS_PANEL = 'DETAILS:CLOSE_DETAILS_PANEL'; +const DETAILS_LOADED = 'DETAILS:DETAILS_LOADED'; +const DETAILS_SAVING = 'DETAILS:DETAILS_SAVING'; + /** - * reset current map metadata, `RESET_CURRENT_MAP` + * saves details section in the resurce map on geostore * @memberof actions.maps - * @return {action} of type `RESET_CURRENT_MAP` - */ -function resetCurrentMap() { + * @return {action} type `SAVE_RESOURCE_DETAILS` +*/ +function saveResourceDetails() { return { - type: RESET_CURRENT_MAP + type: SAVE_RESOURCE_DETAILS }; } @@ -436,6 +456,7 @@ function updateMapMetadata(resourceId, newName, newDescription, onReset, options dispatch(mapMetadataUpdated(resourceId, newName, newDescription, "success")); if (onReset) { dispatch(onReset); + dispatch(onDisplayMetadataEdit(false)); dispatch(resetCurrentMap()); } }).catch((e) => { @@ -505,9 +526,9 @@ function createThumbnail(map, metadataMap, nameThumbnail, dataThumbnail, categor let metadata = { name: nameThumbnail }; + let state = getState(); return GeoStoreApi.createResource(metadata, dataThumbnail, categoryThumbnail, options).then((response) => { - let state = getState(); - let groups = get(state, "security.user.groups.group"); + let groups = userGroupSecuritySelector(state); let index = findIndex(groups, function(g) { return g.groupName === "everyone"; }); let group; if (index < 0 && groups && groups.groupName === "everyone") { @@ -515,7 +536,7 @@ function createThumbnail(map, metadataMap, nameThumbnail, dataThumbnail, categor } else { group = groups[index]; } - let user = get(state, "security.user"); + let user = userSelector(state); let userPermission = { canRead: true, canWrite: true @@ -552,46 +573,53 @@ function createThumbnail(map, metadataMap, nameThumbnail, dataThumbnail, categor * @param {string} dataThumbnail the data to save for the thubnail * @param {string} categoryThumbnail the category for the thumbnails * @param {number} resourceIdMap the id of the map - * @param {object} [options] options for the request - * @return {thunk} perform the update and dispatch proper actions + * @param {object} [options] options for the request + * @return {thunk} perform the update and dispatch proper actions */ function saveAll(map, metadataMap, nameThumbnail, dataThumbnail, categoryThumbnail, resourceIdMap, options) { - return (dispatch) => { + return (dispatch, getState) => { dispatch(mapUpdating(resourceIdMap)); dispatch(updatePermissions(resourceIdMap)); - if (dataThumbnail !== null && metadataMap !== null) { + const detailsChanged = currentMapDetailsChangedSelector(getState()); + if (detailsChanged) { + dispatch(saveResourceDetails()); + } + if (!isNil(dataThumbnail) && !isNil(metadataMap)) { dispatch(createThumbnail(map, metadataMap, nameThumbnail, dataThumbnail, categoryThumbnail, resourceIdMap, - updateMapMetadata(resourceIdMap, metadataMap.name, metadataMap.description, onDisplayMetadataEdit(false), options), null, options)); - } else if (dataThumbnail !== null) { - dispatch(createThumbnail(map, metadataMap, nameThumbnail, dataThumbnail, categoryThumbnail, resourceIdMap, null, onDisplayMetadataEdit(false), options)); - } else if (metadataMap !== null) { - dispatch(updateMapMetadata(resourceIdMap, metadataMap.name, metadataMap.description, onDisplayMetadataEdit(false), options)); - } else { + updateMapMetadata(resourceIdMap, metadataMap.name, metadataMap.description, !detailsChanged ? onDisplayMetadataEdit(false) : null, options), null, options, detailsChanged)); + } else if (!isNil(dataThumbnail)) { + dispatch(createThumbnail(map, metadataMap, nameThumbnail, dataThumbnail, categoryThumbnail, resourceIdMap, null, !detailsChanged ? onDisplayMetadataEdit(false) : null, options)); + } else if (!isNil(metadataMap)) { + dispatch(updateMapMetadata(resourceIdMap, metadataMap.name, metadataMap.description, !detailsChanged ? onDisplayMetadataEdit(false) : null, options)); + } + if (isNil(dataThumbnail) && isNil(metadataMap) && !detailsChanged) { dispatch(resetUpdating(resourceIdMap)); - dispatch(onDisplayMetadataEdit(false)); + /*dispatch(onDisplayMetadataEdit(false)); + dispatch(resetCurrentMap());*/ } - dispatch(resetCurrentMap()); }; } /** - * Deletes a thubnail. + * Deletes a thumbnail. * @memberof actions.maps * @param {number} resourceId the id of the thumbnail * @param {number} resourceIdMap the id of the map * @param {object} [options] options for the request, if any * @return {thunk} performs thumbnail cancellation */ -function deleteThumbnail(resourceId, resourceIdMap, options) { +function deleteThumbnail(resourceId, resourceIdMap, options, reset) { return (dispatch) => { + dispatch(mapUpdating(resourceIdMap)); GeoStoreApi.deleteResource(resourceId, options).then(() => { - dispatch(mapUpdating(resourceIdMap)); if (resourceIdMap) { dispatch(updateAttribute(resourceIdMap, "thumbnail", "NODATA", "STRING", options)); - dispatch(resetUpdating(resourceIdMap)); + if (reset) { + dispatch(resetUpdating(resourceIdMap)); + } } - dispatch(onDisplayMetadataEdit(false)); - dispatch(resetCurrentMap()); + /*dispatch(onDisplayMetadataEdit(false)); + dispatch(resetCurrentMap());*/ }).catch((e) => { // Even if is not possible to delete the Thumbnail from geostore -> reset the attribute in order to display the default thumbnail if (e.status === 403) { @@ -624,6 +652,7 @@ function createMap(metadata, content, thumbnail, options) { if (thumbnail && thumbnail.data) { dispatch(createThumbnail(null, null, thumbnail.name, thumbnail.data, thumbnail.category, resourceId, options)); } + dispatch(mapCreated(response.data, assign({id: response.data, canDelete: true, canEdit: true, canCopy: true}, metadata), content)); dispatch(onDisplayMetadataEdit(false)); }).catch((e) => { @@ -640,19 +669,180 @@ function createMap(metadata, content, thumbnail, options) { * @return {thunk} performs the delete operations and dispatches mapDeleted and loadMaps */ function deleteMap(resourceId, options) { - return (dispatch, getState) => { - dispatch(mapDeleting(resourceId)); - GeoStoreApi.deleteResource(resourceId, options).then(() => { - dispatch(mapDeleted(resourceId, "success")); - let state = getState && getState(); - if ( state && state.maps && state.maps.totalCount === state.maps.start) { - dispatch(loadMaps(false, state.maps.searchText || ConfigUtils.getDefaults().initialMapFilter || "*")); - } - }).catch((e) => { - dispatch(mapDeleted(resourceId, "failure", e)); - }); + return { + type: DELETE_MAP, + resourceId, + options + }; +} + +/** + * Toggles details modal + * @memberof actions.maps + * @return {action} type `TOGGLE_DETAILS_SHEET` +*/ +function toggleDetailsSheet(detailsSheetReadOnly) { + return { + type: TOGGLE_DETAILS_SHEET, + detailsSheetReadOnly }; } +/** + * Toggles groups properties section + * @memberof actions.maps + * @return {action} type `TOGGLE_GROUP_PROPERTIES` +*/ +function toggleGroupProperties() { + return { + type: TOGGLE_GROUP_PROPERTIES + }; +} +/** + * Toggles unsaved changes modal + * @memberof actions.maps + * @return {action} type `TOGGLE_UNSAVED_CHANGES` +*/ +function toggleUnsavedChanges() { + return { + type: TOGGLE_UNSAVED_CHANGES + }; +} +/** + * updates details section + * @memberof actions.maps + * @return {action} type `UPDATE_DETAILS` +*/ +function updateDetails(detailsText, doBackup, originalDetails) { + return { + type: UPDATE_DETAILS, + detailsText, + doBackup, + originalDetails + }; +} + +/** + * saves details section in the map state + * @memberof actions.maps + * @prop {string} detailsText string generated from html + * @return {action} type `SAVE_DETAILS` +*/ +function saveDetails(detailsText) { + return { + type: SAVE_DETAILS, + detailsText + }; +} + +/** + * deletes details section in the map state + * @memberof actions.maps + * @return {action} type `DELETE_DETAILS` +*/ +function deleteDetails() { + return { + type: DELETE_DETAILS + }; +} +/** + * set unsaved changes in the current map state, type `SET_DETAILS_CHANGED` + * @memberof actions.maps + * @prop {boolean} detailsChanged flag used to trigger the opening of the unsavedChangesModal + * @return {action} type `SET_DETAILS_CHANGED` +*/ +function setDetailsChanged(detailsChanged) { + return { + type: SET_DETAILS_CHANGED, + detailsChanged + }; +} +/** + * back details + * @memberof actions.maps + * @return {action} type `BACK_DETAILS` +*/ +function backDetails(backupDetails) { + return { + type: BACK_DETAILS, + backupDetails + }; +} +/** + * undo details + * @memberof actions.maps + * @return {action} type `UNDO_DETAILS` +*/ +function undoDetails() { + return { + type: UNDO_DETAILS + }; +} +/** + * setUnsavedChanged + * @memberof actions.maps + * @return {action} type `SET_UNSAVED_CHANGES` +*/ +function setUnsavedChanged(value) { + return { + type: SET_UNSAVED_CHANGES, + value + }; +} +/** + * openDetailsPanel + * @memberof actions.maps + * @return {action} type `OPEN_DETAILS_PANEL` +*/ +function openDetailsPanel() { + return { + type: OPEN_DETAILS_PANEL + }; +} +/** + * closeDetailsPanel + * @memberof actions.maps + * @return {action} type `CLOSE_DETAILS_PANEL` +*/ +function closeDetailsPanel() { + return { + type: CLOSE_DETAILS_PANEL + }; +} +/** + * detailsLoaded + * @memberof actions.maps + * @return {action} type `DETAILS_LOADED` +*/ +function detailsLoaded(mapId, detailsUri) { + return { + type: DETAILS_LOADED, + mapId, + detailsUri + }; +} +/** + * detailsSaving + * @memberof actions.maps + * @return {action} type `DETAILS_SAVING` +*/ +function detailsSaving(saving) { + return { + type: DETAILS_SAVING, + saving + }; +} +/** + * do nothing action + * @memberof actions.maps + * @return {action} type `DO_NOTHING` +*/ +function doNothing() { + return { + type: DO_NOTHING + }; +} + + /** * Actions for maps * @name actions.maps @@ -678,9 +868,25 @@ module.exports = { DISPLAY_METADATA_EDIT, RESET_UPDATING, MAP_ERROR, - RESET_CURRENT_MAP, MAPS_SEARCH_TEXT_CHANGED, METADATA_CHANGED, + toggleDetailsSheet, TOGGLE_DETAILS_SHEET, + toggleGroupProperties, TOGGLE_GROUP_PROPERTIES, + toggleUnsavedChanges, TOGGLE_UNSAVED_CHANGES, + updateDetails, UPDATE_DETAILS, + saveDetails, SAVE_DETAILS, + deleteDetails, DELETE_DETAILS, + setDetailsChanged, SET_DETAILS_CHANGED, + saveResourceDetails, SAVE_RESOURCE_DETAILS, + backDetails, BACK_DETAILS, + undoDetails, UNDO_DETAILS, + doNothing, DO_NOTHING, + setUnsavedChanged, SET_UNSAVED_CHANGES, + openDetailsPanel, OPEN_DETAILS_PANEL, + closeDetailsPanel, CLOSE_DETAILS_PANEL, + deleteMap, DELETE_MAP, + detailsLoaded, DETAILS_LOADED, + detailsSaving, DETAILS_SAVING, metadataChanged, loadMaps, mapsLoading, @@ -691,7 +897,6 @@ module.exports = { updateMap, updateMapMetadata, mapMetadataUpdated, - deleteMap, deleteThumbnail, createThumbnail, mapUpdating, @@ -709,7 +914,6 @@ module.exports = { saveAll, onDisplayMetadataEdit, resetUpdating, - resetCurrentMap, mapError, mapsSearchTextChanged, updateAttribute diff --git a/web/client/api/GeoStoreDAO.js b/web/client/api/GeoStoreDAO.js index 1c5d254b32..752a728f6d 100644 --- a/web/client/api/GeoStoreDAO.js +++ b/web/client/api/GeoStoreDAO.js @@ -29,6 +29,7 @@ let parseUserGroups = (groupsObj) => { return groupsObj.User.groups.group.filter(obj => !!obj.id).map((obj) => _.pick(obj, ["id", "groupName", "description"])); }; +const boolToString = (b) => b ? "true" : "false"; const encodeContent = function(content) { return utfEncode(content); }; @@ -54,7 +55,7 @@ var Api = { }, getResourcesByCategory: function(category, query, options) { const q = query || "*"; - const url = "extjs/search/category/" + category + "/*" + q + "*/thumbnail"; // comma-separated list of wanted attributes + const url = "extjs/search/category/" + category + "/*" + q + "*/thumbnail,details"; // comma-separated list of wanted attributes return axios.get(url, this.addBaseUrl(parseOptions(options))).then(function(response) {return response.data; }); }, getUserDetails: function(username, password, options) { @@ -111,6 +112,15 @@ var Api = { } }, options))); }, + getResourceAttribute: function(resourceId, name, options = {}) { + return axios.get( + "resources/resource/" + resourceId + "/attributes/" + name, + this.addBaseUrl(_.merge({ + headers: { + 'Content-Type': "application/xml" + } + }, options))); + }, putResourceMetadata: function(resourceId, newName, newDescription, options) { return axios.put( "resources/resource/" + resourceId, @@ -139,14 +149,14 @@ var Api = { if (rule.canRead || rule.canWrite) { if (rule.user) { payload = payload + "{this.props.errorImage}
- { (this.props.map.errors.map((error) =>{this.props.errorImage}
+ { (this.props.map.errors.map((error) =>
diff --git a/web/client/components/security/__tests__/PermissionEditor-test.jsx b/web/client/components/security/__tests__/PermissionEditor-test.jsx
index b82a119933..a0a2944a0a 100644
--- a/web/client/components/security/__tests__/PermissionEditor-test.jsx
+++ b/web/client/components/security/__tests__/PermissionEditor-test.jsx
@@ -14,6 +14,7 @@ const PermissionEditor = require('../PermissionEditor');
let setupEditor = (docElement, actions) => {
return ReactDOM.render( details of this map "; +const detailsUri = "data/2"; +let map1 = { + id: mapId, + name: "name" +}; +let map8 = { + id: mapId8, + name: "name" +}; +const mapsState = { + maps: { + results: [map1] + }, + mapInitialConfig: { + mapId + }, + map: { + present: { + info: { + details: encodeURIComponent(detailsUri) + } + } + } +}; +describe('maps Epics', () => { + let store; + beforeEach(() => { + store = mockStore(); + }); + + afterEach(() => { + epicMiddleware.replaceEpic(rootEpic); + }); + it('test mapCreatedNotificationEpic', (done) => { + + store.dispatch(mapCreated(1, {name: "name", description: "description"}, "content", null)); + store.dispatch(clear()); + + setTimeout( () => { + try { + const actions = store.getActions(); + expect(actions.length).toBe(3); + expect(actions[1].type).toBe(SHOW_NOTIFICATION); + } catch (e) { + return done(e); + } + done(); + }, 100); + + }); + it('test setDetailsChangedEpic', (done) => { + + store.dispatch(saveDetails("some details ")); + + setTimeout( () => { + try { + const actions = store.getActions(); + expect(actions.length).toBe(3); + expect(actions[1].type).toBe(TOGGLE_DETAILS_SHEET); + expect(actions[2].type).toBe(SET_DETAILS_CHANGED); + } catch (e) { + return done(e); + } + done(); + }, 100); + + }); + it('test setDetailsChangedEpic with details resource present', (done) => { + testEpic(setDetailsChangedEpic, 1, saveDetails(detailsText), actions => { + expect(actions.length).toBe(1); + actions.map((action) => { + switch (action.type) { + case SET_DETAILS_CHANGED: + expect(action.detailsChanged).toBe(false); + break; + case TOGGLE_DETAILS_SHEET: + expect(action.detailsSheetReadOnly).toBe(true); + break; + default: + expect(true).toBe(false); + } + }); + done(); + }, { + locale, + currentMap: { + id: mapId, + details: "wrong/uri/4", + detailsText, + originalDetails: detailsText + } + }); + }); + + it('test closeDetailsPanel', (done) => { + + store.dispatch(closeDetailsPanel()); + + setTimeout( () => { + try { + const actions = store.getActions(); + expect(actions.length).toBe(3); + expect(actions[0].type).toBe(CLOSE_DETAILS_PANEL); + expect(actions[1].type).toBe(TOGGLE_CONTROL); + expect(actions[2].type).toBe(RESET_CURRENT_MAP); + } catch (e) { + return done(e); + } + done(); + }, 100); + + }); + it('test fetchDataForDetailsPanel', (done) => { + map1.details = encodeURIComponent(detailsUri); + testEpic(addTimeoutEpic(fetchDataForDetailsPanel), 3, openDetailsPanel(), actions => { + expect(actions.length).toBe(3); + actions.map((action) => { + switch (action.type) { + case TOGGLE_CONTROL: + expect(action.control).toBe("details"); + expect(action.property).toBe("enabled"); + break; + case CLOSE_FEATURE_GRID: + expect(action.type).toBe(CLOSE_FEATURE_GRID); + break; + case UPDATE_DETAILS: + expect(action.detailsText.indexOf(detailsText)).toNotBe(-1); + expect(action.originalDetails.indexOf(detailsText)).toNotBe(-1); + expect(action.doBackup).toBe(true); + break; + default: + expect(true).toBe(false); + } + }); + done(); + }, mapsState); + }); + it('test fetchDataForDetailsPanel with Error', (done) => { + testEpic(addTimeoutEpic(fetchDataForDetailsPanel), 2, openDetailsPanel(), actions => { + expect(actions.length).toBe(2); + actions.map((action) => { + switch (action.type) { + case TOGGLE_CONTROL: + expect(action.control).toBe("details"); + expect(action.property).toBe("enabled"); + break; + case SHOW_NOTIFICATION: + expect(action.message).toBe("errorFetchingDetailsOfMap 1"); + break; + default: + expect(true).toBe(false); + } + }); + done(); + }, { + locale: { + messages: { + maps: { + feedback: { + errorFetchingDetailsOfMap: "errorFetchingDetailsOfMap " + } + } + } + }, + mapInitialConfig: { + mapId + }, + map: { + present: { + info: {} + } + } + }); + }); + it('test fetchDetailsFromResourceEpic, map without saved Details', (done) => { + delete map1.details; + testEpic(addTimeoutEpic(fetchDetailsFromResourceEpic), 1, editMap({}, true), actions => { + expect(actions.length).toBe(1); + actions.map((action) => { + switch (action.type) { + case UPDATE_DETAILS: + expect(action.detailsText).toBe(""); + expect(action.originalDetails).toBe(""); + expect(action.doBackup).toBe(true); + break; + default: + expect(true).toBe(false); + } + }); + done(); + }, { + currentMap: { + id: mapId + } + }); + }); + + it('test fetchDetailsFromResourceEpic, map with saved Details', (done) => { + testEpic(addTimeoutEpic(fetchDetailsFromResourceEpic), 1, editMap({}, true), actions => { + expect(actions.length).toBe(1); + actions.map((action) => { + switch (action.type) { + case UPDATE_DETAILS: + expect(action.detailsText.indexOf(detailsText)).toNotBe(-1); + expect(action.originalDetails.indexOf(detailsText)).toNotBe(-1); + expect(action.doBackup).toBe(true); + break; + default: + expect(true).toBe(false); + } + }); + done(); + }, { + currentMap: { + id: mapId, + details: encodeURIComponent(detailsUri) + } + }); + }); + + it('test fetchDetailsFromResourceEpic, withError', (done) => { + testEpic(addTimeoutEpic(fetchDetailsFromResourceEpic), 1, editMap({}, true), actions => { + expect(actions.length).toBe(1); + actions.map((action) => { + switch (action.type) { + case SHOW_NOTIFICATION: + expect(action.message).toBe("errorFetchingDetailsOfMap 1"); + break; + default: + expect(true).toBe(false); + } + }); + done(); + }, { + locale, + currentMap: { + id: mapId, + details: "wrong/uri/sfdsdfs" + } + }); + }); + it('test deleteMapAndAssociatedResourcesEpic, with map, details, thumbnail errors', (done) => { + map1.thumbnail = "wronguri/5/"; + map1.details = "wronguri/6/"; + testEpic(addTimeoutEpic(deleteMapAndAssociatedResourcesEpic), 5, deleteMap(mapId, {}), actions => { + expect(actions.length).toBe(5); + actions.map((action, i) => { + switch (action.type) { + case SHOW_NOTIFICATION: + if (i === 1) { + expect(action.message).toBe("errorDeletingDetailsOfMap " + mapId); + } + if (i === 2) { + expect(action.message).toBe("errorDeletingThumbnailOfMap " + mapId); + } + if (i === 3) { + expect(action.message).toBe("errorDeletingMap " + mapId); + } + break; + case MAP_DELETING: + expect(action.resourceId).toBe(mapId); + break; + case MAP_DELETED: + expect(action.resourceId).toBe(mapId); + expect(action.result).toBe("failure"); + break; + default: + expect(true).toBe(false); + } + }); + done(); + }, { + locale, + maps: { + results: [map1], + totalCount: 1, + start: 1 + }, + currentMap: { + id: mapId, + details: "wrong/uri/4" + } + }); + }); + it('test deleteMapAndAssociatedResourcesEpic, only map deleted, details and thumbnail not provided', (done) => { + testEpic(addTimeoutEpic(deleteMapAndAssociatedResourcesEpic), 3, deleteMap(mapId8, {}), actions => { + map8.thumbnail = "NODATA"; + map8.details = "NODATA"; + expect(actions.length).toBe(3); + actions.map((action) => { + switch (action.type) { + case SHOW_NOTIFICATION: + expect(action.message).toBe("allResDeleted " + mapId8); + break; + case MAP_DELETING: + expect(action.resourceId).toBe(mapId8); + break; + case MAP_DELETED: + expect(action.resourceId).toBe(mapId8); + expect(action.result).toBe("success"); + break; + default: + expect(true).toBe(false); + } + }); + done(); + }, { + locale, + maps: { + results: [map8], + totalCount: 2, + start: 1 + }, + currentMap: { + id: mapId8 + } + }); + }); + it('test deleteMapAndAssociatedResourcesEpic, map deleted, but details, thumbnail errors', (done) => { + map8.thumbnail = "wronguri/5/"; + map8.details = "wronguri/6/"; + testEpic(addTimeoutEpic(deleteMapAndAssociatedResourcesEpic), 6, deleteMap(mapId8, {}), actions => { + expect(actions.length).toBe(6); + actions.filter(a => !!a.type).map((action, i) => { + switch (action.type) { + case SHOW_NOTIFICATION: + if (i === 1) { + expect(action.message).toBe("errorDeletingDetailsOfMap " + mapId8); + } + if (i === 2) { + expect(action.message).toBe("errorDeletingThumbnailOfMap " + mapId8); + } + break; + case MAP_DELETING: + expect(action.resourceId).toBe(mapId8); + break; + case MAP_DELETED: + expect(action.resourceId).toBe(mapId8); + expect(action.result).toBe("success"); + break; + case TEST_TIMEOUT: + expect(action.type).toBe(TEST_TIMEOUT); + break; + default: + expect(true).toBe(false); + } + }); + done(); + }, { + locale, + maps: { + results: [map8] + }, + currentMap: { + id: mapId8, + details: "wrong/uri/4" + } + }); + }); + it('test deleteMapAndAssociatedResourcesEpic, map, details, thumbnail deleted', (done) => { + map8.thumbnail = "wronguri/9/"; + map8.details = "wronguri/10/"; + testEpic(addTimeoutEpic(deleteMapAndAssociatedResourcesEpic), 4, deleteMap(mapId8, {}), actions => { + expect(actions.length).toBe(4); + actions.filter(a => !!a.type).map((action) => { + switch (action.type) { + case SHOW_NOTIFICATION: + expect(action.message).toBe("allResDeleted " + mapId8); + break; + case MAP_DELETING: + expect(action.resourceId).toBe(mapId8); + break; + case MAP_DELETED: + expect(action.resourceId).toBe(mapId8); + expect(action.result).toBe("success"); + break; + case TEST_TIMEOUT: + expect(action.type).toBe(TEST_TIMEOUT); + break; + default: + expect(true).toBe(false); + } + }); + done(); + }, { + locale, + maps: { + results: [map8] + }, + currentMap: { + id: mapId8, + details: "wrong/uri/4" + } + }); + }); + +}); diff --git a/web/client/epics/featuregrid.js b/web/client/epics/featuregrid.js index b73941d008..6f8d7519fa 100644 --- a/web/client/epics/featuregrid.js +++ b/web/client/epics/featuregrid.js @@ -396,10 +396,13 @@ module.exports = { .switchMap(() => Rx.Observable.of(drawSupportReset()))) ), - closeCatalogOnFeatureGridOpen: (action$) => + closeRightPanelOnFeatureGridOpen: (action$) => action$.ofType(OPEN_FEATURE_GRID) .switchMap( () => { - return Rx.Observable.of(setControlProperty('metadataexplorer', 'enabled', false)); + return Rx.Observable.from( + [setControlProperty('metadataexplorer', 'enabled', false), + setControlProperty('annotations', 'enabled', false), + setControlProperty('details', 'enabled', false)]); }), /** * intercept geometry changed events in draw support to update current diff --git a/web/client/epics/maplayout.js b/web/client/epics/maplayout.js index 88dc5e24d4..18b4232825 100644 --- a/web/client/epics/maplayout.js +++ b/web/client/epics/maplayout.js @@ -54,6 +54,7 @@ const updateMapLayoutEpic = (action$, store) => ].filter(panel => panel)) || {left: 0}; const rightPanels = head([ + get(store.getState(), "controls.details.enabled") && {right: mapLayout.right.md} || null, get(store.getState(), "controls.annotations.enabled") && {right: mapLayout.right.md} || null, get(store.getState(), "controls.metadataexplorer.enabled") && {right: mapLayout.right.md} || null, mapInfoRequestsSelector(store.getState()).length > 0 && {right: mapLayout.right.md} || null diff --git a/web/client/epics/maps.js b/web/client/epics/maps.js new file mode 100644 index 0000000000..2901f185b8 --- /dev/null +++ b/web/client/epics/maps.js @@ -0,0 +1,258 @@ +/* + * Copyright 2017, 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. +*/ + +const Rx = require('rxjs'); +const uuidv1 = require('uuid/v1'); +const {CLEAR_NOTIFICATIONS} = require('../actions/notifications'); +const {basicError, basicSuccess} = require('../utils/NotificationUtils'); +const LocaleUtils = require('../utils/LocaleUtils'); +const GeoStoreApi = require('../api/GeoStoreDAO'); +const { MAP_INFO_LOADED } = require('../actions/config'); + +const { + SAVE_DETAILS, SAVE_RESOURCE_DETAILS, MAP_CREATED, + DELETE_MAP, OPEN_DETAILS_PANEL, + CLOSE_DETAILS_PANEL, + setDetailsChanged, updateDetails, + mapDeleting, mapDeleted, loadMaps, + doNothing, detailsLoaded, detailsSaving, onDisplayMetadataEdit, + RESET_UPDATING, resetUpdating, toggleDetailsSheet +} = require('../actions/maps'); +const { + resetCurrentMap, EDIT_MAP +} = require('../actions/currentMap'); +const {closeFeatureGrid} = require('../actions/featuregrid'); +const {toggleControl} = require('../actions/controls'); +const { + mapPermissionsFromIdSelector, mapThumbnailsUriFromIdSelector, + mapDetailsUriFromIdSelector, isMapsLastPageSelector +} = require('../selectors/maps'); +const { + mapIdSelector, mapInfoDetailsUriFromIdSelector +} = require('../selectors/map'); +const { + currentMapDetailsTextSelector, currentMapIdSelector, + currentMapDetailsUriSelector, currentMapSelector, + currentMapDetailsChangedSelector, currentMapOriginalDetailsTextSelector +} = require('../selectors/currentmap'); +const { + currentMessagesSelector +} = require('../selectors/locale'); + +const {userParamsSelector} = require('../selectors/security'); +const {manageMapResource, deleteResourceById, getIdFromUri} = require('../utils/ObservableUtils'); +const ConfigUtils = require('../utils/ConfigUtils'); + +/** + If details are changed from the original ones then set unsavedChanges to true +*/ +const setDetailsChangedEpic = (action$, store) => + action$.ofType(SAVE_DETAILS) + .switchMap((a) => { + let actions = []; + const state = store.getState(); + const detailsUri = currentMapDetailsUriSelector(state); + if (a.detailsText.length <= 500000) { + actions.push(toggleDetailsSheet(true)); + } else { + actions.push(basicError({message: "maps.feedback.errorSizeExceeded"})); + } + if (!detailsUri) { + actions.push(setDetailsChanged(a.detailsText !== "adsojvasova "; +const originalDetails = "old value "; +const mapId = 1; +const currentMapState = { + currentMap: { + name, + description, + details: uri, + thumbnail: uri, + id: mapId, + detailsText, + originalDetails, + detailsChanged: true + }}; +describe('Test current map selectors', () => { + it('test currentMapSelector', () => { + const props = currentMapSelector(currentMapState); + expect(props.detailsText).toBe("adsojvasova "); + }); + it('test currentMapIdSelector', () => { + const props = currentMapIdSelector(currentMapState); + expect(props).toBe(mapId); + }); + it('test currentMapNameSelector', () => { + const props = currentMapNameSelector(currentMapState); + expect(props).toBe("name"); + }); + it('test currentMapDetailsUriSelector', () => { + const props = currentMapDetailsUriSelector(currentMapState); + expect(props).toBe(uri); + }); + it('test currentMapDecriptionSelector', () => { + const props = currentMapDecriptionSelector(currentMapState); + expect(props).toBe(description); + }); + it('test currentMapDetailsTextSelector', () => { + const props = currentMapDetailsTextSelector(currentMapState); + expect(props).toBe("adsojvasova "); + }); + it('test currentMapThumbnailUriSelector', () => { + const props = currentMapThumbnailUriSelector(currentMapState); + expect(props).toBe(uri); + }); + it('test currentMapDetailsChangedSelector', () => { + const props = currentMapDetailsChangedSelector(currentMapState); + expect(props).toBeTruthy(); + }); + it('test currentMapOriginalDetailsTextSelector', () => { + const props = currentMapOriginalDetailsTextSelector(currentMapState); + expect(props).toBe(originalDetails); + }); + +}); diff --git a/web/client/selectors/__tests__/locale-test.js b/web/client/selectors/__tests__/locale-test.js index 89d3081691..c77fb552d6 100644 --- a/web/client/selectors/__tests__/locale-test.js +++ b/web/client/selectors/__tests__/locale-test.js @@ -7,11 +7,16 @@ */ const expect = require('expect'); -const {currentLocaleSelector} = require('../locale'); +const {currentLocaleSelector, currentMessagesSelector } = require('../locale'); const state = { locale: { - current: 'en-US' + current: 'en-US', + messages: { + "details": { + "title": "Details" + } + } } }; @@ -21,4 +26,9 @@ describe('Test locale selectors', () => { expect(currentLocale).toExist(); expect(currentLocale).toBe(state.locale.current); }); + it('test currentMessagesSelector ', () => { + const messages = currentMessagesSelector(state); + expect(messages).toExist(); + expect(messages.details.title).toBe("Details"); + }); }); diff --git a/web/client/selectors/__tests__/map-test.js b/web/client/selectors/__tests__/map-test.js index 3b0f0dc421..63d047de0d 100644 --- a/web/client/selectors/__tests__/map-test.js +++ b/web/client/selectors/__tests__/map-test.js @@ -7,7 +7,15 @@ */ const expect = require('expect'); -const {mapSelector, projectionSelector, mapVersionSelector, mapIdSelector, projectionDefsSelector, mapNameSelector} = require('../map'); +const { + mapSelector, + projectionSelector, + mapVersionSelector, + mapIdSelector, + projectionDefsSelector, + mapNameSelector, + mapInfoDetailsUriFromIdSelector +} = require('../map'); const center = {x: 1, y: 1}; let state = { map: {center: center}, @@ -17,6 +25,20 @@ let state = { }; describe('Test map selectors', () => { + it('test mapInfoDetailsUriFromIdSelector from config', () => { + const details = "rest%2Fgeostore%2Fdata%2F3495%2Fraw%3Fdecode%3Ddatauri"; + const props = mapInfoDetailsUriFromIdSelector({ + map: { + present: { + info: { + details + } + } + }}); + + expect(props).toExist(); + expect(props).toBe(details); + }); it('test mapSelector from config', () => { const props = mapSelector({config: state}); @@ -61,6 +83,8 @@ describe('Test map selectors', () => { it('test mapIdSelector', () => { const props = mapIdSelector(state); expect(props).toBe(123); + const propsEmpty = mapIdSelector({}); + expect(propsEmpty).toBe(null); }); it('test mapVersionSelector', () => { diff --git a/web/client/selectors/__tests__/maps-test.js b/web/client/selectors/__tests__/maps-test.js new file mode 100644 index 0000000000..a051ead7e9 --- /dev/null +++ b/web/client/selectors/__tests__/maps-test.js @@ -0,0 +1,96 @@ +/* +* Copyright 2017, 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. +*/ + +const expect = require('expect'); +const { + mapNameSelector, + mapFromIdSelector, + mapsResultsSelector, + mapMetadataSelector, + isMapsLastPageSelector, + mapDescriptionSelector, + mapDetailsUriFromIdSelector, + mapPermissionsFromIdSelector, + mapThumbnailsUriFromIdSelector +} = require('../maps'); + +const name = "name"; +const description = "description"; +const details = "%2Fmapstore%2Frest%2Fgeostore%2Fdata%2F10%2Fraw%3Fdecode%3Ddatauri"; +const thumbnail = "%2Fmapstore%2Frest%2Fgeostore%2Fdata%2F10%2Fraw%3Fdecode%3Ddatauri"; +const detailsText = "name "; +const mapId = 1; +const creation = '2017-12-01 10:58:46.337'; +const mapsState = { + maps: { + metadata: { + name, + description + }, + results: [ + { + canDelete: true, + canEdit: true, + canCopy: true, + creation, + description, + id: mapId, + name, + thumbnail, + details, + detailsText, + owner: 'admin', + permissions: [ + {name: "name"} + ] + } + ] + } +}; +describe('Test maps selectors', () => { + + it('test mapsResultsSelector no state', () => { + const props = mapsResultsSelector(mapsState); + expect(props.length).toBe(1); + expect(props[0].creation).toBe(creation); + expect(props[0].id).toBe(mapId); + }); + it('test mapFromIdSelector no state', () => { + const props = mapFromIdSelector(mapsState, mapId); + expect(props.creation).toBe(creation); + }); + it('test mapNameSelector no state', () => { + const props = mapNameSelector(mapsState, mapId); + expect(props).toBe(name); + }); + it('test mapMetadataSelector no state', () => { + const props = mapMetadataSelector(mapsState); + expect(props.name).toBe(name); + expect(props.description).toBe(description); + }); + it('test isMapsLastPageSelector no state', () => { + const props = isMapsLastPageSelector(mapsState); + expect(props).toBeTruthy(); + }); + it('test mapDescriptionSelector no state', () => { + const props = mapDescriptionSelector(mapsState, mapId); + expect(props).toBe(description); + }); + it('test mapDetailsUriFromIdSelector no state', () => { + const props = mapDetailsUriFromIdSelector(mapsState, mapId); + expect(props).toBe("%2Fmapstore%2Frest%2Fgeostore%2Fdata%2F10%2Fraw%3Fdecode%3Ddatauri"); + }); + it('test mapPermissionsFromIdSelector no state', () => { + const props = mapPermissionsFromIdSelector(mapsState, mapId); + expect(props.length).toBe(1); + }); + it('test mapThumbnailsUriFromIdSelector no state', () => { + const props = mapThumbnailsUriFromIdSelector(mapsState, mapId); + expect(props).toBe(thumbnail); + }); +}); diff --git a/web/client/selectors/__tests__/security-test.js b/web/client/selectors/__tests__/security-test.js index 233a243e4a..054c7aa5bf 100644 --- a/web/client/selectors/__tests__/security-test.js +++ b/web/client/selectors/__tests__/security-test.js @@ -11,7 +11,9 @@ const { userSelector, userRoleSelector, isAdminUserSelector, - rulesSelector + rulesSelector, + userGroupSecuritySelector, + userParamsSelector } = require('../security'); const id = 1833; const name = 'teo'; @@ -77,5 +79,16 @@ describe('Test security selectors', () => { const rules = rulesSelector(initialState); expect(rules).toExist(); }); + it('test userGroupSecuritySelector ', () => { + const group = userGroupSecuritySelector(initialState); + expect(group).toExist(); + expect(group.id).toBe(479); + }); + it('test userParamsSelector ', () => { + const userParams = userParamsSelector(initialState); + expect(userParams).toExist(); + expect(userParams.id).toBe(id); + expect(userParams.name).toBe(name); + }); }); diff --git a/web/client/selectors/currentmap.js b/web/client/selectors/currentmap.js new file mode 100644 index 0000000000..efe9da40de --- /dev/null +++ b/web/client/selectors/currentmap.js @@ -0,0 +1,37 @@ +/* +* Copyright 2017, 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. +*/ + +const {get} = require('lodash'); + +/** + * selects currentmap state + * @name currentmap + * @memberof selectors + * @static +*/ + +const currentMapSelector = (state) => get(state, "currentMap", {}); +const currentMapIdSelector = (state) => get(state, "currentMap.id", ""); +const currentMapNameSelector = (state) => get(state, "currentMap.name", ""); +const currentMapDetailsUriSelector = (state) => get(state, "currentMap.details", ""); +const currentMapDecriptionSelector = (state) => get(state, "currentMap.description", ""); +const currentMapDetailsTextSelector = (state) => get(state, "currentMap.detailsText", ""); +const currentMapThumbnailUriSelector = (state) => get(state, "currentMap.thumbnail", ""); +const currentMapDetailsChangedSelector = (state) => get(state, "currentMap.detailsChanged", false); +const currentMapOriginalDetailsTextSelector = (state) => get(state, "currentMap.originalDetails", false); +module.exports = { + currentMapSelector, + currentMapIdSelector, + currentMapNameSelector, + currentMapDecriptionSelector, + currentMapDetailsUriSelector, + currentMapDetailsTextSelector, + currentMapThumbnailUriSelector, + currentMapDetailsChangedSelector, + currentMapOriginalDetailsTextSelector +}; diff --git a/web/client/selectors/locale.js b/web/client/selectors/locale.js index ae36252310..aecd456558 100644 --- a/web/client/selectors/locale.js +++ b/web/client/selectors/locale.js @@ -7,7 +7,9 @@ */ const currentLocaleSelector = (state) => state.locale && state.locale.current || 'en-US'; +const currentMessagesSelector = (state) => state.locale && state.locale.messages || {}; module.exports = { - currentLocaleSelector + currentLocaleSelector, + currentMessagesSelector }; diff --git a/web/client/selectors/map.js b/web/client/selectors/map.js index e5f156e45a..35ce985ca4 100644 --- a/web/client/selectors/map.js +++ b/web/client/selectors/map.js @@ -29,7 +29,8 @@ const mapSelector = (state) => state.map && state.map.present || state.map || st const projectionDefsSelector = (state) => state.localConfig && state.localConfig.projectionDefs || []; const projectionSelector = createSelector([mapSelector], (map) => map && map.projection); -const mapIdSelector = (state) => get(state, "mapInitialConfig.mapId"); +const mapIdSelector = (state) => get(state, "mapInitialConfig.mapId") && parseInt(get(state, "mapInitialConfig.mapId"), 10) || null; +const mapInfoDetailsUriFromIdSelector = (state) => mapSelector(state) && mapSelector(state).info && mapSelector(state).info.details; /** * Get the scales of the current map @@ -68,6 +69,7 @@ const mapVersionSelector = (state) => state.map && state.map.present && state.ma const mapNameSelector = (state) => state.map && state.map.present && state.map.present.info && state.map.present.info.name || ''; module.exports = { + mapInfoDetailsUriFromIdSelector, mapSelector, scalesSelector, projectionSelector, diff --git a/web/client/selectors/maps.js b/web/client/selectors/maps.js new file mode 100644 index 0000000000..3e5d9a395a --- /dev/null +++ b/web/client/selectors/maps.js @@ -0,0 +1,37 @@ +/* +* Copyright 2017, 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. +*/ +const {find, get} = require('lodash'); + +/** + * selects maps state + * @name maps + * @memberof selectors + * @static +*/ + +const mapsResultsSelector = (state) => get(state, "maps.results", []); +const mapFromIdSelector = (state, id) => find(mapsResultsSelector(state), m => m.id === id); +const mapNameSelector = (state, id) => mapFromIdSelector(state, id) && mapFromIdSelector(state, id).name || ""; +const mapMetadataSelector = (state) => get(state, "maps.metadata", {}); +const isMapsLastPageSelector = (state) => state && state.maps && state.maps.totalCount === state.maps.start; +const mapDescriptionSelector = (state, id) => mapFromIdSelector(state, id) && mapFromIdSelector(state, id).description || ""; +const mapDetailsUriFromIdSelector = (state, id) => mapFromIdSelector(state, id) && mapFromIdSelector(state, id).details || ""; +const mapPermissionsFromIdSelector = (state, id) => mapFromIdSelector(state, id) && mapFromIdSelector(state, id).permissions || ""; +const mapThumbnailsUriFromIdSelector = (state, id) => mapFromIdSelector(state, id) && mapFromIdSelector(state, id).thumbnail || ""; + +module.exports = { + mapNameSelector, + mapFromIdSelector, + mapsResultsSelector, + mapMetadataSelector, + isMapsLastPageSelector, + mapDescriptionSelector, + mapDetailsUriFromIdSelector, + mapPermissionsFromIdSelector, + mapThumbnailsUriFromIdSelector +}; diff --git a/web/client/selectors/security.js b/web/client/selectors/security.js index a4b03c89f6..c3b8bf03c0 100644 --- a/web/client/selectors/security.js +++ b/web/client/selectors/security.js @@ -7,6 +7,7 @@ */ const assign = require('object-assign'); +const {get} = require('lodash'); const rulesSelector = (state) => { if (!state.security || !state.security.rules) { @@ -29,12 +30,22 @@ const rulesSelector = (state) => { }; const userSelector = (state) => state && state.security && state.security.user; +const userGroupSecuritySelector = (state) => get(state, "security.user.groups.group"); const userRoleSelector = (state) => userSelector(state) && userSelector(state).role; +const userParamsSelector = (state) => { + const user = userSelector(state); + return { + id: user.id, + name: user.name + }; +}; module.exports = { rulesSelector, userSelector, + userParamsSelector, userRoleSelector, + userGroupSecuritySelector, isAdminUserSelector: (state) => userRoleSelector(state) === "ADMIN" }; diff --git a/web/client/test-resources/geostore/data/2 b/web/client/test-resources/geostore/data/2 new file mode 100644 index 0000000000..3d9a54dd8a --- /dev/null +++ b/web/client/test-resources/geostore/data/2 @@ -0,0 +1 @@ + details of this map diff --git a/web/client/test-resources/geostore/resources/resource/10 b/web/client/test-resources/geostore/resources/resource/10 new file mode 100644 index 0000000000..e69de29bb2 diff --git a/web/client/test-resources/geostore/resources/resource/8 b/web/client/test-resources/geostore/resources/resource/8 new file mode 100644 index 0000000000..e69de29bb2 diff --git a/web/client/test-resources/geostore/resources/resource/9 b/web/client/test-resources/geostore/resources/resource/9 new file mode 100644 index 0000000000..e69de29bb2 diff --git a/web/client/themes/default/icons.less b/web/client/themes/default/icons.less index edd0ed234d..be16cd6909 100644 --- a/web/client/themes/default/icons.less +++ b/web/client/themes/default/icons.less @@ -855,6 +855,10 @@ content: "\00ab"; } +.glyphicon-sheet:before { + content: "\00ac"; +} + /* TODO: icons table positions (ie/firefox compatibility) */ .glyphicon-polyline-remove:before { diff --git a/web/client/themes/default/icons/icons.eot b/web/client/themes/default/icons/icons.eot index 969254f3d5..aaefcbae54 100644 Binary files a/web/client/themes/default/icons/icons.eot and b/web/client/themes/default/icons/icons.eot differ diff --git a/web/client/themes/default/icons/icons.svg b/web/client/themes/default/icons/icons.svg index 7df1c14916..1861076d74 100644 --- a/web/client/themes/default/icons/icons.svg +++ b/web/client/themes/default/icons/icons.svg @@ -2,7 +2,7 @@ diff --git a/web/client/themes/default/icons/icons.ttf b/web/client/themes/default/icons/icons.ttf index ac12c90a70..69bf2e195b 100644 Binary files a/web/client/themes/default/icons/icons.ttf and b/web/client/themes/default/icons/icons.ttf differ diff --git a/web/client/themes/default/icons/icons.woff b/web/client/themes/default/icons/icons.woff index d98cb5ce2f..9373a6b78c 100644 Binary files a/web/client/themes/default/icons/icons.woff and b/web/client/themes/default/icons/icons.woff differ diff --git a/web/client/themes/default/less/details.less b/web/client/themes/default/less/details.less new file mode 100644 index 0000000000..6304f57ecd --- /dev/null +++ b/web/client/themes/default/less/details.less @@ -0,0 +1,16 @@ +.details-close { + float: right +} + +.details-panel .panel-heading { + background-color: @ms2-color-primary; + color: @ms2-color-text-primary; +} + +.details-panel div div div.ms2-border-layout-body { + background-color: @ms2-color-background; +} + +.details-panel .panel-body { + height: ~'calc(100% - 31px)' +} diff --git a/web/client/themes/default/less/maps-properties.less b/web/client/themes/default/less/maps-properties.less new file mode 100644 index 0000000000..dfe41fdf6e --- /dev/null +++ b/web/client/themes/default/less/maps-properties.less @@ -0,0 +1,295 @@ +.ms-detail-body { + padding: 15px; + + p { + word-wrap: break-word; + } + + img { + /*width: 100%;*/ + } +} + +.mapstore-permission-group { + width: 100%; + .row { + margin-top: 10px !important; + display: flex; + padding: 0 15px; + .ms-col-grab { + width: @icon-size / 2; + padding: 0; + overflow: hidden; + display: flex; + span { + width: @icon-size / 4; + height: @icon-size / 2; + margin: auto; + } + } + + .ms-col { + padding: 0 5px; + margin: auto; + width: auto; + &:first-child { + padding: 0; + margin-left: 0; + } + &:last-child { + padding: 0; + margin-right: 0; + } + } + } +} + +.modal-properties-container { + flex: 1; + padding: 0; + overflow-y: auto; + &.ms-no-scroll { + overflow-y: hidden; + } + .container-fluid { + padding: 0; + width: 100%; + .row { + width: 100%; + margin: 0; + } + } + .col-xs-12 { + margin-top: ( @square-btn-size - 34px) / 2; + .form-group { + + height: @square-btn-size; + .row; + margin-left: 0; + margin-right: 0; + margin: 0; + label { + + .col-xs-6; + float: left; + font-weight: normal; + line-height: 34px; + margin: 0; + margin-top:( @square-btn-size - 34px) / 2; + } + input { + margin-top:( @square-btn-size - 34px) / 2; + float: left; + .col-xs-6; + } + } + } + .dropzone-thumbnail-container { + label { + .col-xs-6; + float: left; + font-weight: normal; + } + .dropzone { + overflow: hidden; + float: left; + .col-xs-6; + transition: 0.3s; + &:hover { + transform: scale(1.05); + .dropzone-content-image { + font-size: inherit !important; + } + } + background-color: @ms2-color-background; + .shadow-soft; + border: none; + max-width: 300px; + height: 141px; + margin: auto; + } + } + .ms-section { + overflow: hidden; + height: @square-btn-size; + border-bottom: 1px solid transparent; + box-shadow: none; + display: flex; + flex-direction: column; + .ms-details-preview-container { + overflow-y: auto; + flex: 1; + overflow-y: auto; + .ms-details-preview { + width: ~"calc(100% - 30px)"; + margin: 5px 15px; + padding: 10px; + height: auto; + min-height: 500px; + .shadow; + display: table; + table-layout: fixed; + img { + width: 100%; + height: auto; + } + p { + word-wrap: break-word; + width: 100%; + } + } + } + + &.ms-transition { + transition: 1.5s; + + flex: 1; + height: auto; + border-bottom: 1px solid @ms2-color-shade-lighter; + /* workaround for height transition */ + + /* end - workaround for height transition */ + .shadow-soft-inset-up { + -webkit-box-shadow: inset 0 -3px 6px rgba(0, 0, 0, 0.06), inset 0 -4px 8px rgba(0, 0, 0, 0.12); + -moz-box-shadow: inset 0 -3px 6px rgba(0, 0, 0, 0.06), inset 0 -4px 8px rgba(0, 0, 0, 0.12); + box-shadow: inset 0 -3px 6px rgba(0, 0, 0, 0.06), inset 0 -4px 8px rgba(0, 0, 0, 0.12); + } + + } + } + .ms-map-properties { + padding-top: 15px; + display: flex; + flex-direction: column; + } + @media screen and (min-height: 900px) { + &.ms-flex { + display: flex; + overflow: hidden; + .container-fluid { + display: flex; + flex-direction: column; + .ms-permissions-container { + flex: 1; + display: flex; + flex-direction: column; + .mapstore-permission-group { + flex: 1; + display: flex; + flex-direction: column; + + & > span { + display: block; + flex: 1; + overflow-y: auto; + } + } + } + } + } + } + + .ms-permissions-container { + & > .row { + & > .col-xs-12 { + + } + } + + + /*border-bottom: 1px solid @ms2-color-shade-lighter;*/ + + z-index: 50; + & > .row { + & > .col-xs-12 { + margin-top: 15px; + } + } + .ms-permission-row { + width: 100%; + + /*border-left: 2px solid contrast(@ms2-color-background);*/ + height: @square-btn-size; + & > * { + float: left; + } + .Select:first-child { + width: ~"calc(50% - 20px)"; + margin-right: 20px; + } + .Select { + width: ~"calc(50% - @{square-btn-medium-size} - 5px)"; + margin-right: 5px; + margin-top: (@square-btn-size - 34px) / 2; + .Select-control { + border-radius: 0; + .Select-placeholder { + border: none; + } + .Select-arrow-zone { + padding-left: 5px; + border-left: 1px solid @ms2-color-shade-lighter; + } + } + } + button { + float: right; + margin-top: (@square-btn-size - @square-btn-medium-size) / 2; + } + .ms-permission-title { + width: ~"calc(50% - 20px)"; + height: @square-btn-size; + line-height: @square-btn-size; + margin-right: 20px; + font-style: italic; + } + } + + .ms-row-head { + border-left: none; + margin-bottom: @square-btn-size / 4; + margin-top: 10px; + /*border-bottom: 1px solid @ms2-color-shade-lighter;*/ + } + } + + + .mapstore-block-width { + margin: 0; + + .m-label { + margin-top:( @square-btn-size - @square-btn-medium-size) / 2; + height: @square-btn-medium-size; + line-height: @square-btn-medium-size; + } + + .btn-group { + margin-top:( @square-btn-size - @square-btn-medium-size) / 2; + } + + .ms-details-sheet { + height: @square-btn-size; + padding: 10px 0; + overflow: hidden; + .btn-group { + margin-top: 0; + } + strong { + height: @square-btn-medium-size; + line-height: @square-btn-medium-size; + font-weight: normal; + } + img { + width: 100%; + height: auto; + } + /*.shadow;*/ + + /*&:hover { + @move-up: @square-btn-size * 3 / 4; + cursor: pointer; + transform: ~"translateY(-@{move-up})"; + }*/ + } + } +} diff --git a/web/client/themes/default/less/modal.less b/web/client/themes/default/less/modal.less new file mode 100644 index 0000000000..97d7670ea7 --- /dev/null +++ b/web/client/themes/default/less/modal.less @@ -0,0 +1,201 @@ + +.ms-resizable-modal { + position: absolute; + width: 100%; + height: 100%; + margin: 0; + top: 0; + left: 0; + + .modal-content { + position: absolute; + overflow: hidden; + display: flex; + flex-direction: column; + top: 15%; + left: ~"calc((100% - 500px) / 2)"; + width: 500px; + height: 70%; + margin: 0; + border: none; + .shadow; + + &.ms-xs { + height: 30%; + top: 35%; + } + + &.ms-sm { + height: 40%; + top: 30%; + } + + &.ms-lg { + width: 1080px; + left: ~"calc((100% - 1080px) / 2)"; + } + + &.ms-fullscreen { + width: 98%; + height: 98%; + left: 1%; + top: 1%; + } + + &.ms-fullscreen-v { + height: 98%; + top: 1%; + } + + &.ms-fullscreen-h { + width: 98%; + left: 1%; + } + + .modal-header { + height: @square-btn-size; + pading: (@square-btn-size - @font-size-h4) / 2; + border: none; + + + .modal-title { + display: flex; + .ms-title { + flex: 1; + height: @font-size-h4; + line-height: @font-size-h4; + text-overflow: ellipsis; + white-space: nowrap; + } + .ms-header-btn { + cursor: pointer; + margin-right: 10px; + transition: 0.3s; + display: inline-block; + &:hover { + transform: scale(1.2); + } + &:last-child { + margin-right: 0; + } + } + + } + } + + .modal-body { + overflow-y: auto; + flex: 1; + padding: 0; + display: flex; + height: auto; + flex-direction: column; + & > div { + overflow-y: auto; + flex: 1; + padding: 0; + height: auto; + &.ms-no-scroll { + overflow-y: hidden; + } + } + + .ms-alert { + margin: 0; + flex: 1; + height: 100%; + overflow-y: auto; + display: flex; + .ms-alert-center { + margin: auto; + } + } + } + + .modal-footer { + min-height: @square-btn-size; + border: none; + padding: 0; + z-index: 10; + .btn-group { + margin: (@square-btn-size - 34) / 2; + } + } + } +} + +@media (min-width: 505px) and (max-width: 768px) { + .ms-resizable-modal { + .modal-content { + width: 500px; + } + } +} + +@media (max-width: 505px) { + .ms-resizable-modal { + .modal-content { + width: ~"calc(100% - 6px)"; + left: 3px; + } + } +} + +@media (max-width: 1085px) { + .ms-resizable-modal { + .modal-content { + &.ms-lg { + width: ~"calc(100% - 6px)"; + left: 3px; + } + } + } +} + +.ms-modal-quill-container { + .quill { + display: flex; + flex-direction: column; + height: 100%; + position: absolute; + width: 100%; + .ql-toolbar { + border: none; + .shadow-soft; + min-height: @square-btn-size; + // 24px height of quill icons + padding: (@square-btn-size - 24px) / 2; + + } + .ql-container { + flex: 1; + overflow-y: auto; + width: 100%; + height: auto; + border: none; + .ql-editor { + min-height: 100px + } + } + + .ql-tooltip { + z-index: 1000; + } + } + .ql-snow { + .ql-tooltip.ql-flip { + transform: translateY(55px); + } + } + +} + +.modal-fixed { + position: fixed; + width: 100%; + height: 100%; + margin: 0; + top: 0; + left: 0; + z-index: 3000; +} diff --git a/web/client/themes/default/ms2-theme.less b/web/client/themes/default/ms2-theme.less index bb5a1e1ced..dc9856ca47 100644 --- a/web/client/themes/default/ms2-theme.less +++ b/web/client/themes/default/ms2-theme.less @@ -43,3 +43,6 @@ @import "./less/wizard.less"; @import "./less/version.less"; @import "./less/annotations.less"; +@import "./less/maps-properties.less"; +@import "./less/modal.less"; +@import "./less/details.less"; diff --git a/web/client/translations/data.de-DE b/web/client/translations/data.de-DE index b35179d58e..f2bf85fba3 100644 --- a/web/client/translations/data.de-DE +++ b/web/client/translations/data.de-DE @@ -29,6 +29,9 @@ "updating": "Updating...", "layers": "layers" }, + "details": { + "title": "Infos zu dieser Karte" + }, "layerProperties": { "windowTitle": "Ebenen Eigenschaften", "title": "Titel", @@ -103,6 +106,8 @@ "elevation": "Höhe", "close": "Schliessen", "cancel": "Abbrechen", + "no": "Nein", + "yes": "Ja", "confirm": "Bestätigen", "confirmTitle": "Bestätigst du das?", "pageInfo": "{total, plural, =0 {Keine Artikel} =1 {{total} Artikel von {total}} other {Artikel {start}-{end} von {total}}}", @@ -192,6 +197,28 @@ }, "newMap": "Neue Karte", "maps": { + "feedback": { + "successSavedMap": "Die Karte wurde korrekt erstellt", + "errorDeletingMap": "Fehler beim Löschen dieser Map mit ID: ", + "errorDeletingThumbnailOfMap": "Fehler beim Löschen der Miniaturansicht für die Karte mit ID: ", + "errorDeletingDetailsOfMap": "Fehler beim Löschen der Details für die Karte mit ID: ", + "allResDeleted": "Alle Ressourcen, die mit dieser Karte verknüpft sind, wurden erfolgreich gelöscht: ", + "errorFetchingDetailsOfMap": "Fehler beim Abrufen von Details für die Karte mit der ID: ", + "details": { + "deletedSuccesfully": "Die Details wurden korrekt entfernt", + "savedSuccesfully": "Die Details wurden korrekt entfernt", + "updatedSuccesfully": "Die Details wurden korrekt aktualisiert" + }, + "thumbnail": { + "deletedSuccesfully": "Das Vorschaubild wurde korrekt entfernt", + "savedSuccesfully": "Das Vorschaubild wurde korrekt entfernt", + "updatedSuccesfully": "Das Thumbnail wurde korrekt aktualisiert" + }, + "errorWhenSaving": "Beim Speichern ist ein Fehler aufgetreten", + "errorWhenUpdating": "Während des Aktualisierungsprozesses ist ein Fehler aufgetreten", + "errorWhenDeleting": "Beim Löschen ist ein Fehler aufgetreten", + "errorSizeExceeded": "Bitte verkleinern Sie die Größe der Details oder die Qualität der Bilder" + }, "search": "Suche nach Karten..." }, "map": { @@ -229,6 +256,21 @@ "canWrite": "kann bearbeiten", "noResult": "keine Resultate gefunden", "title": "Gruppen Berechtigungen" + }, + "details": { + "back": "Zurück", + "save": "Sparen", + "show": "Details anzeigen", + "add": "Neue Details hinzufügen", + "edit": "Details bearbeiten", + "title": "Detailblatt", + "undo": "Rückgängig entfernen", + "showPreview": "Vorschau zeigen", + "hidePreview": "Vorschau ausblenden", + "delete": "Löschen Sie das Detailblatt", + "titleUnsavedChanges": "Sind Sie sicher, dass Sie schließen, ohne Ihre Änderungen zu speichern??", + "sureToClose": "Sind Sie sicher, dass Sie schließen, ohne Ihre Änderungen zu speichern??", + "fieldsChanged": "Einige Felder wurden geändert" } }, "toc": { diff --git a/web/client/translations/data.en-US b/web/client/translations/data.en-US index cb84b1eb22..99f89cda13 100644 --- a/web/client/translations/data.en-US +++ b/web/client/translations/data.en-US @@ -29,6 +29,9 @@ "updating": "Updating...", "layers": "layers" }, + "details": { + "title": "About this map" + }, "layerProperties": { "windowTitle": "Layer Properties", "title": "Title", @@ -103,6 +106,8 @@ "elevation": "Elevation", "close": "Close", "cancel": "Cancel", + "no": "No", + "yes": "Yes", "confirm": "Confirm", "confirmTitle": "Do you confirm?", "pageInfo": "{total, plural, =0 {No items} =1 {{total} Item of {total}} other {Items {start}-{end} of {total}}}", @@ -192,6 +197,28 @@ }, "newMap": "New Map", "maps": { + "feedback": { + "successSavedMap": "The map has been created correctly", + "errorDeletingMap": "Error when deleting this map with id: ", + "errorDeletingThumbnailOfMap": "Error when deleting thumbnail for the map with id: ", + "errorDeletingDetailsOfMap": "Error when deleting details for the map with id: ", + "allResDeleted": "Have been deleted successfully all resources associated with this map: ", + "errorFetchingDetailsOfMap": "Error when fetching details for the map with id: ", + "details": { + "deletedSuccesfully": "The details have been removed correctly", + "savedSuccesfully": "The details have been saved correctly", + "updatedSuccesfully": "The details have been updated correctly" + }, + "thumbnail": { + "deletedSuccesfully": "The thumbnail have been removed correctly", + "savedSuccesfully": "The thumbnail have been saved correctly", + "updatedSuccesfully": "The thumbnail have been updated correctly" + }, + "errorWhenSaving": "An error occurred during saving process", + "errorWhenUpdating": "An error occurred during updating process", + "errorWhenDeleting": "An error occurred during deleting process", + "errorSizeExceeded": "Please, reduce the size of the details or the quality of the images" + }, "search": "search for maps..." }, "map": { @@ -229,6 +256,21 @@ "canWrite": "can edit", "noResult": "no results found", "title": "Permissions Groups" + }, + "details": { + "back": "Back", + "save": "Save", + "show": "Show details sheet", + "add": "Add New Details", + "edit": "Edit Details", + "title": "Details Sheet", + "undo": "Undo remove", + "showPreview": "Show preview", + "hidePreview": "Hide preview", + "delete": "Delete details sheet", + "titleUnsavedChanges": "Are you sure to close without save your changes?", + "sureToClose": "Are you sure to close without save your changes?", + "fieldsChanged": "Some fields has been changed" } }, "toc": { diff --git a/web/client/translations/data.es-ES b/web/client/translations/data.es-ES index 963102e736..818671374c 100644 --- a/web/client/translations/data.es-ES +++ b/web/client/translations/data.es-ES @@ -29,6 +29,9 @@ "updating": "Updating...", "layers": "layers" }, + "details": { + "title": "Información en este mapa" + }, "layerProperties": { "windowTitle": "Propiedades de la capa", "title": "Título", @@ -103,6 +106,8 @@ "elevation": "Elevación", "close": "Cerrar", "cancel": "Cancelar", + "no": "No", + "yes": "Sí", "confirm": "Confirmar", "confirmTitle": "¿Lo confirma?", "pageInfo": "{total, plural, =0 {ningún item} =1 {{total} item de {total}} other {objets {start}-{end} de {total}}}", @@ -192,6 +197,28 @@ }, "newMap": "Nuevo mapa", "maps": { + "feedback": { + "successSavedMap": "El mapa ha sido creado correctamente", + "errorDeletingMap": "Error al eliminar este mapa con id: ", + "errorDeletingThumbnailOfMap": "Error al eliminar la miniatura del mapa con id: ", + "errorDeletingDetailsOfMap": "Error al eliminar los detalles del mapa con id: ", + "allResDeleted": "Se han eliminado con éxito todos los recursos asociados con este mapa: ", + "errorFetchingDetailsOfMap": "Error al recuperar detalles para el mapa con id: ", + "details": { + "deletedSuccesfully": "Los detalles han sido eliminados correctamente", + "savedSuccesfully": "Los detalles han sido eliminados correctamente", + "updatedSuccesfully": "Los detalles se han actualizado correctamente" + }, + "thumbnail": { + "deletedSuccesfully": "La miniatura ha sido eliminada correctamente", + "savedSuccesfully": "La miniatura ha sido eliminada correctamente", + "updatedSuccesfully": "La miniatura se ha actualizado correctamente" + }, + "errorWhenSaving": "Se produjo un error durante el proceso de guardado", + "errorWhenUpdating": "Se produjo un error durante el proceso de actualización", + "errorWhenDeleting": "Se produjo un error durante el proceso de eliminación", + "errorSizeExceeded": "Por favor, reduzca el tamaño de los detalles o la calidad de las imágenes" + }, "search": "buscador de mapas ..." }, "map": { @@ -229,6 +256,21 @@ "canWrite": "puedo editar", "noResult": "nigún resultado encontrado", "title": "Grupos de permisos" + }, + "details": { + "back": "Espalda", + "save": "Salvar", + "show": "Mostrar hoja de detalles", + "add": "Agregar nuevos detalles", + "edit": "Editar detalles", + "title": "Hoja de detalles", + "undo": "Deshacer quitar", + "showPreview": "Mostrar vista previa", + "hidePreview": "Ocultar vista previa", + "delete": "Eliminar hoja de detalles", + "titleUnsavedChanges": "Estás seguro de cerrar sin guardar los cambios?", + "sureToClose": "Estás seguro de cerrar sin guardar los cambios?", + "fieldsChanged": "Algunos campos han sido cambiados" } }, "toc": { diff --git a/web/client/translations/data.fr-FR b/web/client/translations/data.fr-FR index 2906640702..fa63ecffb2 100644 --- a/web/client/translations/data.fr-FR +++ b/web/client/translations/data.fr-FR @@ -30,6 +30,9 @@ "updating": "Rafraichissement...", "layers": "Couches" }, + "details": { + "title": "Info sur cette carte" + }, "layerProperties": { "windowTitle": "Propriétés de la couche", "title": "Titre", @@ -104,6 +107,8 @@ "elevation": "Elévation", "close": "Fermer", "cancel": "Annuler", + "no": "Non", + "yes": "Oui", "confirm": "Confirmer", "confirmTitle": "Confirmez-vous?", "pageInfo": "{total, plural, =0 {aucun objet} =1 {{total} objet parmi {total}} other {objets {start}-{end} parmi {total}}}", @@ -193,7 +198,29 @@ }, "newMap": "Nouvelle carte", "maps": { - "search": "rechercher des cartes ..." + "feedback": { + "successSavedMap": "La carte a été créée correctement", + "errorDeletingMap": "Erreur lors de la suppression de cette carte avec l'ID: ", + "errorDeletingThumbnailOfMap": "Erreur lors de la suppression de la vignette de la carte avec l'ID: ", + "errorDeletingDetailsOfMap": "Erreur lors de la suppression des détails de la carte avec l'ID: ", + "allResDeleted": "Ont été supprimées avec succès toutes les ressources associées à cette carte: ", + "errorFetchingDetailsOfMap": "Erreur lors de la récupération des détails de la carte avec l'ID: ", + "details": { + "deletedSuccesfully": "Les détails ont été supprimés correctement", + "savedSuccesfully": "Les détails ont été supprimés correctement", + "updatedSuccesfully": "Les détails ont été mis à jour correctement" + }, + "thumbnail": { + "deletedSuccesfully": "La vignette a été supprimée correctement", + "savedSuccesfully": "La vignette a été supprimée correctement", + "updatedSuccesfully": "La vignette a été mise à jour correctement" + }, + "errorWhenSaving": "Une erreur est survenue lors du processus d'enregistrement", + "errorWhenUpdating": "Une erreur s'est produite lors du processus de mise à jour", + "errorWhenDeleting": "Une erreur s'est produite lors du processus de suppression", + "errorSizeExceeded": "S'il vous plaît, réduisez la taille des détails ou la qualité des images" + }, + "search": "rechercher des cartes ..." }, "map": { "loading": "Chargement...", @@ -230,6 +257,21 @@ "canWrite": "peut éditer", "noResult": "aucun résultat trouvé", "title": "groupes autorisés" + }, + "details": { + "back": "Arrière", + "save": "Sauvegarder", + "show": "Montrer la fiche détaillée", + "add": "Ajouter de nouveaux détails", + "edit": "Modifier les détails", + "title": "Fiche de détails", + "undo": "Annuler supprimer", + "showPreview": "Afficher l'aperçu", + "hidePreview": "Masquer l'aperçu", + "delete": "Supprimer la fiche détaillée", + "titleUnsavedChanges": "Êtes-vous sûr de fermer sans enregistrer vos modifications?", + "sureToClose": "Êtes-vous sûr de fermer sans enregistrer vos modifications?", + "fieldsChanged": "Certains champs ont été modifiés" } }, "toc": { diff --git a/web/client/translations/data.it-IT b/web/client/translations/data.it-IT index b00e10d06f..4402646537 100644 --- a/web/client/translations/data.it-IT +++ b/web/client/translations/data.it-IT @@ -29,6 +29,9 @@ "updating": "In aggiornamento...", "layers": "layers" }, + "details": { + "title": "Info su questa mappa" + }, "layerProperties": { "windowTitle": "Proprietà del livello", "title": "Titolo", @@ -103,6 +106,8 @@ "elevation": "Elevazione", "close": "Chiudi", "cancel": "Annulla", + "no": "No", + "yes": "Si", "confirm": "Conferma", "confirmTitle": "Confermi?", "pageInfo": "{total, plural, =0 {Nessun Elemento} =1 {{total} Elemento di {total}} other {Elementi {start}-{end} di {total}}}", @@ -192,6 +197,28 @@ }, "newMap": "Nuova Mappa", "maps": { + "feedback": { + "successSavedMap": "La mappa è stata salvata correttamente", + "errorDeletingMap": "Errore durante l'eliminazione della mappa con id: ", + "errorDeletingThumbnailOfMap": "Errore durante l'eliminazione dell'anteprima della mappa con id: ", + "errorDeletingDetailsOfMap": "Errore durante l'eliminazione dei dettagli della mappa con id: ", + "allResDeleted": "Sono state eliminate tutte le risorse associate a questa mappa: ", + "errorFetchingDetailsOfMap": "Errore durante il recupero dei dettagli della mappa con id: ", + "details": { + "deletedSuccesfully": "I dettagli sono stati eliminati correttamente", + "savedSuccesfully": "I dettagli sono stati salvati correttamente", + "updatedSuccesfully": "I dettagli sono stati aggiornati correttamente" + }, + "thumbnail": { + "deletedSuccesfully": "L'anteprima è stata eliminata correttamente", + "savedSuccesfully": "L'anteprima è stata salvata correttamente", + "updatedSuccesfully": "L'anteprima è stata aggiornata correttamente" + }, + "errorWhenSaving": "C'è stato un errore durante il salvataggio", + "errorWhenUpdating": "C'è stato un errore durante l'aggiornamento", + "errorWhenDeleting": "C'è stato un errore durante la cancellazione", + "errorSizeExceeded": "Riduci il contenuto o la qualità delle immagini" + }, "search": "Cerca mappe..." }, "map": { @@ -229,6 +256,21 @@ "canWrite": "Può modificare", "noResult": "Nessun gruppo rimasto", "title": "Permessi dei gruppi" + }, + "details": { + "back": "Indietro", + "save": "Salva", + "show": "Mostra i dettagli", + "add": "Aggiungi dettagli", + "edit": "Modifica dettagli", + "title": "Dettagli", + "undo": "Annulla eliminazione", + "showPreview": "Mostra anteprima", + "hidePreview": "Nascondi anteprima", + "delete": "Elimina dettagli", + "titleUnsavedChanges": "Sicuro di chiudere senza salvare?", + "sureToClose": "Sicuro di chiudere senza salvare?", + "fieldsChanged": "Alcuni dati sono cambiati" } }, "toc": { diff --git a/web/client/utils/NotificationUtils.js b/web/client/utils/NotificationUtils.js new file mode 100644 index 0000000000..0dbc8f3795 --- /dev/null +++ b/web/client/utils/NotificationUtils.js @@ -0,0 +1,10 @@ +const {error, success} = require('../actions/notifications'); + + +module.exports = { + + basicError: ({ title = "notification.warning", autoDismiss = 6, position = "tc", message = "Error" } = {}) => + error({ title, autoDismiss, position, message }), + basicSuccess: ({ title = "notification.success", autoDismiss = 6, position = "tc", message = "Success" } = {}) => + success({ title, autoDismiss, position, message }) +}; diff --git a/web/client/utils/ObservableUtils.js b/web/client/utils/ObservableUtils.js index 4ca15e63f6..3ea271c878 100644 --- a/web/client/utils/ObservableUtils.js +++ b/web/client/utils/ObservableUtils.js @@ -1,7 +1,13 @@ const Rx = require('rxjs'); -const {get} = require('lodash'); +const {get, isNil} = require('lodash'); const {parseString} = require('xml2js'); const {stripPrefix} = require('xml2js/lib/processors'); +const GeoStoreApi = require('../api/GeoStoreDAO'); +const {updatePermissions, updateAttribute, doNothing} = require('../actions/maps'); +const ConfigUtils = require('../utils/ConfigUtils'); +const LocaleUtils = require('../utils/LocaleUtils'); +const {basicSuccess, basicError} = require('../utils/NotificationUtils'); + class OGCError extends Error { constructor(message, code) { super(message); @@ -31,8 +37,96 @@ const interceptOGCError = (observable) => observable.switchMap(response => { return Rx.Observable.of(response); }); +const getIdFromUri = (uri) => { + const decodedUri = decodeURIComponent(uri); + return /\d+/.test(decodedUri) ? decodedUri.match(/\d+/)[0] : null; +}; +const createAssociatedResource = ({attribute, permissions, mapId, metadata, value, category, type, optionsRes, optionsAttr, messages} = {}) => { + return Rx.Observable.fromPromise( + GeoStoreApi.createResource(metadata, value, category, optionsRes) + .then(res => res.data)) + .switchMap((resourceId) => { + // update permissions + let actions = []; + actions.push(updatePermissions(resourceId, permissions)); + const attibuteUri = ConfigUtils.getDefaults().geoStoreUrl + "data/" + resourceId + "/raw?decode=datauri"; + const encodedResourceUri = encodeURIComponent(encodeURIComponent(attibuteUri)); + // UPDATE resource map with new attribute + actions.push(updateAttribute(mapId, attribute, encodedResourceUri, type, optionsAttr)); + // display a success message + actions.push(basicSuccess({message: LocaleUtils.getMessageById(messages, "maps.feedback." + attribute + ".savedSuccesfully" ) })); + return Rx.Observable.from(actions); + }) + .catch(() => Rx.Observable.of(basicError({message: "maps.feedback.errorWhenSaving"}))); +}; -module.exports = { - interceptOGCError +const updateAssociatedResource = ({permissions, resourceId, value, attribute, options, messages} = {}) => { + return Rx.Observable.fromPromise(GeoStoreApi.putResource(resourceId, value, options) + .then(res => res.data)) + .switchMap((id) => { + let actions = []; + actions.push(basicSuccess({ message: LocaleUtils.getMessageById(messages, "maps.feedback." + attribute + ".updatedSuccesfully" )})); + actions.push(updatePermissions(id, permissions)); + return Rx.Observable.from(actions); + }) + .catch(() => Rx.Observable.of(basicError({message: "maps.feedback.errorWhenUpdating"}))); +}; +const deleteAssociatedResource = ({mapId, attribute, type, resourceId, options, messages} = {}) => { + return Rx.Observable.fromPromise(GeoStoreApi.deleteResource(resourceId, options) + .then(res => res.status === 204)) + .switchMap((deleted) => { + let actions = []; + if (deleted) { + actions.push(basicSuccess({ message: LocaleUtils.getMessageById(messages, "maps.feedback." + attribute + ".deletedSuccesfully" ) })); + actions.push(updateAttribute(mapId, attribute, "NODATA", type, options)); + return Rx.Observable.from(actions); + } + actions.push(doNothing()); + return Rx.Observable.from(actions); + }) + .catch(() => Rx.Observable.of(basicError({message: "maps.feedback.errorWhenDeleting"}))); +}; + +const deleteResourceById = (resId, options) => resId ? + GeoStoreApi.deleteResource(resId, options) + .then((res) => {return {data: res.data, resType: "success", error: null}; }) + .catch((e) => {return {error: e, resType: "error"}; }) : + Rx.Observable.of({resType: "success"}); +const manageMapResource = ({map = {}, attribute = "", resource = null, type = "STRING", optionsDel = {}, messages = {}} = {}) => { + const attrVal = map[attribute]; + const mapId = map.id; + // create + if ((isNil(attrVal) || attrVal === "NODATA") && !isNil(resource)) { + return createAssociatedResource({...resource, attribute, mapId, type, messages}); + } + if (isNil(resource)) { + // delete + return deleteAssociatedResource({ + mapId, + attribute, + type, + resourceId: getIdFromUri(attrVal), + options: optionsDel, + messages}); + } + // update + return updateAssociatedResource({ + permissions: resource.permissions, + resourceId: getIdFromUri(attrVal), + value: resource.value, + attribute, + options: resource.optionsAttr, + messages}); + +}; + +module.exports = { + getIdFromUri, + deleteResourceById, + createAssociatedResource, + updateAssociatedResource, + deleteAssociatedResource, + interceptOGCError, + manageMapResource }; diff --git a/web/client/utils/__tests__/NotificationUtils-test.js b/web/client/utils/__tests__/NotificationUtils-test.js new file mode 100644 index 0000000000..826006006e --- /dev/null +++ b/web/client/utils/__tests__/NotificationUtils-test.js @@ -0,0 +1,41 @@ +/* + * Copyright 2017, 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. +*/ +const expect = require('expect'); +const {basicSuccess, basicError} = require('../NotificationUtils'); +const {SHOW_NOTIFICATION} = require('../../actions/notifications'); + + +describe('NotificationUtils', () => { + beforeEach( () => { + + }); + afterEach(() => { + + }); + it('test basicError', () => { + const action = basicError(); + expect(action).toExist(); + expect(action.type).toBe(SHOW_NOTIFICATION); + expect(action.level).toBe("error"); + expect(action.title).toBe("notification.warning"); + expect(action.autoDismiss).toBe(6); + expect(action.message).toBe("Error"); + expect(action.position).toBe("tc"); + }); + it('test basicSuccess', () => { + const action = basicSuccess('Thunderforest.OpenCycleMap'); + expect(action).toExist(); + expect(action.type).toBe(SHOW_NOTIFICATION); + expect(action.level).toBe("success"); + expect(action.title).toBe("notification.success"); + expect(action.autoDismiss).toBe(6); + expect(action.message).toBe("Success"); + expect(action.position).toBe("tc"); + }); + +}); |