Skip to content

Implementation of nested search services #1122 #1523

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 21 additions & 1 deletion web/client/actions/__tests__/search-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,15 @@ var {
TEXT_SEARCH_ERROR,
TEXT_SEARCH_STARTED,
TEXT_SEARCH_ITEM_SELECTED,
TEXT_SEARCH_NESTED_SERVICES_SELECTED,
TEXT_SEARCH_CANCEL_ITEM,
searchResultLoaded,
searchTextLoading,
searchResultError,
textSearch,
selectSearchItem
selectSearchItem,
selectNestedService,
cancelSelectedItem
} = require('../search');

describe('Test correctness of the search actions', () => {
Expand Down Expand Up @@ -58,5 +62,21 @@ describe('Test correctness of the search actions', () => {
expect(retval.item).toEqual("A");
expect(retval.mapConfig).toBe("B");
});
it('serch item cancelled', () => {
const retval = cancelSelectedItem("ITEM");
expect(retval).toExist();
expect(retval.type).toBe(TEXT_SEARCH_CANCEL_ITEM);
expect(retval.item).toEqual("ITEM");
});
it('serch nested service selected', () => {
const items = [{text: "TEXT"}];
const services = [{type: "wfs"}, {type: "wms"}];
const retval = selectNestedService(services, items, "TEST");
expect(retval).toExist();
expect(retval.type).toBe(TEXT_SEARCH_NESTED_SERVICES_SELECTED);
expect(retval.items).toEqual(items);
expect(retval.services).toEqual(services);
});


});
23 changes: 21 additions & 2 deletions web/client/actions/search.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,9 @@ const TEXT_SEARCH_RESET = 'TEXT_SEARCH_RESET';
const TEXT_SEARCH_ADD_MARKER = 'TEXT_SEARCH_ADD_MARKER';
const TEXT_SEARCH_TEXT_CHANGE = 'TEXT_SEARCH_TEXT_CHANGE';
const TEXT_SEARCH_LOADING = 'TEXT_SEARCH_LOADING';
const TEXT_SEARCH_NESTED_SERVICES_SELECTED = 'TEXT_SEARCH_NESTED_SERVICE_SELECTED';
const TEXT_SEARCH_ERROR = 'TEXT_SEARCH_ERROR';

const TEXT_SEARCH_CANCEL_ITEM = 'TEXT_SEARCH_CANCEL_ITEM';
const TEXT_SEARCH_ITEM_SELECTED = 'TEXT_SEARCH_ITEM_SELECTED';

function searchResultLoaded(results, append=false, services) {
Expand Down Expand Up @@ -83,7 +84,21 @@ function selectSearchItem(item, mapConfig) {
};

}
function selectNestedService(services, items, searchText) {
return {
type: TEXT_SEARCH_NESTED_SERVICES_SELECTED,
searchText,
services,
items
};
}

function cancelSelectedItem(item) {
return {
type: TEXT_SEARCH_CANCEL_ITEM,
item
};
}

