From 7425b0df0c071014420b1918f19b6437c16cba07 Mon Sep 17 00:00:00 2001
From: kappu <kappu72gmail.com>
Date: Thu, 18 May 2017 12:01:08 +0200
Subject: [PATCH] Add ui to configure text search services

---
 .../actions/__tests__/searchconfig-test.js    |  51 +++
 web/client/actions/searchconfig.js            |  53 +++
 web/client/api/Nominatim.js                   |   3 +-
 .../searchservicesconfig/ResultsProps.jsx     |  76 ++++
 .../searchservicesconfig/SearchServices.css   | 403 ++++++++++++++++++
 .../searchservicesconfig/ServicesList.jsx     |  81 ++++
 .../searchservicesconfig/WFSOptionalProps.jsx |  67 +++
 .../searchservicesconfig/WFSServiceProps.jsx  |  97 +++++
 .../__tests__/SearchServiceConfig-test.jsx    |  48 +++
 web/client/localConfig.json                   |   2 +-
 web/client/plugins/Save.jsx                   |   8 +-
 web/client/plugins/SaveAs.jsx                 |   8 +-
 web/client/plugins/Search.jsx                 |  19 +-
 web/client/plugins/SearchServicesConfig.jsx   | 211 +++++++++
 .../searchservicesconfig/ToggleButton.jsx     |  43 ++
 web/client/product/plugins.js                 |   3 +-
 .../reducers/__tests__/searchconfig-test.js   |  78 ++++
 web/client/reducers/searchconfig.js           |  45 ++
 web/client/translations/data.en-US            |  26 +-
 web/client/translations/data.it-IT            |  26 +-
 20 files changed, 1334 insertions(+), 14 deletions(-)
 create mode 100644 web/client/actions/__tests__/searchconfig-test.js
 create mode 100644 web/client/actions/searchconfig.js
 create mode 100644 web/client/components/mapcontrols/searchservicesconfig/ResultsProps.jsx
 create mode 100644 web/client/components/mapcontrols/searchservicesconfig/SearchServices.css
 create mode 100644 web/client/components/mapcontrols/searchservicesconfig/ServicesList.jsx
 create mode 100644 web/client/components/mapcontrols/searchservicesconfig/WFSOptionalProps.jsx
 create mode 100644 web/client/components/mapcontrols/searchservicesconfig/WFSServiceProps.jsx
 create mode 100644 web/client/components/mapcontrols/searchservicesconfig/__tests__/SearchServiceConfig-test.jsx
 create mode 100644 web/client/plugins/SearchServicesConfig.jsx
 create mode 100644 web/client/plugins/searchservicesconfig/ToggleButton.jsx
 create mode 100644 web/client/reducers/__tests__/searchconfig-test.js
 create mode 100644 web/client/reducers/searchconfig.js

