diff --git a/package.json b/package.json index 604540c51b..113a881300 100644 --- a/package.json +++ b/package.json @@ -84,6 +84,7 @@ "ag-grid": "3.3.3", "ag-grid-react": "3.3.1", "axios": "0.11.1", + "b64-to-blob": "1.2.19", "babel-polyfill": "6.8.0", "babel-standalone": "6.7.7", "bootstrap": "3.3.5", @@ -174,7 +175,7 @@ "reselect": "2.5.1", "rxjs": "5.1.1", "screenfull": "3.1.0", - "shpjs": "3.3.2", + "shpjs": "3.4.2", "turf-bbox": "3.0.10", "turf-buffer": "3.0.10", "turf-intersect": "3.0.10", diff --git a/web/client/components/TOC/Toolbar.jsx b/web/client/components/TOC/Toolbar.jsx index 66f2c8e5cb..890456d4dc 100644 --- a/web/client/components/TOC/Toolbar.jsx +++ b/web/client/components/TOC/Toolbar.jsx @@ -20,6 +20,7 @@ class Toolbar extends React.Component { static propTypes = { groups: PropTypes.array, selectedLayers: PropTypes.array, + generalInfoFormat: PropTypes.string, selectedGroups: PropTypes.array, onToolsActions: PropTypes.object, text: PropTypes.object, @@ -128,6 +129,7 @@ class Toolbar extends React.Component { {...this.props.options.settingsOptions} settings={this.props.settings} element={this.props.selectedLayers[0]} + generalInfoFormat={this.props.generalInfoFormat} retrieveLayerData={this.props.onToolsActions.onRetrieveLayerData} updateSettings={this.props.onToolsActions.onUpdateSettings} hideSettings={this.props.onToolsActions.onHideSettings} diff --git a/web/client/components/TOC/fragments/SettingsModal.jsx b/web/client/components/TOC/fragments/SettingsModal.jsx index 5746c1ede8..547197b2b0 100644 --- a/web/client/components/TOC/fragments/SettingsModal.jsx +++ b/web/client/components/TOC/fragments/SettingsModal.jsx @@ -35,6 +35,7 @@ class SettingsModal extends React.Component { updateNode: PropTypes.func, removeNode: PropTypes.func, retrieveLayerData: PropTypes.func, + generalInfoFormat: PropTypes.string, titleText: PropTypes.oneOfType([PropTypes.string, PropTypes.element]), opacityText: PropTypes.oneOfType([PropTypes.string, PropTypes.element]), elevationText: PropTypes.oneOfType([PropTypes.string, PropTypes.element]), @@ -176,6 +177,7 @@ class SettingsModal extends React.Component { return (} element={this.props.element} + generalInfoFormat={this.props.generalInfoFormat} onInfoFormatChange={(key, value) => this.updateParams({[key]: value}, this.props.realtimeUpdate)} />); } } diff --git a/web/client/components/TOC/fragments/settings/FeatureInfoFormat.jsx b/web/client/components/TOC/fragments/settings/FeatureInfoFormat.jsx index f44e29e9a7..6e7b19dd08 100644 --- a/web/client/components/TOC/fragments/settings/FeatureInfoFormat.jsx +++ b/web/client/components/TOC/fragments/settings/FeatureInfoFormat.jsx @@ -1,4 +1,3 @@ -const PropTypes = require('prop-types'); /** * Copyright 2017, GeoSolutions Sas. * All rights reserved. @@ -8,26 +7,45 @@ const PropTypes = require('prop-types'); */ const React = require('react'); +const PropTypes = require('prop-types'); const {DropdownList} = require('react-widgets'); const MapInfoUtils = require('../../../../utils/MapInfoUtils'); + +/** + * FeatureInfoFormat shows the infoformat selected for that layer or the default one taken + * from the general settings. + * @class + * @memberof components.toc + * @prop {object} [element] the layer options + * @prop {object} [label] the label shown for the combobox + * @prop {object} [defaultInfoFormat] the object used to show options labels + * @prop {string} [generalInfoFormat] the infoFormat value set in the general settings + * @prop {function} [onInfoFormatChange] it updates the infoFormat and/or viewer for the given layer + */ module.exports = class extends React.Component { static propTypes = { element: PropTypes.object, label: PropTypes.object, defaultInfoFormat: PropTypes.object, + generalInfoFormat: PropTypes.string, onInfoFormatChange: PropTypes.func }; static defaultProps = { defaultInfoFormat: MapInfoUtils.getAvailableInfoFormat(), + generalInfoFormat: "text/plain", onInfoFormatChange: () => {} }; - render() { - const data = Object.keys(this.props.defaultInfoFormat).map((infoFormat) => { + getInfoFormat = (infoFormats) => { + return Object.keys(infoFormats).map((infoFormat) => { return infoFormat; }); + } + render() { + // the selected value if missing on that layer should be set to the general info format value and not the first one. + const data = this.getInfoFormat(this.props.defaultInfoFormat); const checkDisabled = !!(this.props.element.featureInfo && this.props.element.featureInfo.viewer); return (
@@ -42,7 +60,7 @@ module.exports = class extends React.Component { ( { diff --git a/web/client/components/TOC/fragments/settings/__tests__/FeatureInfoFormat-test.jsx b/web/client/components/TOC/fragments/settings/__tests__/FeatureInfoFormat-test.jsx index 701f316447..11f7fbf390 100644 --- a/web/client/components/TOC/fragments/settings/__tests__/FeatureInfoFormat-test.jsx +++ b/web/client/components/TOC/fragments/settings/__tests__/FeatureInfoFormat-test.jsx @@ -73,4 +73,22 @@ describe('test Layer Properties FeatureInfoFormat module component', () => { li[0].click(); expect(spy.calls.length).toBe(1); }); + it('tests FeatureInfoFormat component for wms using generalInfoFormat', () => { + const l = { + name: 'layer00', + title: 'Layer', + visibility: true, + storeIndex: 9, + type: 'wms', + url: 'fakeurl' + }; + const generalInfoFormat = "text/html"; + const label = "label"; + const comp = ReactDOM.render( {}}/>, document.getElementById("container")); + expect(comp).toExist(); + const div = ReactTestUtils.scryRenderedDOMComponentsWithTag( comp, "div" ); + expect(div[2]).toExist(); + expect(div[2].textContent).toBe("HTML"); + + }); }); diff --git a/web/client/components/mapcontrols/annotations/Annotations.jsx b/web/client/components/mapcontrols/annotations/Annotations.jsx index 03a9b9e5ee..1595efdb62 100644 --- a/web/client/components/mapcontrols/annotations/Annotations.jsx +++ b/web/client/components/mapcontrols/annotations/Annotations.jsx @@ -52,6 +52,8 @@ const defaultConfig = require('./AnnotationsConfig'); * @prop {function} onCleanHighlight triggered when the mouse is out of any annotation card * @prop {function} onDetail triggered when the user clicks on an annotation card * @prop {function} onFilter triggered when the user enters some text in the filtering widget + * @prop {function} classNameSelector optional selector to assign custom a CSS class to annotations, based on + * the annotation's attributes. */ class Annotations extends React.Component { static propTypes = { @@ -72,7 +74,8 @@ class Annotations extends React.Component { current: PropTypes.string, config: PropTypes.object, filter: PropTypes.string, - onFilter: PropTypes.func + onFilter: PropTypes.func, + classNameSelector: PropTypes.func }; static contextTypes = { @@ -81,7 +84,8 @@ class Annotations extends React.Component { static defaultProps = { mode: 'list', - config: defaultConfig + config: defaultConfig, + classNameSelector: () => '' }; getConfig = () => { @@ -112,7 +116,7 @@ class Annotations extends React.Component { }; renderCard = (annotation) => { - return (
this.props.onHighlight(annotation.properties.id)} onMouseOut={this.props.onCleanHighlight} onClick={() => this.props.onDetail(annotation.properties.id)}> + return (
this.props.onHighlight(annotation.properties.id)} onMouseOut={this.props.onCleanHighlight} onClick={() => this.props.onDetail(annotation.properties.id)}> {this.renderThumbnail(annotation.style)} {this.getConfig().fields.map(f => this.renderField(f, annotation))}
); diff --git a/web/client/components/mapcontrols/annotations/__tests__/Annotations-test.jsx b/web/client/components/mapcontrols/annotations/__tests__/Annotations-test.jsx index d295614bad..dc894e5885 100644 --- a/web/client/components/mapcontrols/annotations/__tests__/Annotations-test.jsx +++ b/web/client/components/mapcontrols/annotations/__tests__/Annotations-test.jsx @@ -186,4 +186,44 @@ describe("test the Annotations Panel", () => { expect(TestUtils.scryRenderedDOMComponentsWithClass(annotations, "mapstore-annotations-panel-card").length).toBe(0); expect(TestUtils.scryRenderedDOMComponentsWithClass(annotations, "myeditor").length).toBe(1); }); + + it('test rendering custom class', () => { + const annotationsList = [{ + properties: { + title: 'a', + description: 'b' + }, + style: { + iconShape: 'square', + iconColor: 'blue' + } + }, { + properties: { + external: true, + title: 'c', + description: 'd' + }, + style: { + iconShape: 'square', + iconColor: 'blue' + } + }]; + + const classNameSelector = (annotation) => { + if (annotation && annotation.properties && annotation.properties.external) { + return 'external'; + } + return ''; + }; + + const annotations = ReactDOM.render(, document.getElementById("container")); + + expect(annotations).toExist(); + + const cards = TestUtils.scryRenderedDOMComponentsWithClass(annotations, "mapstore-annotations-panel-card"); + expect(cards.length).toBe(2); + + const cardsExternal = TestUtils.scryRenderedDOMComponentsWithClass(annotations, "mapstore-annotations-panel-card external"); + expect(cardsExternal.length).toBe(1); + }); }); diff --git a/web/client/components/shapefile/SelectShape.jsx b/web/client/components/shapefile/SelectShape.jsx index 635a244cac..5939003ea2 100644 --- a/web/client/components/shapefile/SelectShape.jsx +++ b/web/client/components/shapefile/SelectShape.jsx @@ -13,6 +13,10 @@ const Spinner = require('react-spinkit'); const LocaleUtils = require('../../utils/LocaleUtils'); +const JSZip = require('jszip'); +const FileUtils = require('../../utils/FileUtils'); +const {Promise} = require('es6-promise'); + class SelectShape extends React.Component { static propTypes = { text: PropTypes.oneOfType([PropTypes.string, PropTypes.element]), @@ -43,17 +47,32 @@ class SelectShape extends React.Component { ); } + tryUnzip = (file) => { + return FileUtils.readZip(file).then((buffer) => { + return new JSZip(buffer); + }); + }; + + isZip = (file) => { + return new Promise((resolve, reject) => { + if (file.type === 'application/zip' || file.type === 'application/x-zip-compressed') { + resolve(); + } else { + this.tryUnzip(file).then(resolve).catch(reject); + } + }); + }; + checkfile = (files) => { - const allZip = files.filter((file) => { return file.type !== 'application/zip' && file.type !== 'application/x-zip-compressed'; }).length === 0; - if (allZip) { + Promise.all(files.map(file => this.isZip(file))).then(() => { if (this.props.error) { this.props.onShapeError(null); } this.props.onShapeChoosen(files); - } else { + }).catch(() => { const error = LocaleUtils.getMessageById(this.context.messages, this.props.errorMessage); this.props.onShapeError(error); - } + }); }; } diff --git a/web/client/components/shapefile/__tests__/SelectShape-test.jsx b/web/client/components/shapefile/__tests__/SelectShape-test.jsx index f3ed01520a..3b68fa88a5 100644 --- a/web/client/components/shapefile/__tests__/SelectShape-test.jsx +++ b/web/client/components/shapefile/__tests__/SelectShape-test.jsx @@ -2,6 +2,7 @@ const expect = require('expect'); const React = require('react'); const ReactDOM = require('react-dom'); const SelectShape = require('../SelectShape'); +const b64toBlob = require('b64-to-blob'); const TestUtils = require('react-dom/test-utils'); @@ -43,10 +44,9 @@ describe("Test the select shapefile component", () => { expect(dom.innerHTML.indexOf('TEST') !== -1).toBe(true); }); - it('upload file', () => { - let dropped = false; + it('upload file', (done) => { const handler = () => { - dropped = true; + done(); }; const cmp = ReactDOM.render(, document.getElementById("container")); expect(cmp).toExist(); @@ -59,13 +59,24 @@ describe("Test the select shapefile component", () => { type: 'application/zip' }]; TestUtils.Simulate.drop(content, { dataTransfer: { files } }); - expect(dropped).toBe(true); }); - it('upload wrong file', () => { - let error = false; + it('upload wrong mime, right file', (done) => { const handler = () => { - error = true; + done(); + }; + const cmp = ReactDOM.render(, document.getElementById("container")); + expect(cmp).toExist(); + const dom = ReactDOM.findDOMNode(cmp); + expect(dom.getElementsByTagName('input').length).toBe(1); + const content = TestUtils.findRenderedDOMComponentWithClass(cmp, 'dropzone-content'); + const files = [b64toBlob('UEsDBAoAAAAAACGPaktDvrfoAQAAAAEAAAAKAAAAc2FtcGxlLnR4dGFQSwECPwAKAAAAAAAhj2pLQ7636AEAAAABAAAACgAkAAAAAAAAACAAAAAAAAAAc2FtcGxlLnR4dAoAIAAAAAAAAQAYAGILh+1EWtMBy3f86URa0wHLd/zpRFrTAVBLBQYAAAAAAQABAFwAAAApAAAAAAA=', 'application/pdf')]; + TestUtils.Simulate.drop(content, { dataTransfer: { files } }); + }); + + it('upload wrong file', (done) => { + const handler = () => { + done(); }; const cmp = ReactDOM.render(, document.getElementById("container")); expect(cmp).toExist(); @@ -78,6 +89,5 @@ describe("Test the select shapefile component", () => { type: 'application/pdf' }]; TestUtils.Simulate.drop(content, { dataTransfer: { files } }); - expect(error).toBe(true); }); }); diff --git a/web/client/plugins/TOC.jsx b/web/client/plugins/TOC.jsx index 741cf72202..d7d9ed9696 100644 --- a/web/client/plugins/TOC.jsx +++ b/web/client/plugins/TOC.jsx @@ -12,13 +12,15 @@ const {createSelector} = require('reselect'); const {Glyphicon} = require('react-bootstrap'); const {changeLayerProperties, changeGroupProperties, toggleNode, contextNode, - sortNode, showSettings, hideSettings, updateSettings, updateNode, removeNode, browseData, selectNode, filterLayers, refreshLayerVersion} = require('../actions/layers'); + sortNode, showSettings, hideSettings, updateSettings, updateNode, removeNode, + browseData, selectNode, filterLayers, refreshLayerVersion} = require('../actions/layers'); const {getLayerCapabilities} = require('../actions/layerCapabilities'); const {zoomToExtent} = require('../actions/map'); const {groupsSelector, layersSelector, selectedNodesSelector, layerFilterSelector, layerSettingSelector} = require('../selectors/layers'); const {mapSelector, mapNameSelector} = require('../selectors/map'); const {currentLocaleSelector} = require("../selectors/locale"); const {widgetBuilderAvailable} = require('../selectors/controls'); +const {generalInfoFormatSelector} = require("../selectors/mapInfo"); const LayersUtils = require('../utils/LayersUtils'); const mapUtils = require('../utils/MapUtils'); @@ -71,8 +73,9 @@ const tocSelector = createSelector( layersSelector, mapNameSelector, activeSelector, - widgetBuilderAvailable - ], (enabled, groups, settings, map, currentLocale, selectedNodes, filterText, layers, mapName, catalogActive, activateWidgetTool) => ({ + widgetBuilderAvailable, + generalInfoFormatSelector + ], (enabled, groups, settings, map, currentLocale, selectedNodes, filterText, layers, mapName, catalogActive, activateWidgetTool, generalInfoFormat) => ({ enabled, groups, settings, @@ -84,6 +87,7 @@ const tocSelector = createSelector( currentLocale, selectedNodes, filterText, + generalInfoFormat, selectedLayers: layers.filter((l) => head(selectedNodes.filter(s => s === l.id))), noFilterResults: layers.filter((l) => filterLayersByTitle(l, filterText, currentLocale)).length === 0, selectedGroups: selectedNodes.map(n => LayersUtils.getNode(groups, n)).filter(n => n && n.nodes), @@ -163,6 +167,7 @@ class LayerTree extends React.Component { currentLocale: PropTypes.string, onFilter: PropTypes.func, filterText: PropTypes.string, + generalInfoFormat: PropTypes.string, selectedLayers: PropTypes.array, selectedGroups: PropTypes.array, mapName: PropTypes.string, @@ -295,6 +300,7 @@ class LayerTree extends React.Component { groups={this.props.groups} selectedLayers={this.props.selectedLayers} selectedGroups={this.props.selectedGroups} + generalInfoFormat={this.props.generalInfoFormat} settings={this.props.settings} activateTool={{ activateToolsContainer: this.props.activateToolsContainer, diff --git a/web/client/selectors/__tests__/mapinfo-test.js b/web/client/selectors/__tests__/mapinfo-test.js new file mode 100644 index 0000000000..fa67ace5a3 --- /dev/null +++ b/web/client/selectors/__tests__/mapinfo-test.js @@ -0,0 +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 expect = require('expect'); +const { + generalInfoFormatSelector +} = require('../mapinfo'); + +describe('Test mapinfo', () => { + it('test generalInfoFormatSelector default value', () => { + const mapinfo = generalInfoFormatSelector({}); + expect(mapinfo).toBe("text/plain"); + }); + it('test generalInfoFormatSelector infoFormat: undefined', () => { + const mapinfo = generalInfoFormatSelector({mapInfo: {infoFormat: undefined}}); + expect(mapinfo).toBe("text/plain"); + }); + it('test generalInfoFormatSelector ', () => { + const mapinfo = generalInfoFormatSelector({mapInfo: {infoFormat: "text/html"}}); + + expect(mapinfo).toExist(); + expect(mapinfo).toBe("text/html"); + }); +}); diff --git a/web/client/selectors/mapinfo.js b/web/client/selectors/mapinfo.js new file mode 100644 index 0000000000..3a1916a421 --- /dev/null +++ b/web/client/selectors/mapinfo.js @@ -0,0 +1,27 @@ +/* +* 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 mapinfo state + * @name mapinfo + * @memberof selectors + * @static + */ + +/** + * selects generalInfoFormat from state + * @memberof selectors.mapinfo + * @param {object} state the state + * @return {string} the maptype in the state + */ +const generalInfoFormatSelector = (state) => get(state, "mapInfo.infoFormat", "text/plain"); + +module.exports = { + generalInfoFormatSelector +}; diff --git a/web/client/utils/FileUtils.js b/web/client/utils/FileUtils.js index 8489b7ee01..b6a6a60601 100644 --- a/web/client/utils/FileUtils.js +++ b/web/client/utils/FileUtils.js @@ -9,6 +9,7 @@ const FileSaver = require('file-saver'); const toBlob = require('canvas-to-blob'); const shp = require('shpjs'); +const {Promise} = require('es6-promise'); const FileUtils = { download: function(blob, name, mimetype) { diff --git a/web/client/utils/MapInfoUtils.js b/web/client/utils/MapInfoUtils.js index df671d4c56..bd37b7e4b9 100644 --- a/web/client/utils/MapInfoUtils.js +++ b/web/client/utils/MapInfoUtils.js @@ -31,6 +31,12 @@ const MapInfoUtils = { return prev; }, {}); }, + /** + * @return the label of an inputformat given the value + */ + getLabelFromValue(value) { + return MapInfoUtils.getAvailableInfoFormatLabels().filter(info => INFO_FORMATS[info] === value)[0] || "TEXT"; + }, /** * @return like getAvailableInfoFormat but return an array filled with the keys */ diff --git a/web/client/utils/__tests__/MapInfoUtils-test.js b/web/client/utils/__tests__/MapInfoUtils-test.js index a154590a00..d2483a0432 100644 --- a/web/client/utils/__tests__/MapInfoUtils-test.js +++ b/web/client/utils/__tests__/MapInfoUtils-test.js @@ -16,7 +16,8 @@ var { buildIdentifyRequest, getValidator, getViewer, - setViewer + setViewer, + getLabelFromValue } = require('../MapInfoUtils'); const CoordinatesUtils = require('../CoordinatesUtils'); @@ -329,4 +330,16 @@ describe('MapInfoUtils', () => { let validResponses = validator.getValidResponses(response); expect(validResponses.length).toBe(1); }); + it('get the label given the text/plain value', () => { + let label = getLabelFromValue("text/plain"); + expect(label).toBe("TEXT"); + }); + it('get the label given the text/html value', () => { + let label = getLabelFromValue("text/html"); + expect(label).toBe("HTML"); + }); + it('get the default label given the wrong value', () => { + let label = getLabelFromValue("text_or_something_else"); + expect(label).toBe("TEXT"); + }); });