module.exports = {
TEXT_SEARCH_STARTED,
Expand All @@ -96,6 +111,8 @@ module.exports = {
TEXT_SEARCH_ADD_MARKER,
TEXT_SEARCH_TEXT_CHANGE,
TEXT_SEARCH_ITEM_SELECTED,
TEXT_SEARCH_NESTED_SERVICES_SELECTED,
TEXT_SEARCH_CANCEL_ITEM,
searchTextLoading,
searchResultError,
searchResultLoaded,
Expand All @@ -104,5 +121,7 @@ module.exports = {
resetSearch,
addMarker,
searchTextChanged,
selectSearchItem
selectNestedService,
selectSearchItem,
cancelSelectedItem
};
6 changes: 3 additions & 3 deletions web/client/api/searchText.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,18 +20,18 @@ const toNominatim = (fc) =>
*/

module.exports = {
nominatim: (searchText, {options = null} = {}) =>
nominatim: (searchText, options = {}) =>
require('./Nominatim')
.geocode(searchText, options)
.then( res => GeoCodeUtils.nominatimToGeoJson(res.data)),
wfs: (searchText, {url, typeName, queriableAttributes, outputFormat="application/json", predicate ="ILIKE", ...params }) => {
wfs: (searchText, {url, typeName, queriableAttributes, outputFormat="application/json", predicate ="ILIKE", staticFilter="", ...params }) => {
return WFS
.getFeatureSimple(url, assign({
maxFeatures: 10,
startIndex: 0,
typeName,
outputFormat,
cql_filter: queriableAttributes.map( attr => `${attr} ${predicate} '%${searchText}%'`).join(' OR ')
cql_filter: queriableAttributes.map( attr => `${attr} ${predicate} '%${searchText}%'`).join(' OR ').concat(staticFilter)
}, params))
.then( response => response.features );
}
Expand Down
49 changes: 43 additions & 6 deletions web/client/components/mapcontrols/search/SearchBar.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -33,13 +33,16 @@ let SearchBar = React.createClass({
onSearchReset: React.PropTypes.func,
onPurgeResults: React.PropTypes.func,
onSearchTextChange: React.PropTypes.func,
onCancelSelectedItem: React.PropTypes.func,
placeholder: React.PropTypes.string,
placeholderMsgId: React.PropTypes.string,
delay: React.PropTypes.number,
hideOnBlur: React.PropTypes.bool,
blurResetDelay: React.PropTypes.number,
typeAhead: React.PropTypes.bool,
searchText: React.PropTypes.string,
selectedItems: React.PropTypes.array,
autoFocusOnSelect: React.PropTypes.bool,
loading: React.PropTypes.bool,
error: React.PropTypes.object,
style: React.PropTypes.object,
Expand All @@ -54,14 +57,27 @@ let SearchBar = React.createClass({
onSearchReset: () => {},
onPurgeResults: () => {},
onSearchTextChange: () => {},
onCancelSelectedItem: () => {},
selectedItems: [],
placeholderMsgId: "search.placeholder",
delay: 1000,
blurResetDelay: 300,
autoFocusOnSelect: true,
hideOnBlur: true,
typeAhead: true,
searchText: ""
};
},
componentDidUpdate(prevProps) {
let shouldFocus = this.props.autoFocusOnSelect && this.props.selectedItems &&
(
(prevProps.selectedItems && prevProps.selectedItems.length < this.props.selectedItems.length)
|| (!prevProps.selectedItems && this.props.selectedItems.length === 1)
);
if (shouldFocus) {
this.focusToInput();
}
},
onChange(e) {
var text = e.target.value;
this.props.onSearchTextChange(text);
Expand All @@ -70,8 +86,16 @@ let SearchBar = React.createClass({
}
},
onKeyDown(event) {
if (event.keyCode === 13) {
this.search();
switch (event.keyCode) {
case 13:
this.search();
break;
case 8:
if (!this.props.searchText && this.props.selectedItems && this.props.selectedItems.length > 0) {
this.props.onCancelSelectedItem(this.props.selectedItems[this.props.selectedItems.length - 1]);
}
break;
default:
}
},
onFocus() {
Expand All @@ -85,9 +109,14 @@ let SearchBar = React.createClass({
delay(() => {this.props.onPurgeResults(); }, this.props.blurResetDelay);
}
},
renderAddonBefore() {
return this.props.selectedItems && this.props.selectedItems.map((item, index) =>
<span key={"selected-item" + index} className="input-group-addon"><div className="selectedItem-text">{item.text}</div></span>
);
},
renderAddonAfter() {
const remove = <Glyphicon className="searchclear" glyph="remove" onClick={this.clearSearch}/>;
var showRemove = this.props.searchText !== "";
var showRemove = this.props.searchText !== "" || (this.props.selectedItems && this.props.selectedItems.length > 0);
let addonAfter = showRemove ? [remove] : [<Glyphicon glyph="search"/>];
if (this.props.loading) {
addonAfter = [<Spinner style={{
Expand Down Expand Up @@ -117,10 +146,13 @@ let SearchBar = React.createClass({
return (
<div id="map-search-bar" style={this.props.style} className={"MapSearchBar" + (this.props.className ? " " + this.props.className : "")}>
<FormGroup>
<div className="input-group"><FormControl
<div className="input-group">
{this.renderAddonBefore()}
<FormControl
key="search-input"
placeholder={placeholder}
type="text"
inputRef={ref => { this.input = ref; }}
style={{
textOverflow: "ellipsis"
}}
Expand All @@ -138,14 +170,19 @@ let SearchBar = React.createClass({
},
search() {
var text = this.props.searchText;
if (text === undefined || text === "") {
if ((text === undefined || text === "") && (!this.props.selectedItems || this.props.selectedItems.length === 0)) {
this.props.onSearchReset();
} else {
this.props.onSearch(text, this.props.searchOptions);
}

},

focusToInput() {
let node = this.input;
if (node && node.focus instanceof Function) {
setTimeout( () => node.focus(), 200);
}
},
clearSearch() {
this.props.onSearchReset();
}
Expand Down
15 changes: 13 additions & 2 deletions web/client/components/mapcontrols/search/SearchResult.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,20 @@
const React = require('react');
const {get} = require('lodash');

const {generateTemplateString} = require('../../../utils/TemplateUtils');

let SearchResult = React.createClass({
propTypes: {
/* field name or template.
* e.g. "properties.subTitle"
* e.g. "This is a subtitle for ${properties.subTitle}"
*/
subTitle: React.PropTypes.string,
item: React.PropTypes.object,
/* field name or template.
* e.g. "properties.displayName"
* e.g. "This is a title for ${properties.title}"
*/
displayName: React.PropTypes.string,
idField: React.PropTypes.string,
icon: React.PropTypes.string,
Expand All @@ -35,9 +45,10 @@ let SearchResult = React.createClass({
}
let item = this.props.item;
return (
<div key={item.osm_id} className="search-result NominatimResult" onClick={this.onClick}>
<div key={item.osm_id} className="search-result" onClick={this.onClick}>
<div className="icon"> <img src={item.icon} /></div>
{get(item, this.props.displayName) }
<div className="text-result-title">{get(item, this.props.displayName) || generateTemplateString(this.props.displayName || "")(item) }</div>
<small className="text-info">{this.props.subTitle && get(item, this.props.subTitle) || generateTemplateString(this.props.subTitle || "")(item) }</small>
</div>
);
}
Expand Down
9 changes: 5 additions & 4 deletions web/client/components/mapcontrols/search/SearchResultList.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ let SearchResultList = React.createClass({
return this.props.results.map((item, idx)=> {
const service = this.findService(item) || {};
return (<SearchResult
subTitle={service.subTitle}
idField={service.idField}
displayName={service.displayName}
key={item.osm_id || "res_" + idx} item={item} onItemClick={this.onItemClick}/>);
Expand All @@ -59,8 +60,8 @@ let SearchResultList = React.createClass({
},
findService(item) {
const services = this.props.searchOptions && this.props.searchOptions.services;
if (services && item.__SERVICE__ !== null) {
if ( typeof item.__SERVICE__ === "string" ) {
if (item.__SERVICE__ !== null) {
if (services && typeof item.__SERVICE__ === "string" ) {
for (let i = 0; i < services.length; i++) {
if (services[i] && services[i].id === item.__SERVICE__) {
return services[i];
Expand All @@ -72,8 +73,8 @@ let SearchResultList = React.createClass({
}
}

} else {
return services[item.__SERVICE__];
} else if (typeof item.__SERVICE__ === "object") {
return item.__SERVICE__;
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,20 @@ describe("test the SearchBar", () => {
done();
}, 50);
});
it('test autofocus on selected items', (done) => {
let tb = ReactDOM.render(<SearchBar searchText="test" delay={0} typeAhead={true} blurResetDelay={0} />, document.getElementById("container"));
let input = TestUtils.scryRenderedDOMComponentsWithTag(tb, "input")[0];
expect(input).toExist();
let spyOnFocus = expect.spyOn(input, 'focus');
input = ReactDOM.findDOMNode(input);
input.value = "test";
TestUtils.Simulate.blur(input);
ReactDOM.render(<SearchBar searchText="test" delay={0} typeAhead={true} blurResetDelay={0} selectedItems={[{text: "TEST"}]}/>, document.getElementById("container"));
setTimeout(() => {
expect(spyOnFocus.calls.length).toEqual(1);
done();
}, 210);
});

it('test that options are passed to search action', () => {
var tb;
Expand Down Expand Up @@ -163,4 +177,24 @@ describe("test the SearchBar", () => {
let error = ReactDOM.findDOMNode(TestUtils.scryRenderedDOMComponentsWithClass(tb, "searcherror")[0]);
expect(error).toExist();
});

it('test cancel items', (done) => {
const testHandlers = {
onCancelSelectedItem: () => {}
};

const spy = expect.spyOn(testHandlers, 'onCancelSelectedItem');
let tb = ReactDOM.render(<SearchBar searchText="test" delay={0} typeAhead={true} blurResetDelay={0} onCancelSelectedItem={testHandlers.onCancelSelectedItem} />, document.getElementById("container"));
let input = TestUtils.scryRenderedDOMComponentsWithTag(tb, "input")[0];
expect(input).toExist();
input = ReactDOM.findDOMNode(input);

// backspace with empty searchText causes trigger of onCancelSelectedItem
ReactDOM.render(<SearchBar searchText="" delay={0} typeAhead={true} blurResetDelay={0} onCancelSelectedItem={testHandlers.onCancelSelectedItem} selectedItems={[{text: "TEST"}]}/>, document.getElementById("container"));
TestUtils.Simulate.keyDown(input, {key: "Backspace", keyCode: 8, which: 8});
setTimeout(() => {
expect(spy.calls.length).toEqual(1);
done();
}, 10);
});
});
Loading