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 + ""; - payload = payload + "" + (rule.canRead || rule.canWrite ? "true" : "false") + ""; - payload = payload + "" + (rule.canWrite ? "true" : "false") + ""; + payload = payload + "" + boolToString(rule.canRead || rule.canWrite) + ""; + payload = payload + "" + boolToString(rule.canWrite) + ""; payload = payload + "" + (rule.user.id || "") + "" + (rule.user.name || "") + ""; payload = payload + ""; } else if (rule.group) { payload = payload + ""; - payload = payload + "" + (rule.canRead || rule.canWrite ? "true" : "false") + ""; - payload = payload + "" + (rule.canWrite ? "true" : "false") + ""; + payload = payload + "" + boolToString(rule.canRead || rule.canWrite) + ""; + payload = payload + "" + boolToString(rule.canWrite) + ""; payload = payload + "" + (rule.group.id || "") + "" + (rule.group.groupName || "") + ""; payload = payload + ""; } diff --git a/web/client/components/details/DetailsPanel.jsx b/web/client/components/details/DetailsPanel.jsx new file mode 100644 index 0000000000..70859f9a49 --- /dev/null +++ b/web/client/components/details/DetailsPanel.jsx @@ -0,0 +1,88 @@ +/* + * 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 React = require('react'); +const PropTypes = require('prop-types'); +const Message = require('../I18N/Message'); +const {Glyphicon, Panel} = require('react-bootstrap'); +const ContainerDimensions = require('react-container-dimensions').default; +const Dock = require('react-dock').default; +const BorderLayout = require('../layout/BorderLayout'); +const Spinner = require('react-spinkit'); + +class DetailsPanel extends React.Component { + static propTypes = { + id: PropTypes.string, + active: PropTypes.bool, + closeGlyph: PropTypes.string, + panelStyle: PropTypes.object, + panelClassName: PropTypes.string, + style: PropTypes.object, + onClose: PropTypes.function, + dockProps: PropTypes.object, + width: PropTypes.number, + detailsText: PropTypes.string, + dockStyle: PropTypes.object + } + static defaultProps = { + id: "mapstore-details", + panelStyle: { + zIndex: 100, + overflow: "hidden", + height: "100%", + marginBottom: 0 + }, + onClose: () => {}, + active: false, + panelClassName: "details-panel", + width: 658, + closeGlyph: "1-close", + dockProps: { + dimMode: "none", + size: 0.30, + fluid: true, + position: "right", + zIndex: 1030, + bottom: 0 + }, + detailsText: "", + dockStyle: {} + } + + render() { + const panelHeader = ( + + + + + + + ); + + return ( + { ({ width }) => + 1 ? 1 : this.props.width / width} > + + +
+ {!this.props.detailsText ? + : +
} +
+ + + + } + ); + } +} + + +module.exports = DetailsPanel; diff --git a/web/client/components/maps/MapCard.jsx b/web/client/components/maps/MapCard.jsx index 8d927ea54a..5e6fb576ec 100644 --- a/web/client/components/maps/MapCard.jsx +++ b/web/client/components/maps/MapCard.jsx @@ -1,18 +1,16 @@ -const PropTypes = require('prop-types'); -/** - * Copyright 2016, GeoSolutions Sas. +/* + * 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 PropTypes = require('prop-types'); const React = require('react'); const Message = require('../I18N/Message'); const GridCard = require('../misc/GridCard'); const thumbUrl = require('./style/default.jpg'); const assign = require('object-assign'); - const ConfirmModal = require('./modals/ConfirmModal'); const LocaleUtils = require('../../utils/LocaleUtils'); @@ -23,6 +21,7 @@ class MapCard extends React.Component { // props style: PropTypes.object, map: PropTypes.object, + detailsSheetActions: PropTypes.object, mapType: PropTypes.string, // CALLBACKS viewerUrl: PropTypes.oneOfType([PropTypes.string, PropTypes.func]), @@ -41,13 +40,16 @@ class MapCard extends React.Component { backgroundPosition: "center", backgroundRepeat: "repeat-x" }, + detailsSheetActions: { + onToggleDetailsSheet: () => {} + }, // CALLBACKS onMapDelete: ()=> {}, onEdit: ()=> {} }; - onEdit = (map) => { - this.props.onEdit(map); + onEdit = (map, openModalProperties) => { + this.props.onEdit(map, openModalProperties); }; onConfirmDelete = () => { @@ -74,25 +76,35 @@ class MapCard extends React.Component { }; render() { - var availableAction = [{ - onClick: (evt) => {this.stopPropagate(evt); this.props.viewerUrl(this.props.map); }, - glyph: "chevron-right", - tooltip: LocaleUtils.getMessageById(this.context.messages, "manager.openInANewTab") - }]; - + let availableAction = []; if (this.props.map.canEdit === true) { + availableAction.push( + { + onClick: (evt) => {this.stopPropagate(evt); this.displayDeleteDialog(); }, + glyph: "trash", + disabled: this.props.map.deleting, + loading: this.props.map.deleting, + tooltip: LocaleUtils.getMessageById(this.context.messages, "manager.deleteMap") + }, { + onClick: (evt) => { + this.stopPropagate(evt); + this.onEdit(this.props.map, true); + }, + glyph: "wrench", + disabled: this.props.map.updating, + loading: this.props.map.updating, + tooltip: LocaleUtils.getMessageById(this.context.messages, "manager.editMapMetadata") + }); + } + if (this.props.map.details && this.props.map.details !== "NODATA") { availableAction.push({ - onClick: (evt) => {this.stopPropagate(evt); this.onEdit(this.props.map); }, - glyph: "wrench", - disabled: this.props.map.updating, - loading: this.props.map.updating, - tooltip: LocaleUtils.getMessageById(this.context.messages, "manager.editMapMetadata") - }, { - onClick: (evt) => {this.stopPropagate(evt); this.displayDeleteDialog(); }, - glyph: "remove-circle", - disabled: this.props.map.deleting, - loading: this.props.map.deleting, - tooltip: LocaleUtils.getMessageById(this.context.messages, "manager.deleteMap") + onClick: (evt) => { + this.stopPropagate(evt); + this.onEdit(this.props.map, false); + this.props.detailsSheetActions.onToggleDetailsSheet(true); + }, + glyph: "sheet", + tooltip: LocaleUtils.getMessageById(this.context.messages, "map.details.show") }); } return ( diff --git a/web/client/components/maps/MapGrid.jsx b/web/client/components/maps/MapGrid.jsx index 1d710dd017..0cbf8a5bba 100644 --- a/web/client/components/maps/MapGrid.jsx +++ b/web/client/components/maps/MapGrid.jsx @@ -32,6 +32,7 @@ class MapGrid extends React.Component { removeThumbnail: PropTypes.func, errorCurrentMap: PropTypes.func, updateCurrentMap: PropTypes.func, + detailsSheetActions: PropTypes.object, createThumbnail: PropTypes.func, deleteThumbnail: PropTypes.func, deleteMap: PropTypes.func, @@ -56,6 +57,17 @@ class MapGrid extends React.Component { // CALLBACKS onChangeMapType: function() {}, updateMapMetadata: () => {}, + detailsSheetActions: { + onBackDetails: () => {}, + onUndoDetails: () => {}, + onToggleDetailsSheet: () => {}, + onToggleGroupProperties: () => {}, + onToggleUnsavedChangesModal: () => {}, + onsetDetailsChanged: () => {}, + onUpdateDetails: () => {}, + onDeleteDetails: () => {}, + onSaveDetails: () => {} + }, createThumbnail: () => {}, deleteThumbnail: () => {}, errorCurrentMap: () => {}, @@ -70,7 +82,6 @@ class MapGrid extends React.Component { updatePermissions: () => {}, groups: [] }; - renderMaps = (maps, mapType) => { const viewerUrl = this.props.viewerUrl; return maps.map((map) => { @@ -81,6 +92,7 @@ class MapGrid extends React.Component { ; }); @@ -93,13 +105,15 @@ class MapGrid extends React.Component { renderMetadataModal = () => { if (this.props.metadataModal) { let MetadataModal = this.props.metadataModal; - return (); diff --git a/web/client/components/maps/__tests__/MapCard-test.jsx b/web/client/components/maps/__tests__/MapCard-test.jsx index e354fe8a76..c68088895e 100644 --- a/web/client/components/maps/__tests__/MapCard-test.jsx +++ b/web/client/components/maps/__tests__/MapCard-test.jsx @@ -52,24 +52,21 @@ describe('This test for MapCard', () => { expect(headings[0].innerHTML).toBe(testName); }); - it('test viewer url', () => { + it('test details tool', () => { const testName = "test"; const testDescription = "testDescription"; - var component = TestUtils.renderIntoDocument(); + let component = TestUtils.renderIntoDocument(); const handlers = { - onclick: () => {} + onToggleDetailsSheet: () => {}, + onEdit: () => {} }; - const buttonLink = TestUtils.findRenderedDOMComponentWithTag( + let spy = expect.spyOn(handlers, "onToggleDetailsSheet"); + component = TestUtils.renderIntoDocument(); + const detailsTool = TestUtils.findRenderedDOMComponentWithTag( component, 'button' ); - expect(buttonLink).toExist(); - let spy = expect.spyOn(handlers, "onclick"); - component = TestUtils.renderIntoDocument(); - const button = TestUtils.findRenderedDOMComponentWithTag( - component, 'button' - ); - TestUtils.Simulate.click(button); - expect(button).toExist(); + expect(detailsTool).toExist(); + TestUtils.Simulate.click(detailsTool); expect(spy.calls.length).toEqual(1); }); it('test edit/delete', () => { @@ -82,7 +79,7 @@ describe('This test for MapCard', () => { onMapDelete: () => {} }; - let spyviewerUrl = expect.spyOn(handlers, "viewerUrl"); + let spyonEdit = expect.spyOn(handlers, "onEdit"); let spyonMapDelete = expect.spyOn(handlers, "onMapDelete"); const component = ReactDOM.render( { const buttons = TestUtils.scryRenderedDOMComponentsWithTag( component, 'button' ); - expect(buttons.length).toBe(3); + expect(buttons.length).toBe(2); buttons.forEach(b => TestUtils.Simulate.click(b)); - expect(spyviewerUrl.calls.length).toEqual(1); expect(spyonEdit.calls.length).toEqual(1); // wait for confirm expect(spyonMapDelete.calls.length).toEqual(0); diff --git a/web/client/components/maps/forms/Metadata.jsx b/web/client/components/maps/forms/Metadata.jsx index fc056e966e..da213a2ac0 100644 --- a/web/client/components/maps/forms/Metadata.jsx +++ b/web/client/components/maps/forms/Metadata.jsx @@ -52,8 +52,10 @@ class Metadata extends React.Component { key="mapName" type="text" onChange={this.changeName} + disabled={this.props.map.saving} placeholder={this.props.namePlaceholderText} - defaultValue={this.props.map ? this.props.map.name : ""} /> + defaultValue={this.props.map ? this.props.map.name : ""} + value={this.props.map && this.props.map.metadata && this.props.map.metadata.name || ""}/> {this.props.descriptionFieldText} @@ -61,8 +63,10 @@ class Metadata extends React.Component { key="mapDescription" type="text" onChange={this.changeDescription} + disabled={this.props.map.saving} placeholder={this.props.descriptionPlaceholderText} - defaultValue={this.props.map ? this.props.map.description : ""} /> + defaultValue={this.props.map ? this.props.map.description : ""} + value={this.props.map && this.props.map.metadata && this.props.map.metadata.description || ""}/> ); } diff --git a/web/client/components/maps/forms/Thumbnail.jsx b/web/client/components/maps/forms/Thumbnail.jsx index d46db5fe7a..db1ac70dad 100644 --- a/web/client/components/maps/forms/Thumbnail.jsx +++ b/web/client/components/maps/forms/Thumbnail.jsx @@ -98,7 +98,7 @@ class Thumbnail extends React.Component { // without errors this.props.onError([], this.props.map.id); this.files = images; - this.props.onUpdate(null, images && images[0].preview); + this.props.onUpdate(data, images && images[0].preview); } else { // with at least one error if (!isAnImage) { @@ -129,37 +129,41 @@ class Thumbnail extends React.Component { return uuid; }; + processUpdateThumbnail = (map, metadata, data) => { + const name = this.generateUUID(); // create new unique name + const category = "THUMBNAIL"; + // user removed the thumbnail (the original url is present but not the preview) + if (this.props.map && !data && this.props.map.thumbnail && !this.refs.imgThumbnail && !metadata) { + this.deleteThumbnail(this.props.map.thumbnail, this.props.map.id, true); + // there is a thumbnail to upload + } + if (this.props.map && !data && this.props.map.newThumbnail && !this.refs.imgThumbnail && metadata) { + this.deleteThumbnail(this.props.map.thumbnail, this.props.map.id, false); + this.props.onSaveAll(map, metadata, name, data, category, this.props.map.id); + // there is a thumbnail to upload + } + // remove old one if present + if (this.props.map.newThumbnail && data && this.refs.imgThumbnail) { + this.deleteThumbnail(this.props.map.thumbnail, null, false); + // create the new one (and update the thumbnail attribute) + this.props.onSaveAll(map, metadata, name, data, category, this.props.map.id); + } + // nothing dropped it will be closed the modal + if (this.props.map.newThumbnail && !data && this.refs.imgThumbnail) { + this.props.onSaveAll(map, metadata, name, data, category, this.props.map.id); + } + if (!this.props.map.newThumbnail && !data && !this.refs.imgThumbnail) { + if (this.props.map.thumbnail && metadata) { + this.deleteThumbnail(this.props.map.thumbnail, this.props.map.id, false); + } + this.props.onSaveAll(map, metadata, name, data, category, this.props.map.id); + } + } + updateThumbnail = (map, metadata) => { if (!this.props.map.errors || !this.props.map.errors.length ) { this.getDataUri(this.files, (data) => { - const name = this.generateUUID(); // create new unique name - const category = "THUMBNAIL"; - // user removed the thumbnail (the original url is present but not the preview) - if (this.props.map && !data && this.props.map.thumbnail && !this.refs.imgThumbnail && !metadata) { - this.deleteThumbnail(this.props.map.thumbnail, this.props.map.id); - // there is a thumbnail to upload - } - if (this.props.map && !data && this.props.map.newThumbnail && !this.refs.imgThumbnail && metadata) { - this.deleteThumbnail(this.props.map.thumbnail, this.props.map.id); - this.props.onSaveAll(map, metadata, name, data, category, this.props.map.id); - // there is a thumbnail to upload - } - // remove old one if present - if (this.props.map.newThumbnail && data && this.refs.imgThumbnail) { - this.deleteThumbnail(this.props.map.thumbnail, null); - // create the new one (and update the thumbnail attribute) - this.props.onSaveAll(map, metadata, name, data, category, this.props.map.id); - } - // nothing dropped it will be closed the modal - if (this.props.map.newThumbnail && !data && this.refs.imgThumbnail) { - this.props.onSaveAll(map, metadata, name, data, category, this.props.map.id); - } - if (!this.props.map.newThumbnail && !data && !this.refs.imgThumbnail) { - if (this.props.map.thumbnail && metadata) { - this.deleteThumbnail(this.props.map.thumbnail, this.props.map.id); - } - this.props.onSaveAll(map, metadata, name, data, category, this.props.map.id); - } + this.processUpdateThumbnail(map, metadata, data); return data; }); } @@ -189,7 +193,9 @@ class Thumbnail extends React.Component { return ( this.props.loading ?
: -
+
{ this.getThumbnailUrl() ? diff --git a/web/client/components/maps/forms/__tests__/Thumbnail-test.jsx b/web/client/components/maps/forms/__tests__/Thumbnail-test.jsx index dee1d9446c..0f91468550 100644 --- a/web/client/components/maps/forms/__tests__/Thumbnail-test.jsx +++ b/web/client/components/maps/forms/__tests__/Thumbnail-test.jsx @@ -37,7 +37,7 @@ describe('This test for Thumbnail', () => { }); it('creates the component with defaults, loading=false', () => { - const thumbnailItem = ReactDOM.render(, document.getElementById("container")); + const thumbnailItem = ReactDOM.render(, document.getElementById("container")); expect(thumbnailItem).toExist(); const thumbnailItemDom = ReactDOM.findDOMNode(thumbnailItem); diff --git a/web/client/components/maps/modals/MetadataModal.jsx b/web/client/components/maps/modals/MetadataModal.jsx index f265f8aa44..a058890e19 100644 --- a/web/client/components/maps/modals/MetadataModal.jsx +++ b/web/client/components/maps/modals/MetadataModal.jsx @@ -1,28 +1,30 @@ +/* +* 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 PropTypes = require('prop-types'); -/** - * Copyright 2016, 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 React = require('react'); const Metadata = require('../forms/Metadata'); const Thumbnail = require('../forms/Thumbnail'); const PermissionEditor = require('../../security/PermissionEditor'); - +const ReactQuill = require('react-quill'); +const Portal = require('../../misc/Portal'); +const ResizableModal = require('../../misc/ResizableModal'); +require('react-quill/dist/quill.snow.css'); require('./css/modals.css'); - -const {Button, Grid, Row, Col} = require('react-bootstrap'); -const Modal = require('../../misc/Modal'); -const Message = require('../../I18N/Message'); - const Spinner = require('react-spinkit'); +const {Grid, Row, Col} = require('react-bootstrap'); +const {isNil} = require('lodash'); +const Message = require('../../I18N/Message'); +const Toolbar = require('../../misc/toolbar/Toolbar'); const LocaleUtils = require('../../../utils/LocaleUtils'); + /** -* A Modal window to show map metadata form + * A Modal window to show map metadata form */ class MetadataModal extends React.Component { static propTypes = { @@ -32,19 +34,22 @@ class MetadataModal extends React.Component { authHeader: PropTypes.string, show: PropTypes.bool, options: PropTypes.object, + modules: PropTypes.object, metadata: PropTypes.object, loadPermissions: PropTypes.func, loadAvailableGroups: PropTypes.func, onSave: PropTypes.func, + detailsSheetActions: PropTypes.object, onCreateThumbnail: PropTypes.func, onDeleteThumbnail: PropTypes.func, onGroupsChange: PropTypes.func, onAddPermission: PropTypes.func, - onClose: PropTypes.func, + onResetCurrentMap: PropTypes.func, useModal: PropTypes.bool, closeGlyph: PropTypes.string, buttonSize: PropTypes.string, includeCloseButton: PropTypes.bool, + showDetailsRow: PropTypes.bool, map: PropTypes.object, style: PropTypes.object, fluid: PropTypes.bool, @@ -56,6 +61,7 @@ class MetadataModal extends React.Component { onRemoveThumbnail: PropTypes.func, onErrorCurrentMap: PropTypes.func, onUpdateCurrentMap: PropTypes.func, + onDisplayMetadataEdit: PropTypes.func, onNewGroupChoose: PropTypes.func, onNewPermissionChoose: PropTypes.func, metadataChanged: PropTypes.func, @@ -77,10 +83,21 @@ class MetadataModal extends React.Component { loadPermissions: () => {}, loadAvailableGroups: () => {}, onSave: ()=> {}, + detailsSheetActions: { + onBackDetails: () => {}, + onUndoDetails: () => {}, + onToggleGroupProperties: () => {}, + onToggleUnsavedChangesModal: () => {}, + onToggleDetailsSheet: () => {}, + onUpdateDetails: () => {}, + onDeleteDetails: () => {}, + onSaveDetails: () => {} + }, onCreateThumbnail: ()=> {}, onDeleteThumbnail: ()=> {}, onGroupsChange: ()=> {}, onAddPermission: ()=> {}, + onDisplayMetadataEdit: ()=> {}, metadataChanged: ()=> {}, onNewGroupChoose: ()=> {}, onNewPermissionChoose: ()=> {}, @@ -88,12 +105,20 @@ class MetadataModal extends React.Component { name: "Guest" }, metadata: {name: "", description: ""}, + modules: { + toolbar: [ + [{ 'size': ['small', false, 'large', 'huge'] }, 'bold', 'italic', 'underline', 'blockquote'], + [{ 'list': 'bullet' }, { 'align': [] }], + [{ 'color': [] }, { 'background': [] }, 'clean'], ['image', 'video', 'link'] + ] + }, options: {}, useModal: true, closeGlyph: "", style: {}, buttonSize: "small", includeCloseButton: true, + showDetailsRow: true, fluid: true, // CALLBACKS onErrorCurrentMap: ()=> {}, @@ -101,7 +126,7 @@ class MetadataModal extends React.Component { onSaveAll: () => {}, onRemoveThumbnail: ()=> {}, onSaveMap: ()=> {}, - onClose: () => {}, + onResetCurrentMap: () => {}, // I18N errorMessages: {"FORMAT": , "SIZE": }, errorImage: , @@ -122,7 +147,7 @@ class MetadataModal extends React.Component { this.setState({ saving: false }); - this.props.onClose(); + // this.props.onResetCurrentMap(); } } @@ -135,6 +160,16 @@ class MetadataModal extends React.Component { } } + onCloseMapPropertiesModal = () => { + // TODO write only a single function used also in onClose property + if ( this.props.map.unsavedChanges) { + this.props.detailsSheetActions.onToggleUnsavedChangesModal(); + } else { + this.props.onDisplayMetadataEdit(false); + this.props.onResetCurrentMap(); + } + } + onSave = () => { this.setState({ saving: true @@ -150,10 +185,158 @@ class MetadataModal extends React.Component { }; this.props.onSave(this.props.map.id, name, description); } + this.refs.thumbnail.processUpdateThumbnail(this.props.map, metadata, this.props.map.thumbnailData); this.props.updatePermissions(this.props.map.id, this.props.map.permissions); - this.refs.thumbnail.updateThumbnail(this.props.map, metadata); }; + renderDetailsSheet = (readOnly) => { + return ( + + {readOnly ? ( + { + this.props.onResetCurrentMap(); + }} + title={LocaleUtils.getMessageById(this.context.messages, "map.details.title") + ' - ' + this.props.map.name} + show + > +
+ {!this.props.map.detailsText ? :
} +
+ + ) : ( { this.props.detailsSheetActions.onBackDetails(this.props.map.detailsBackup); }} + buttons={[{ + text: LocaleUtils.getMessageById(this.context.messages, "map.details.back"), + onClick: () => { + this.props.detailsSheetActions.onBackDetails(this.props.map.detailsBackup); + } + }, { + text: LocaleUtils.getMessageById(this.context.messages, "map.details.save"), + onClick: () => { + this.props.detailsSheetActions.onSaveDetails(this.props.map.detailsText); + } + }]}> +
+ { + if (details && details !== '


') { + this.props.detailsSheetActions.onUpdateDetails(details, false); + } + }} + modules={this.props.modules}/> +
+
)} + ); + } + /** + * @return the modal for unsaved changes + */ + renderUnsavedChanges = () => { + return ( + { + this.props.detailsSheetActions.onToggleUnsavedChangesModal(); + this.props.onDisplayMetadataEdit(true); + } + }, { + text: LocaleUtils.getMessageById(this.context.messages, "yes"), + onClick: () => { + this.props.onResetCurrentMap(); + } + }]}> +
+ + +
+ +
+
+
+
); + } + renderDetailsRow = () => { + return ( +
+
+ + +
+ {this.props.map.detailsText === "" ? : } +
+ + +
+
+ {this.props.map.saving ? : null} + {isNil(this.props.map.detailsText) ? : { this.props.detailsSheetActions.onToggleGroupProperties(); }, + disabled: this.props.map.saving + }, { + glyph: 'undo', + tooltip: LocaleUtils.getMessageById(this.context.messages, "map.details.undo"), + visible: !!this.props.map.detailsBackup, + onClick: () => { this.props.detailsSheetActions.onUndoDetails(this.props.map.detailsBackup); }, + disabled: this.props.map.saving + }, { + glyph: 'pencil-add', + tooltip: LocaleUtils.getMessageById(this.context.messages, "map.details.add"), + visible: !this.props.map.detailsText, + onClick: () => { + this.props.detailsSheetActions.onToggleDetailsSheet(false); + }, + disabled: this.props.map.saving + }, { + glyph: 'pencil', + tooltip: LocaleUtils.getMessageById(this.context.messages, "map.details.edit"), + visible: !!this.props.map.detailsText, + onClick: () => { + this.props.detailsSheetActions.onToggleDetailsSheet(false); + if (this.props.map.detailsText) { + this.props.detailsSheetActions.onUpdateDetails(this.props.map.detailsText, true); + } + }, + disabled: this.props.map.saving + }, { + glyph: 'trash', + tooltip: LocaleUtils.getMessageById(this.context.messages, "map.details.delete"), + visible: !!this.props.map.detailsText, + onClick: () => { this.props.detailsSheetActions.onDeleteDetails(); }, + disabled: this.props.map.saving + }]}/>} +
+
+ +
+
+ {this.props.map.detailsText &&
} +
+ ); + } renderPermissionEditor = () => { if (this.props.displayPermissionEditor && this.props.user.name === this.props.map.owner || this.props.user.role === "ADMIN" ) { // Hack to convert map permissions to a simpler format, TODO: remove this @@ -169,6 +352,7 @@ class MetadataModal extends React.Component { } return ( { - return this.props.map && this.props.map.updating ? : null; - }; - - render() { - const footer = (
{this.renderLoading()}
- - {this.props.includeCloseButton ? : } - ); - const body = - (} - descriptionFieldText={} - namePlaceholderText={LocaleUtils.getMessageById(this.context.messages, "map.namePlaceholder") || "Map Name"} - descriptionPlaceholderText={LocaleUtils.getMessageById(this.context.messages, "map.descriptionPlaceholder") || "Map Description"} - />); + renderMapProperties = () => { const mapErrorStatus = this.props.map && this.props.map.mapError && this.props.map.mapError.status ? this.props.map.mapError.status : null; let messageIdMapError = ""; if (mapErrorStatus === 404 || mapErrorStatus === 403 || mapErrorStatus === 409) { @@ -228,63 +383,92 @@ class MetadataModal extends React.Component { } else { messageIdError = "Default"; } - return ( - - - - - - - - - - {this.props.map && this.props.map.mapError ? -
+ return ( + { this.onSave(); }, + disabled: this.props.map.saving + }]} + showClose={!this.props.map.saving} + onClose={this.onCloseMapPropertiesModal}> + +
+ {/* TODO fix this error messages*/} + + {this.props.map && this.props.map.mapError ? +
- -
+
- : null } - {this.props.map && this.props.map.errors && this.props.map.errors.length > 0 ? -
-

{this.props.errorImage}

- { (this.props.map.errors.map((error) =>
{this.props.errorMessages[error]}
))}
- : null } - {this.props.map && this.props.map.thumbnailError ? -
-
- -
+ : null } + {this.props.map && this.props.map.errors && this.props.map.errors.length > 0 ? +
+

{this.props.errorImage}

+ { (this.props.map.errors.map((error) =>
{this.props.errorMessages[error]}
))} +
+ : null } + {this.props.map && this.props.map.thumbnailError ? +
+
+
- : null } - - - - - - - {body} - - - {this.renderPermissionEditor()} - - - - {footer} - - ); +
+ : null } + + + + + + + } + descriptionFieldText={} + namePlaceholderText={LocaleUtils.getMessageById(this.context.messages, "map.namePlaceholder") || "Map Name"} + descriptionPlaceholderText={LocaleUtils.getMessageById(this.context.messages, "map.descriptionPlaceholder") || "Map Description"} + /> + + + {this.props.showDetailsRow ? this.renderDetailsRow() : null} + {!this.props.map.hideGroupProperties && this.props.displayPermissionEditor && this.renderPermissionEditor()} + +
+ + ); + } + // TODO restore this + renderLoading = () => { + return this.props.map && this.props.map.updating ? : null; + }; + + render() { + return ( + + {this.props.map.showDetailEditor && this.renderDetailsSheet(this.props.map.detailsSheetReadOnly)} + {this.props.map.showUnsavedChanges && this.renderUnsavedChanges()} + {this.props.show && !this.props.map.showDetailEditor && this.renderMapProperties()} + ); } loadAvailableGroups = () => { diff --git a/web/client/components/maps/modals/__tests__/MetaDataModal-test.jsx b/web/client/components/maps/modals/__tests__/MetaDataModal-test.jsx index 96aa96e8c7..2951af0807 100644 --- a/web/client/components/maps/modals/__tests__/MetaDataModal-test.jsx +++ b/web/client/components/maps/modals/__tests__/MetaDataModal-test.jsx @@ -54,7 +54,7 @@ describe('This test for MetadataModal', () => { const modalDivList = document.getElementsByClassName("modal-content"); const closeBtnList = modalDivList.item(0).getElementsByTagName('button'); - expect(closeBtnList.length).toBe(3); + expect(closeBtnList.length).toBe(2); }); it('creates the component with a format error', () => { @@ -78,7 +78,7 @@ describe('This test for MetadataModal', () => { const modalDivList = document.getElementsByClassName("modal-content"); const closeBtnList = modalDivList.item(0).getElementsByTagName('button'); - expect(closeBtnList.length).toBe(3); + expect(closeBtnList.length).toBe(2); const errorFORMAT = modalDivList.item(0).getElementsByTagName('errorFORMAT'); expect(errorFORMAT).toExist(); @@ -105,7 +105,7 @@ describe('This test for MetadataModal', () => { const modalDivList = document.getElementsByClassName("modal-content"); const closeBtnList = modalDivList.item(0).getElementsByTagName('button'); - expect(closeBtnList.length).toBe(3); + expect(closeBtnList.length).toBe(2); const errorFORMAT = modalDivList.item(0).getElementsByTagName('errorSIZE'); expect(errorFORMAT).toExist(); diff --git a/web/client/components/misc/ResizableModal.jsx b/web/client/components/misc/ResizableModal.jsx new file mode 100644 index 0000000000..4cdfd5aeca --- /dev/null +++ b/web/client/components/misc/ResizableModal.jsx @@ -0,0 +1,123 @@ +/* +* 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 PropTypes = require('prop-types'); +const React = require('react'); +const {Glyphicon} = require('react-bootstrap'); +const Dialog = require('./Dialog'); +const Toolbar = require('./toolbar/Toolbar'); + +const sizes = { + xs: ' ms-xs', + sm: ' ms-sm', + md: '', + lg: ' ms-lg' +}; + +const fullscreen = { + className: { + vertical: ' ms-fullscreen-v', + horizontal: ' ms-fullscreen-h', + full: ' ms-fullscreen' + }, + glyph: { + expanded: { + vertical: 'resize-vertical', + horizontal: 'resize-horizontal', + full: 'resize-small' + }, + collapsed: { + vertical: 'resize-vertical', + horizontal: 'resize-horizontal', + full: 'resize-full' + } + } +}; + +class ResizableModal extends React.Component { + static propTypes = { + show: PropTypes.bool, + showFullscreen: PropTypes.bool, + clickOutEnabled: PropTypes.bool, + fullscreenType: PropTypes.string, + onClose: PropTypes.func, + title: PropTypes.node, + buttons: PropTypes.array, + size: PropTypes.string, + showClose: PropTypes.bool, + disabledClose: PropTypes.bool, + bodyClassName: PropTypes.string + }; + + static defaultProps = { + show: false, + onClose: () => {}, + title: '', + clickOutEnabled: true, + showClose: true, + disabledClose: false, + fullscreen: false, + fullscreenType: 'full', + buttons: [], + size: '', + bodyClassName: '' + }; + + state = { + fullscreen: 'collapsed' + }; + + render() { + // TODO VERIFY that the dialog id can be customizable or a fixed value + const sizeClassName = sizes[this.props.size] || ''; + const fullscreenClassName = this.props.showFullscreen && this.state.fullscreen === 'expanded' && fullscreen.className[this.props.fullscreenType] || ''; + return ( + + {}} + containerClassName="ms-resizable-modal" + draggable={false} + modal + className={'modal-dialog modal-content' + sizeClassName + fullscreenClassName}> + +

