diff --git a/web/client/actions/__tests__/dashboard-test.js b/web/client/actions/__tests__/dashboard-test.js index 7fc11224ee..15fc864694 100644 --- a/web/client/actions/__tests__/dashboard-test.js +++ b/web/client/actions/__tests__/dashboard-test.js @@ -9,16 +9,86 @@ var expect = require('expect'); const { setEditing, SET_EDITING, - setEditorAvailable, SET_EDITOR_AVAILABLE + setEditorAvailable, SET_EDITOR_AVAILABLE, + triggerShowConnections, SHOW_CONNECTIONS, + triggerSave, TRIGGER_SAVE_MODAL, + saveDashboard, SAVE_DASHBOARD, + dashboardSaveError, SAVE_ERROR, + dashboardSaved, DASHBOARD_SAVED, + loadDashboard, LOAD_DASHBOARD, + dashboardLoaded, DASHBOARD_LOADED, + dashboardLoading, DASHBOARD_LOADING } = require('../dashboard'); -it('setEditing', () => { - const retval = setEditing(); - expect(retval).toExist(); - expect(retval.type).toBe(SET_EDITING); -}); -it('setEditorAvailable', () => { - const retval = setEditorAvailable(); - expect(retval).toExist(); - expect(retval.type).toBe(SET_EDITOR_AVAILABLE); +describe('Test correctness of the dashboard actions', () => { + it('setEditing', () => { + const retval = setEditing(); + expect(retval).toExist(); + expect(retval.type).toBe(SET_EDITING); + }); + it('setEditorAvailable', () => { + const retval = setEditorAvailable(); + expect(retval).toExist(); + expect(retval.type).toBe(SET_EDITOR_AVAILABLE); + }); + it('triggerShowConnections', () => { + const retval = triggerShowConnections(true); + expect(retval).toExist(); + expect(retval.type).toBe(SHOW_CONNECTIONS); + expect(retval.show).toBe(true); + }); + it('triggerShowConnections', () => { + const retval = triggerShowConnections(); + expect(retval).toExist(); + expect(retval.type).toBe(SHOW_CONNECTIONS); + }); + it('triggerSave', () => { + const retval = triggerSave(); + expect(retval).toExist(); + expect(retval.type).toBe(TRIGGER_SAVE_MODAL); + }); + it('saveDashboard', () => { + const retval = saveDashboard({TEST: "TEST"}); + expect(retval).toExist(); + expect(retval.type).toBe(SAVE_DASHBOARD); + expect(retval.resource.TEST).toBe("TEST"); + }); + it('dashboardSaveError', () => { + const retval = dashboardSaveError("ERROR"); + expect(retval).toExist(); + expect(retval.type).toBe(SAVE_ERROR); + expect(retval.error).toBe("ERROR"); + }); + it('dashboardSaved', () => { + const retval = dashboardSaved(); + expect(retval).toExist(); + expect(retval.type).toBe(DASHBOARD_SAVED); + }); + it('loadDashboard', () => { + const retval = loadDashboard(1); + expect(retval).toExist(); + expect(retval.type).toBe(LOAD_DASHBOARD); + expect(retval.id).toBe(1); + }); + it('dashboardLoaded', () => { + const retval = dashboardLoaded("RES", "DATA"); + expect(retval).toExist(); + expect(retval.type).toBe(DASHBOARD_LOADED); + expect(retval.resource).toBe("RES"); + expect(retval.data).toBe("DATA"); + }); + it('dashboardLoading default', () => { + const retval = dashboardLoading(false); + expect(retval).toExist(); + expect(retval.type).toBe(DASHBOARD_LOADING); + expect(retval.name).toBe("loading"); + expect(retval.value).toBe(false); + }); + it('dashboardLoading', () => { + const retval = dashboardLoading(true, "saving"); + expect(retval).toExist(); + expect(retval.type).toBe(DASHBOARD_LOADING); + expect(retval.name).toBe("saving"); + expect(retval.value).toBe(true); + }); }); diff --git a/web/client/actions/dashboard.js b/web/client/actions/dashboard.js index 43a6471e77..f4a01498b7 100644 --- a/web/client/actions/dashboard.js +++ b/web/client/actions/dashboard.js @@ -1,11 +1,43 @@ const SET_EDITOR_AVAILABLE = "DASHBOARD:SET_AVAILABLE"; const SET_EDITING = "DASHBOARD:SET_EDITING"; const SHOW_CONNECTIONS = "DASHBOARD:SHOW_CONNECTIONS"; +const TRIGGER_SAVE_MODAL = "DASHBOARD:TRIGGER_SAVE_MODAL"; + +const SAVE_DASHBOARD = "DASHBOARD:SAVE_DASHBOARD"; +const SAVE_ERROR = "DASHBOARD:SAVE_ERROR"; +const DASHBOARD_SAVED = "DASHBOARD:DASHBOARD_SAVED"; + +const LOAD_DASHBOARD = "DASHBOARD:LOAD_DASHBOARD"; +const DASHBOARD_LOADED = "DASHBOARD:DASHBOARD_LOADED"; +const DASHBOARD_LOADING = "DASHBOARD:DASHBOARD_LOADING"; + module.exports = { SET_EDITING, setEditing: (editing) => ({type: SET_EDITING, editing }), SET_EDITOR_AVAILABLE, setEditorAvailable: available => ({type: SET_EDITOR_AVAILABLE, available}), SHOW_CONNECTIONS, - triggerShowConnections: show => ({ type: SHOW_CONNECTIONS, show}) + triggerShowConnections: show => ({ type: SHOW_CONNECTIONS, show}), + TRIGGER_SAVE_MODAL, + triggerSave: show => ({ type: TRIGGER_SAVE_MODAL, show}), + SAVE_DASHBOARD, + saveDashboard: resource => ({ type: SAVE_DASHBOARD, resource}), + SAVE_ERROR, + dashboardSaveError: error => ({type: SAVE_ERROR, error}), + DASHBOARD_SAVED, + dashboardSaved: id => ({type: DASHBOARD_SAVED, id}), + LOAD_DASHBOARD, + loadDashboard: id => ({ type: LOAD_DASHBOARD, id}), + DASHBOARD_LOADED, + dashboardLoaded: (resource, data) => ({ type: DASHBOARD_LOADED, resource, data}), + DASHBOARD_LOADING, + /** + * @param {boolean} value the value of the flag + * @param {string} [name] the name of the flag to set. loading is anyway always triggered + */ + dashboardLoading: (value, name = "loading") => ({ + type: DASHBOARD_LOADING, + name, + value + }) }; diff --git a/web/client/api/GeoStoreDAO.js b/web/client/api/GeoStoreDAO.js index 8f2af0cf14..b1b168203b 100644 --- a/web/client/api/GeoStoreDAO.js +++ b/web/client/api/GeoStoreDAO.js @@ -74,6 +74,11 @@ const Api = { "resources/resource/" + resourceId, this.addBaseUrl(parseOptions(options))).then(function(response) {return response.data; }); }, + getShortResource: function(resourceId, options) { + return axios.get( + "extjs/resource/" + resourceId, + this.addBaseUrl(parseOptions(options))).then(function(response) { return response.data; }); + }, getResourcesByCategory: function(category, query, options) { const q = query || "*"; const url = "extjs/search/category/" + category + "/*" + q + "*/thumbnail,details,featured"; // comma-separated list of wanted attributes @@ -142,6 +147,26 @@ const Api = { } }, options))); }, + getResourceAttributes: function(resourceId, options = {}) { + return axios.get( + "resources/resource/" + resourceId + "/attributes", + this.addBaseUrl({ + headers: { + 'Accept': "application/json" + }, + ...options + })).then(({ data } = {}) => data) + .then(data => _.castArray(_.get(data, "AttributeList.Attribute"))) + .then(attributes => (attributes && attributes[0] && attributes[0] !== "") ? attributes : []); + }, + /** + * same of getPermissions but clean data properly and returns only the array of rules. + */ + getResourcePermissions: function(resourceId, options) { + return Api.getPermissions(resourceId, options) + .then(rl => _.castArray(_.get(rl, 'SecurityRuleList.SecurityRule'))) + .then(rules => (rules && rules[0] && rules[0] !== "") ? rules : []); + }, putResourceMetadata: function(resourceId, newName, newDescription, options) { return axios.put( "resources/resource/" + resourceId, diff --git a/web/client/components/dashboard/forms/Metadata.jsx b/web/client/components/dashboard/forms/Metadata.jsx new file mode 100644 index 0000000000..cfdedfb733 --- /dev/null +++ b/web/client/components/dashboard/forms/Metadata.jsx @@ -0,0 +1,85 @@ +/** + * 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. + */ +/** + * 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 PropTypes = require('prop-types'); +const {FormControl: BFormControl, FormGroup, ControlLabel} = require('react-bootstrap'); +const FormControl = require('../../misc/enhancers/localizedProps')('placeholder')(BFormControl); + +/** + * A DropDown menu for user details: + */ +class Metadata extends React.Component { + static propTypes = { + resource: PropTypes.object, + // CALLBACKS + onChange: PropTypes.func, + + // I18N + nameFieldText: PropTypes.node, + descriptionFieldText: PropTypes.node, + namePlaceholderText: PropTypes.string, + descriptionPlaceholderText: PropTypes.string + }; + + static defaultProps = { + // CALLBACKS + onChange: () => {}, + resource: {}, + // I18N + nameFieldText: "Name", + descriptionFieldText: "Description", + namePlaceholderText: "Map Name", + descriptionPlaceholderText: "Map Description" + }; + + render() { + return (
+ + {this.props.nameFieldText} + + + + {this.props.descriptionFieldText} + + +
); + } + + changeName = (e) => { + this.props.onChange('metadata.name', e.target.value); + }; + + changeDescription = (e) => { + this.props.onChange('metadata.description', e.target.value); + }; +} + + +module.exports = Metadata; diff --git a/web/client/components/dashboard/forms/Thumbnail.jsx b/web/client/components/dashboard/forms/Thumbnail.jsx new file mode 100644 index 0000000000..d9b55074aa --- /dev/null +++ b/web/client/components/dashboard/forms/Thumbnail.jsx @@ -0,0 +1,136 @@ +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 {Glyphicon} = require('react-bootstrap'); +const Dropzone = require('react-dropzone'); +const Spinner = require('react-spinkit'); +const Message = require('../../../components/I18N/Message'); +/** + * A Dropzone area for a thumbnail. + */ + +class Thumbnail extends React.Component { + static propTypes = { + glyphiconRemove: PropTypes.string, + style: PropTypes.object, + loading: PropTypes.bool, + resource: PropTypes.object, + onError: PropTypes.func, + onUpdate: PropTypes.func, + onRemove: PropTypes.func, + // I18N + message: PropTypes.oneOfType([PropTypes.string, PropTypes.element]), + suggestion: PropTypes.oneOfType([PropTypes.string, PropTypes.element]) + }; + + static contextTypes = { + messages: PropTypes.object + }; + + static defaultProps = { + loading: false, + glyphiconRemove: "remove-circle", + resource: {}, + // CALLBACKS + onError: () => {}, + onUpdate: () => {}, + onSaveAll: () => {}, + onRemove: () => {}, + // I18N + message: , + suggestion: + }; + + state = {}; + + onRemoveThumbnail = (event) => { + if (event !== null) { + event.stopPropagation(); + } + + this.files = null; + this.props.onError([]); + this.props.onRemove(); + }; + + getThumbnailUrl = () => { + return this.props.thumbnail && this.props.thumbnail !== "NODATA" ? decodeURIComponent(this.props.thumbnail) : null; + }; + + isImage = (images) => { + return images && images[0].type === "image/png" || images && images[0].type === "image/jpeg" || images && images[0].type === "image/jpg"; + }; + + getDataUri = (images, callback) => { + let filesSelected = images; + if (filesSelected && filesSelected.length > 0) { + let fileToLoad = filesSelected[0]; + let fileReader = new FileReader(); + fileReader.onload = (event) => callback(event.target.result); + return fileReader.readAsDataURL(fileToLoad); + } + return callback(null); + }; + + onDrop = (images) => { + // check formats and sizes + const isAnImage = this.isImage(images); + let errors = []; + + this.getDataUri(images, (data) => { + if (isAnImage && data && data.length < 500000) { + // without errors + this.props.onError([], this.props.resource.id); + this.files = images; + this.props.onUpdate(data, images && images[0].preview); + } else { + // with at least one error + if (!isAnImage) { + errors.push("FORMAT"); + } + if (data && data.length >= 500000) { + errors.push("SIZE"); + } + this.props.onError(errors, this.props.resource.id); + this.files = images; + this.props.onUpdate(null, null); + } + }); + }; + getThumbnailDataUri = (callback) => { + this.getDataUri(this.files, callback); + }; + render() { + return ( + this.props.loading ?
: + +
+ + + { this.getThumbnailUrl() + ?
+ +
{this.props.message}
{this.props.suggestion}
+
+ +
+
+ :
{this.props.message}
{this.props.suggestion}
+ } +
+
+ + ); + } +} + +module.exports = Thumbnail; diff --git a/web/client/components/dashboard/forms/__tests__/Metadata-test.jsx b/web/client/components/dashboard/forms/__tests__/Metadata-test.jsx new file mode 100644 index 0000000000..4351f7eacb --- /dev/null +++ b/web/client/components/dashboard/forms/__tests__/Metadata-test.jsx @@ -0,0 +1,56 @@ +/* + * Copyright 2018, 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 ReactDOM = require('react-dom'); +const ReactTestUtils = require('react-dom/test-utils'); +const expect = require('expect'); +const Metadata = require('../Metadata'); +describe('Metadata component', () => { + beforeEach((done) => { + document.body.innerHTML = '
'; + setTimeout(done); + }); + afterEach((done) => { + ReactDOM.unmountComponentAtNode(document.getElementById("container")); + document.body.innerHTML = ''; + setTimeout(done); + }); + it('Metadata rendering with defaults', () => { + ReactDOM.render(, document.getElementById("container")); + const container = document.getElementById('container'); + const el = container.querySelectorAll('input'); + expect(el.length).toBe(2); + }); + it('Metadata rendering with meta-data', () => { + const resource = { + metadata: { + name: "NAME", + description: "DESCRIPTION" + } + }; + ReactDOM.render(, document.getElementById("container")); + const container = document.getElementById('container'); + const el = container.querySelectorAll('input'); + expect(el.length).toBe(2); + expect(el[0].value).toBe("NAME"); + expect(el[1].value).toBe("DESCRIPTION"); + }); + it('Test Metadata onChange', () => { + const actions = { + onChange: () => {} + }; + const spyonChange = expect.spyOn(actions, 'onChange'); + ReactDOM.render(, document.getElementById("container")); + const container = document.getElementById('container'); + const input = container.querySelector('input'); + input.value = "test"; + ReactTestUtils.Simulate.change(input); // <-- trigger event callback + expect(spyonChange).toHaveBeenCalled(); + }); +}); diff --git a/web/client/components/dashboard/forms/__tests__/Thumbnail-test.jsx b/web/client/components/dashboard/forms/__tests__/Thumbnail-test.jsx new file mode 100644 index 0000000000..238b5bb7e7 --- /dev/null +++ b/web/client/components/dashboard/forms/__tests__/Thumbnail-test.jsx @@ -0,0 +1,79 @@ +/** + * 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. + */ +var React = require('react'); +var ReactDOM = require('react-dom'); +var Thumbnail = require('../Thumbnail.jsx'); +var expect = require('expect'); +const TestUtils = require('react-dom/test-utils'); + +describe('This test for Thumbnail', () => { + + + beforeEach((done) => { + document.body.innerHTML = '
'; + setTimeout(done); + }); + + afterEach((done) => { + ReactDOM.unmountComponentAtNode(document.getElementById("container")); + document.body.innerHTML = ''; + setTimeout(done); + }); + + // test DEFAULTS + it('creates the component with defaults, loading=true', () => { + const thumbnailItem = ReactDOM.render(, document.getElementById("container")); + expect(thumbnailItem).toExist(); + + const thumbnailItemDom = ReactDOM.findDOMNode(thumbnailItem); + expect(thumbnailItemDom).toExist(); + + expect(thumbnailItemDom.className).toBe('btn btn-info'); + }); + + it('creates the component with defaults, loading=false', () => { + const thumbnailItem = ReactDOM.render(, document.getElementById("container")); + expect(thumbnailItem).toExist(); + + const thumbnailItemDom = ReactDOM.findDOMNode(thumbnailItem); + expect(thumbnailItemDom).toExist(); + + expect(thumbnailItemDom.className).toBe('dropzone-thumbnail-container'); + }); + + it('creates the component without a thumbnail', () => { + let thumbnail = "http://localhost:8081/%2Fgeostore%2Frest%2Fdata%2F2214%2Fraw%3Fdecode%3Ddatauri"; + let map = { + thumbnail: thumbnail, + id: 123, + canWrite: true, + errors: [] + }; + const thumbnailItem = ReactDOM.render(, document.getElementById("container")); + expect(thumbnailItem).toExist(); + + const thumbnailItemDom = ReactDOM.findDOMNode(thumbnailItem); + expect(thumbnailItemDom).toExist(); + + const content = TestUtils.findRenderedDOMComponentWithClass(thumbnailItem, 'dropzone-content-image'); + expect(content).toExist(); + }); + + it('creates the component with a thumbnail', () => { + let thumbnail = "http://localhost:8081/%2Fgeostore%2Frest%2Fdata%2F2214%2Fraw%3Fdecode%3Ddatauri"; + const thumbnailItem = ReactDOM.render(, document.getElementById("container")); + expect(thumbnailItem).toExist(); + + const thumbnailItemDom = ReactDOM.findDOMNode(thumbnailItem); + expect(thumbnailItemDom).toExist(); + + const content = TestUtils.findRenderedDOMComponentWithClass(thumbnailItem, 'dropzone-content-image-added'); + expect(content).toExist(); + }); + +}); diff --git a/web/client/components/dashboard/modals/Confirm.jsx b/web/client/components/dashboard/modals/Confirm.jsx new file mode 100644 index 0000000000..eda7a1cf39 --- /dev/null +++ b/web/client/components/dashboard/modals/Confirm.jsx @@ -0,0 +1,93 @@ +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 {Button} = require('react-bootstrap'); +const Modal = require('../../misc/Modal'); +const Spinner = require('react-spinkit'); + +/** + * A Modal window to show a confirmation dialog + */ +class ConfirmModal extends React.Component { + static propTypes = { + // props + className: PropTypes.string, + show: PropTypes.bool, + options: PropTypes.object, + onConfirm: PropTypes.func, + onClose: PropTypes.func, + closeGlyph: PropTypes.string, + style: PropTypes.object, + buttonSize: PropTypes.string, + includeCloseButton: PropTypes.bool, + body: PropTypes.oneOfType([PropTypes.string, PropTypes.element]), + titleText: PropTypes.oneOfType([PropTypes.string, PropTypes.element]), + confirmText: PropTypes.oneOfType([PropTypes.string, PropTypes.element]), + cancelText: PropTypes.oneOfType([PropTypes.string, PropTypes.element]), + running: PropTypes.bool + }; + + static defaultProps = { + onConfirm: ()=> {}, + onClose: () => {}, + options: { + animation: false + }, + className: "", + closeGlyph: "", + style: {}, + includeCloseButton: true, + body: "", + titleText: "Confirm Delete", + confirmText: "Delete", + cancelText: "Cancel" + }; + + onConfirm = () => { + this.props.onConfirm(); + }; + + render() { + const footer = (
+ + {this.props.includeCloseButton ? : } + ); + const body = this.props.body; + return ( + + + {this.props.titleText} + + + {body} + + + {footer} + + ); + } +} + +module.exports = ConfirmModal; diff --git a/web/client/components/dashboard/modals/DetailSheet.jsx b/web/client/components/dashboard/modals/DetailSheet.jsx new file mode 100644 index 0000000000..4b9f883785 --- /dev/null +++ b/web/client/components/dashboard/modals/DetailSheet.jsx @@ -0,0 +1,84 @@ +/* + * Copyright 2018, 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 ReactQuill = require('react-quill'); +const Spinner = require('react-spinkit'); + +const Message = require('../../I18N/Message'); +const Portal = require('../../misc/Portal'); +const ResizableModal = require('../../misc/ResizableModal'); +require('react-quill/dist/quill.snow.css'); + +// NOTE: partial porting of details sheet from map, still to be tested and added to the save form +module.exports = ({ + readOnly, + showDetailEditor, + modules = { + toolbar: [ + [{ 'size': ['small', false, 'large', 'huge'] }, 'bold', 'italic', 'underline', 'blockquote'], + [{ 'list': 'bullet' }, { 'align': [] }], + [{ 'color': [] }, { 'background': [] }, 'clean'], ['image', 'video', 'link'] + ] + }, + detailsText, + detailsBackup, + onSaveDetails = () => {}, + onResetCurrentMap = () => {}, + onBackDetails = () => {}, + onUpdateDetails = () => {}} + ) => { + return ( + + {readOnly ? ( + { + onResetCurrentMap(); + }} + title={} + show + > +
+ {!detailsText ? :
} +
+ + ) : (} + bodyClassName="ms-modal-quill-container" + size="lg" + clickOutEnabled={false} + showFullscreen + fullscreenType="full" + onClose={() => { onBackDetails(detailsBackup); }} + buttons={[{ + text: , + onClick: () => { + onBackDetails(detailsBackup); + } + }, { + text: , + onClick: () => { + onSaveDetails(detailsText); + } + }]}> +
+ { + if (details && details !== '


') { + onUpdateDetails(details, false); + } + }} + modules={modules} /> +
+
)} + ); + }; diff --git a/web/client/components/dashboard/modals/Save.jsx b/web/client/components/dashboard/modals/Save.jsx new file mode 100644 index 0000000000..eb2029a450 --- /dev/null +++ b/web/client/components/dashboard/modals/Save.jsx @@ -0,0 +1,125 @@ +/* +* 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 {get} = require('lodash'); + +const Portal = require('../../misc/Portal'); +const ResizableModal = require('../../misc/ResizableModal'); +// require('./css/modals.css'); +const {Grid} = require('react-bootstrap'); +const Message = require('../../I18N/Message'); +const ErrorBox = require('./fragments/ErrorBox'); +const MainForm = require('./fragments/MainForm'); +const ruleEditor = require('./enhancers/ruleEditor'); +const PermissionEditor = ruleEditor(require('./fragments/PermissionEditor')); + +/** + * A Modal window to show map metadata form +*/ +class SaveModal extends React.Component { + static propTypes = { + show: PropTypes.bool, + loading: PropTypes.bool, + errors: PropTypes.array, + rules: PropTypes.array, + onSave: PropTypes.func, + onUpdateRules: PropTypes.func, + closeGlyph: PropTypes.string, + resource: PropTypes.object, + linkedResources: PropTypes.object, + style: PropTypes.object, + modalSize: PropTypes.string, + // CALLBACKS + onError: PropTypes.func, + onUpdate: PropTypes.func, + onUpdateLinkedResource: PropTypes.func, + onClose: PropTypes.func, + metadataChanged: PropTypes.func, + availablePermissions: PropTypes.arrayOf(PropTypes.string), + availableGroups: PropTypes.arrayOf(PropTypes.object) + }; + + static contextTypes = { + messages: PropTypes.object + }; + + static defaultProps = { + id: "MetadataModal", + modalSize: "", + resource: {}, + linkedResources: {}, + onUpdateRules: ()=> {}, + metadataChanged: ()=> {}, + metadata: {name: "", description: ""}, + options: {}, + closeGlyph: "", + style: {}, + // CALLBACKS + onError: ()=> {}, + onUpdate: ()=> {}, + onUpdateLinkedResource: () => {}, + onSaveAll: () => {}, + onSave: ()=> {}, + onReset: () => {}, + availablePermissions: ["canRead", "canWrite"], + availableGroups: [] + }; + onCloseMapPropertiesModal = () => { + this.props.onClose(); + } + + onSave = () => { + this.props.onSave({...this.props.resource, permission: this.props.rules}); + }; + + /** + * @return the modal for unsaved changes + */ + render() { + return ( + {} + show={this.props.show} + clickOutEnabled + bodyClassName="ms-flex modal-properties-container" + buttons={[{ + text: , + onClick: this.onCloseMapPropertiesModal, + disabled: this.props.resource.loading + }, { + text: , + onClick: () => { this.onSave(); }, + disabled: !this.isValidForm() || this.props.loading + }]} + showClose={!this.props.resource.loading} + onClose={this.onCloseMapPropertiesModal}> + +
+ + + + +
+
} +
); + } + isValidForm = () => get(this.props.resource, "metadata.name"); +} + +module.exports = SaveModal; diff --git a/web/client/components/dashboard/modals/__tests__/Confirm-test.jsx b/web/client/components/dashboard/modals/__tests__/Confirm-test.jsx new file mode 100644 index 0000000000..742504b20c --- /dev/null +++ b/web/client/components/dashboard/modals/__tests__/Confirm-test.jsx @@ -0,0 +1,34 @@ +var expect = require('expect'); +var React = require('react'); +var ReactDOM = require('react-dom'); +var ConfirmDialog = require('../Confirm'); + +describe("ConfirmDialog component", () => { + beforeEach((done) => { + document.body.innerHTML = '
'; + setTimeout(done); + }); + + afterEach((done) => { + ReactDOM.unmountComponentAtNode(document.getElementById("container")); + document.body.innerHTML = ''; + setTimeout(done); + }); + + it('creates component with defaults', () => { + const cmp = ReactDOM.render(, document.getElementById("container")); + expect(cmp).toExist(); + }); + + it('creates component with content', () => { + const cmp = ReactDOM.render(
some content
, document.getElementById("container")); + expect(cmp).toExist(); + + let background = document.getElementsByClassName("modal").item(0); + let dialog = document.getElementsByClassName("modal-dialog").item(0); + expect(background).toExist(); + expect(dialog).toExist(); + expect(document.querySelectorAll('button').length).toBe(3); // close, confirm, cancel + }); + +}); diff --git a/web/client/components/dashboard/modals/__tests__/Save-test.jsx b/web/client/components/dashboard/modals/__tests__/Save-test.jsx new file mode 100644 index 0000000000..cacc7bacc0 --- /dev/null +++ b/web/client/components/dashboard/modals/__tests__/Save-test.jsx @@ -0,0 +1,80 @@ +/** + * 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. + */ +var React = require('react'); +var ReactDOM = require('react-dom'); +var MetadataModal = require('../Save.jsx'); +var expect = require('expect'); + +describe('This test for dashboard save form', () => { + + + beforeEach((done) => { + document.body.innerHTML = '
'; + setTimeout(done); + }); + + afterEach((done) => { + ReactDOM.unmountComponentAtNode(document.getElementById("container")); + document.body.innerHTML = ''; + setTimeout(done); + }); + + // test DEFAULTS + it('creates the component with defaults, show=false', () => { + const metadataModalItem = ReactDOM.render(, document.getElementById("container")); + expect(metadataModalItem).toExist(); + + const metadataModalItemDom = ReactDOM.findDOMNode(metadataModalItem); + expect(metadataModalItemDom).toNotExist(); + + const getModals = function() { + return document.getElementsByTagName("body")[0].getElementsByClassName('modal-dialog'); + }; + expect(getModals().length).toBe(0); + + }); + + + it('creates the component with a format error', () => { + const metadataModalItem = ReactDOM.render(, document.getElementById("container")); + expect(metadataModalItem).toExist(); + + const getModals = function() { + return document.getElementsByTagName("body")[0].getElementsByClassName('modal-dialog'); + }; + + expect(getModals().length).toBe(1); + + const modalDivList = document.getElementsByClassName("modal-content"); + const closeBtnList = modalDivList.item(0).querySelectorAll('.modal-footer button'); + + expect(closeBtnList.length).toBe(2); + + const errorFORMAT = modalDivList.item(0).querySelector('.errorFORMAT'); + expect(errorFORMAT).toExist(); + }); + + it('creates the component with a size error', () => { + const metadataModalItem = ReactDOM.render(, document.getElementById("container")); + expect(metadataModalItem).toExist(); + + const getModals = function() { + return document.getElementsByTagName("body")[0].getElementsByClassName('modal-dialog'); + }; + + expect(getModals().length).toBe(1); + + const modalDivList = document.getElementsByClassName("modal-content"); + const closeBtnList = modalDivList.item(0).querySelectorAll('.modal-footer button'); + expect(closeBtnList.length).toBe(2); + + const errorSIZE = modalDivList.item(0).querySelector('.errorSIZE'); + expect(errorSIZE).toExist(); + }); + +}); diff --git a/web/client/components/dashboard/modals/enhancers/__tests__/handlePermission-test.jsx b/web/client/components/dashboard/modals/enhancers/__tests__/handlePermission-test.jsx new file mode 100644 index 0000000000..169c45d52e --- /dev/null +++ b/web/client/components/dashboard/modals/enhancers/__tests__/handlePermission-test.jsx @@ -0,0 +1,71 @@ +/* + * Copyright 2018, 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 ReactDOM = require('react-dom'); +const { createSink, setObservableConfig } = require('recompose'); +const rxjsConfig = require('recompose/rxjsObservableConfig').default; +setObservableConfig(rxjsConfig); +const expect = require('expect'); +const { Promise } = require('es6-promise'); + + +const handlePermission = require('../handlePermission'); + +describe('handlePermission enhancer', () => { + beforeEach((done) => { + document.body.innerHTML = '
'; + setTimeout(done); + }); + afterEach((done) => { + ReactDOM.unmountComponentAtNode(document.getElementById("container")); + document.body.innerHTML = ''; + setTimeout(done); + }); + it('handlePermission rendering with defaults', (done) => { + const DUMMY_API = { + getAvailableGroups: () => { + return new Promise(resolve => resolve([])); + } + }; + const Sink = handlePermission(DUMMY_API)(createSink( props => { + if (props.availableGroups) { + done(); + } + })); + ReactDOM.render(, document.getElementById("container")); + }); + it('Test Sink callback', (done) => { + const DUMMY_API = { + getAvailableGroups: () => { + return new Promise(resolve => resolve([])); + }, + getResourcePermissions: () => { + const result = [ + { + "canRead": true, + "canWrite": true, + "user": { + "id": 534, + "name": "testuser" + } + + }]; + + return new Promise(resolve => resolve(result)); + } + }; + + const Sink = handlePermission(DUMMY_API)(createSink(props => { + if (props.rules && props.rules.length > 0) { + expect(props.rules.length).toBe(1); + done(); + } + })); + ReactDOM.render(, document.getElementById("container")); + }); +}); diff --git a/web/client/components/dashboard/modals/enhancers/__tests__/handleResourceData-test.jsx b/web/client/components/dashboard/modals/enhancers/__tests__/handleResourceData-test.jsx new file mode 100644 index 0000000000..d48a15f6cb --- /dev/null +++ b/web/client/components/dashboard/modals/enhancers/__tests__/handleResourceData-test.jsx @@ -0,0 +1,82 @@ +/* + * Copyright 2018, 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 ReactDOM = require('react-dom'); +const {createSink} = require('recompose'); +const expect = require('expect'); +const handleResourceData = require('../handleResourceData'); + +describe('handleResourceData enhancer', () => { + beforeEach((done) => { + document.body.innerHTML = '
'; + setTimeout(done); + }); + afterEach((done) => { + ReactDOM.unmountComponentAtNode(document.getElementById("container")); + document.body.innerHTML = ''; + setTimeout(done); + }); + it('handleResourceData rendering with defaults', (done) => { + const Sink = handleResourceData(createSink( props => { + expect(props.onSave).toExist(); + done(); + })); + ReactDOM.render(, document.getElementById("container")); + }); + it('handleResourceData onUpdate', (done) => { + const Sink = handleResourceData(createSink( props => { + expect(props).toExist(); + expect(props.onUpdate).toExist(); + expect(props.metadata.name).toBe("TEST"); + if (props.resource.metadata.name === "TEST") { + props.onUpdate("metadata.name", "NEW_VALUE"); + } else { + expect(props.resource.metadata.name).toBe("NEW_VALUE"); + done(); + } + + })); + ReactDOM.render(, document.getElementById("container")); + }); + it('handleResourceData onUpdateLinkedResource', (done) => { + const Sink = handleResourceData(createSink(props => { + expect(props).toExist(); + expect(props.onUpdateLinkedResource).toExist(); + expect(props.metadata.name).toBe("TEST"); + if (!props.linkedResources) { + props.onUpdateLinkedResource("thumbnail", "DATA", "CATEGORY", {tail: "TEST"}); + } else { + const thumb = props.linkedResources.thumbnail; + expect(thumb.category).toExist("CATEGORY"); + expect(thumb.data).toExist("DATA"); + expect(thumb.tail).toBe("TEST"); + done(); + } + + })); + ReactDOM.render(, document.getElementById("container")); + }); + it('handleResourceData confirm dialog', (done) => { + const Sink = handleResourceData(createSink(props => { + expect(props).toExist(); + expect(props.onClose).toExist(); + if (props.resource.metadata.name === "TEST") { + props.onUpdate("metadata.name", "TEST"); + props.onClose(); + setTimeout(() => { + const m = document.querySelector('.modal-dialog'); + expect(m).toExist(); + done(); + }, 10); + } + + + })); + ReactDOM.render(, document.getElementById("container")); + }); +}); diff --git a/web/client/components/dashboard/modals/enhancers/handlePermission.jsx b/web/client/components/dashboard/modals/enhancers/handlePermission.jsx new file mode 100644 index 0000000000..737c8277c8 --- /dev/null +++ b/web/client/components/dashboard/modals/enhancers/handlePermission.jsx @@ -0,0 +1,76 @@ +/* + * Copyright 2018, 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 { mapPropsStream, compose, withStateHandlers } = require('recompose'); +const GeoStoreDAO = require('../../../../api/GeoStoreDAO'); + +/** + * retrieves groups for permission handling and returns as props + * @param {Object} API the API to use + */ +const retrieveGroups = (API) => + mapPropsStream(props$ => + props$.combineLatest( + props$ + .take(1) + .switchMap(({ user }) => + Rx.Observable.defer(() => API.getAvailableGroups(user)) + .map(availableGroups => ({ availableGroups })) + .startWith({ loading: true }) + ) + .startWith({}) + .catch( () => Rx.Observable.of({})), + (props, overrides) => ({ + ...props, + ...overrides + }) + ) +); + +/** + * retrieves permission for the resource + * @param {object} API the API to use + */ +const retrievePermission = (API) => + mapPropsStream(props$ => + props$.combineLatest( + props$ + // trigger when resource changes + .distinctUntilKeyChanged('resource') + .pluck('resource') + .filter(resource => resource.id) + .pluck('id') + .distinctUntilChanged() + .switchMap(id => + Rx.Observable.defer(() => API.getResourcePermissions(id)) + .map(rules => ({ rules })) + .startWith({ loading: true }) + ) + .startWith({}) + .catch(() => Rx.Observable.of({})), + (props, overrides) => ({ + ...props, + ...overrides + }) + ) + ); +const manageLocalPermissionChanges = withStateHandlers( + () => ({}), + { + onUpdateRules: () => (rules) => ({ + rules: rules + }) + } +); + +module.exports = ( API = GeoStoreDAO ) => compose( + retrieveGroups(API), + retrievePermission(API), + manageLocalPermissionChanges +); + diff --git a/web/client/components/dashboard/modals/enhancers/handleResourceData.jsx b/web/client/components/dashboard/modals/enhancers/handleResourceData.jsx new file mode 100644 index 0000000000..cf2d9e2950 --- /dev/null +++ b/web/client/components/dashboard/modals/enhancers/handleResourceData.jsx @@ -0,0 +1,81 @@ +/* + * Copyright 2018, 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 { compose, withStateHandlers, withState, branch, withHandlers, renderComponent} = require('recompose'); +const {set} = require('../../../../utils/ImmutableUtils'); +const Message = require('../../../I18N/Message'); +const ConfirmDialog = require('../Confirm'); + +/** + * Enhancer to manage resource data for a Save dialog. + * Stores the original data to handle changes. + */ +module.exports = compose( + withStateHandlers( + ({resource = {}}) => ({ + originalData: resource, + metadata: { + name: resource.name, + description: resource.description + }, + resource: { + id: resource.id, + attributes: resource.attributes, + metadata: { + name: resource.name, + description: resource.description + } + } + + }), + { + onUpdate: ({resource}) => (key, value) => ({ + hasChanges: true, + resource: set(key, value, resource) + }), + onUpdateLinkedResource: ({ linkedResources = {} }) => (key, data, category, options = {}) => ({ + linkedResources: set(key, { + category, + data, + ...options + }, linkedResources) + }) + } + ), + withState('confirmClose', 'onCloseConfirm', false), + branch( + ({ confirmClose }) => confirmClose, + renderComponent(({ onCloseConfirm, onClose }) => + (} + cancelText={} + onConfirm={() => onClose()} + onClose={() => onCloseConfirm(false)} + body={
} + >
)) + ), + withHandlers({ + onClose: ({ + hasChanges, + onClose = () => {}, + onCloseConfirm = () => {}} + ) => () => + hasChanges + ? onCloseConfirm(true) + : onClose() + }), + withHandlers({ + onSave: ({onSave = () => {}, category = "DASHBOARD", data, linkedResources}) => resource => onSave({ + category, + linkedResources, + data, + ...resource + }) + }) +); diff --git a/web/client/components/dashboard/modals/enhancers/ruleEditor.js b/web/client/components/dashboard/modals/enhancers/ruleEditor.js new file mode 100644 index 0000000000..d363b4d72e --- /dev/null +++ b/web/client/components/dashboard/modals/enhancers/ruleEditor.js @@ -0,0 +1,15 @@ +/* + * Copyright 2018, 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 {compose, withState} = require('recompose'); + + +module.exports = compose( + withState('newGroup', 'onNewGroupChoose'), + withState('newPermission', 'onNewPermissionChoose') +); diff --git a/web/client/components/dashboard/modals/fragments/DetailsRow.jsx b/web/client/components/dashboard/modals/fragments/DetailsRow.jsx new file mode 100644 index 0000000000..bdf8bff354 --- /dev/null +++ b/web/client/components/dashboard/modals/fragments/DetailsRow.jsx @@ -0,0 +1,87 @@ +const React = require('react'); +const {Row, Col} = require('react-bootstrap'); +const Spinner = require('react-spinkit'); +const { isNil } = require('lodash'); +const Toolbar = require('../../misc/toolbar/Toolbar'); + +const Message = require('../../I18N/Message'); +const { NO_DETAILS_AVAILABLE } = require('../../../actions/maps'); + + +module.exports = ({ + saving, + hideGroupProperties, + editDetailsDisabled, + detailsText, + detailsBackup, + onToggleGroupProperties = () => { }, + onUndoDetails = () => { }, + onToggleDetailsSheet = () => { }, + onUpdateDetails = () => { } +}) => { + return ( +
+
+ + +
+ {detailsText === "" ? : } +
+ + +
+
+ {saving ? : null} + {isNil(detailsText) ? : { onToggleGroupProperties(); }, + disabled: saving, + tooltipId: !hideGroupProperties ? "map.details.showPreview" : "map.details.hidePreview" + }, { + glyph: 'undo', + tooltipId: "map.details.undo", + visible: !!detailsBackup, + onClick: () => { onUndoDetails(detailsBackup); }, + disabled: saving + }, { + glyph: 'pencil-add', + tooltipId: "map.details.add", + visible: !detailsText, + onClick: () => { + onToggleDetailsSheet(false); + }, + disabled: saving + }, { + glyph: 'pencil', + tooltipId: "map.details.edit", + visible: !!detailsText && !editDetailsDisabled, + onClick: () => { + onToggleDetailsSheet(false); + if (detailsText) { + onUpdateDetails(detailsText, true); + } + }, + disabled: saving + }, { + glyph: 'trash', + tooltipId: "map.details.delete", + visible: !!detailsText, + onClick: () => { this.props.detailsSheetActions.onDeleteDetails(); }, + disabled: saving + }]} />} +
+
+ +
+
+ {detailsText &&
+ {detailsText !== NO_DETAILS_AVAILABLE ?
+ :
} +
} +
+ ); +}; diff --git a/web/client/components/dashboard/modals/fragments/ErrorBox.jsx b/web/client/components/dashboard/modals/fragments/ErrorBox.jsx new file mode 100644 index 0000000000..495285e72b --- /dev/null +++ b/web/client/components/dashboard/modals/fragments/ErrorBox.jsx @@ -0,0 +1,33 @@ +/* + * Copyright 2018, 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 DEFAULT_MESSAGES = { "FORMAT": "map.errorFormat", "SIZE": "map.errorSize", 409: "dashboard.errors.resourceAlreadyExists"}; + +const Message = require('../../../I18N/Message'); +const { Row } = require('react-bootstrap'); +const errorString = err => typeof err === 'string' ? err : err.statusText; +const errorCode = err => typeof err === 'string' ? err : err.status; +const errorData = err => typeof err === 'string' ? undefined : err; +const errorMessage = error => { + const code = errorCode(error); + return ; +}; + +module.exports = ({ errors = []}) => { + return ( + {errors.length > 0 ? +
+ {errors.map(error => + (
+ {errorMessage(error)} +
))} +
+ : null} +
); +}; diff --git a/web/client/components/dashboard/modals/fragments/MainForm.jsx b/web/client/components/dashboard/modals/fragments/MainForm.jsx new file mode 100644 index 0000000000..058410026a --- /dev/null +++ b/web/client/components/dashboard/modals/fragments/MainForm.jsx @@ -0,0 +1,54 @@ +/* + * Copyright 2018, 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 Message = require('../../../I18N/Message'); +const {Row, Col} = require('react-bootstrap'); +const Metadata = require('../../forms/Metadata'); +const Thumbnail = require('../../forms/Thumbnail'); + +module.exports = class MainForm extends React.Component { + render() { + const { + resource, + linkedResources={}, + onError = () => { }, + onUpdate = () => { }, + onUpdateLinkedResource = () => { } + } = this.props; + return ( + + onUpdateLinkedResource("thumbnail", "NODATA", "THUMBNAIL", { + tail: '/raw?decode=datauri' + })} + onUpdate={(data) => onUpdateLinkedResource("thumbnail", data, "THUMBNAIL", { + tail: '/raw?decode=datauri' + })} /> + + + } + descriptionFieldText={} + namePlaceholderText={"dashboard.saveDialog.namePlaceholder"} + descriptionPlaceholderText={"dashboard.saveDialog.descriptionPlaceholder"} + /> + + ); + } +}; + + diff --git a/web/client/components/dashboard/modals/fragments/PermissionEditor.jsx b/web/client/components/dashboard/modals/fragments/PermissionEditor.jsx new file mode 100644 index 0000000000..380d05105e --- /dev/null +++ b/web/client/components/dashboard/modals/fragments/PermissionEditor.jsx @@ -0,0 +1,238 @@ +/** +* 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 PropTypes = require('prop-types'); +const assign = require('object-assign'); +const _ = require('lodash'); +const Select = require('react-select'); +const Spinner = require('react-spinkit'); +const { Table, Button, Glyphicon } = require('react-bootstrap'); +const Message = require('../../../I18N/Message'); +const LocaleUtils = require('../../../../utils/LocaleUtils'); + +require('react-select/dist/react-select.css'); + +class PermissionEditor extends React.Component { + static propTypes = { + // props + id: PropTypes.string, + user: PropTypes.object, + loading: PropTypes.bool, + onUpdateRules: PropTypes.func, + buttonSize: PropTypes.string, + disabled: PropTypes.bool, + style: PropTypes.object, + fluid: PropTypes.bool, + // CALLBACKS + onErrorCurrentMap: PropTypes.func, + onUpdateCurrentMap: PropTypes.func, + onNewGroupChoose: PropTypes.func, + onNewPermissionChoose: PropTypes.func, + availablePermissions: PropTypes.arrayOf(PropTypes.string), + availableGroups: PropTypes.arrayOf(PropTypes.object), + updatePermissions: PropTypes.func, + rules: PropTypes.arrayOf(PropTypes.object), + newGroup: PropTypes.object, + newPermission: PropTypes.string + }; + + static contextTypes = { + messages: PropTypes.object + }; + + static defaultProps = { + disabled: false, + id: "PermissionEditor", + onUpdateRules: () => { }, + onNewGroupChoose: () => { }, + onNewPermissionChoose: () => { }, + user: { + name: "Guest" + }, + style: {}, + buttonSize: "small", + // CALLBACKS + onErrorCurrentMap: () => { }, + onUpdateCurrentMap: () => { }, + availablePermissions: ["canRead", "canWrite"], + availableGroups: [], + updatePermissions: () => { }, + rules: [] + }; + + onGroupChange = (selected) => { + // TODO: use _.find(this.props.availableGroups,['id', _.toInteger(id)]) when lodash will be updated to version 4 + this.props.onNewGroupChoose(_.find(this.props.availableGroups, (o) => o.id === selected.value)); + }; + + onAddPermission = () => { + // Check if the new permission will edit ad existing one + if (this.isPermissionPresent(this.props.newGroup.groupName)) { + this.props.onUpdateRules(this.props.rules.map( + (rule) => { + if (rule.group && rule.group.groupName === this.props.newGroup.groupName) { + if (this.props.newPermission === "canWrite") { + return assign({}, rule, { canRead: true, canWrite: true }); + } + return assign({}, rule, { canRead: true, canWrite: false }); + } + return rule; + }, this + ).filter(rule => rule.canRead || rule.canWrite)); + + } else { + this.props.onUpdateRules(this.props.rules.concat([{ + canRead: true, + canWrite: this.props.newPermission === "canWrite", + group: this.props.newGroup + }])); + } + }; + + onChangePermission = (groupName, input) => { + this.props.onUpdateRules(this.props.rules + // remove items when delete + .filter(rule => !(input === 'delete') || !(rule.group) || !(rule.group.groupName === groupName)) + // change rules + .map( + (rule) => { + if (rule.group && rule.group.groupName === groupName) { + if (input === "canWrite") { + return assign({}, rule, { canRead: true, canWrite: true }); + } else if (input === "canRead") { + return assign({}, rule, { canRead: true, canWrite: false }); + } + return assign({}, rule, { canRead: false, canWrite: false }); + } + return rule; + } + ).filter(rule => rule.canRead || rule.canWrite)); + }; + + getSelectableGroups = () => { + return this.props.availableGroups && this.props.availableGroups.filter((group) => { + return !this.isPermissionPresent(group.groupName); + }).map((group) => ({ label: group.groupName, value: group.id })); + }; + + getPermissionLabel = (perm) => { + switch (perm) { + case "canRead": + return LocaleUtils.getMessageById(this.context.messages, "map.permissions.canView"); + case "canWrite": + return LocaleUtils.getMessageById(this.context.messages, "map.permissions.canWrite"); + default: + return perm; + } + }; + + getAvailablePermissions = () => { + return this.props.availablePermissions.map((perm) => ({ value: perm, label: this.getPermissionLabel(perm) })); + }; + + renderPermissionRows = () => { + if (this.props.rules.length === 0) { + return ; + } + return this.props.rules + .filter(rule => rule.group) + .map(({canWrite, group}, index) => { + return ( + + {group.groupName} + + + + +