diff --git a/web/client/components/data/featuregrid/FeatureGrid.jsx b/web/client/components/data/featuregrid/FeatureGrid.jsx index a2c683d707..b1ac0f51ea 100644 --- a/web/client/components/data/featuregrid/FeatureGrid.jsx +++ b/web/client/components/data/featuregrid/FeatureGrid.jsx @@ -31,6 +31,7 @@ require("./featuregrid.css"); */ class FeatureGrid extends React.PureComponent { static propTypes = { + autocompleteEnabled: PropTypes.bool, gridOpts: PropTypes.object, changes: PropTypes.object, selectBy: PropTypes.object, @@ -50,6 +51,7 @@ class FeatureGrid extends React.PureComponent { isProperty: PropTypes.func }; static defaultProps = { + autocompleteEnabled: false, gridComponent: AdaptiveGrid, changes: {}, gridEvents: {}, diff --git a/web/client/components/data/featuregrid/editors/AttributeEditor.jsx b/web/client/components/data/featuregrid/editors/AttributeEditor.jsx index f522dce505..633bd98bb6 100644 --- a/web/client/components/data/featuregrid/editors/AttributeEditor.jsx +++ b/web/client/components/data/featuregrid/editors/AttributeEditor.jsx @@ -18,7 +18,7 @@ class AttributeEditor extends editors.SimpleTextEditor { // Otherwise this will trigger before other events out of the editors // and so the tempChanges seems to be not present. if (this.props.onTemporaryChanges) { - setTimeout( () => this.props.onTemporaryChanges(false), 300); + setTimeout( () => this.props.onTemporaryChanges(false), 500); } } } diff --git a/web/client/components/data/featuregrid/editors/AutocompleteEditor.jsx b/web/client/components/data/featuregrid/editors/AutocompleteEditor.jsx new file mode 100644 index 0000000000..ad5308d792 --- /dev/null +++ b/web/client/components/data/featuregrid/editors/AutocompleteEditor.jsx @@ -0,0 +1,49 @@ +/* + * 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 AttributeEditor = require('./AttributeEditor'); +const {AutocompleteCombobox} = require('../../../misc/AutocompleteCombobox'); +const {createPagedUniqueAutompleteStream} = require('../../../../observables/autocomplete'); + +class AutocompleteEditor extends AttributeEditor { + static propTypes = { + column: PropTypes.object, + dataType: PropTypes.string, + inputProps: PropTypes.object, + isValid: PropTypes.func, + onBlur: PropTypes.func, + autocompleteStreamFactory: PropTypes.func, + url: PropTypes.string, + typeName: PropTypes.string, + value: PropTypes.string + }; + static defaultProps = { + isValid: () => true, + dataType: "string" + }; + constructor(props) { + super(props); + this.validate = (value) => { + try { + return this.props.isValid(value[this.props.column && this.props.column.key]); + } catch (e) { + return false; + } + }; + this.getValue = () => { + const updated = super.getValue(); + return updated; + }; + } + render() { + return ; + } +} + +module.exports = AutocompleteEditor; diff --git a/web/client/components/data/featuregrid/editors/__tests__/AutocompleteEditor-test.jsx b/web/client/components/data/featuregrid/editors/__tests__/AutocompleteEditor-test.jsx new file mode 100644 index 0000000000..6e152e332d --- /dev/null +++ b/web/client/components/data/featuregrid/editors/__tests__/AutocompleteEditor-test.jsx @@ -0,0 +1,39 @@ +const React = require('react'); +const ReactDOM = require('react-dom'); +const AutocompleteEditor = require('../AutocompleteEditor'); +const {createPagedUniqueAutompleteStream} = require('../../../../../observables/autocomplete'); + +var expect = require('expect'); + +let testColumn = { + key: 'columnKey' +}; +const value = "1.1"; +describe('FeatureGrid AutocompleteEditor component', () => { + beforeEach((done) => { + document.body.innerHTML = '
'; + setTimeout(done); + }); + + afterEach((done) => { + ReactDOM.unmountComponentAtNode(document.getElementById("container")); + document.body.innerHTML = ''; + setTimeout(done); + }); + it('AutocompleteEditor Editor no stream provided', () => { + const cmp = ReactDOM.render(, document.getElementById("container")); + expect(cmp.getValue().columnKey).toBe(value); + expect(cmp.validate(value)).toBe(true); + }); + it('AutocompleteEditor Editor no stream provided', () => { + const cmp = ReactDOM.render(, document.getElementById("container")); + expect(cmp).toExist(); + }); +}); diff --git a/web/client/components/data/featuregrid/editors/index.jsx b/web/client/components/data/featuregrid/editors/index.jsx index 9d51c80fd8..cc4256535f 100644 --- a/web/client/components/data/featuregrid/editors/index.jsx +++ b/web/client/components/data/featuregrid/editors/index.jsx @@ -1,10 +1,14 @@ const React = require('react'); const Editor = require('./AttributeEditor'); const NumberEditor = require('./NumberEditor'); +const AutocompleteEditor = require('./AutocompleteEditor'); const types = { "defaultEditor": (props) => , "int": (props) => , - "number": (props) => + "number": (props) => , + "string": (props) => props.autocompleteEnabled ? + : + }; module.exports = (type, props) => types[type] ? types[type](props) : types.defaultEditor(props); diff --git a/web/client/components/data/featuregrid/enhancers/editor.js b/web/client/components/data/featuregrid/enhancers/editor.js index c0895fcb74..14423dd1f0 100644 --- a/web/client/components/data/featuregrid/enhancers/editor.js +++ b/web/client/components/data/featuregrid/enhancers/editor.js @@ -4,6 +4,9 @@ const {getFilterRenderer} = require('../filterRenderers'); const {manageFilterRendererState} = require('../enhancers/filterRenderers'); const featuresToGrid = compose( defaultProps({ + autocompleteEnabled: false, + url: "", + typeName: "", enableColumnFilters: false, columns: [], features: [], @@ -20,6 +23,18 @@ const featuresToGrid = compose( ["enableColumnFilters"], props => ({displayFilters: props.enableColumnFilters}) ), + withPropsOnChange( + ["autocompleteEnabled"], + props => ({autocompleteEnabled: props.autocompleteEnabled}) + ), + withPropsOnChange( + ["url"], + props => ({url: props.url}) + ), + withPropsOnChange( + ["typeName"], + props => ({typeName: props.typeName}) + ), withPropsOnChange( ["features", "newFeatures", "changes"], props => ({ @@ -55,7 +70,10 @@ const featuresToGrid = compose( sortable: !props.isFocused }, { getEditor: ({localType=""} = {}) => props.editors(localType, { - onTemporaryChanges: props.gridEvents && props.gridEvents.onTemporaryChanges + onTemporaryChanges: props.gridEvents && props.gridEvents.onTemporaryChanges, + autocompleteEnabled: props.autocompleteEnabled, + url: props.url, + typeName: props.typeName }), getFilterRenderer: ({localType=""} = {}, name) => { if (props.filterRenderers && props.filterRenderers[name]) { diff --git a/web/client/components/data/query/AutocompleteFieldHOC.jsx b/web/client/components/data/query/AutocompleteFieldHOC.jsx new file mode 100644 index 0000000000..9032d7c8c6 --- /dev/null +++ b/web/client/components/data/query/AutocompleteFieldHOC.jsx @@ -0,0 +1,147 @@ +/* + * 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 assign = require('object-assign'); +const AutocompleteListItem = require('./AutocompleteListItem'); +const PagedCombobox = require('../../misc/PagedCombobox'); +const {isLikeOrIlike} = require('../../../utils/FilterUtils'); +const HTML = require('../../../components/I18N/HTML'); + +class AutocompleteFieldHOC extends React.Component { + static propTypes = { + disabled: PropTypes.bool, + filterField: PropTypes.object, + label: PropTypes.string, + itemComponent: PropTypes.oneOfType([PropTypes.object, PropTypes.func]), + maxFeaturesWPS: PropTypes.number, + onUpdateField: PropTypes.func, + pagination: PropTypes.object, + textField: PropTypes.string, + tooltip: PropTypes.object, + toggleMenu: PropTypes.func, + valueField: PropTypes.string + }; + + static contextTypes = { + messages: PropTypes.object + }; + + static defaultProps = { + label: null, + onUpdateField: () => {}, + pagination: { + paginated: true, + nextPageIcon: "chevron-right", + prevPageIcon: "chevron-left" + }, + itemComponent: AutocompleteListItem, + toggleMenu: () => {} + }; + + getOptions = () => { + return this.props.filterField && + this.props.filterField.options && + this.props.filterField.options[this.props.filterField.attribute] && + this.props.filterField.options[this.props.filterField.attribute].map(o => { + return { value: o, label: o }; + }); + }; + + getPagination = (options) => { + const numberOfPages = Math.ceil(this.props.filterField.fieldOptions.valuesCount / this.props.maxFeaturesWPS); + const firstPage = this.props.filterField.fieldOptions.currentPage === 1 || !this.props.filterField.fieldOptions.currentPage; + const lastPage = this.props.filterField.fieldOptions.currentPage === numberOfPages || !this.props.filterField.fieldOptions.currentPage; + return assign({}, this.props.pagination, { + paginated: options.length !== 1, + firstPage, + lastPage, + loadPrevPage: () => this.props.onUpdateField(this.props.filterField.rowId, "value", this.props.filterField.value, "string", {currentPage: this.props.filterField.fieldOptions.currentPage - 1, delayDebounce: 0}), + loadNextPage: () => this.props.onUpdateField(this.props.filterField.rowId, "value", this.props.filterField.value, "string", {currentPage: this.props.filterField.fieldOptions.currentPage + 1, delayDebounce: 0}) + }); + }; + + getTooltip = () => { + return assign({}, this.props.tooltip, { + enabled: isLikeOrIlike(this.props.filterField.operator), + id: "autocompleteField-tooltip" + this.props.filterField && this.props.filterField.rowId, + message: (), + overlayTriggerKey: "autocompleteField-overlay" + this.props.filterField && this.props.filterField.rowId, + placement: "top" + }); + }; + renderField = () => { + let selectedValue; + // CHECK this.props.filterField.value AS "" + if (this.props.filterField && this.props.filterField.value && this.props.filterField.value !== "*") { + selectedValue = { + 'value': this.props.filterField.value, + 'label': this.props.filterField.value + }; + } + let options = this.getOptions() ? this.getOptions().slice(0) : []; + + return ( this.handleChange(value)} + onFocus={() => this.handleFocus(options)} + onSelect={() => this.handleSelect()} + onToggle={() => this.handleToggle(options)} + pagination={this.getPagination(options)} + selectedValue={selectedValue && selectedValue.value} + tooltip={this.getTooltip()} + />); + } + render() { + let label = this.props.label ? () : (); + return ( +
+ {label} + {this.renderField()} +
); + } + + // called before onChange + handleSelect = () => { + this.selected = true; + }; + + handleChange = (input) => { + if (this.selected) { + this.selected = false; + if (input && input.value !== "") { + this.props.onUpdateField(this.props.filterField.rowId, "value", input.value, "string", {currentPage: 1, selected: "selected", delayDebounce: 0}); + } + } else { + this.props.onUpdateField(this.props.filterField.rowId, "value", typeof input === "string" ? input : input.value, "string", {currentPage: 1, delayDebounce: 1000}); + } + }; + + // called before onToggle + handleFocus = (options) => { + this.loadWithoutfilter(options); + }; + + handleToggle = () => { + this.props.toggleMenu(this.props.filterField.rowId, !this.props.filterField.openAutocompleteMenu); + }; + + loadWithoutfilter = (options) => { + if (options.length === 0 && !this.props.filterField.value) { + this.props.onUpdateField(this.props.filterField.rowId, "value", "", "string", {currentPage: 1, delayDebounce: 0}); + } + }; +} + +module.exports = AutocompleteFieldHOC; diff --git a/web/client/components/data/query/ComboField.jsx b/web/client/components/data/query/ComboField.jsx index 0119ce96f9..87dbac3478 100644 --- a/web/client/components/data/query/ComboField.jsx +++ b/web/client/components/data/query/ComboField.jsx @@ -23,7 +23,7 @@ class ComboField extends React.Component { style: PropTypes.object, valueField: PropTypes.string, textField: PropTypes.string, - placeholder: PropTypes.string, + placeholder: PropTypes.object, fieldOptions: PropTypes.array, fieldName: PropTypes.string, fieldRowId: PropTypes.number, diff --git a/web/client/components/data/query/GroupField.jsx b/web/client/components/data/query/GroupField.jsx index 58d923a1fb..4b3b9195fe 100644 --- a/web/client/components/data/query/GroupField.jsx +++ b/web/client/components/data/query/GroupField.jsx @@ -16,7 +16,7 @@ const ComboField = require('./ComboField'); const DateField = require('./DateField'); const NumberField = require('./NumberField'); const TextField = require('./TextField'); -const AutocompleteField = require('./AutocompleteField'); +const AutocompleteField = require('./AutocompleteFieldHOC'); const LocaleUtils = require('../../../utils/LocaleUtils'); const I18N = require('../../I18N/I18N'); diff --git a/web/client/components/misc/AutocompleteCombobox.jsx b/web/client/components/misc/AutocompleteCombobox.jsx new file mode 100644 index 0000000000..a00d2a7821 --- /dev/null +++ b/web/client/components/misc/AutocompleteCombobox.jsx @@ -0,0 +1,134 @@ +/* + * 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 {isArray} = require('lodash'); +const PagedCombobox = require('./PagedCombobox'); +const {setObservableConfig, mapPropsStreamWithConfig, compose, withStateHandlers} = require('recompose'); +const rxjsConfig = require('recompose/rxjsObservableConfig').default; +setObservableConfig(rxjsConfig); +const mapPropsStream = mapPropsStreamWithConfig(rxjsConfig); + +// fetch data from wps service +const streamEnhancer = mapPropsStream(props$ => { + let fetcherStream = props$.take(1).switchMap(p => { + return p.autocompleteStreamFactory(props$); + }); + return fetcherStream.combineLatest(props$, (data, props) => ({ + data: isArray(data && data.fetchedData && data.fetchedData.values) ? data.fetchedData.values.map(v => {return {label: v, value: v}; }) : [], + valuesCount: data && data.fetchedData && data.fetchedData.size, + currentPage: props && props.currentPage, + maxFeatures: props && props.maxFeatures, + select: props && props.select, + focus: props && props.focus, + loadNextPage: props && props.loadNextPage, + loadPrevPage: props && props.loadPrevPage, + toggle: props && props.toggle, + change: props.change, + open: props.open, + selected: props && props.selected, + value: props.value, + busy: data.busy + })); +}); + +// component enhanced with props from stream, and local state +const PagedComboboxEnhanced = streamEnhancer( + ({ open, toggle, select, focus, change, value, valuesCount, + loadNextPage, loadPrevPage, maxFeatures, currentPage, + busy, data, loading = false }) => { + const numberOfPages = Math.ceil(valuesCount / maxFeatures); + return (); + }); + +// state enhancer for local props +const addStateHandlers = compose( + withStateHandlers((props) => ({ + delayDebounce: 0, + performFetch: false, + open: false, + currentPage: 1, + maxFeatures: 5, + url: props.url, + typeName: props.typeName, + value: props.value, + attribute: props.column && props.column.key, + autocompleteStreamFactory: props.autocompleteStreamFactory + }), { + select: (state) => () => ({ + ...state, + selected: true + }), + change: (state) => (v) => { + if (state.selected && state.changingPage) { + return ({ + ...state, + delayDebounce: state.selected ? 0 : 500, + selected: false, + changingPage: false, + performFetch: state.selected && !state.changingPage ? false : true, + value: state.value, + currentPage: !state.changingPage ? 1 : state.currentPage + }); + } + const value = typeof v === "string" ? v : v.value; + return ({ + ...state, + delayDebounce: state.selected ? 0 : 500, + selected: false, + changingPage: false, + performFetch: state.selected && !state.changingPage ? false : true, + value: value, + currentPage: !state.changingPage ? 1 : state.currentPage + }); + }, + focus: (state) => (options) => { + if (options && options.length === 0 && state.value === "") { + return ({ + ...state, + delayDebounce: 0, + currentPage: 1, + performFetch: true, + isToggled: false, + open: true + }); + } + return (state); + }, + toggle: (state) => () => ({ + ...state, + open: state.changingPage ? true : !state.open + }), + loadNextPage: (state) => () => ({ + ...state, + currentPage: state.currentPage + 1, + performFetch: true, + changingPage: true, + delayDebounce: 0, + value: state.value + }), + loadPrevPage: (state) => () => ({ + ...state, + currentPage: state.currentPage - 1, + performFetch: true, + changingPage: true, + delayDebounce: 0, + value: state.value + }) + }) +); + +const AutocompleteCombobox = addStateHandlers(PagedComboboxEnhanced); + +module.exports = { + AutocompleteCombobox +}; diff --git a/web/client/components/misc/ConfirmDialog.jsx b/web/client/components/misc/ConfirmDialog.jsx index 60d4d9b71f..cbd311ed2b 100644 --- a/web/client/components/misc/ConfirmDialog.jsx +++ b/web/client/components/misc/ConfirmDialog.jsx @@ -1,21 +1,12 @@ +/* +* 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 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. - */ -/** - * 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, ButtonGroup, Glyphicon} = require('react-bootstrap'); const Dialog = require('./Dialog'); diff --git a/web/client/components/misc/PagedCombobox.jsx b/web/client/components/misc/PagedCombobox.jsx new file mode 100644 index 0000000000..c94ae77f3c --- /dev/null +++ b/web/client/components/misc/PagedCombobox.jsx @@ -0,0 +1,158 @@ +/* + * 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 Combobox = require('react-widgets').Combobox; +const {Glyphicon, Tooltip} = require('react-bootstrap'); +const LocaleUtils = require('../../utils/LocaleUtils'); +const OverlayTrigger = require('./OverlayTrigger'); +const AutocompleteListItem = require('../data/query/AutocompleteListItem'); + +/** + * Combobox with remote autocomplete functionality. + * @memberof components.query + * @class + * @prop {bool} [disabled] if the combobox should be disabled + * @prop {string} [label] the label of the combobox + * @prop {bool} [paginated] if true it displays the pagination if there is more than one page + * @prop {string} [textField] the key used for the labes corresponding to filterField.options[x].label + * @prop {string} [valueField] the key used for the values corresponding to filterField.options[x].value + * + */ +class PagedCombobox extends React.Component { + // sorted alphabetically + static propTypes = { + busy: PropTypes.bool, + data: PropTypes.array, + disabled: PropTypes.bool, + dropUp: PropTypes.bool, + itemComponent: PropTypes.oneOfType([PropTypes.object, PropTypes.func]), + label: PropTypes.string, + loading: PropTypes.bool, + messages: PropTypes.object, + onChange: PropTypes.func, + onFocus: PropTypes.func, + onSelect: PropTypes.func, + onToggle: PropTypes.func, + open: PropTypes.bool, + pagination: PropTypes.object, + nextPageIcon: PropTypes.string, + prevPageIcon: PropTypes.string, + selectedValue: PropTypes.string, + textField: PropTypes.string, + tooltip: PropTypes.object, + valueField: PropTypes.string + }; + + static contextTypes = { + messages: PropTypes.object + }; + + static defaultProps = { + dropUp: false, + itemComponent: AutocompleteListItem, + loading: false, + label: null, + pagination: { + paginated: true, + firstPage: false, + lastPage: false, + loadPrevPage: () => {}, + loadNextPage: () => {} + }, + nextPageIcon: "chevron-right", + prevPageIcon: "chevron-left", + onFocus: () => {}, + onToggle: () => {}, + onChange: () => {}, + onSelect: () => {}, + textField: "label", + tooltip: { + customizedTooltip: undefined, + enabled: false, + id: "", + message: undefined, + overlayTriggerKey: "", + placement: "top" + }, + valueField: "value" + }; + + renderWithTooltip = (field) => { + if (this.props.tooltip.customizedTooltip) { + const CustomTooltip = this.props.tooltip.customizedTooltip; + return ( + { field } + ); + } + const tooltip = ( + this.props.tooltip.message); + return ( + { field } + ); + }; + + renderPagination = () => { + const firstPage = this.props.pagination.firstPage; + const lastPage = this.props.pagination.lastPage; + return ( +
+ { !firstPage && + this.props.pagination.loadPrevPage() }/> + } + { !lastPage && + this.props.pagination.loadNextPage()}/> + } +
+ ); + }; + renderField = () => { + const messages = { + emptyList: LocaleUtils.getMessageById(this.context.messages, "queryform.attributefilter.autocomplete.emptyList"), + open: LocaleUtils.getMessageById(this.context.messages, "queryform.attributefilter.autocomplete.open"), + emptyFilter: LocaleUtils.getMessageById(this.context.messages, "queryform.attributefilter.autocomplete.emptyFilter") + }; + let options = []; + if (this.props.data && this.props.data.length > 0) { + options = this.props.data.map(d => d); + } + + if (this.props.pagination && this.props.pagination.paginated && options.length > 0) { + options.push({ label: '', value: '', disabled: true, pagination: this.renderPagination() }); + } + const data = this.props.loading ? [] : options; + const field = ( this.props.onChange(val)} + onFocus={() => this.props.onFocus(this.props.data)} + onSelect={(v) => this.props.onSelect(v)} + onToggle={() => this.props.onToggle()} + textField={this.props.textField} + valueField={this.props.valueField} + value={this.props.selectedValue} + />); + return this.props.tooltip && this.props.tooltip.enabled ? this.renderWithTooltip(field) : field; + } + render() { + let label = this.props.label ? () : (); // TODO change "the else case" value with null ? + return ( +
+ {label} + {this.renderField()} +
); + } +} + +module.exports = PagedCombobox; diff --git a/web/client/components/misc/__tests__/AutocompleteCombobox-test.jsx b/web/client/components/misc/__tests__/AutocompleteCombobox-test.jsx new file mode 100644 index 0000000000..89a56cbd18 --- /dev/null +++ b/web/client/components/misc/__tests__/AutocompleteCombobox-test.jsx @@ -0,0 +1,29 @@ +/* + * 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 React = require('react'); +const ReactDOM = require('react-dom'); +const {AutocompleteCombobox} = require('../AutocompleteCombobox'); +const {createPagedUniqueAutompleteStream} = require('../../../observables/autocomplete'); + +describe("This test for AutocompleteCombobox component", () => { + beforeEach((done) => { + document.body.innerHTML = '
'; + setTimeout(done); + }); + afterEach((done) => { + ReactDOM.unmountComponentAtNode(document.getElementById("container")); + document.body.innerHTML = ''; + setTimeout(done); + }); + it('creates AutocompleteCombobox with defaults', () => { + const comp = ReactDOM.render(, document.getElementById("container")); + expect(comp).toExist(); + }); + +}); diff --git a/web/client/components/misc/__tests__/PagedCombobox-test.jsx b/web/client/components/misc/__tests__/PagedCombobox-test.jsx new file mode 100644 index 0000000000..ee92b5ea4f --- /dev/null +++ b/web/client/components/misc/__tests__/PagedCombobox-test.jsx @@ -0,0 +1,139 @@ +/* + * 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 React = require('react'); +const ReactDOM = require('react-dom'); +const PagedCombobox = require('../PagedCombobox'); +const TestUtils = require('react-dom/test-utils'); +const {Tooltip} = require('react-bootstrap'); + +describe("This test for PagedCombobox component", () => { + beforeEach((done) => { + document.body.innerHTML = '
'; + setTimeout(done); + }); + afterEach((done) => { + ReactDOM.unmountComponentAtNode(document.getElementById("container")); + document.body.innerHTML = ''; + setTimeout(done); + }); + it('creates PagedCombobox with defaults', () => { + const comp = ReactDOM.render(, document.getElementById("container")); + expect(comp).toExist(); + + const input = ReactDOM.findDOMNode(TestUtils.scryRenderedDOMComponentsWithClass(comp, "rw-input")[0]); + // triggering default actions + TestUtils.Simulate.change(input, { + target: { + value: "other" + } + }); + TestUtils.Simulate.focus(input); + + }); + it('creates PagedCombobox with defaults and with basic tooltip', () => { + const tooltip = { + message: "wow", + enabled: true, + id: "1", + overlayTriggerKey: "key", + placement: "top" + }; + const comp = ReactDOM.render(, document.getElementById("container")); + expect(comp).toExist(); + }); + it('creates PagedCombobox with defaults and with customized tooltip', () => { + const tooltip = "a message for the tooltip"; + + const comp = ReactDOM.render(, document.getElementById("container")); + expect(comp).toExist(); + }); + it('tests PagedCombobox onToggle and opening of option lists', () => { + const actions = { + onToggle: () => {} + }; + const spy = expect.spyOn(actions, "onToggle"); + const data = [{ + label: "label", value: "value" + }]; + const comp = ReactDOM.render(, document.getElementById("container")); + expect(comp).toExist(); + const domNode = ReactDOM.findDOMNode(comp); + expect(domNode).toExist(); + const tool = ReactDOM.findDOMNode(TestUtils.scryRenderedDOMComponentsWithClass(comp, "rw-i rw-i-caret-down")[0]); + tool.click(); + expect(spy.calls.length).toBe(1); + // this tests if the option list is opened + const firstOption = ReactDOM.findDOMNode(TestUtils.scryRenderedDOMComponentsWithClass(comp, "rw-list-option")[0]); + expect(firstOption).toExist(); + const valueOption = firstOption.getElementsByTagName("span")[0]; + expect(valueOption).toExist(); + expect(valueOption.innerText).toBe("label"); + }); + it('tests PagedCombobox onChange', () => { + const actions = { + onChange: () => {} + }; + const spy = expect.spyOn(actions, "onChange"); + const data = [{ + label: "label", value: "value" + }]; + const comp = ReactDOM.render(, document.getElementById("container")); + expect(comp).toExist(); + const input = ReactDOM.findDOMNode(TestUtils.scryRenderedDOMComponentsWithClass(comp, "rw-input")[0]); + TestUtils.Simulate.change(input, { + target: { + value: "other" + } + }); + expect(spy.calls.length).toBe(1); + }); + it('tests PagedCombobox onFocus', (done) => { + const actions = { + onFocus: () => {} + }; + const spy = expect.spyOn(actions, "onFocus"); + const data = [{ + label: "label", value: "value" + }]; + const comp = ReactDOM.render(, document.getElementById("container")); + expect(comp).toExist(); + const input = ReactDOM.findDOMNode(TestUtils.scryRenderedDOMComponentsWithClass(comp, "rw-input")[0]); + TestUtils.Simulate.focus(input); + setTimeout(() => { + expect(spy.calls.length).toEqual(1); + done(); + }, 50); + }); + + it('tests PagedCombobox onSelect and opening of option lists', (done) => { + const actions = { + onSelect: () => {} + }; + const spy = expect.spyOn(actions, "onSelect"); + const data = [{ + label: "label", value: "value" + }]; + const comp = ReactDOM.render(, document.getElementById("container")); + expect(comp).toExist(); + const domNode = ReactDOM.findDOMNode(comp); + expect(domNode).toExist(); + const tool = ReactDOM.findDOMNode(TestUtils.scryRenderedDOMComponentsWithClass(comp, "rw-i rw-i-caret-down")[0]); + tool.click(); + // this tests if the option list is opened + const firstOption = ReactDOM.findDOMNode(TestUtils.scryRenderedDOMComponentsWithClass(comp, "rw-list-option")[0]); + expect(firstOption).toExist(); + const valueOption = firstOption.getElementsByTagName("span")[0]; + expect(valueOption).toExist(); + TestUtils.Simulate.click(firstOption); + setTimeout(() => { + expect(spy.calls.length).toEqual(1); + done(); + }, 50); + }); +}); diff --git a/web/client/epics/autocomplete.js b/web/client/epics/autocomplete.js index ddb7b627fd..138e593494 100644 --- a/web/client/epics/autocomplete.js +++ b/web/client/epics/autocomplete.js @@ -10,33 +10,13 @@ const Rx = require('rxjs'); const axios = require('../libs/ajax'); const {UPDATE_FILTER_FIELD, updateFilterFieldOptions, loadingFilterFieldOptions, setAutocompleteMode, toggleMenu} = require('../actions/queryform'); const {FEATURE_TYPE_SELECTED} = require('../actions/wfsquery'); -const {getRequestBody, getRequestBodyWithFilter} = require('../utils/ogc/WPS/autocomplete'); -const {isArray, startsWith, endsWith} = require('lodash'); +const {getWpsPayload} = require('../utils/ogc/WPS/autocomplete'); +const {isArray, startsWith} = require('lodash'); const {error} = require('../actions/notifications'); -const urlUtil = require('url'); -const assign = require('object-assign'); +const {typeNameSelector} = require('../selectors/query'); +const {maxFeaturesWPSSelector} = require('../selectors/queryform'); +const {getParsedUrl} = require('../utils/ConfigUtils'); -// create wps request body -function getWpsPayload(options) { - return options.value ? getRequestBodyWithFilter(options) : getRequestBody(options); -} - -function getParsedUrl(url, options) { - if (url) { - const parsed = urlUtil.parse(url, true); - let newPathname = null; - if (endsWith(parsed.pathname, "wfs") || endsWith(parsed.pathname, "wms") || endsWith(parsed.pathname, "ows")) { - newPathname = parsed.pathname.replace(/(wms|ows|wfs|wps)$/, "wps"); - return urlUtil.format(assign({}, parsed, {search: null, pathname: newPathname }, { - query: assign({ - service: "WPS", - ...options - }, parsed.query) - })); - } - } - return null; -} /** * Epics for WFS query requests * @name epics.wfsquery @@ -75,8 +55,8 @@ module.exports = { .filter( (action) => action.fieldName === "value" && action.fieldType === "string" && store.getState().queryform.autocompleteEnabled ) .switchMap((action) => { const state = store.getState(); - const maxFeaturesWPS = state.queryform.maxFeaturesWPS; - const filterField = state.queryform.filterFields.filter((f) => f.rowId === action.rowId)[0]; + const maxFeaturesWPS = maxFeaturesWPSSelector(state); + const filterField = state.queryform && state.queryform.filterFields && state.queryform.filterFields.filter((f) => f.rowId === action.rowId)[0]; if (action.fieldOptions.selected === "selected") { return Rx.Observable.from([ @@ -85,7 +65,7 @@ module.exports = { } const data = getWpsPayload({ attribute: filterField.attribute, - layerName: state.query.typeName, + layerName: typeNameSelector(state), maxFeatures: maxFeaturesWPS, startIndex: (action.fieldOptions.currentPage - 1) * maxFeaturesWPS, value: action.fieldValue diff --git a/web/client/localConfig.json b/web/client/localConfig.json index 6a9294c9f7..27bca2b4a8 100644 --- a/web/client/localConfig.json +++ b/web/client/localConfig.json @@ -268,12 +268,7 @@ }, { "name": "MetadataExplorer", "cfg": { - "wrap": true, - "initialCatalogURL": { - "csw": "https://demo.geo-solutions.it/geoserver/csw", - "wms": "https://demo.geo-solutions.it/geoserver/wms", - "wmts": "https://demo.geo-solutions.it/geoserver/gwc/service/wmts" - } + "wrap": true } }, { "name": "About", diff --git a/web/client/observables/__tests__/autocomplete-test.js b/web/client/observables/__tests__/autocomplete-test.js new file mode 100644 index 0000000000..3d59e49990 --- /dev/null +++ b/web/client/observables/__tests__/autocomplete-test.js @@ -0,0 +1,87 @@ +/* + * 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 React = require('react'); +const ReactDOM = require('react-dom'); +const {isEmpty} = require('lodash'); +const assign = require('object-assign'); +const {createPagedUniqueAutompleteStream} = require('../autocomplete'); +const AutocompleteEditor = require('../../components/data/featuregrid/editors/AutocompleteEditor'); +const rxjsConfig = require('recompose/rxjsObservableConfig').default; +const {setObservableConfig, mapPropsStreamWithConfig} = require('recompose'); +setObservableConfig(rxjsConfig); +const mapPropsStream = mapPropsStreamWithConfig(rxjsConfig); +const props = { + attribute: "STATE_NAME", + performFetch: false, + typeName: "topp:states", + maxFeatures: 5, + currentPage: 1, + value: "", + url: "base/web/client/test-resources/wps/pageUniqueResponse.json", + delayDebounce: 0 +}; +describe('\nautocomplete Observables', () => { + beforeEach((done) => { + document.body.innerHTML = '
'; + setTimeout(done); + }); + + afterEach((done) => { + ReactDOM.unmountComponentAtNode(document.getElementById("container")); + document.body.innerHTML = ''; + setTimeout(done); + }); + + it('test with fake stream, performFetch=false', (done) => { + const ReactItem = mapPropsStream(props$ => + createPagedUniqueAutompleteStream(props$).map(p => { + if (!isEmpty(p) && p.fetchedData !== undefined) { + expect(p.fetchedData.values.length).toBe(0); + expect(p.fetchedData.size).toBe(0); + expect(p.busy).toBe(false); + done(); + } + }) + )(AutocompleteEditor); + const item = ReactDOM.render(, document.getElementById("container")); + expect(item).toExist(); + }); + + it('test with fake stream, performFetch=true', (done) => { + const ReactItem = mapPropsStream(props$ => + createPagedUniqueAutompleteStream(props$).map(p => { + if (!isEmpty(p) && p.fetchedData !== undefined ) { + expect(p.fetchedData.values.length).toBe(2); + expect(p.fetchedData.values[0]).toBe("value1"); + expect(p.fetchedData.size).toBe(2); + expect(p.busy).toBe(false); + done(); + } + }) + )(AutocompleteEditor); + const item = ReactDOM.render(, document.getElementById("container")); + expect(item).toExist(); + }); + + it('test a failure case intercepted into catch statement ', (done) => { + const ReactItem = mapPropsStream(props$ => + createPagedUniqueAutompleteStream(props$).map(p => { + if (!isEmpty(p) && p.fetchedData !== undefined ) { + expect(p.fetchedData.values.length).toBe(0); + expect(p.fetchedData.size).toBe(0); + expect(p.busy).toBe(false); + done(); + } + }) + )(AutocompleteEditor); + const item = ReactDOM.render(, document.getElementById("container")); + expect(item).toExist(); + }); +}); diff --git a/web/client/observables/__tests__/fakeresponse.json b/web/client/observables/__tests__/fakeresponse.json new file mode 100644 index 0000000000..4ea209833a --- /dev/null +++ b/web/client/observables/__tests__/fakeresponse.json @@ -0,0 +1,4 @@ +{ + "values": ["value1", "value2"], + "size": 2 +} diff --git a/web/client/observables/autocomplete.js b/web/client/observables/autocomplete.js new file mode 100644 index 0000000000..b94de47801 --- /dev/null +++ b/web/client/observables/autocomplete.js @@ -0,0 +1,52 @@ +/* + * 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. + */ + /** + * Observables used for autocomplete in the feature editor + * @name observables.autocomplete + * @type {Object} + * +*/ +const Rx = require('rxjs'); +const axios = require('../libs/ajax'); +const {getWpsPayload} = require('../utils/ogc/WPS/autocomplete'); + +/** + * creates a stream for fetching data via WPS + * @param {external:Observable} props + * @memberof observables.autocomplete + * @return {external:Observable} the stream used for fetching data for the autocomplete editor +*/ +const createPagedUniqueAutompleteStream = (props$) => props$ + .distinctUntilChanged( ({value, currentPage}, newProps = {}) => !(newProps.value !== value || newProps.currentPage !== currentPage)) + .throttle(props => Rx.Observable.timer(props.delayDebounce || 0)) + .merge(props$.debounce(props => Rx.Observable.timer(props.delayDebounce || 0))).distinctUntilChanged() + .switchMap((p) => { + if (p.performFetch) { + const data = getWpsPayload({ + attribute: p.attribute, + layerName: p.typeName, + maxFeatures: p.maxFeatures, + startIndex: (p.currentPage - 1) * p.maxFeatures, + value: p.value + }); + return Rx.Observable.fromPromise( + axios.post(p.url, data, { + timeout: 60000, + headers: {'Accept': 'application/json', 'Content-Type': 'application/xml'} + }).then(response => { return {fetchedData: response.data, busy: false}; })) + .catch(() => { + return Rx.Observable.of({fetchedData: {values: [], size: 0}, busy: false}); + }).startWith({busy: true}); + } + return Rx.Observable.of({fetchedData: {values: [], size: 0}, busy: false}); + }).startWith({}); + + +module.exports = { + createPagedUniqueAutompleteStream +}; diff --git a/web/client/plugins/FeatureEditor.jsx b/web/client/plugins/FeatureEditor.jsx index c863d61404..4a7641c07b 100644 --- a/web/client/plugins/FeatureEditor.jsx +++ b/web/client/plugins/FeatureEditor.jsx @@ -12,7 +12,7 @@ const {bindActionCreators} = require('redux'); const {get} = require('lodash'); const Dock = require('react-dock').default; const Grid = require('../components/data/featuregrid/FeatureGrid'); -const {resultsSelector, describeSelector} = require('../selectors/query'); +const {resultsSelector, describeSelector, wfsURLSelector, typeNameSelector} = require('../selectors/query'); const {modeSelector, changesSelector, newFeaturesSelector, hasChangesSelector, selectedFeaturesSelector} = require('../selectors/featuregrid'); const { toChangesMap} = require('../utils/FeatureGridUtils'); const {getPanels, getHeader, getFooter, getDialogs, getEmptyRowsView, getFilterRenderers} = require('./featuregrid/panels/index'); @@ -21,6 +21,7 @@ const EMPTY_ARR = []; const EMPTY_OBJ = {}; const {gridTools, gridEvents, pageEvents, toolbarEvents} = require('./featuregrid/index'); const ContainerDimensions = require('react-container-dimensions').default; +const {getParsedUrl} = require('../utils/ConfigUtils'); const FeatureDock = (props = { tools: EMPTY_OBJ, @@ -52,6 +53,9 @@ const FeatureDock = (props = { footer={getFooter(props)}> {getDialogs(props.tools)} get(state, "featuregrid.open"), + state => get(state, "queryform.autocompleteEnabled"), + state => wfsURLSelector(state), + state => typeNameSelector(state), resultsSelector, describeSelector, state => get(state, "featuregrid.attributes"), @@ -86,8 +93,11 @@ const selector = createSelector( hasChangesSelector, state => get(state, 'featuregrid.focusOnEdit') || [], state => get(state, 'featuregrid.enableColumnFilters'), - (open, features = EMPTY_ARR, describe, attributes, tools, select, mode, changes, newFeatures = EMPTY_ARR, hasChanges, focusOnEdit, enableColumnFilters) => ({ + (open, autocompleteEnabled, url, typeName, features = EMPTY_ARR, describe, attributes, tools, select, mode, changes, newFeatures = EMPTY_ARR, hasChanges, focusOnEdit, enableColumnFilters) => ({ open, + autocompleteEnabled, + url: getParsedUrl(url, {"outputFormat": "json"}), + typeName, hasChanges, newFeatures, features, diff --git a/web/client/plugins/MapFooter.jsx b/web/client/plugins/MapFooter.jsx index 803ad4235e..4d16d1dbff 100644 --- a/web/client/plugins/MapFooter.jsx +++ b/web/client/plugins/MapFooter.jsx @@ -66,6 +66,6 @@ class MapFooter extends React.Component { } module.exports = { - MapFooterPlugin: assign(MapFooter, {disablePluginIf: "{state('featuregridmode') === 'EDIT'}"}), + MapFooterPlugin: assign(MapFooter, {}), reducers: {} }; diff --git a/web/client/selectors/__tests__/query-test.js b/web/client/selectors/__tests__/query-test.js index a6a984a786..4eacdcac34 100644 --- a/web/client/selectors/__tests__/query-test.js +++ b/web/client/selectors/__tests__/query-test.js @@ -9,7 +9,9 @@ const expect = require('expect'); const { wfsURL, + wfsURLSelector, wfsFilter, + typeNameSelector, resultsSelector, featureCollectionResultSelector, paginationInfo, @@ -17,7 +19,8 @@ const { isDescribeLoaded, describeSelector, getFeatureById, - attributesSelector + attributesSelector, + isSyncWmsActive } = require('../query'); const idFt1 = "idFt1"; @@ -271,6 +274,7 @@ const initialState = { }, searchUrl: 'http://localhost:8081/geoserver/wfs?', typeName: 'editing:polygons', + syncWmsFilter: true, url: 'http://localhost:8081/geoserver/wfs?', featureLoading: false }, @@ -290,6 +294,21 @@ describe('Test query selectors', () => { expect(searchUrl).toExist(); expect(searchUrl).toBe("http://localhost:8081/geoserver/wfs?"); }); + it('test wfsURLSelector selector', () => { + const url = wfsURLSelector(initialState); + expect(url).toExist(); + expect(url).toBe("http://localhost:8081/geoserver/wfs?"); + }); + it('test typeNameSelector selector', () => { + const typename = typeNameSelector(initialState); + expect(typename).toExist(); + expect(typename).toBe("editing:polygons"); + }); + it('test isSyncWmsActive selector', () => { + const sync = isSyncWmsActive(initialState); + expect(sync).toExist(); + expect(sync).toBe(true); + }); it('test wfsFilter selector', () => { const filterObj = wfsFilter(initialState); expect(filterObj).toExist(); diff --git a/web/client/selectors/query.js b/web/client/selectors/query.js index 182d38f9a3..33215db381 100644 --- a/web/client/selectors/query.js +++ b/web/client/selectors/query.js @@ -1,8 +1,10 @@ const {get, head} = require('lodash'); module.exports = { wfsURL: state => state && state.query && state.query.searchUrl, + wfsURLSelector: state => state && state.query && state.query.url, wfsFilter: state => state && state.query && state.query.filterObj, attributesSelector: state => get(state, `query.featureTypes.${get(state, "query.filterObj.featureTypeName")}.attributes`), + typeNameSelector: state => get(state, "query.typeName"), resultsSelector: (state) => get(state, "query.result.features"), featureCollectionResultSelector: state => get(state, "query.result"), getFeatureById: (state, id) => { diff --git a/web/client/selectors/queryform.js b/web/client/selectors/queryform.js index ed45c03c96..ff6e6dbb06 100644 --- a/web/client/selectors/queryform.js +++ b/web/client/selectors/queryform.js @@ -5,6 +5,7 @@ module.exports = { spatialFieldSelector: state => get(state, "queryform.spatialField"), spatialFieldMethodSelector: state => get(state, "queryform.spatialField.method"), spatialFieldGeomSelector, + maxFeaturesWPSSelector: state => get(state, "queryform.maxFeaturesWPS"), spatialFieldGeomTypeSelector: state => spatialFieldGeomSelector(state) && spatialFieldGeomSelector(state).type || "Polygon", spatialFieldGeomProjSelector: state => spatialFieldGeomSelector(state) && spatialFieldGeomSelector(state).projection || "EPSG:4326", spatialFieldGeomCoordSelector: state => spatialFieldGeomSelector(state) && spatialFieldGeomSelector(state).coordinates || [] diff --git a/web/client/test-resources/wps/pageUniqueResponse.json b/web/client/test-resources/wps/pageUniqueResponse.json new file mode 100644 index 0000000000..4ea209833a --- /dev/null +++ b/web/client/test-resources/wps/pageUniqueResponse.json @@ -0,0 +1,4 @@ +{ + "values": ["value1", "value2"], + "size": 2 +} diff --git a/web/client/themes/default/less/autocomplete.less b/web/client/themes/default/less/autocomplete.less index 74f2f69882..1663629870 100644 --- a/web/client/themes/default/less/autocomplete.less +++ b/web/client/themes/default/less/autocomplete.less @@ -14,3 +14,7 @@ .autocompleteField ul li:last-child { border: none !important; } + +.rw-combobox .rw-list { + width: -webkit-fill-available; +} diff --git a/web/client/utils/ConfigUtils.js b/web/client/utils/ConfigUtils.js index 39b5f07ebb..0a5492f4ef 100644 --- a/web/client/utils/ConfigUtils.js +++ b/web/client/utils/ConfigUtils.js @@ -10,8 +10,7 @@ const PropTypes = require('prop-types'); var url = require('url'); var axios = require('axios'); - -const {isArray, isObject} = require('lodash'); +const {isArray, isObject, endsWith} = require('lodash'); const assign = require('object-assign'); const {Promise} = require('es6-promise'); @@ -63,6 +62,22 @@ var ConfigUtils = { }), mapStateSource: PropTypes.string }, + getParsedUrl: (urlToParse, options) => { + if (urlToParse) { + const parsed = url.parse(urlToParse, true); + let newPathname = null; + if (endsWith(parsed.pathname, "wfs") || endsWith(parsed.pathname, "wms") || endsWith(parsed.pathname, "ows")) { + newPathname = parsed.pathname.replace(/(wms|ows|wfs|wps)$/, "wps"); + return url.format(assign({}, parsed, {search: null, pathname: newPathname }, { + query: assign({ + service: "WPS", + ...options + }, parsed.query) + })); + } + } + return null; + }, getDefaults: function() { return defaultConfig; }, diff --git a/web/client/utils/__tests__/ConfigUtils-test.js b/web/client/utils/__tests__/ConfigUtils-test.js index a4fcd3413f..e523226fba 100644 --- a/web/client/utils/__tests__/ConfigUtils-test.js +++ b/web/client/utils/__tests__/ConfigUtils-test.js @@ -245,6 +245,23 @@ describe('ConfigUtils', () => { expect(retval.configUrl).toBe(testval.configUrl); expect(retval.legacy).toBe(testval.legacy); }); + it('getParsedUrl with valid url ending with wfs', () => { + const url = "http://somepath/wfs"; + const testval = "http://somepath/wps?service=WPS"; + const retval = ConfigUtils.getParsedUrl(url, {}); + expect(retval).toExist(); + expect(retval).toBe(testval); + }); + it('getParsedUrl with valid url ending with asd return null', () => { + const url = "http://somepath/asd"; + const retval = ConfigUtils.getParsedUrl(url, {}); + expect(retval).toBe(null); + }); + it('getParsedUrl with not valid url', () => { + const url = null; + const retval = ConfigUtils.getParsedUrl(url, {}); + expect(retval).toBe(null); + }); it('loadConfiguration', (done) => { var retval = ConfigUtils.loadConfiguration(); diff --git a/web/client/utils/ogc/WPS/__tests__/autocomplete-test.js b/web/client/utils/ogc/WPS/__tests__/autocomplete-test.js new file mode 100644 index 0000000000..dd24092fc9 --- /dev/null +++ b/web/client/utils/ogc/WPS/__tests__/autocomplete-test.js @@ -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 expect = require('expect'); +const assign = require('object-assign'); +const {getWpsPayload, getRequestBody, getRequestBodyWithFilter} = require('../autocomplete'); + +const defaultOptions = { + value: "somevalue", + layerName: "layerName", + attribute: "attribute", + maxFeatures: 5, + startIndex: 0 +}; + +const getBodyPart1 = ({layerName}) => ' ' ++ ' gs:PagedUnique ' ++ ' ' ++ ' ' ++ ' features ' ++ ' features ' ++ ' ' ++ ' ' ++ ' ' ++ ' ' ++ ' '; + +const getBodyPart2 = ({attribute, value}) => ' ' ++ ' ' ++ ' ' + attribute + '' ++ ' *' + value + '*' ++ ' ' ++ ' '; + +const getBodyPart3 = ({attribute, maxFeatures, startIndex}) => ' ' ++ ' ' ++ ' ' + attribute + '' ++ ' ' ++ ' ' ++ ' ' ++ ' ' ++ ' ' ++ ' ' ++ ' ' ++ ' ' ++ ' fieldName' ++ ' fieldName' ++ ' ' ++ ' ' + attribute + '' ++ ' ' ++ ' ' ++ ' ' ++ ' maxFeatures' ++ ' maxFeatures' ++ ' ' ++ ' ' + maxFeatures + '' ++ ' ' ++ ' ' ++ ' ' ++ ' startIndex' ++ ' startIndex' ++ ' ' ++ ' ' + startIndex + '' ++ ' ' ++ ' ' ++ ' ' ++ ' ' ++ ' ' ++ ' result' ++ ' ' ++ ' ' ++ ''; + + +describe('Test WPS requests', () => { + + it('getWpsPayload with value', () => { + const options = assign({}, defaultOptions); + const requestBody = getWpsPayload(options); + expect(requestBody).toExist(); + const expectedBody = getBodyPart1(options) + getBodyPart2(options) + getBodyPart3(options); + expect(requestBody).toBe(expectedBody); + }); + it('getWpsPayload with value null', () => { + const options = assign({}, defaultOptions, {value: null}); + const requestBody = getWpsPayload(options); + expect(requestBody).toExist(); + const expectedBody = getBodyPart1(options) + getBodyPart3(options); + expect(requestBody).toBe(expectedBody); + }); + it('getWpsPayload with value undefined', () => { + const options = assign({}, defaultOptions, {value: undefined}); + const requestBody = getWpsPayload(options); + expect(requestBody).toExist(); + const expectedBody = getBodyPart1(options) + getBodyPart3(options); + expect(requestBody).toBe(expectedBody); + }); + it('getRequestBody with value null', () => { + const options = assign({}, defaultOptions, {value: null}); + const requestBody = getRequestBody(options); + const expectedBody = getBodyPart1(options) + getBodyPart3(options); + expect(requestBody).toBe(expectedBody); + }); + it('getRequestBodyWithFilter with value', () => { + const options = assign({}, defaultOptions); + const requestBody = getRequestBodyWithFilter(options); + expect(requestBody).toExist(); + const expectedBody = getBodyPart1(options) + getBodyPart2(options) + getBodyPart3(options); + expect(requestBody).toBe(expectedBody); + }); + it('getWpsPayload with no data', () => { + const requestBody = getRequestBodyWithFilter({}); + expect(requestBody).toExist(); + const expectedBody = getBodyPart1({}) + getBodyPart2({}) + getBodyPart3({}); + expect(requestBody).toBe(expectedBody); + }); + +}); diff --git a/web/client/utils/ogc/WPS/autocomplete.js b/web/client/utils/ogc/WPS/autocomplete.js index 78629b28e4..166a7e99f9 100644 --- a/web/client/utils/ogc/WPS/autocomplete.js +++ b/web/client/utils/ogc/WPS/autocomplete.js @@ -1,118 +1,122 @@ // const wfsRequestBuilder = require('../WFS/RequestBuilder'); // const {getFeature, property, query} = wfsRequestBuilder({wfsVersion: "1.1.0"}); -module.exports = { - getRequestBody: ({layerName, attribute, maxFeatures, startIndex}) => { +const getRequestBody = ({layerName, attribute, maxFeatures, startIndex}) => { - let requestBody = - ' ' - + ' gs:PagedUnique ' - + ' ' - + ' ' - + ' features ' - + ' features ' - + ' ' - + ' ' - + ' ' - + ' ' - + ' ' - + ' ' - + ' ' - + ' ' + attribute + '' - + ' ' - + ' ' - + ' ' - + ' ' - + ' ' - + ' ' - + ' ' - + ' ' - + ' fieldName' - + ' fieldName' - + ' ' - + ' ' + attribute + '' - + ' ' - + ' ' - + ' ' - + ' maxFeatures' - + ' maxFeatures' - + ' ' - + ' ' + maxFeatures + '' - + ' ' - + ' ' - + ' ' - + ' startIndex' - + ' startIndex' - + ' ' - + ' ' + startIndex + '' - + ' ' - + ' ' - + ' ' - + ' ' - + ' ' - + ' result' - + ' ' - + ' ' - + ''; - return requestBody; - }, - getRequestBodyWithFilter: ({layerName, attribute, maxFeatures, startIndex, value}) => { - let requestBody = - ' ' - + ' gs:PagedUnique ' - + ' ' - + ' ' - + ' features ' - + ' features ' - + ' ' - + ' ' - + ' ' - + ' ' - + ' ' - + ' ' - + ' ' - + ' ' + attribute + '' - + ' *' + value + '*' - + ' ' - + ' ' - + ' ' - + ' ' - + ' ' + attribute + '' - + ' ' - + ' ' - + ' ' - + ' ' - + ' ' - + ' ' - + ' ' - + ' ' - + ' fieldName' - + ' fieldName' - + ' ' - + ' ' + attribute + '' - + ' ' - + ' ' - + ' ' - + ' maxFeatures' - + ' maxFeatures' - + ' ' - + ' ' + maxFeatures + '' - + ' ' - + ' ' - + ' ' - + ' startIndex' - + ' startIndex' - + ' ' - + ' ' + startIndex + '' - + ' ' - + ' ' - + ' ' - + ' ' - + ' ' - + ' result' - + ' ' - + ' ' - + ''; - return requestBody; - } + let requestBody = + ' ' + + ' gs:PagedUnique ' + + ' ' + + ' ' + + ' features ' + + ' features ' + + ' ' + + ' ' + + ' ' + + ' ' + + ' ' + + ' ' + + ' ' + + ' ' + attribute + '' + + ' ' + + ' ' + + ' ' + + ' ' + + ' ' + + ' ' + + ' ' + + ' ' + + ' fieldName' + + ' fieldName' + + ' ' + + ' ' + attribute + '' + + ' ' + + ' ' + + ' ' + + ' maxFeatures' + + ' maxFeatures' + + ' ' + + ' ' + maxFeatures + '' + + ' ' + + ' ' + + ' ' + + ' startIndex' + + ' startIndex' + + ' ' + + ' ' + startIndex + '' + + ' ' + + ' ' + + ' ' + + ' ' + + ' ' + + ' result' + + ' ' + + ' ' + + ''; + return requestBody; +}; +const getRequestBodyWithFilter = ({layerName, attribute, maxFeatures, startIndex, value}) => { + let requestBody = + ' ' + + ' gs:PagedUnique ' + + ' ' + + ' ' + + ' features ' + + ' features ' + + ' ' + + ' ' + + ' ' + + ' ' + + ' ' + + ' ' + + ' ' + + ' ' + attribute + '' + + ' *' + value + '*' + + ' ' + + ' ' + + ' ' + + ' ' + + ' ' + attribute + '' + + ' ' + + ' ' + + ' ' + + ' ' + + ' ' + + ' ' + + ' ' + + ' ' + + ' fieldName' + + ' fieldName' + + ' ' + + ' ' + attribute + '' + + ' ' + + ' ' + + ' ' + + ' maxFeatures' + + ' maxFeatures' + + ' ' + + ' ' + maxFeatures + '' + + ' ' + + ' ' + + ' ' + + ' startIndex' + + ' startIndex' + + ' ' + + ' ' + startIndex + '' + + ' ' + + ' ' + + ' ' + + ' ' + + ' ' + + ' result' + + ' ' + + ' ' + + ''; + return requestBody; +}; + +module.exports = { + getWpsPayload: (options) => options.value ? getRequestBodyWithFilter(options) : getRequestBody(options), + getRequestBodyWithFilter, + getRequestBody };