diff --git a/web/client/components/mapcontrols/search/SearchBar.jsx b/web/client/components/mapcontrols/search/SearchBar.jsx index 7cac654f0c..d9a3b757f8 100644 --- a/web/client/components/mapcontrols/search/SearchBar.jsx +++ b/web/client/components/mapcontrols/search/SearchBar.jsx @@ -6,9 +6,9 @@ * LICENSE file in the root directory of this source tree. */ -import React from 'react'; +import React, { useEffect, useState } from 'react'; import {FormGroup, Glyphicon, MenuItem} from 'react-bootstrap'; -import {isEmpty, isUndefined} from 'lodash'; +import { isEmpty, isEqual, isUndefined, get } from 'lodash'; import Message from '../../I18N/Message'; import SearchBarMenu from './SearchBarMenu'; @@ -20,6 +20,49 @@ import SearchBarToolbar from '../../search/SearchBarToolbar'; import { defaultSearchWrapper } from '../../search/SearchBarUtils'; import BookmarkSelect, {BookmarkOptions} from "../searchbookmarkconfig/BookmarkSelect"; import CoordinatesSearch, {CoordinateOptions} from "../searchcoordinates/CoordinatesSearch"; +import tooltip from '../../misc/enhancers/tooltip'; + +const TMenuItem = tooltip(MenuItem); +const SearchServicesSelectorMenu = ({activeTool, searchIcon, services = [], selectedService = -1, onServiceSelect = () => {}}) => { + if (services.length === 0) { + return null; + } + if (services.length === 1) { + return ( + onServiceSelect(-1)}> + + + + ); + } + return (<> + onServiceSelect(-1)} + > + + + + {services.map((service, index) => { + const name = service.name || service.type; + return ( onServiceSelect(index)} + key={index} + active={activeTool === "addressSearch" && selectedService === index} + > + + + {name} + + ); + })} + + ); +}; export default ({ activeSearchTool: activeTool = 'addressSearch', @@ -61,8 +104,24 @@ export default ({ items = [], ...props }) => { + const [selectedSearchService, setSearchServiceSelected] = useState(-1); + useEffect(() => { + // Reset selected service, when service changes + if (!isEqual(searchOptions?.services, searchOptions?.services)) { + setSearchServiceSelected(-1); + } + }, [searchOptions?.services]); - const search = defaultSearchWrapper({searchText, selectedItems, searchOptions, maxResults, onSearch, onSearchReset}); + const selectedServices = searchOptions?.services?.filter((_, index) => selectedSearchService >= 0 ? selectedSearchService === index : true) ?? []; + const search = defaultSearchWrapper({ + searchText, + selectedItems, + searchOptions: { + ...searchOptions, + services: selectedServices + }, + maxResults, onSearch, onSearchReset + }); const clearSearch = () => { onSearchReset(); @@ -78,14 +137,20 @@ export default ({ let searchMenuOptions = []; if (showAddressSearchOption) { searchMenuOptions.push( - { - onClearCoordinatesSearch({owner: "search"}); - onClearBookmarkSearch("selected"); - onChangeActiveSearchTool("addressSearch"); - }} - > - - ); + { + setSearchServiceSelected(index === -1 ? undefined : index); + onClearCoordinatesSearch({owner: "search"}); + onClearBookmarkSearch("selected"); + onChangeActiveSearchTool("addressSearch"); + return; + }} + services={searchOptions?.services} + /> + ); } if (showCoordinatesSearchOption) { searchMenuOptions.push( @@ -129,6 +194,16 @@ export default ({ return null; }; + const getPlaceholder = () => { + // when placeholder is present, nested service's placeholder is applied + if (!placeholder && selectedServices?.length === 1 && searchOptions?.services?.length > 1) { + const [service] = selectedServices; + const name = service.name || service.type; + return get(service, 'options.placeholder', `Search by ${name}`); + } + return placeholder; + }; + return (
@@ -140,7 +215,7 @@ export default ({ delay={delay} typeAhead={typeAhead} blurResetDelay={blurResetDelay} - placeholder={placeholder} + placeholder={getPlaceholder()} placeholderMsgId={placeholderMsgId} searchText={searchText} selectedItems={selectedItems} diff --git a/web/client/components/mapcontrols/search/__tests__/SearchBar-test.jsx b/web/client/components/mapcontrols/search/__tests__/SearchBar-test.jsx index 23eb3aef80..e1a3eabc90 100644 --- a/web/client/components/mapcontrols/search/__tests__/SearchBar-test.jsx +++ b/web/client/components/mapcontrols/search/__tests__/SearchBar-test.jsx @@ -39,6 +39,52 @@ describe("test the SearchBar", () => { expect(rootDiv).toExist(); }); + it('test search service menu', () => { + ReactDOM.render(, document.getElementById("container")); + let search = document.getElementsByClassName("glyphicon-search"); + expect(search).toBeTruthy(); + expect(search.length).toBe(2); + }); + it('test search with multiple services', () => { + ReactDOM.render(, document.getElementById("container")); + let search = document.getElementsByClassName("glyphicon-search"); + let menuItems = document.querySelectorAll('[role="menuitem"]'); + expect(search).toBeTruthy(); + expect(search.length).toBe(4); + expect(menuItems.length).toBe(4); + expect(menuItems[1].innerText).toBe('nominatim'); + expect(menuItems[2].innerText).toBe('test'); // Service name is menu name + }); + it('test onSearch with multiple services', () => { + const actions = { + onSearch: () => {} + }; + const services = [{type: "nominatim"}, {type: "wfs", name: "test"}]; + const spyOnSearch = expect.spyOn(actions, 'onSearch'); + ReactDOM.render(, document.getElementById("container")); + let search = document.getElementsByClassName("glyphicon-search"); + let input = document.querySelector(".searchInput"); + let menuItems = document.querySelectorAll('[role="menuitem"]'); + expect(search).toBeTruthy(); + expect(input).toBeTruthy(); + expect(search.length).toBe(4); + + expect(menuItems.length).toBe(4); + TestUtils.Simulate.click(menuItems[1]); // Select single search + + TestUtils.Simulate.keyDown(input, { key: 'Enter', keyCode: 13 }); + expect(spyOnSearch).toHaveBeenCalled(); + expect(spyOnSearch.calls[0].arguments[0]).toBe("test"); + expect(spyOnSearch.calls[0].arguments[1]).toEqual({"services": [services[0]]}); + expect(spyOnSearch.calls[0].arguments[2]).toBe(15); + + TestUtils.Simulate.click(menuItems[0]); // Select all search + TestUtils.Simulate.keyDown(input, { key: 'Enter', keyCode: 13 }); + expect(spyOnSearch).toHaveBeenCalled(); + expect(spyOnSearch.calls[1].arguments[0]).toBe("test"); + expect(spyOnSearch.calls[1].arguments[1]).toEqual({services}); + expect(spyOnSearch.calls[1].arguments[2]).toBe(30); + }); it('test search and reset buttons', () => { const renderSearchBar = (testHandlers, text) => { return ReactDOM.render( @@ -211,7 +257,7 @@ describe("test the SearchBar", () => { let search = document.getElementsByClassName("glyphicon-search"); expect(reset).toExist(); expect(search).toExist(); - expect(search.length).toBe(2); + expect(search.length).toBe(1); }); it('test only search present, splitTools=false', () => { ReactDOM.render(, document.getElementById("container")); @@ -237,7 +283,7 @@ describe("test the SearchBar", () => { let reset = document.getElementsByClassName("glyphicon-1-close")[0]; let search = document.getElementsByClassName("glyphicon-search"); expect(reset).toExist(); - expect(search.length).toBe(1); + expect(search.length).toBe(0); }); it('test zoomToPoint, with search, with decimal, with reset', () => { const store = {dispatch: () => {}, subscribe: () => {}, getState: () => ({search: {coordinate: {lat: 2, lon: 2}}})}; @@ -246,7 +292,7 @@ describe("test the SearchBar", () => { let search = document.getElementsByClassName("glyphicon-search"); let cog = document.getElementsByClassName("glyphicon-cog"); expect(reset.length).toBe(1); - expect(search.length).toBe(2); + expect(search.length).toBe(1); expect(cog.length).toBe(1); }); @@ -258,7 +304,7 @@ describe("test the SearchBar", () => { let cog = document.getElementsByClassName("glyphicon-cog"); let inputs = document.getElementsByTagName("input"); expect(reset.length).toBe(0); - expect(search.length).toBe(2); + expect(search.length).toBe(1); expect(cog.length).toBe(1); expect(inputs.length).toBe(6); }); @@ -404,9 +450,9 @@ describe("test the SearchBar", () => { TestUtils.Simulate.click(buttons[1]); const links = container.querySelectorAll('a'); const bookmark = container.getElementsByClassName('glyphicon-bookmark'); - expect(links.length).toBe(3); + expect(links.length).toBe(2); expect(bookmark).toExist(); - expect(links[2].innerText).toBe('Search by bookmark'); + expect(links[1].innerText).toBe('Search by bookmark'); }); it('test searchByBookmark, search button disabled', () => { const store = {dispatch: () => {}, subscribe: () => {}, getState: () => ({searchbookmarkconfig: {selected: {}}})}; diff --git a/web/client/components/mapcontrols/searchservicesconfig/WFSOptionalProps.jsx b/web/client/components/mapcontrols/searchservicesconfig/WFSOptionalProps.jsx index 806eba3906..458f356aea 100644 --- a/web/client/components/mapcontrols/searchservicesconfig/WFSOptionalProps.jsx +++ b/web/client/components/mapcontrols/searchservicesconfig/WFSOptionalProps.jsx @@ -65,6 +65,26 @@ class WFSOptionalProps extends React.Component { /> + + + + + + + + + + + + ); } diff --git a/web/client/components/mapcontrols/searchservicesconfig/__tests__/WFSOptionalProps-test.jsx b/web/client/components/mapcontrols/searchservicesconfig/__tests__/WFSOptionalProps-test.jsx index e2181d8a6b..0ea6afffe4 100644 --- a/web/client/components/mapcontrols/searchservicesconfig/__tests__/WFSOptionalProps-test.jsx +++ b/web/client/components/mapcontrols/searchservicesconfig/__tests__/WFSOptionalProps-test.jsx @@ -28,7 +28,7 @@ describe("test ResultProps component", () => { const wfsOptionalProps = ReactDOM.render(, document.getElementById("container")); expect(wfsOptionalProps).toExist(); const labels = TestUtils.scryRenderedDOMComponentsWithClass(wfsOptionalProps, "control-label"); - expect(labels.length).toBe(3); + expect(labels.length).toBe(5); }); it('test WFSOptionalProps with preconfigured service', () => { @@ -45,12 +45,14 @@ describe("test ResultProps component", () => { let wfsOptionalProps = ReactDOM.render(, document.getElementById("container")); expect(wfsOptionalProps).toExist(); const labels = TestUtils.scryRenderedDOMComponentsWithClass(wfsOptionalProps, "control-label"); - expect(labels.length).toBe(3); + expect(labels.length).toBe(5); expect(labels[0].innerText).toBe('search.s_sort'); expect(labels[1].innerText).toBe('search.s_max_features'); expect(labels[2].innerText).toBe('search.s_max_zoom'); + expect(labels[3].innerText).toBe('search.s_placeholder'); + expect(labels[4].innerText).toBe('search.s_tooltip'); const inputs = TestUtils.scryRenderedDOMComponentsWithClass(wfsOptionalProps, 'form-control'); - expect(inputs.length).toBe(1); + expect(inputs.length).toBe(3); expect(inputs[0].value).toBe('NAME'); let sliders = TestUtils.scryRenderedDOMComponentsWithClass(wfsOptionalProps, 'slider-label'); expect(sliders.length).toBe(2); diff --git a/web/client/translations/data.de-DE.json b/web/client/translations/data.de-DE.json index 620f3f97c4..4d7279d1d8 100644 --- a/web/client/translations/data.de-DE.json +++ b/web/client/translations/data.de-DE.json @@ -788,6 +788,7 @@ "aeronautical": "Luftfahrt", "changeSearchInputField": "Suchwerkzeug ändern", "addressSearch": "Suche nach Standortname", + "searchOnAllServices": "Suchen Sie unten nach allen Diensten", "coordinatesSearch": "Suche nach Koordinaten", "searchservicesbutton": "Suchdienste konfigurieren", "configpaneltitle": "Einen Suchdienst anlegen / bearbeiten", @@ -840,6 +841,8 @@ "s_wfs_opt_props_label" : "Optionale Eigenschaften", "s_result_props_label": "Ergebnisanzeigeeigenschaften", "s_priority_info": "Wird verwendet, um Suchergebnisse zu sortieren, niedrigere Werte zuerst. Nominatim Ergebnisse haben Priorität = 5", + "s_placeholder": "Platzhalter für die Sucheingabe", + "s_tooltip": "Kurzinfo zum Servicemenü", "serviceslistempty": "Keine benutzerdefinierten Services definiert", "service_missing": "{serviceType} dienst ist nicht konfiguriert", "generic_error": "Bei der Suche ist ein Fehler aufgetreten. Fehlerdetails: {message}", diff --git a/web/client/translations/data.en-US.json b/web/client/translations/data.en-US.json index e0d9143125..6d2cd97ad7 100644 --- a/web/client/translations/data.en-US.json +++ b/web/client/translations/data.en-US.json @@ -749,6 +749,7 @@ "aeronautical": "Aeronautical", "changeSearchInputField": "Change the search tool", "addressSearch": "Search by location name", + "searchOnAllServices": "Search on all services below", "coordinatesSearch": "Search by coordinates", "searchservicesbutton": "Configure search services", "configpaneltitle": "Create/edit a search service", @@ -801,6 +802,8 @@ "s_wfs_opt_props_label" : "Optional properties", "s_result_props_label": "Result display properties", "s_priority_info": "Used to sort search results, lower values first. Nominatim results have priority = 5", + "s_placeholder": "Search input placeholder", + "s_tooltip": "Service menu tooltip", "serviceslistempty": "No custom services defined", "service_missing": "{serviceType} service is not configured", "generic_error": "An error occurred during search. Error details: {message}", diff --git a/web/client/translations/data.es-ES.json b/web/client/translations/data.es-ES.json index 2c4c88a870..73baf4b437 100644 --- a/web/client/translations/data.es-ES.json +++ b/web/client/translations/data.es-ES.json @@ -749,6 +749,7 @@ "aeronautical": "Aeronáutica", "changeSearchInputField": "Cambiar la herramienta de búsqueda", "addressSearch": "Buscar por nombre de ubicación", + "searchOnAllServices": "Busque en todos los servicios a continuación", "coordinatesSearch": "buscar por coordenadas", "searchservicesbutton": "Configurar los servicios de búsqueda", "configpaneltitle": "Crear / modificar un servicio de búsqueda", @@ -801,6 +802,8 @@ "s_wfs_opt_props_label" : "Propiedades opcionales", "s_result_props_label": "Resultados de la visualización de las propiedades", "s_priority_info": "Usado para ordenar los resultados de búsqueda, de menor a mayor. Los resultados Nominatim tienen prioridad = 5", + "s_placeholder": "Marcador de posición de entrada de búsqueda", + "s_tooltip": "Información sobre herramientas del menú de servicio", "serviceslistempty": "No se ha definido ningún servicio personalizado", "service_missing": "el servicio {serviceType} no está configurado", "generic_error": "Se ha producido un error durante la búsqueda. Error de detalles: {message}", diff --git a/web/client/translations/data.fr-FR.json b/web/client/translations/data.fr-FR.json index b7cf4193b9..10402213d7 100644 --- a/web/client/translations/data.fr-FR.json +++ b/web/client/translations/data.fr-FR.json @@ -749,6 +749,7 @@ "aeronautical": "Aéronautique", "changeSearchInputField": "Changer l'outil de recherche", "addressSearch": "Recherche par nom de lieu", + "searchOnAllServices": "Recherchez tous les services ci-dessous", "coordinatesSearch": "Rechercher par coordonnées", "searchservicesbutton": "Configurer les services de recherche", "configpaneltitle": "Créer / modifier un service de recherche", @@ -801,6 +802,8 @@ "s_wfs_opt_props_label": "Propriétés optionnelles", "s_result_props_label": "Propriétés d'affichage des résultats", "s_priority_info": "Utilisé pour trier les résultats de recherche, les valeurs les plus élevées en premier. Les résultats Nominatim ont priorité = 5", + "s_placeholder": "Espace réservé de saisie de recherche", + "s_tooltip": "Info-bulle du menu Service", "serviceslistempty": "Aucun service personnalisé n'a été défini", "service_missing": "Le service {serviceType} n'est pas configuré", "generic_error": "Une erreur s'est produite lors de la recherche. Détails de l'erreur : {message}", diff --git a/web/client/translations/data.it-IT.json b/web/client/translations/data.it-IT.json index f6d86b0414..3e4923c0be 100644 --- a/web/client/translations/data.it-IT.json +++ b/web/client/translations/data.it-IT.json @@ -749,6 +749,7 @@ "aeronautical": "Aeronautiche", "changeSearchInputField": "Cambia lo strumento di ricerca", "addressSearch": "Cerca un indirizzo", + "searchOnAllServices": "Cerca su tutti i servizi di seguito", "coordinatesSearch": "Cerca per coordinate", "searchservicesbutton": "Configura servizi di ricerca", "configpaneltitle": "Aggiungi/modifica un servizio di ricerca", @@ -801,6 +802,8 @@ "s_wfs_opt_props_label" : "Proprietà facoltative", "s_result_props_label": "Proprietà di visualizzazione dei risultati", "s_priority_info": "Usato per ordinare i risultati, i valori più alti vengono visualizzati per primi. Nominatim ha priorità = 5", + "s_placeholder": "Cerca segnaposto di input", + "s_tooltip": "Descrizione comando del menu di servizio", "serviceslistempty": "Nessun servizio definito", "service_missing": "Il servizio {serviceType} non è configurato", "generic_error": "Un errore e' occorso durante la ricerca: Dettagli errore: {message}",