diff --git a/web/client/actions/__tests__/searchconfig-test.js b/web/client/actions/__tests__/searchconfig-test.js
new file mode 100644
index 0000000000..e375c09a0a
--- /dev/null
+++ b/web/client/actions/__tests__/searchconfig-test.js
@@ -0,0 +1,51 @@
+/**
+* Copyright 2017, GeoSolutions Sas.
+* All rights reserved.
+*
+* This source code is licensed under the BSD-style license found in the
+* LICENSE file in the root directory of this source tree.
+*/
+
+var expect = require('expect');
+var {
+    SET_SEARCH_CONFIG_PROP,
+    RESET_SEARCH_CONFIG,
+    UPDATE_SERVICE,
+    setSearchConfigProp,
+    restServiceConfig,
+    updateService
+} = require('../searchconfig');
+
+describe('Test correctness of the searchconfig actions', () => {
+
+
+    it('resetServiceConfig', () => {
+        const testPage = 1;
+        var retval = restServiceConfig(testPage);
+
+        expect(retval).toExist();
+        expect(retval.type).toBe(RESET_SEARCH_CONFIG);
+        expect(retval.page).toBe(testPage);
+    });
+
+    it('setSearchConfigProp', () => {
+        const testProperty = 'prop';
+        const testValue = 'val';
+        var retval = setSearchConfigProp(testProperty, testValue);
+
+        expect(retval).toExist();
+        expect(retval.type).toBe(SET_SEARCH_CONFIG_PROP);
+        expect(retval.property).toBe(testProperty);
+        expect(retval.value).toBe(testValue);
+    });
+
+    it('updateService', () => {
+        const testService = "service";
+        const testIdx = 1;
+        var retval = updateService(testService, testIdx);
+        expect(retval).toExist();
+        expect(retval.type).toBe(UPDATE_SERVICE);
+        expect(retval.service).toBe(testService);
+        expect(retval.idx).toBe(testIdx);
+    });
+});
diff --git a/web/client/actions/searchconfig.js b/web/client/actions/searchconfig.js
new file mode 100644
index 0000000000..7c14dcc7a8
--- /dev/null
+++ b/web/client/actions/searchconfig.js
@@ -0,0 +1,53 @@
+/**
+ * Copyright 2017, GeoSolutions Sas.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree.
+ */
+
+const SET_SEARCH_CONFIG_PROP = 'SET_SEARCH_CONFIG_PROP';
+const RESET_SEARCH_CONFIG = 'RESET_SEARCH_CONFIG';
+const UPDATE_SERVICE = 'UPDATE_SERVICE';
+
+/**
+* Sets a property
+* @memberof actions.search
+* @param {string} property the property to set
+* @param {string|number|boolean|object} value the value to set or to check for toggling
+* @return {object} of type `SET_SEARCH_CONFIG_PROP` with property and value params
+*/
+function setSearchConfigProp(property, value) {
+    return {
+        type: SET_SEARCH_CONFIG_PROP,
+        property,
+        value
+    };
+}
+
+function restServiceConfig(page = 0 ) {
+    return {
+        type: RESET_SEARCH_CONFIG,
+        page
+    };
+}
+function updateService(service, idx = -1) {
+    return {
+        type: UPDATE_SERVICE,
+        service,
+        idx
+    };
+}
+
+/**
+* Actions for search
+* @name actions.searchconfig
+*/
+module.exports = {
+    SET_SEARCH_CONFIG_PROP,
+    RESET_SEARCH_CONFIG,
+    UPDATE_SERVICE,
+    setSearchConfigProp,
+    restServiceConfig,
+    updateService
+};
diff --git a/web/client/api/Nominatim.js b/web/client/api/Nominatim.js
index 474e11994b..11f2639bee 100644
--- a/web/client/api/Nominatim.js
+++ b/web/client/api/Nominatim.js
@@ -13,7 +13,8 @@ const DEFAULT_REVERSE_URL = 'nominatim.openstreetmap.org/reverse';
 const defaultOptions = {
     format: 'json',
     bounded: 0,
-    polygon_geojson: 1
+    polygon_geojson: 1,
+    priority: 5
 };
 /**
  * API for local config
diff --git a/web/client/components/mapcontrols/searchservicesconfig/ResultsProps.jsx b/web/client/components/mapcontrols/searchservicesconfig/ResultsProps.jsx
new file mode 100644
index 0000000000..5a7dfc6359
--- /dev/null
+++ b/web/client/components/mapcontrols/searchservicesconfig/ResultsProps.jsx
@@ -0,0 +1,76 @@
+/**
+ * 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 {FormGroup, ControlLabel, FormControl, Label} = require('react-bootstrap');
+const Message = require('../../I18N/Message');
+const Slider = require('react-nouislider');
+const assign = require('object-assign');
+function validate(service = {}) {
+    return service.displayName && service.displayName.length > 0;
+}
+const ResultsProps = React.createClass({
+    propTypes: {
+        service: React.PropTypes.object,
+        onPropertyChange: React.PropTypes.func
+    },
+    getDefaultProps() {
+        return {
+            service: {},
+            onPropertyChange: () => {}
+        };
+    },
+    render() {
+        const {service} = this.props;
+        return (
+            <form>
+                <span className="wfs-required-props-title"><Message msgId="search.s_result_props_label" /></span>
+                <FormGroup>
+                    <ControlLabel>
+                        <Message msgId="search.s_title" />
+                    </ControlLabel>
+                    <FormControl
+                    value={service.displayName}
+                    key="displayName"
+                    type="text"
+                    placeholder={'e.g. "${properties.name}\"'}
+                    onChange={this.updateProp.bind(null, "displayName")}/>
+                </FormGroup>
+                <FormGroup>
+                    <ControlLabel>
+                        <Message msgId="search.s_description" />
+                    </ControlLabel>
+                    <FormControl
+                    value={service.subTitle}
+                    key="subTitle"
+                    type="text"
+                    onChange={this.updateProp.bind(null, "subTitle")}/>
+                </FormGroup>
+                <FormGroup>
+                    <ControlLabel>
+                        <Message msgId="search.s_priority" />
+                        <Label key="priority-labeel" className="slider-label">{parseInt(service.priority || 1, 10)}</Label>
+                    </ControlLabel>
+                    <Slider key="priority" start={[service.priority || 1]}
+                        range={{min: 1, max: 10}}
+                        onSlide={this.updatePriority}
+                        />
+                    <span className="priority-info"><Message msgId="search.s_priority_info" /></span>
+                </FormGroup>
+            </form>);
+    },
+    updateProp(prop, event) {
+        const value = event.target.value;
+        this.props.onPropertyChange("service", assign({}, this.props.service, {[prop]: value}));
+    },
+    updatePriority(val) {
+        this.props.onPropertyChange("service", assign({}, this.props.service, {priority: parseFloat(val[0], 10)}));
+    }
+});
+
+module.exports = { Element: ResultsProps, validate};
diff --git a/web/client/components/mapcontrols/searchservicesconfig/SearchServices.css b/web/client/components/mapcontrols/searchservicesconfig/SearchServices.css
new file mode 100644
index 0000000000..4b583fa98d
--- /dev/null
+++ b/web/client/components/mapcontrols/searchservicesconfig/SearchServices.css
@@ -0,0 +1,403 @@
+/* all this css rules are related to the slider, copy paste from advanced layertree example */
+
+.noUi-target,
+.noUi-target * {
+-webkit-touch-callout: none;
+-webkit-user-select: none;
+-ms-touch-action: none;
+  touch-action: none;
+-ms-user-select: none;
+-moz-user-select: none;
+-moz-box-sizing: border-box;
+  box-sizing: border-box;
+}
+.noUi-target {
+  position: relative;
+  direction: ltr;
+}
+.noUi-base {
+  width: 100%;
+  height: 100%;
+  position: relative;
+  z-index: 1; /* Fix 401 */
+}
+.noUi-origin {
+  position: absolute;
+  right: 0;
+  top: 0;
+  left: 0;
+  bottom: 0;
+}
+.noUi-handle {
+  position: relative;
+  z-index: 1;
+}
+.noUi-stacking .noUi-handle {
+/* This class is applied to the lower origin when
+   its values is > 50%. */
+  z-index: 10;
+}
+.noUi-state-tap .noUi-origin {
+-webkit-transition: left 0.3s, top 0.3s;
+  transition: left 0.3s, top 0.3s;
+}
+.noUi-state-drag * {
+  cursor: inherit !important;
+}
+
+/* Painting and performance;
+ * Browsers can paint handles in their own layer.
+ */
+.noUi-base {
+  -webkit-transform: translate3d(0,0,0);
+  transform: translate3d(0,0,0);
+}
+
+/* Slider size and handle placement;
+ */
+.noUi-horizontal {
+  height: 18px;
+}
+.noUi-horizontal .noUi-handle {
+  width: 34px;
+  height: 28px;
+  left: -17px;
+  top: -6px;
+}
+.noUi-vertical {
+  width: 18px;
+}
+.noUi-vertical .noUi-handle {
+  width: 28px;
+  height: 34px;
+  left: -6px;
+  top: -17px;
+}
+
+/* Styling;
+ */
+.noUi-background {
+  background: #FAFAFA;
+  box-shadow: inset 0 1px 1px #f0f0f0;
+}
+.noUi-connect {
+  background: #3FB8AF;
+  box-shadow: inset 0 0 3px rgba(51,51,51,0.45);
+-webkit-transition: background 450ms;
+  transition: background 450ms;
+}
+.noUi-origin {
+  border-radius: 2px;
+}
+.noUi-target {
+  border-radius: 4px;
+  border: 1px solid #D3D3D3;
+  box-shadow: inset 0 1px 1px #F0F0F0, 0 3px 6px -5px #BBB;
+}
+.noUi-target.noUi-connect {
+  box-shadow: inset 0 0 3px rgba(51,51,51,0.45), 0 3px 6px -5px #BBB;
+}
+
+/* Handles and cursors;
+ */
+.noUi-draggable {
+  cursor: w-resize;
+}
+.noUi-vertical .noUi-draggable {
+  cursor: n-resize;
+}
+.noUi-handle {
+  border: 1px solid #D9D9D9;
+  border-radius: 3px;
+  background: #FFF;
+  cursor: default;
+  box-shadow: inset 0 0 1px #FFF,
+        inset 0 1px 7px #EBEBEB,
+        0 3px 6px -3px #BBB;
+}
+.noUi-active {
+  box-shadow: inset 0 0 1px #FFF,
+        inset 0 1px 7px #DDD,
+        0 3px 6px -3px #BBB;
+}
+
+/* Handle stripes;
+ */
+.noUi-handle:before,
+.noUi-handle:after {
+  content: "";
+  display: block;
+  position: absolute;
+  height: 14px;
+  width: 1px;
+  background: #E8E7E6;
+  left: 14px;
+  top: 6px;
+}
+.noUi-handle:after {
+  left: 17px;
+}
+.noUi-vertical .noUi-handle:before,
+.noUi-vertical .noUi-handle:after {
+  width: 14px;
+  height: 1px;
+  left: 6px;
+  top: 14px;
+}
+.noUi-vertical .noUi-handle:after {
+  top: 17px;
+}
+
+/* Disabled state;
+ */
+[disabled].noUi-connect,
+[disabled] .noUi-connect {
+  background: #B8B8B8;
+}
+[disabled].noUi-origin,
+[disabled] .noUi-handle {
+  cursor: not-allowed;
+}
+
+.noUi-tooltip {
+  display: block;
+  position: absolute;
+  border: 1px solid #D9D9D9;
+  border-radius: 3px;
+  background: #fff;
+  padding: 5px;
+  left: -9px;
+  text-align: center;
+  width: 50px;
+}
+
+.noUi-handle-lower .noUi-tooltip {
+  top: -32px;
+}
+
+.noUi-handle-upper .noUi-tooltip {
+  bottom: -32px;
+}
+
+/* Pips
+ */
+.noUi-pips,
+.noUi-pips * {
+-moz-box-sizing: border-box;
+  box-sizing: border-box;
+}
+.noUi-pips {
+  position: absolute;
+  color: #999;
+}
+
+/* Values;
+ *
+ */
+.noUi-value {
+  width: 40px;
+  position: absolute;
+  text-align: center;
+}
+.noUi-value-sub {
+  color: #ccc;
+  font-size: 10px;
+}
+
+/* Markings;
+ *
+ */
+.noUi-marker {
+  position: absolute;
+  background: #CCC;
+}
+.noUi-marker-sub {
+  background: #AAA;
+}
+.noUi-marker-large {
+  background: #AAA;
+}
+
+/* Horizontal layout;
+ *
+ */
+.noUi-pips-horizontal {
+  padding: 10px 0;
+  height: 50px;
+  top: 100%;
+  left: 0;
+  width: 100%;
+}
+.noUi-value-horizontal {
+  margin-left: -20px;
+  padding-top: 20px;
+}
+.noUi-value-horizontal.noUi-value-sub {
+  padding-top: 15px;
+}
+
+.noUi-marker-horizontal.noUi-marker {
+  margin-left: -1px;
+  width: 2px;
+  height: 5px;
+}
+.noUi-marker-horizontal.noUi-marker-sub {
+  height: 10px;
+}
+.noUi-marker-horizontal.noUi-marker-large {
+  height: 15px;
+}
+
+/* Vertical layout;
+ *
+ */
+.noUi-pips-vertical {
+  padding: 0 10px;
+  height: 100%;
+  top: 0;
+  left: 100%;
+}
+.noUi-value-vertical {
+  width: 15px;
+  margin-left: 20px;
+  margin-top: -5px;
+}
+
+.noUi-marker-vertical.noUi-marker {
+  width: 5px;
+  height: 2px;
+  margin-top: -1px;
+}
+.noUi-marker-vertical.noUi-marker-sub {
+  width: 10px;
+}
+.noUi-marker-vertical.noUi-marker-large {
+  width: 15px;
+}
+
+
+#search-serivices-ui .noUi-background {
+    background:#CCC;
+}
+
+#search-serivices-ui .noUi-horizontal {
+    height: 2px;
+    width: 260px;
+    margin:auto;
+}
+#search-serivices-ui .noUi-target {
+    position: relative;
+    direction: ltr;
+}
+
+#search-serivices-ui .noUi-base {
+    width: 100%;
+    height: 100%;
+    position: relative;
+    z-index: 1;
+}
+
+#search-serivices-ui .noUi-horizontal .noUi-handle {
+    width: 23px;
+    height: 23px;
+    top: -10px;
+}
+
+#search-serivices-ui .noUi-handle {
+    background: #078AA3;
+    cursor: default;
+    border-radius:50px;
+}
+
+#search-serivices-ui .noUi-handle {
+    position: relative;
+    z-index: 1;
+}
+#search-serivices-ui .noUi-target, .noUi-target * {
+    -webkit-touch-callout: none;
+    -webkit-user-select: none;
+    -ms-touch-action: none;
+    touch-action: none;
+    -ms-user-select: none;
+    -moz-user-select: none;
+    -moz-box-sizing: border-box;
+    box-sizing: border-box;
+}
+
+#search-serivices-ui .noUi-target {
+    position: relative;
+    direction: ltr;
+}
+
+#search-serivices-ui .noUi-target {
+    width: 94%;
+    margin-bottom: 10px;
+    margin-top: 10px
+}
+
+#search-serivices-ui .noUi-handle:before, #search-serivices-ui .noUi-handle:after {
+    background: none;
+}
+#search-serivices-ui .service-radio{
+    padding-left: 0
+}
+#search-serivices-ui .services-list{
+    height: 130px;
+    border-radius: 6px;
+    overflow: auto;
+    border: 1px solid rgba(128, 128, 128, 0.55);
+}
+.search-service-btn-group {
+    margin-bottom: 10px;
+    margin-top: 10px;
+}
+.services-panel .panel-body{
+    padding-top: 0px;
+}
+.services-list .glyphicon{
+    cursor: pointer;
+    padding: 2px;
+    color: #078aa3;
+    float: right;
+}
+.services-list .search-serivce-name{
+    text-transform: capitalize;
+    padding-left: 4px;
+}
+.search-service-item {
+   margin: 4px;
+   margin-left: 6px;
+   margin-right: 10px;
+}
+#search-serivices-ui .checkbox{
+    padding-left: 0px;
+}
+#search-serivices-ui .wfs-required-props-title{
+    margin-bottom: 15px;
+    display: inline-block;
+    margin-left: -10px;
+    margin-right: -10px;
+    width: calc(100% + 20px);
+    border-bottom: 1px solid gray;
+    font-size: larger;
+}
+.confirm-close{
+    position: absolute;
+    right: 10px;
+    top: 4px;
+    border-radius: 6px;
+}
+#search-serivices-ui .slider-label {
+    margin-left: 10px;
+    width: 20px;
+}
+.list-remove-btn {
+    float: right;
+    border: 0px;
+    background-color: transparent;
+}
+#search-serivices-ui .priority-info {
+    font-size: smaller;
+    display: inline-block;
+    padding-left: 2px;
+}
\ No newline at end of file
diff --git a/web/client/components/mapcontrols/searchservicesconfig/ServicesList.jsx b/web/client/components/mapcontrols/searchservicesconfig/ServicesList.jsx
new file mode 100644
index 0000000000..9c9bde3238
--- /dev/null
+++ b/web/client/components/mapcontrols/searchservicesconfig/ServicesList.jsx
@@ -0,0 +1,81 @@
+/**
+ * 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 {FormGroup, Checkbox, ControlLabel, Glyphicon} = require('react-bootstrap');
+const Message = require('../../I18N/Message');
+const ConfirmButton = require('../../buttons/ConfirmButton');
+
+function validate() {
+    return true;
+}
+
+const ServicesList = React.createClass({
+    propTypes: {
+        services: React.PropTypes.array,
+        override: React.PropTypes.bool,
+        service: React.PropTypes.object,
+        onPropertyChange: React.PropTypes.func
+    },
+    contextTypes: {
+        messages: React.PropTypes.object
+    },
+    getDefaultProps() {
+        return {
+            services: [],
+            override: false,
+            onPropertyChange: () => {}
+        };
+    },
+    getOptions() {
+        return this.props.services.map((s, idx) => {
+            return (
+                <div className="search-service-item" key={idx}>
+                    <span className="search-serivce-name">
+                        {s.name}
+                    </span>
+                    <ConfirmButton className="list-remove-btn" onConfirm={() => this.remove(idx)} text={<Glyphicon glyph="remove-circle" />} confirming={{className: "text-warning list-remove-btn", text: <Message msgId="search.confirmremove" />}}/>
+                        <Glyphicon onClick={() => this.edit(s, idx)} glyph="pencil"/>
+                </div>);
+        });
+    },
+    render() {
+        const {override} = this.props;
+        return (
+            <form>
+                <FormGroup>
+                    <ControlLabel>
+                        <Message msgId="search.serviceslistlabel" />
+                    </ControlLabel>
+                    <div className="services-list">
+                        {this.getOptions()}
+                    </div>
+                </FormGroup>
+                 <Checkbox checked={override} onChange={this.toggleOverride}>
+                    <Message msgId="search.overriedservice" />
+                </Checkbox>
+            </form>);
+    },
+    edit(s, idx) {
+        this.props.onPropertyChange("init_service_values", s);
+        this.props.onPropertyChange("service", s);
+        this.props.onPropertyChange("editIdx", idx);
+        this.props.onPropertyChange("page", 1);
+    },
+    toggleOverride() {
+        const {services, override} = this.props;
+        this.props.onPropertyChange("textSearchConfig", {services, override: !override});
+    },
+    remove(idx) {
+        const {services, override} = this.props;
+        const newServices = services.filter((el, i) => i !== idx);
+        this.props.onPropertyChange("textSearchConfig", {services: newServices, override});
+    }
+});
+
+module.exports = {Element: ServicesList, validate};
diff --git a/web/client/components/mapcontrols/searchservicesconfig/WFSOptionalProps.jsx b/web/client/components/mapcontrols/searchservicesconfig/WFSOptionalProps.jsx
new file mode 100644
index 0000000000..a3a4ca6f6f
--- /dev/null
+++ b/web/client/components/mapcontrols/searchservicesconfig/WFSOptionalProps.jsx
@@ -0,0 +1,67 @@
+/**
+ * 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 {FormGroup, ControlLabel, FormControl, Label} = require('react-bootstrap');
+const Message = require('../../I18N/Message');
+const Slider = require('react-nouislider');
+const assign = require('object-assign');
+function validate() {
+    return true;
+}
+const WFSOptionalProps = React.createClass({
+    propTypes: {
+        service: React.PropTypes.object,
+        onPropertyChange: React.PropTypes.func
+    },
+    getDefaultProps() {
+        return {
+            service: {},
+            onPropertyChange: () => {}
+        };
+    },
+    render() {
+        const {service} = this.props;
+        const {options = {}} = service;
+        return (
+            <form>
+              <span className="wfs-required-props-title"><Message msgId="search.s_wfs_opt_props_label" /></span>
+                <FormGroup>
+                    <ControlLabel>
+                        <Message msgId="search.s_sort" />
+                    </ControlLabel>
+                    <FormControl
+                        value={options.sortBy}
+                        key="sortBy"
+                        type="text"
+                        onChange={this.updateProp.bind(null, "sortBy")}/>
+                </FormGroup>
+                <FormGroup>
+                    <ControlLabel>
+                        <Message msgId="search.s_max_features" />
+                    </ControlLabel>
+                    <Slider key="maxFeatures" start={[options.maxFeatures || 1]}
+                        range={{min: 1, max: 50}}
+                        onSlide={this.updateMaxFeatures}
+                        />
+                    <Label key="maxFeatures-labeel" className="slider-label" >{options.maxFeatures || 1}</Label>
+                </FormGroup>
+            </form>);
+    },
+    updateProp(prop, event) {
+        const value = event.target.value;
+        const options = assign({}, this.props.service.options, {[prop]: value});
+        this.props.onPropertyChange("service", assign({}, this.props.service, {options}));
+    },
+    updateMaxFeatures(val) {
+        const options = assign({}, this.props.service.options, {maxFeatures: parseInt(val[0], 10)});
+        this.props.onPropertyChange("service", assign({}, this.props.service, {options}));
+    }
+});
+
+module.exports = {Element: WFSOptionalProps, validate};
diff --git a/web/client/components/mapcontrols/searchservicesconfig/WFSServiceProps.jsx b/web/client/components/mapcontrols/searchservicesconfig/WFSServiceProps.jsx
new file mode 100644
index 0000000000..e4433d7576
--- /dev/null
+++ b/web/client/components/mapcontrols/searchservicesconfig/WFSServiceProps.jsx
@@ -0,0 +1,97 @@
+/**
+ * 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 {FormGroup, ControlLabel, FormControl} = require('react-bootstrap');
+const Message = require('../../I18N/Message');
+const assign = require('object-assign');
+// const weburl = new RegExp(/((([A-Za-z]{3,9}:(?:\/\/)?)(?:[\-;:&=\+\$,\w]+@)?[A-Za-z0-9\.\-]+|(?:www\.|[\-;:&=\+\$,\w]+@)[A-Za-z0-9\.\-]+)((?:\/[\+~%\/\.\w\-_]*)?\??(?:[\-\+=&;%@\.\w_]*)#?(?:[\.\!\/\\\w]*))?)/);
+function validate(service = {}) {
+    const {options = {}, name = ''} = service;
+    const {url = '', typeName = '', queriableAttributes = ''} = options;
+    // const p = url.search(weburl);
+    return name.length > 0 && url.length > 0 && typeName.length > 0 && queriableAttributes.length > 0;
+}
+
+const WFSServiceProps = React.createClass({
+    propTypes: {
+        service: React.PropTypes.object,
+        onPropertyChange: React.PropTypes.func
+    },
+    getDefaultProps() {
+        return {
+            service: {},
+            onPropertyChange: () => {}
+        };
+    },
+    render() {
+        const {service} = this.props;
+        const {options = {}} = service;
+        return (
+            <form>
+              <span className="wfs-required-props-title"><Message msgId="search.s_wfs_props_label" /></span>
+                <FormGroup>
+                    <ControlLabel>
+                        <Message msgId="search.s_name" />
+                    </ControlLabel>
+                    <FormControl
+                        value={service.name}
+                        key="name"
+                        type="text"
+                        onChange={this.updateName}/>
+                </FormGroup>
+                <FormGroup>
+                    <ControlLabel>
+                        <Message msgId="search.s_url" />
+                    </ControlLabel>
+                    <FormControl
+                    value={options.url}
+                    key="url"
+                    type="text"
+                    onChange={this.updateProp.bind(null, "url")}/>
+                </FormGroup>
+                <FormGroup>
+                    <ControlLabel>
+                        <Message msgId="search.s_layer" />
+                    </ControlLabel>
+                    <FormControl
+                    value={options.typeName}
+                    key="typeName"
+                    type="text"
+                    onChange={this.updateProp.bind(null, "typeName")}/>
+                </FormGroup>
+                <FormGroup>
+                    <ControlLabel>
+                        <Message msgId="search.s_attributes" />
+                    </ControlLabel>
+                    <FormControl
+                    value={([options.queriableAttributes] || []).join(",")}
+                    key="queriableAttributes"
+                    type="text"
+                    onChange={this.updateProp.bind(null, "queriableAttributes")}/>
+                </FormGroup>
+            </form>);
+    },
+    updateProp(prop, event) {
+        let value = event.target.value;
+        if (prop === "queriableAttributes") {
+            value = value.split(",");
+        }
+        const options = assign({}, this.props.service.options, {[prop]: value} );
+        this.props.onPropertyChange("service", assign({}, this.props.service, {options}));
+    },
+    updateName(event) {
+        const value = event.target.value;
+        this.props.onPropertyChange("service", assign({}, this.props.service, {name: value}));
+    },
+    updateMaxFeatures(val) {
+        const options = assign({}, this.props.service.options, {maxFeatures: parseFloat(val[0], 10)});
+        this.props.onPropertyChange("service", assign({}, this.props.service, {options}));
+    }
+});
+
+module.exports = { Element: WFSServiceProps, validate};
diff --git a/web/client/components/mapcontrols/searchservicesconfig/__tests__/SearchServiceConfig-test.jsx b/web/client/components/mapcontrols/searchservicesconfig/__tests__/SearchServiceConfig-test.jsx
new file mode 100644
index 0000000000..4f86908c99
--- /dev/null
+++ b/web/client/components/mapcontrols/searchservicesconfig/__tests__/SearchServiceConfig-test.jsx
@@ -0,0 +1,48 @@
+/**
+* Copyright 2017, GeoSolutions Sas.
+* All rights reserved.
+*
+* This source code is licensed under the BSD-style license found in the
+* LICENSE file in the root directory of this source tree.
+*/
+
+var expect = require('expect');
+
+var React = require('react');
+var ReactDOM = require('react-dom');
+var ResultsProps = require('../ResultsProps');
+var ServicesList = require('../ServicesList');
+var WFSOptionalProps = require('../WFSOptionalProps');
+var WFSServiceProps = require('../WFSServiceProps');
+
+
+describe("test text search service config components", () => {
+    beforeEach((done) => {
+        document.body.innerHTML = '<div id="container"></div>';
+        setTimeout(done);
+    });
+
+    afterEach((done) => {
+        ReactDOM.unmountComponentAtNode(document.getElementById("container"));
+        document.body.innerHTML = '';
+        setTimeout(done);
+    });
+
+    it('test Result Props creation', () => {
+        const tb = ReactDOM.render(<ResultsProps.Element/>, document.getElementById("container"));
+        expect(tb).toExist();
+    });
+    it('test Services List creation', () => {
+        const tb = ReactDOM.render(<ServicesList.Element/>, document.getElementById("container"));
+        expect(tb).toExist();
+    });
+    it('test WFS optional props creation', () => {
+        const tb = ReactDOM.render(<WFSOptionalProps.Element/>, document.getElementById("container"));
+        expect(tb).toExist();
+    });
+    it('test WFS serivece props creation', () => {
+        const tb = ReactDOM.render(<WFSServiceProps.Element/>, document.getElementById("container"));
+        expect(tb).toExist();
+    });
+
+});
diff --git a/web/client/localConfig.json b/web/client/localConfig.json
index f82e0dacd0..b7f057ec54 100644
--- a/web/client/localConfig.json
+++ b/web/client/localConfig.json
@@ -196,7 +196,7 @@
                     }
                 }
             },