+
{this.props.title}
+ {this.props.showFullscreen && fullscreen.className[this.props.fullscreenType] && + { + this.setState({ + fullscreen: this.state.fullscreen === 'expanded' ? 'collapsed' : 'expanded' + }); + }} + glyph={fullscreen.glyph[this.state.fullscreen][this.props.fullscreenType]}/> + } + {this.props.showClose && this.props.onClose && + + } +

+
+
+ {this.props.children} +
+
+ +
+
+
+ ); + } +} + +module.exports = ResizableModal; diff --git a/web/client/components/security/PermissionEditor.jsx b/web/client/components/security/PermissionEditor.jsx index 3714a8e1bd..9fc3388ba9 100644 --- a/web/client/components/security/PermissionEditor.jsx +++ b/web/client/components/security/PermissionEditor.jsx @@ -30,6 +30,7 @@ class PermissionEditor extends React.Component { onAddPermission: PropTypes.func, buttonSize: PropTypes.string, includeCloseButton: PropTypes.bool, + disabled: PropTypes.bool, map: PropTypes.object, style: PropTypes.object, fluid: PropTypes.bool, @@ -51,6 +52,7 @@ class PermissionEditor extends React.Component { }; static defaultProps = { + disabled: true, id: "PermissionEditor", onGroupsChange: ()=> {}, onAddPermission: ()=> {}, @@ -198,7 +200,6 @@ class PermissionEditor extends React.Component { } return (
- @@ -239,7 +240,7 @@ class PermissionEditor extends React.Component {
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( { }, {}); }); - it('test closeCatalogOnFeatureGridOpen', (done) => { - testEpic(closeCatalogOnFeatureGridOpen, 1, openFeatureGrid(), actions => { - expect(actions.length).toBe(1); - actions.map((action) => { + it('test closeRightPanelOnFeatureGridOpen', (done) => { + testEpic(closeRightPanelOnFeatureGridOpen, 3, openFeatureGrid(), actions => { + expect(actions.length).toBe(3); + actions.map((action, i) => { switch (action.type) { - case SET_CONTROL_PROPERTY: - expect(action.control).toBe('metadataexplorer'); - expect(action.property).toBe('enabled'); - expect(action.value).toBe(false); - expect(action.toggle).toBe(undefined); + case SET_CONTROL_PROPERTY: { + switch (i) { + case 0: { + expect(action.control).toBe('metadataexplorer'); + expect(action.property).toBe('enabled'); + expect(action.value).toBe(false); + expect(action.toggle).toBe(undefined); + break; + } + case 1: { + expect(action.control).toBe('annotations'); + expect(action.property).toBe('enabled'); + expect(action.value).toBe(false); + expect(action.toggle).toBe(undefined); + break; + } + case 2: { + expect(action.control).toBe('details'); + expect(action.property).toBe('enabled'); + expect(action.value).toBe(false); + expect(action.toggle).toBe(undefined); + break; + } + default: expect(true).toBe(false); + } break; + } default: expect(true).toBe(false); } diff --git a/web/client/epics/__tests__/maps-test.js b/web/client/epics/__tests__/maps-test.js new file mode 100644 index 0000000000..12550c7318 --- /dev/null +++ b/web/client/epics/__tests__/maps-test.js @@ -0,0 +1,450 @@ +/* + * 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. + */ + +var expect = require('expect'); + +const configureMockStore = require('redux-mock-store').default; +const {createEpicMiddleware, combineEpics } = require('redux-observable'); +const { + saveDetails, SET_DETAILS_CHANGED, mapCreated, + CLOSE_DETAILS_PANEL, closeDetailsPanel, + openDetailsPanel, UPDATE_DETAILS, + MAP_DELETING, MAP_DELETED, deleteMap, TOGGLE_DETAILS_SHEET +} = require('../../actions/maps'); +const {clear, SHOW_NOTIFICATION} = require('../../actions/notifications'); +const {TOGGLE_CONTROL} = require('../../actions/controls'); +const {RESET_CURRENT_MAP, editMap} = require('../../actions/currentMap'); +const {CLOSE_FEATURE_GRID} = require('../../actions/featuregrid'); + +const { + setDetailsChangedEpic, mapCreatedNotificationEpic, + closeDetailsPanelEpic, fetchDataForDetailsPanel, + fetchDetailsFromResourceEpic, deleteMapAndAssociatedResourcesEpic} = require('../maps'); +const rootEpic = combineEpics(setDetailsChangedEpic, mapCreatedNotificationEpic, closeDetailsPanelEpic); +const epicMiddleware = createEpicMiddleware(rootEpic); +const mockStore = configureMockStore([epicMiddleware]); +const {testEpic, addTimeoutEpic, TEST_TIMEOUT} = require('./epicTestUtils'); + +const ConfigUtils = require('../../utils/ConfigUtils'); +const baseUrl = "base/web/client/test-resources/geostore/"; +ConfigUtils.getDefaults = () => ({ + geoStoreUrl: baseUrl +}); +const locale = { + messages: { + maps: { + feedback: { + allResDeleted: "allResDeleted ", + errorFetchingDetailsOfMap: "errorFetchingDetailsOfMap ", + errorDeletingDetailsOfMap: "errorDeletingDetailsOfMap ", + errorDeletingThumbnailOfMap: "errorDeletingThumbnailOfMap ", + errorDeletingMap: "errorDeletingMap " + } + } + } +}; +const mapId = 1; +const mapId8 = 8; +const detailsText = "

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 !== "


")); + return Rx.Observable.from(actions); + } + const originalDetails = currentMapOriginalDetailsTextSelector(state); + const currentDetails = currentMapDetailsTextSelector(state); + actions.push(setDetailsChanged(originalDetails !== currentDetails)); + return Rx.Observable.from(actions); + }); + + +/** + * If the details resource does not exist it saves it, and it updates its permission with the one set for the mapPermissionsFromIdSelector + * and it updates the attribute details in map resource +*/ +const saveResourceDetailsEpic = (action$, store) => + action$.ofType(SAVE_RESOURCE_DETAILS) + .switchMap(() => { + const state = store.getState(); + const mapId = currentMapIdSelector(state); + const value = currentMapDetailsTextSelector(state, mapId); + const detailsChanged = currentMapDetailsChangedSelector(state); + + let params = { + attribute: "details", + map: currentMapSelector(state), + resource: null, + type: "STRING" + }; + if (!detailsChanged) { + return Rx.Observable.of(doNothing()); + } + if (value !== "" && detailsChanged) { + params.resource = { + category: "DETAILS", + userParams: userParamsSelector(state), + metadata: {name: uuidv1()}, + value, + permissions: mapPermissionsFromIdSelector(state, mapId), + optionsAttr: {}, + optionsRes: {} + }; + params.messages = currentMessagesSelector(state); + } else { + params.optionsDel = {}; + } + return manageMapResource({ + ...params + }).concat([detailsSaving(false), resetUpdating(mapId)]).startWith(detailsSaving(true)); + }); + +/** + Epics used to fetch and/or open the details modal +*/ +const fetchDetailsFromResourceEpic = (action$, store) => + action$.ofType(EDIT_MAP) + .switchMap(() => { + const state = store.getState(); + const detailsUri = currentMapDetailsUriSelector(state); + if (!detailsUri || detailsUri === "NODATA") { + return Rx.Observable.of( + updateDetails("", true, "") + ); + } + const detailsId = getIdFromUri(detailsUri); + return Rx.Observable.fromPromise(GeoStoreApi.getData(detailsId) + .then(data => data)) + .switchMap((details) => { + return Rx.Observable.of( + updateDetails(details, true, details) + ); + }).catch(() => { + return Rx.Observable.of(basicError({ + message: LocaleUtils.getMessageById(state.locale.messages, "maps.feedback.errorFetchingDetailsOfMap") + currentMapIdSelector(store.getState())})); + }); + }); + +const deleteMapAndAssociatedResourcesEpic = (action$, store) => + action$.ofType(DELETE_MAP) + .switchMap((action) => { + const state = store.getState(); + const mapId = action.resourceId; + const options = action.options; + const detailsUri = mapDetailsUriFromIdSelector(state, mapId); + const thumbnailUri = mapThumbnailsUriFromIdSelector(state, mapId); + const detailsId = getIdFromUri(detailsUri); + const thumbnailsId = getIdFromUri(thumbnailUri); + + return Rx.Observable.forkJoin( + // delete details + deleteResourceById(thumbnailsId, options), + // delete thumbanil + deleteResourceById(detailsId, options), + // delete map + deleteResourceById(mapId, options) + ).concatMap(([details, thumbnail, map]) => { + let actions = []; + if (details.resType === "error") { + actions.push(basicError({message: LocaleUtils.getMessageById(state.locale.messages, "maps.feedback.errorDeletingDetailsOfMap") + mapId })); + } + if (thumbnail.resType === "error") { + actions.push(basicError({message: LocaleUtils.getMessageById(state.locale.messages, "maps.feedback.errorDeletingThumbnailOfMap") + mapId })); + } + if (map.resType === "error") { + actions.push(basicError({message: LocaleUtils.getMessageById(state.locale.messages, "maps.feedback.errorDeletingMap") + mapId })); + actions.push(mapDeleted(mapId, "failure", map.error)); + } + if (map.resType === "success") { + actions.push(mapDeleted(mapId, "success")); + if ( isMapsLastPageSelector(state)) { + actions.push(loadMaps(false, state.maps.searchText || ConfigUtils.getDefaults().initialMapFilter || "*")); + } + } + if (map.resType === "success" && details.resType === "success" && thumbnail.resType === "success") { + actions.push(basicSuccess({ message: LocaleUtils.getMessageById(state.locale.messages, "maps.feedback.allResDeleted") + mapId })); + + } + return Rx.Observable.from(actions); + }).startWith(mapDeleting(mapId)); + }); + +const mapCreatedNotificationEpic = action$ => + action$.ofType(MAP_CREATED) + .concat(() => action$.ofType(CLEAR_NOTIFICATIONS)) + .switchMap(() => Rx.Observable.of(basicSuccess({message: "maps.feedback.successSavedMap"}))); + +const fetchDataForDetailsPanel = (action$, store) => + action$.ofType(OPEN_DETAILS_PANEL) + .switchMap(() => { + const state = store.getState(); + const mapId = mapIdSelector(state); + const detailsUri = mapInfoDetailsUriFromIdSelector(state); + const detailsId = getIdFromUri(detailsUri); + return Rx.Observable.fromPromise(GeoStoreApi.getData(detailsId) + .then(data => data)) + .switchMap((details) => { + return Rx.Observable.from( [ + closeFeatureGrid(), + updateDetails(details, true, details + )] + ); + }).startWith(toggleControl("details", "enabled")) + .catch(() => { + return Rx.Observable.of(basicError({ + message: LocaleUtils.getMessageById(state.locale.messages, "maps.feedback.errorFetchingDetailsOfMap") + mapId})); + }); + }); + +const closeDetailsPanelEpic = (action$) => + action$.ofType(CLOSE_DETAILS_PANEL) + .switchMap(() => Rx.Observable.from( [ + toggleControl("details", "enabled"), + resetCurrentMap() + ]) + ); +const resetCurrentMapEpic = (action$) => + action$.ofType(RESET_UPDATING) + .switchMap(() => Rx.Observable.from( [ + onDisplayMetadataEdit(false), + resetCurrentMap() + ]) + ); +const storeDetailsInfoEpic = (action$, store) => + action$.ofType(MAP_INFO_LOADED) + .switchMap(() => { + const mapId = mapIdSelector(store.getState()); + return Rx.Observable.fromPromise( + GeoStoreApi.getResourceAttribute(mapId, "details") + .then(res => res.data).catch(() => { + return null; + }) + ) + .switchMap((details) => { + if (!details) { + return Rx.Observable.empty(); + } + return Rx.Observable.of( + detailsLoaded(mapId, details) + ); + }); + }); + + +module.exports = { + resetCurrentMapEpic, + storeDetailsInfoEpic, + closeDetailsPanelEpic, + fetchDataForDetailsPanel, + mapCreatedNotificationEpic, + deleteMapAndAssociatedResourcesEpic, + setDetailsChangedEpic, + fetchDetailsFromResourceEpic, + saveResourceDetailsEpic +}; diff --git a/web/client/localConfig.json b/web/client/localConfig.json index 5219013749..9d2d4606ee 100644 --- a/web/client/localConfig.json +++ b/web/client/localConfig.json @@ -160,8 +160,7 @@ } } ], - - "desktop": [ + "desktop": [ "Details", { "name": "Map", "cfg": { diff --git a/web/client/plugins/Details.jsx b/web/client/plugins/Details.jsx new file mode 100644 index 0000000000..2e349f61ae --- /dev/null +++ b/web/client/plugins/Details.jsx @@ -0,0 +1,53 @@ +/* + * 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 React = require('react'); +const {connect} = require('react-redux'); +const assign = require('object-assign'); +const {Glyphicon} = require('react-bootstrap'); +const Message = require('../components/I18N/Message'); +const {mapFromIdSelector} = require('../selectors/maps'); +const {mapIdSelector, mapInfoDetailsUriFromIdSelector} = require('../selectors/map'); +const {mapLayoutValuesSelector} = require('../selectors/maplayout'); +const {currentMapDetailsTextSelector} = require('../selectors/currentmap'); +const {openDetailsPanel, closeDetailsPanel} = require("../actions/maps"); +const {get} = require("lodash"); + +/** + * Details plugin used for fetching details of the map + * @class + * @memberof plugins + */ + +module.exports = { + DetailsPlugin: connect((state) => ({ + active: get(state, "controls.details.enabled"), + map: mapFromIdSelector(state, mapIdSelector(state)), + dockStyle: mapLayoutValuesSelector(state, {height: true}), + detailsText: currentMapDetailsTextSelector(state) + }), { + onClose: closeDetailsPanel + })(assign(require('../components/details/DetailsPanel'), { + BurgerMenu: { + name: 'details', + position: 1000, + text: , + icon: , + action: openDetailsPanel, + selector: (state) => { + const mapId = mapIdSelector(state); + const detailsUri = mapId && mapInfoDetailsUriFromIdSelector(state, mapId); + if (detailsUri) { + return {}; + } + return { style: {display: "none"} }; + } + } + })) + +}; diff --git a/web/client/plugins/Maps.jsx b/web/client/plugins/Maps.jsx index 27d3ad9937..cc5e91d801 100644 --- a/web/client/plugins/Maps.jsx +++ b/web/client/plugins/Maps.jsx @@ -7,12 +7,18 @@ */ const PropTypes = require('prop-types'); const React = require('react'); +const {bindActionCreators} = require('redux'); const {connect} = require('react-redux'); -const {loadMaps, updateMapMetadata, deleteMap, createThumbnail, deleteThumbnail, saveMap, thumbnailError, saveAll, onDisplayMetadataEdit, resetUpdating, metadataChanged} = require('../actions/maps'); +const {loadMaps, updateMapMetadata, deleteMap, createThumbnail, + updateDetails, deleteDetails, saveDetails, toggleDetailsSheet, toggleGroupProperties, toggleUnsavedChanges, setDetailsChanged, + deleteThumbnail, saveMap, thumbnailError, saveAll, onDisplayMetadataEdit, resetUpdating, metadataChanged, + backDetails, undoDetails} = require('../actions/maps'); const {editMap, updateCurrentMap, errorCurrentMap, removeThumbnail, resetCurrentMap} = require('../actions/currentMap'); const {mapTypeSelector} = require('../selectors/maptype'); const ConfigUtils = require('../utils/ConfigUtils'); +const maptypeEpics = require('../epics/maptype'); +const mapsEpics = require('../epics/maps'); const MapsGrid = connect((state) => { return { bsSize: "small", @@ -21,22 +27,35 @@ const MapsGrid = connect((state) => { loading: state.maps && state.maps.loading, mapType: mapTypeSelector(state) }; -}, { - loadMaps, - updateMapMetadata, - editMap, - saveMap, - removeThumbnail, - onDisplayMetadataEdit, - resetUpdating, - saveAll, - updateCurrentMap, - errorCurrentMap, - thumbnailError, - createThumbnail, - deleteThumbnail, - deleteMap, - resetCurrentMap +}, dispatch => { + return { + loadMaps: (...params) => dispatch(loadMaps(...params)), + updateMapMetadata: (...params) => dispatch(updateMapMetadata(...params)), + editMap: (...params) => dispatch(editMap(...params)), + saveMap: (...params) => dispatch(saveMap(...params)), + removeThumbnail: (...params) => dispatch(removeThumbnail(...params)), + onDisplayMetadataEdit: (...params) => dispatch(onDisplayMetadataEdit(...params)), + resetUpdating: (...params) => dispatch(resetUpdating(...params)), + saveAll: (...params) => dispatch(saveAll(...params)), + updateCurrentMap: (...params) => dispatch(updateCurrentMap(...params)), + errorCurrentMap: (...params) => dispatch(errorCurrentMap(...params)), + thumbnailError: (...params) => dispatch(thumbnailError(...params)), + createThumbnail: (...params) => dispatch(createThumbnail(...params)), + deleteThumbnail: (...params) => dispatch(deleteThumbnail(...params)), + deleteMap: (...params) => dispatch(deleteMap(...params)), + resetCurrentMap: (...params) => dispatch(resetCurrentMap(...params)), + detailsSheetActions: bindActionCreators({ + onBackDetails: backDetails, + onUndoDetails: undoDetails, + onToggleDetailsSheet: toggleDetailsSheet, + onToggleGroupProperties: toggleGroupProperties, + onToggleUnsavedChangesModal: toggleUnsavedChanges, + onsetDetailsChanged: setDetailsChanged, + onUpdateDetails: updateDetails, + onSaveDetails: saveDetails, + onDeleteDetails: deleteDetails + }, dispatch) + }; })(require('../components/maps/MapGrid')); const {loadPermissions, updatePermissions, loadAvailableGroups} = require('../actions/maps'); @@ -45,7 +64,7 @@ const {setControlProperty} = require('../actions/controls'); const MetadataModal = connect( (state = {}) => ({ - metadata: state.maps.metadata, + metadata: state.currentMap.metadata, availableGroups: state.currentMap && state.currentMap.availableGroups || [ ], // TODO: add message when array is empty newGroup: state.controls && state.controls.permissionEditor && state.controls.permissionEditor.newGroup, newPermission: state.controls && state.controls.permissionEditor && state.controls.permissionEditor.newPermission || "canRead", @@ -141,7 +160,10 @@ module.exports = { }), { loadMaps })(Maps), - epics: require('../epics/maptype'), + epics: { + ...maptypeEpics, + ...mapsEpics + }, reducers: { maps: require('../reducers/maps'), maptype: require('../reducers/maptype'), diff --git a/web/client/plugins/SaveAs.jsx b/web/client/plugins/SaveAs.jsx index 3f1d9bb9f0..26327d9e71 100644 --- a/web/client/plugins/SaveAs.jsx +++ b/web/client/plugins/SaveAs.jsx @@ -1,4 +1,3 @@ -const PropTypes = require('prop-types'); /* * Copyright 2017, GeoSolutions Sas. * All rights reserved. @@ -7,6 +6,7 @@ const PropTypes = require('prop-types'); * LICENSE file in the root directory of this source tree. */ +const PropTypes = require('prop-types'); const React = require('react'); const {connect} = require('react-redux'); const {createSelector, createStructuredSelector} = require('reselect'); @@ -68,6 +68,8 @@ class SaveAs extends React.Component { onCreateThumbnail: PropTypes.func, onUpdateCurrentMap: PropTypes.func, onErrorCurrentMap: PropTypes.func, + onResetCurrentMap: PropTypes.func, + onDisplayMetadataEdit: PropTypes.func, onSave: PropTypes.func, editMap: PropTypes.func, resetCurrentMap: PropTypes.func, @@ -84,6 +86,7 @@ class SaveAs extends React.Component { static defaultProps = { additionalOptions: {}, onMapSave: () => {}, + onDisplayMetadataEdit: () => {}, loadMapInfo: () => {}, show: false }; @@ -115,13 +118,15 @@ class SaveAs extends React.Component { metadataChanged={this.props.metadataChanged} metadata={this.props.metadata} displayPermissionEditor={false} + showDetailsRow={false} show={this.props.currentMap.displayMetadataEdit} onEdit={this.props.editMap} onUpdateCurrentMap={this.props.onUpdateCurrentMap} onErrorCurrentMap={this.props.onErrorCurrentMap} onHide={this.close} - onClose={this.close} map={map} + onDisplayMetadataEdit={this.props.onDisplayMetadataEdit} + onResetCurrentMap={this.props.resetCurrentMap} onSave={this.saveMap} /> ); @@ -179,7 +184,7 @@ module.exports = { position: 900, text: , icon: , - action: editMap.bind(null, {}), + action: editMap.bind(null, {}, true), selector: (state) => { if (state && state.controls && state.controls.saveAs && state.controls.saveAs.allowedRoles) { return indexOf(state.controls.saveAs.allowedRoles, state && state.security && state.security.user && state.security.user.role) !== -1 ? {} : { style: {display: "none"} }; diff --git a/web/client/product/appConfig.js b/web/client/product/appConfig.js index 93ddc1cfcf..d64a01e981 100644 --- a/web/client/product/appConfig.js +++ b/web/client/product/appConfig.js @@ -35,6 +35,9 @@ module.exports = { help: { enabled: false }, + details: { + enabled: false + }, print: { enabled: false }, diff --git a/web/client/product/plugins.js b/web/client/product/plugins.js index bca100ec8e..f68d73616c 100644 --- a/web/client/product/plugins.js +++ b/web/client/product/plugins.js @@ -8,6 +8,7 @@ module.exports = { plugins: { + DetailsPlugin: require('../plugins/Details'), MousePositionPlugin: require('../plugins/MousePosition'), PrintPlugin: require('../plugins/Print'), IdentifyPlugin: require('../plugins/Identify'), diff --git a/web/client/reducers/__tests__/config-test.js b/web/client/reducers/__tests__/config-test.js index 6083d64575..e453c1b503 100644 --- a/web/client/reducers/__tests__/config-test.js +++ b/web/client/reducers/__tests__/config-test.js @@ -8,6 +8,7 @@ var expect = require('expect'); var mapConfig = require('../config'); +const {DETAILS_LOADED} = require('../../actions/maps'); describe('Test the mapConfig reducer', () => { @@ -73,4 +74,17 @@ describe('Test the mapConfig reducer', () => { expect(state.map.info).toExist(); expect(state.map.info.canEdit).toBe(true); }); + it('DETAILS_LOADED', () => { + const detailsUri = "details/uri"; + var state = mapConfig({ + map: { + present: { + mapId: 1 + } + } + }, {type: DETAILS_LOADED, mapId: 1, detailsUri}); + expect(state.map).toExist(); + expect(state.map.info).toExist(); + expect(state.map.info.details).toBe(detailsUri); + }); }); diff --git a/web/client/reducers/config.js b/web/client/reducers/config.js index 07f8882c7e..2211bc75a2 100644 --- a/web/client/reducers/config.js +++ b/web/client/reducers/config.js @@ -7,7 +7,7 @@ */ const {MAP_CONFIG_LOADED, MAP_INFO_LOAD_START, MAP_INFO_LOADED, MAP_INFO_LOAD_ERROR, MAP_CONFIG_LOAD_ERROR} = require('../actions/config'); -const {MAP_CREATED} = require('../actions/maps'); +const {MAP_CREATED, DETAILS_LOADED} = require('../actions/maps'); const assign = require('object-assign'); const ConfigUtils = require('../utils/ConfigUtils'); @@ -59,6 +59,18 @@ function mapConfig(state = null, action) { return assign({}, state, {map: map}); } return state; + case DETAILS_LOADED: + map = state && state.map && state.map.present ? state.map.present : state && state.map; + if (map && map.mapId.toString() === action.mapId.toString()) { + map = assign({}, map, { + info: + assign({}, map.info, { + details: action.detailsUri + }) + }); + return assign({}, state, {map: map}); + } + return state; case MAP_CREATED: { map = state && state.map && state.map.present ? state.map.present : state && state.map; if (map && !map.mapId) { diff --git a/web/client/reducers/currentMap.js b/web/client/reducers/currentMap.js index 72ec37f168..e2b283778c 100644 --- a/web/client/reducers/currentMap.js +++ b/web/client/reducers/currentMap.js @@ -6,6 +6,7 @@ * LICENSE file in the root directory of this source tree. */ +// const {isNil} = require('lodash'); const { EDIT_MAP, UPDATE_CURRENT_MAP, @@ -25,19 +26,45 @@ const { MAP_ERROR, MAP_CREATED, PERMISSIONS_LIST_LOADING, - PERMISSIONS_LIST_LOADED + PERMISSIONS_LIST_LOADED, + TOGGLE_DETAILS_SHEET, + UPDATE_DETAILS, + SAVE_DETAILS, + DELETE_DETAILS, + BACK_DETAILS, + UNDO_DETAILS, + TOGGLE_GROUP_PROPERTIES, + TOGGLE_UNSAVED_CHANGES, + SET_DETAILS_CHANGED, + SET_UNSAVED_CHANGES, + METADATA_CHANGED, + DETAILS_SAVING } = require('../actions/maps'); const assign = require('object-assign'); -const _ = require('lodash'); +const {isArray} = require('lodash'); function currentMap(state = {}, action) { switch (action.type) { case EDIT_MAP: { - return assign({}, state, action.map, {newThumbnail: action.map && action.map.thumbnail ? action.map.thumbnail : null, displayMetadataEdit: true, thumbnailError: null, errors: [] }); + return assign({}, state, action.map, { + newThumbnail: action.map && action.map.thumbnail ? action.map.thumbnail : null, + displayMetadataEdit: action.openModalProperties, + thumbnailError: null, + errors: [], + metadata: { + name: action.map.name, + description: action.map.description + }, + hideGroupProperties: false, + detailsSheetReadOnly: true }); } case UPDATE_CURRENT_MAP: { - return assign({}, state, {newThumbnail: action.thumbnail, files: action.files}); + return assign({}, state, { + newThumbnail: action.thumbnail, + thumbnailData: action.thumbnailData, + unsavedChanges: true + }); } case MAP_UPDATING: { return assign({}, state, {updating: true}); @@ -46,7 +73,7 @@ function currentMap(state = {}, action) { // Fix to overcome GeoStore bad encoding of single object arrays let fixedSecurityRule = []; if (action.permissions && action.permissions.SecurityRuleList && action.permissions.SecurityRuleList.SecurityRule) { - if ( _.isArray(action.permissions.SecurityRuleList.SecurityRule)) { + if ( isArray(action.permissions.SecurityRuleList.SecurityRule)) { fixedSecurityRule = action.permissions.SecurityRuleList.SecurityRule; } else { fixedSecurityRule.push(action.permissions.SecurityRuleList.SecurityRule); @@ -102,6 +129,86 @@ function currentMap(state = {}, action) { case RESET_CURRENT_MAP: { return {}; } + case TOGGLE_DETAILS_SHEET: { + return assign({}, state, { + showDetailEditor: !state.showDetailEditor, + detailsBackup: !state.showDetailEditor && !state.detailsDeleted ? "" : state.detailsBackup, + detailsSheetReadOnly: action.detailsSheetReadOnly + }); + } + case METADATA_CHANGED: { + let prop = action.prop; + return assign({}, state, { + metadata: assign({}, state.metadata, {[action.prop]: action.value }), + unsavedChanges: + (prop === "name" ? action.value : state.metadata.name) !== state.name || + (prop === "description" ? action.value : state.metadata.description) !== state.description + }); + } + case UPDATE_DETAILS: { + return assign({}, state, { + detailsText: action.detailsText, + originalDetails: action.originalDetails || state.originalDetails, + detailsBackup: action.doBackup ? state.detailsText : state.detailsBackup + }); + } + case BACK_DETAILS: { + return assign({}, state, { + detailsText: state.detailsDeleted ? "" : action.backupDetails, + detailsBackup: state.detailsDeleted ? state.detailsBackup : "", + showDetailEditor: false + }); + } + case UNDO_DETAILS: { + return assign({}, state, { + detailsText: state.detailsBackup, + detailsBackup: "", + detailsDeleted: false + }); + } + case SAVE_DETAILS: { + return action.detailsText.length <= 500000 ? assign({}, state, { + detailsText: action.detailsText, + detailsBackup: "", + detailsDeleted: false + }) : state; + } + case DETAILS_SAVING: { + return assign({}, state, { + saving: action.saving/*, + unsavedChanges: action.saving === false ? true : state.unsavedChanges*/ + }); + } + case DELETE_DETAILS: { + return assign({}, state, { + detailsText: "", + detailsBackup: state.detailsText, + detailsChanged: true, + unsavedChanges: true, + detailsDeleted: true + }); + } + case SET_UNSAVED_CHANGES: { + return assign({}, state, { + unsavedChanges: action.value + }); + } + case TOGGLE_GROUP_PROPERTIES: { + return assign({}, state, { + hideGroupProperties: !state.hideGroupProperties + }); + } + case TOGGLE_UNSAVED_CHANGES: { + return assign({}, state, { + showUnsavedChanges: !state.showUnsavedChanges + }); + } + case SET_DETAILS_CHANGED: { + return assign({}, state, { + unsavedChanges: action.detailsChanged ? action.detailsChanged : state.unsavedChanges, + detailsChanged: action.detailsChanged + }); + } default: return state; } diff --git a/web/client/selectors/__tests__/currentmap-test.js b/web/client/selectors/__tests__/currentmap-test.js new file mode 100644 index 0000000000..91f09f8307 --- /dev/null +++ b/web/client/selectors/__tests__/currentmap-test.js @@ -0,0 +1,77 @@ +/* +* 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 { + currentMapSelector, + currentMapIdSelector, + currentMapNameSelector, + currentMapDecriptionSelector, + currentMapDetailsUriSelector, + currentMapDetailsTextSelector, + currentMapThumbnailUriSelector, + currentMapDetailsChangedSelector, + currentMapOriginalDetailsTextSelector +} = require('../currentmap'); + +const uri = "some/uri/6/"; +const name = "name"; +const description = "description"; +const 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 @@ -Created by FontForge 20170731 at Wed Nov 22 12:30:24 2017 +Created by FontForge 20170731 at Wed Nov 29 09:57:48 2017 By stefano @@ -19,7 +19,7 @@ Created by FontForge 20170731 at Wed Nov 22 12:30:24 2017 cap-height="1063" bbox="-217 -220 1478 1242" underline-thickness="50" - underline-position="-750" + underline-position="-800" unicode-range="U+002A-1F6AA" /> d="M1200 1080v-1200h-1200v1200h1200zM111 969v-978l977 978h-977z" /> + /> - + 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"); + }); + +});