-            "OmniBar", "Login", "Save", "SaveAs", "BurgerMenu", "Expander", "Undo", "Redo", "FullScreen", "GlobeViewSwitcher"
+            "OmniBar", "Login", "Save", "SaveAs", "BurgerMenu", "Expander", "Undo", "Redo", "FullScreen", "GlobeViewSwitcher", "SearchServicesConfig"
         ],
         "embedded": [{
                 "name": "Map",
diff --git a/web/client/plugins/Save.jsx b/web/client/plugins/Save.jsx
index 4f041710fe..b5e142d4b0 100644
--- a/web/client/plugins/Save.jsx
+++ b/web/client/plugins/Save.jsx
@@ -29,7 +29,8 @@ const selector = createSelector(mapSelector, stateSelector, layersSelector, (map
     show: state.controls && state.controls.save && state.controls.save.enabled,
     map,
     mapId: map && map.mapId,
-    layers
+    layers,
+    textSearchConfig: state.searchconfig && state.searchconfig.textSearchConfig
 }));
 
 const Save = React.createClass({
@@ -41,7 +42,8 @@ const Save = React.createClass({
         loadMapInfo: React.PropTypes.func,
         map: React.PropTypes.object,
         layers: React.PropTypes.array,
-        params: React.PropTypes.object
+        params: React.PropTypes.object,
+        textSearchConfig: React.PropTypes.object
     },
     getDefaultProps() {
         return {
@@ -91,7 +93,7 @@ const Save = React.createClass({
                 let resultingmap = {
                     version: 2,
                     // layers are defined inside the map object
-                    map: assign({}, map, {layers})
+                    map: assign({}, map, {layers, text_serch_config: this.props.textSearchConfig})
                 };
                 this.props.onMapSave(this.props.mapId, JSON.stringify(resultingmap));
                 this.props.onClose();
diff --git a/web/client/plugins/SaveAs.jsx b/web/client/plugins/SaveAs.jsx
index d2b7038f42..e17f1b42df 100644
--- a/web/client/plugins/SaveAs.jsx
+++ b/web/client/plugins/SaveAs.jsx
@@ -33,7 +33,8 @@ const selector = createSelector(mapSelector, stateSelector, layersSelector, (map
     user: state.security && state.security.user,
     currentMap: state.currentMap,
     metadata: state.maps.metadata,
-    layers
+    layers,
+    textSearchConfig: state.searchconfig && state.searchconfig.textSearchConfig
 }));
 
 const SaveAs = React.createClass({
@@ -57,7 +58,8 @@ const SaveAs = React.createClass({
         resetCurrentMap: React.PropTypes.func,
         metadataChanged: React.PropTypes.func,
         onMapSave: React.PropTypes.func,
-        loadMapInfo: React.PropTypes.func
+        loadMapInfo: React.PropTypes.func,
+        textSearchConfig: React.PropTypes.object
     },
     contextTypes: {
         router: React.PropTypes.object
@@ -127,7 +129,7 @@ const SaveAs = React.createClass({
         let resultingmap = {
             version: 2,
             // layers are defined inside the map object
-            map: assign({}, map, {layers})
+            map: assign({}, map, {layers, text_serch_config: this.props.textSearchConfig})
         };
         return resultingmap;
     },
diff --git a/web/client/plugins/Search.jsx b/web/client/plugins/Search.jsx
index f7a7b587d8..65278967ee 100644
--- a/web/client/plugins/Search.jsx
+++ b/web/client/plugins/Search.jsx
@@ -124,21 +124,25 @@ const ToggleButton = require('./searchbar/ToggleButton');
 const SearchPlugin = connect((state) => ({
     enabled: state.controls && state.controls.search && state.controls.search.enabled || false,
     selectedServices: state && state.search && state.search.selectedServices,
-    selectedItems: state && state.search && state.search.selectedItems
+    selectedItems: state && state.search && state.search.selectedItems,
+    textSearchConfig: state && state.searchconfig && state.searchconfig.textSearchConfig
 }))(React.createClass({
     propTypes: {
         fitResultsToMapSize: React.PropTypes.bool,
         searchOptions: React.PropTypes.object,
         selectedItems: React.PropTypes.array,
         selectedServices: React.PropTypes.array,
+        userServices: React.PropTypes.array,
         withToggle: React.PropTypes.oneOfType([React.PropTypes.bool, React.PropTypes.array]),
-        enabled: React.PropTypes.bool
+        enabled: React.PropTypes.bool,
+        textSearchConfig: React.PropTypes.object
     },
     getDefaultProps() {
         return {
             searchOptions: {
                 services: [{type: "nominatim"}]
             },
+            userServices: [],
             fitResultsToMapSize: true,
             withToggle: false,
             enabled: true
@@ -147,8 +151,17 @@ const SearchPlugin = connect((state) => ({
     getServiceOverrides( propSelector ) {
         return this.props.selectedItems && this.props.selectedItems[this.props.selectedItems.length - 1] && get(this.props.selectedItems[this.props.selectedItems.length - 1], propSelector);
     },
+    getSearchOptions() {
+        const{ searchOptions, textSearchConfig} = this.props;
+        if (textSearchConfig && textSearchConfig.services && textSearchConfig.services.length > 0) {
+            return textSearchConfig.override ? assign({}, searchOptions, {services: textSearchConfig.services}) : assign({}, searchOptions, {services: searchOptions.services.concat(textSearchConfig.services)});
+        }
+        return searchOptions;
+    },
     getCurrentServices() {
-        return this.props.selectedServices && this.props.selectedServices.length > 0 ? assign({}, this.props.searchOptions, {services: this.props.selectedServices}) : this.props.searchOptions;
+        const {selectedServices} = this.props;
+        const searchOptions = this.getSearchOptions();
+        return selectedServices && selectedServices.length > 0 ? assign({}, searchOptions, {services: selectedServices}) : searchOptions;
     },
     getSearchAndToggleButton() {
         const search = (<SearchBar
diff --git a/web/client/plugins/SearchServicesConfig.jsx b/web/client/plugins/SearchServicesConfig.jsx
new file mode 100644
index 0000000000..7a6de0d123
--- /dev/null
+++ b/web/client/plugins/SearchServicesConfig.jsx
@@ -0,0 +1,211 @@
+/**
+* Copyright 2017, GeoSolutions Sas.
+* All rights reserved.
+*
+* This source code is licensed under the BSD-style license found in the
+* LICENSE file in the root directory of this source tree.
+*/
+
+const React = require('react');
+const {connect} = require('react-redux');
+const assign = require('object-assign');
+const {Glyphicon, Button} = require('react-bootstrap');
+const ConfirmButton = require('../components/buttons/ConfirmButton');
+const Dialog = require('../components//misc/Dialog');
+const Portal = require('../components/misc/Portal');
+const Message = require('./locale/Message');
+const {isEqual} = require('lodash');
+const {toggleControl} = require('../actions/controls');
+const {setSearchConfigProp, updateService, restServiceConfig} = require('../actions/searchconfig');
+
+require('../components/mapcontrols/searchservicesconfig/SearchServices.css');
+
+const SearchServicesButton = require('./searchservicesconfig/ToggleButton');
+const ServiceList = require('../components/mapcontrols/searchservicesconfig/ServicesList.jsx');
+const WFSServiceProps = require('../components/mapcontrols/searchservicesconfig/WFSServiceProps.jsx');
+const ResultsProps = require('../components/mapcontrols/searchservicesconfig/ResultsProps.jsx');
+const WFSOptionalProps = require('../components/mapcontrols/searchservicesconfig/WFSOptionalProps.jsx');
+
+
+const SearchServicesConfigPanel = React.createClass({
+    propTypes: {
+        id: React.PropTypes.string,
+        enabled: React.PropTypes.bool,
+        panelStyle: React.PropTypes.object,
+        panelClassName: React.PropTypes.string,
+        closeGlyph: React.PropTypes.string,
+        titleText: React.PropTypes.oneOfType([React.PropTypes.string, React.PropTypes.element]),
+        toggleControl: React.PropTypes.func,
+        pages: React.PropTypes.arrayOf(React.PropTypes.shape({
+                Element: React.PropTypes.func.isRequired,
+                validate: React.PropTypes.func.isRequired
+        })),
+        page: React.PropTypes.number,
+        service: React.PropTypes.object,
+        initServiceValues: React.PropTypes.object,
+        onPropertyChange: React.PropTypes.func,
+        newService: React.PropTypes.object.isRequired,
+        updateService: React.PropTypes.func,
+        restServiceConfig: React.PropTypes.func,
+        textSearchConfig: React.PropTypes.object,
+        editIdx: React.PropTypes.number
+    },
+    getDefaultProps() {
+        return {
+            id: "SearchServicesConfigUI",
+            enabled: false,
+            panelStyle: {
+                minWidth: "400px",
+                zIndex: 2000,
+                position: "absolute",
+                // overflow: "auto",
+                top: "100px",
+                minHeight: "300px",
+                left: "calc(50% - 150px)",
+                backgroundColor: "white"
+            },
+            panelClassName: "toolbar-panel",
+            closeGlyph: "1-close",
+            titleText: <Message msgId="search.configpaneltitle" />,
+            closePanel: () => {},
+            onPropertyChange: () => {},
+            page: 0,
+            newService: {
+                type: "wfs",
+                name: "",
+                displayName: "",
+                subTitle: "",
+                priority: 1,
+                options: {
+                    url: "",
+                    typeName: "",
+                    queriableAttributes: "",
+                    sortBy: "",
+                    maxFeatures: 5,
+                    srsName: "EPSG:4326"}
+            }
+        };
+    },
+    canProceed() {
+        const {page, pages, service} = this.props;
+        return pages[page].validate(service);
+    },
+    isDirty() {
+        const {service, initServiceValues} = this.props;
+        return !isEqual(service, initServiceValues);
+    },
+    renderFooter() {
+        const {page, pages} = this.props;
+        if (page === 0) {
+            return (
+                <span role="footer">
+                    <Button onClick={this.addService} bsStyle="primary">
+                        <Message msgId="search.addbtn" />
+                    </Button>
+                </span>);
+        }else if (page === pages.length - 1) {
+            return (
+                <span role="footer">
+                    <Button onClick={this.prev} bsStyle="primary">
+                        <Message msgId="search.prevbtn" />
+                    </Button>
+                    <Button disabled={!this.canProceed()} onClick={this.update} bsStyle="primary">
+                        <Message msgId="search.savebtn" />
+                    </Button>
+                </span>);
+        }
+        return (
+            <span role="footer">
+                {page === 1 && this.isDirty() ? (
+                    <ConfirmButton onConfirm={this.prev} bsStyle="primary"
+                    confirming={{text: <Message msgId="search.cancelconfirm" />}}
+                        text={(<Message msgId="search.cancelbtn" />)}/>
+                    ) : (
+                    <Button onClick={this.prev} bsStyle="primary">
+                        <Message msgId={page === 1 ? "search.cancelbtn" : "search.prevbtn"} />
+                    </Button>)
+                }
+                <Button disabled={!this.canProceed()} onClick={this.next} bsStyle="primary">
+                    <Message msgId="search.nextbtn" />
+                </Button>
+            </span>);
+    },
+    render() {
+        const {enabled, pages, page, id, panelStyle, panelClassName, titleText, closeGlyph, onPropertyChange, service, textSearchConfig = {}} = this.props;
+        const Section = pages && pages[page] || null;
+        return enabled ? (
+            <Portal>
+                <Dialog id={id} style={panelStyle} className={panelClassName}>
+                    <span role="header">
+                        <span>{titleText}</span>
+                        { this.isDirty() ? (
+                            <ConfirmButton className="close" confirming={{
+                                text: <Message msgId="search.cancelbtn" />, className: "btn btn-sm btn-warning confirm-close"}} onConfirm={this.onClose} bsStyle="primary" text={(<Glyphicon glyph={closeGlyph}/>)}/>) : <button onClick={this.onClose} className="close">{closeGlyph ? <Glyphicon glyph={closeGlyph}/> : <span>×</span>}</button>}
+                            }
+                    </span>
+                    <div role="body" id="search-serivices-ui">
+                        <Section.Element services={textSearchConfig.services} override={textSearchConfig.override} onPropertyChange={onPropertyChange} service={service}/>
+                    </div>
+                    {this.renderFooter()}
+                </Dialog>
+            </Portal>) : null;
+    },
+    onClose() {
+        this.props.toggleControl("searchservicesconfig");
+        this.props.restServiceConfig(0);
+        this.props.toggleControl("settings");
+    },
+    addService() {
+        const {newService} = this.props;
+        this.props.onPropertyChange("init_service_values", newService);
+        this.props.onPropertyChange("service", newService);
+        this.props.onPropertyChange("page", 1);
+    },
+    prev() {
+        const {page} = this.props;
+        if (page > 1) {
+            this.props.onPropertyChange("page", page - 1);
+        }else if (page === 1 ) {
+            this.props.restServiceConfig(0);
+        }
+    },
+    next() {
+        const {page, pages} = this.props;
+        if (page < pages.length - 1) {
+            this.props.onPropertyChange("page", page + 1);
+        }
+    },
+    update() {
+        const {service, editIdx} = this.props;
+        this.props.updateService(service, editIdx);
+    }
+});
+
+const SearchServicesPlugin = connect(({controls = {}, searchconfig = {}}) => ({
+    enabled: controls.searchservicesconfig && controls.searchservicesconfig.enabled || false,
+    pages: [ServiceList, WFSServiceProps, ResultsProps, WFSOptionalProps],
+    page: searchconfig && searchconfig.page || 0,
+    service: searchconfig && searchconfig.service,
+    initServiceValues: searchconfig && searchconfig.init_service_values,
+    textSearchConfig: searchconfig && searchconfig.textSearchConfig,
+    editIdx: searchconfig && searchconfig.editIdx
+    }), {
+        toggleControl,
+        onPropertyChange: setSearchConfigProp,
+        restServiceConfig,
+        updateService})(SearchServicesConfigPanel);
+
+module.exports = {
+    SearchServicesConfigPlugin: assign(SearchServicesPlugin, {
+        Settings: {
+            tool:
+                <SearchServicesButton
+                text={<Message msgId="search.searchservicesbutton" />}
+            />,
+            position: 4
+        }
+    }),
+    reducers: {
+        searchconfig: require('../reducers/searchconfig')
+    }
+};
diff --git a/web/client/plugins/searchservicesconfig/ToggleButton.jsx b/web/client/plugins/searchservicesconfig/ToggleButton.jsx
new file mode 100644
index 0000000000..b52ed3bb46
--- /dev/null
+++ b/web/client/plugins/searchservicesconfig/ToggleButton.jsx
@@ -0,0 +1,43 @@
+/**
+ * Copyright 2017, GeoSolutions Sas.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree.
+ */
+const React = require('react');
+const {connect} = require('react-redux');
+const {toggleControl} = require('../../actions/controls');
+const {FormGroup} = require('react-bootstrap');
+const ToggleBtn = require('../../components/buttons/ToggleButton');
+const ToggleServicesConfig = React.createClass({
+    propTypes: {
+        toggleControl: React.PropTypes.func,
+        enabled: React.PropTypes.bool
+    },
+    onClick() {
+        if (!this.props.enabled) {
+            this.props.toggleControl("settings");
+            this.props.toggleControl("searchservicesconfig");
+        }
+    },
+    render() {
+        return (
+            <FormGroup>
+                <ToggleBtn key="searchservicesconfig"
+                    isButton={true} {...this.props} onClick={this.onClick}/>
+            </FormGroup>);
+    }
+});
+
+module.exports = connect((state) => ({
+    enabled: state.controls && state.controls.searchservicesconfig && state.controls.searchservicesconfig.enabled || false,
+    pressedStyle: "default",
+    defaultStyle: "primary",
+    btnConfig: {
+        bsSize: "small"}
+}), {
+    toggleControl: toggleControl
+})(ToggleServicesConfig);
+
+
diff --git a/web/client/product/plugins.js b/web/client/product/plugins.js
index d059277912..9cc9334787 100644
--- a/web/client/product/plugins.js
+++ b/web/client/product/plugins.js
@@ -68,7 +68,8 @@ module.exports = {
         ScrollTopPlugin: require('../plugins/ScrollTop'),
         GoFull: require('../plugins/GoFull'),
         GlobeViewSwitcherPlugin: require('../plugins/GlobeViewSwitcher'),
-        VersionPlugin: require('../plugins/Version')
+        VersionPlugin: require('../plugins/Version'),
+        SearchServicesConfigPlugin: require('../plugins/SearchServicesConfig')
     },
     requires: {
         ReactSwipe: require('react-swipeable-views').default,
diff --git a/web/client/reducers/__tests__/searchconfig-test.js b/web/client/reducers/__tests__/searchconfig-test.js
new file mode 100644
index 0000000000..580d875e64
--- /dev/null
+++ b/web/client/reducers/__tests__/searchconfig-test.js
@@ -0,0 +1,78 @@
+/**
+* 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 searchconfig = require('../searchconfig');
+const {SET_SEARCH_CONFIG_PROP, RESET_SEARCH_CONFIG, UPDATE_SERVICE} = require('../../actions/searchconfig');
+
+describe('Test the searchconfig reducer', () => {
+    it('Map config loaded with textSearchConfig', () => {
+
+        const action = {type: 'MAP_CONFIG_LOADED',
+        config: { version: 2, map: {layers: [], text_serch_config: {override: true}}}};
+
+        const state = searchconfig({}, action);
+        expect(state.textSearchConfig).toExist();
+        expect(state.textSearchConfig.override).toBe(true);
+    });
+    it('Map config loaded without textSearchConfig', () => {
+
+        const action = {type: 'MAP_CONFIG_LOADED',
+        config: { version: 2, map: {layers: []}}};
+
+        const state = searchconfig({}, action);
+        expect(state.textSearchConfig).toBe(undefined);
+    });
+    it('reset searchconfig state', () => {
+        const state = searchconfig(
+            {service: "test", page: 1, init_service_values: "test", editIdx: 2, textSearchConfig: {}}
+            , {
+            type: RESET_SEARCH_CONFIG,
+            page: 0
+        });
+        expect(state.page).toBe(0);
+        expect(state.service).toBe(undefined);
+        expect(state.init_service_values).toBe(undefined);
+        expect(state.editIdx).toBe(undefined);
+        expect(state.textSearchConfig).toExist();
+    });
+
+    it('test service update', () => {
+        const state = searchconfig({
+            textSearchConfig: {services: [{name: "test"}]}
+        }, {
+            type: UPDATE_SERVICE,
+            service: {name: "changed"},
+            idx: 0
+        });
+        expect(state.textSearchConfig).toExist();
+        expect(state.textSearchConfig.services[0].name).toBe("changed");
+    });
+    it('test service add', () => {
+        const state = searchconfig({
+            textSearchConfig: {services: [{name: "test"}]}
+        }, {
+            type: UPDATE_SERVICE,
+            service: {name: "changed"},
+            idx: -1
+        });
+        expect(state.textSearchConfig).toExist();
+        expect(state.textSearchConfig.services[1].name).toBe("changed");
+    });
+
+    it('set a search config property', () => {
+        const state = searchconfig({}, {
+            type: SET_SEARCH_CONFIG_PROP,
+            property: "prop",
+            value: 'val'
+        });
+        expect(state.prop).toExist();
+        expect(state.prop).toBe('val');
+    });
+
+});
diff --git a/web/client/reducers/searchconfig.js b/web/client/reducers/searchconfig.js
new file mode 100644
index 0000000000..d4fd9726c0
--- /dev/null
+++ b/web/client/reducers/searchconfig.js
@@ -0,0 +1,45 @@
+/**
+* Copyright 2017, GeoSolutions Sas.
+* All rights reserved.
+*
+* This source code is licensed under the BSD-style license found in the
+* LICENSE file in the root directory of this source tree.
+*/
+var {
+    SET_SEARCH_CONFIG_PROP, RESET_SEARCH_CONFIG,
+    UPDATE_SERVICE} = require('../actions/searchconfig');
+var {RESET_CONTROLS} = require('../actions/controls');
+var {MAP_CONFIG_LOADED} = require('../actions/config');
+const assign = require('object-assign');
+
+function searchconfig(state = null, action) {
+    switch (action.type) {
+        case SET_SEARCH_CONFIG_PROP:
+            return assign({}, state, {
+                    [action.property]: action.value
+                });
+        case MAP_CONFIG_LOADED: {
+            const textSearchConfig = action.config.map.text_serch_config;
+            return assign({}, state, {textSearchConfig});
+        }
+        case RESET_CONTROLS:
+        case RESET_SEARCH_CONFIG: {
+            return assign({}, state, {service: undefined, page: action.page, init_service_values: undefined, editIdx: undefined});
+        }
+        case UPDATE_SERVICE: {
+            let newServices = (state.textSearchConfig && state.textSearchConfig.services || []).slice();
+            // Convert priority to int
+            const newService = assign({}, action.service, {priority: parseInt(action.service.priority, 10)});
+            if (action.idx !== -1) {
+                newServices[action.idx] = newService;
+            }else {
+                newServices.push(newService);
+            }
+            return assign({}, state, {service: undefined, page: 0, init_service_values: undefined, editIdx: undefined, textSearchConfig: {services: newServices, override: state.textSearchConfig && state.textSearchConfig.override || false}});
+        }
+        default:
+            return state;
+    }
+}
+
+module.exports = searchconfig;
diff --git a/web/client/translations/data.en-US b/web/client/translations/data.en-US
index fae566375f..9d09cd0dba 100644
--- a/web/client/translations/data.en-US
+++ b/web/client/translations/data.en-US
@@ -266,7 +266,31 @@
             "bearingLabel": "Bearing"
         },
         "search":{
-            "placeholder": "Search by location name or coordinates ..."
+            "placeholder": "Search by location name or coordinates ...",
+            "searchservicesbutton": "Configure search services",
+            "configpaneltitle": "Create/edit a search service",
+            "serviceslistlabel": "Available services",
+            "overriedservice": "Overried services",
+            "addbtn": "Add",
+            "nextbtn": "Next",
+            "prevbtn": "Back",
+            "savebtn": "Update",
+            "cancelbtn": "Cancel",
+            "confirmremove": "Delete?",
+            "cancelconfirm": "Confirm Cancel?",
+            "s_name": "Name",
+            "s_title": "Title",
+            "s_description": "Description",
+            "s_priority": "Priority",
+            "s_url": "Service url",
+            "s_layer": "Layer",
+            "s_attributes": "Attributes",
+            "s_sort": "Sort by",
+            "s_max_features": "Max features",
+            "s_wfs_props_label" : "WFS service props",
+            "s_wfs_opt_props_label" : "Optional props",
+            "s_result_props_label": "Result display properties",
+            "s_priority_info": "Used to sort search results, higher values first. Nominatim  results have priority = 5"
         },
         "drawLocal": {
             "draw": {
diff --git a/web/client/translations/data.it-IT b/web/client/translations/data.it-IT
index 57f5414aca..b976a16246 100644
--- a/web/client/translations/data.it-IT
+++ b/web/client/translations/data.it-IT
@@ -266,7 +266,31 @@
             "bearingLabel": "Direzione"
         },
         "search":{
-            "placeholder": "Cerca un indirizzo o inserisci coordinate..."
+            "placeholder": "Cerca un indirizzo o inserisci coordinate...",
+            "searchservicesbutton": "Configura servizi di ricerca",
+            "configpaneltitle": "Aggiungi/modifica un servizio di ricerca",
+            "serviceslistlabel": "Servizi disponibili",
+            "overriedservice": "Sovrascrivi servizi",
+            "addbtn": "Aggiungi",
+            "cancelbtn": "Annulla",
+            "confirmremove": "Elimina?",
+            "cancelconfirm": "Confermi annulla?",
+            "nextbtn": "Avanti",
+            "prevbtn": "Precedente",
+            "savebtn": "Aggiorna",
+            "s_name": "Name",
+            "s_title": "Titolo",
+            "s_description": "Descrizione",
+            "s_priority": "Priorità",
+            "s_url": "Url servizio",
+            "s_layer": "Nome layer",
+            "s_attributes": "Attributi",
+            "s_sort": "Ordina per",
+            "s_max_features": "Max features",
+            "s_wfs_props_label" : "Proprietà WFS service",
+            "s_wfs_opt_props_label" : "Proprietà facoltative",
+            "s_result_props_label": "Proprietà di visualizzazione dei risultati",
+            "s_priority_info": "Usato per oridnare resultati, valori più alti per primi. Nominatim  ha priorità = 5"
         },
         "drawLocal": {
             "draw": {