From d667678d1080573e508e4ad4497da18b7936ea1d Mon Sep 17 00:00:00 2001 From: Felix Sommer Date: Tue, 5 Nov 2024 09:25:30 +0100 Subject: [PATCH] PB-877: add autoselect to swisssearch url param --- .../storeSync/SearchAutoSelectConfig.class.js | 40 +++++++++++++++ .../storeSync/SearchParamConfig.class.js | 31 ++---------- src/router/storeSync/storeSync.config.js | 6 ++- .../storeSync/storeSync.routerPlugin.js | 2 + src/store/modules/search.store.js | 45 +++++++++++------ src/utils/searchParamUtils.js | 23 +++++++++ .../cypress/tests-e2e/legacyParamImport.cy.js | 40 --------------- .../tests-e2e/search/search-results.cy.js | 50 +++++++++++++++++++ 8 files changed, 153 insertions(+), 84 deletions(-) create mode 100644 src/router/storeSync/SearchAutoSelectConfig.class.js create mode 100644 src/utils/searchParamUtils.js diff --git a/src/router/storeSync/SearchAutoSelectConfig.class.js b/src/router/storeSync/SearchAutoSelectConfig.class.js new file mode 100644 index 000000000..37b45aea2 --- /dev/null +++ b/src/router/storeSync/SearchAutoSelectConfig.class.js @@ -0,0 +1,40 @@ +import AbstractParamConfig, { + STORE_DISPATCHER_ROUTER_PLUGIN, +} from '@/router/storeSync/abstractParamConfig.class' +import { removeQueryParamFromHref } from '@/utils/searchParamUtils' +import { URL_PARAM_NAME_SWISSSEARCH } from './SearchParamConfig.class' + +const URL_PARAM_NAME = 'swisssearch_autoselect' +/** + * The goal is to stop centering on the search when sharing a position. When we share a position, + * both the center and the crosshair are sets. + * + * @param {Object} to The route object containing the query + * @param {Object} store The store + * @param {String} urlParamValue The search param + */ +function dispatchSearchFromUrl(to, store, urlParamValue) { + // avoiding setting the swisssearch autoselect to the store when there is nothing to autoselect because there is no swisssearch query + if (urlParamValue && to.query[URL_PARAM_NAME_SWISSSEARCH]) { + store.dispatch('setSwisssearchAutoSelect', { + value: urlParamValue, + dispatcher: STORE_DISPATCHER_ROUTER_PLUGIN, + }) + } +} + +export default class SearchAutoSelectConfig extends AbstractParamConfig { + constructor() { + super({ + urlParamName: URL_PARAM_NAME, + mutationsToWatch: ['setSwisssearchAutoSelect'], + setValuesInStore: dispatchSearchFromUrl, + afterSetValuesInStore: () => removeQueryParamFromHref(URL_PARAM_NAME), + extractValueFromStore: (store) => store.state.search.swisssearchAutoSelect, + keepInUrlWhenDefault: false, + valueType: Boolean, + defaultValue: false, + validateUrlInput: null, + }) + } +} diff --git a/src/router/storeSync/SearchParamConfig.class.js b/src/router/storeSync/SearchParamConfig.class.js index 11cca8dba..59ecdda0c 100644 --- a/src/router/storeSync/SearchParamConfig.class.js +++ b/src/router/storeSync/SearchParamConfig.class.js @@ -1,8 +1,9 @@ import AbstractParamConfig, { STORE_DISPATCHER_ROUTER_PLUGIN, } from '@/router/storeSync/abstractParamConfig.class' +import { removeQueryParamFromHref } from '@/utils/searchParamUtils' -const URL_PARAM_NAME = 'swisssearch' +export const URL_PARAM_NAME_SWISSSEARCH = 'swisssearch' /** * The goal is to stop centering on the search when sharing a position. When we share a position, * both the center and the crosshair are sets. @@ -23,37 +24,13 @@ function dispatchSearchFromUrl(to, store, urlParamValue) { } } -/** - * This will remove the query param from the URL It is necessary to do this in vanilla JS because - * the router does not provide a way to remove a query without reloading the page which then removes - * the value from the store. - * - * @param {Object} key The key to remove from the URL - */ -function removeQueryParamFromHref(key) { - const [baseUrl, queryString] = window.location.href.split('?') - if (!queryString) { - return - } - - const params = new URLSearchParams(queryString) - if (!params.has(key)) { - return - } - params.delete(key) - - const newQueryString = params.toString() - const newUrl = newQueryString ? `${baseUrl}?${newQueryString}` : baseUrl - window.history.replaceState({}, document.title, newUrl) -} - export default class SearchParamConfig extends AbstractParamConfig { constructor() { super({ - urlParamName: URL_PARAM_NAME, + urlParamName: URL_PARAM_NAME_SWISSSEARCH, mutationsToWatch: [], setValuesInStore: dispatchSearchFromUrl, - afterSetValuesInStore: () => removeQueryParamFromHref(URL_PARAM_NAME), + afterSetValuesInStore: () => removeQueryParamFromHref(URL_PARAM_NAME_SWISSSEARCH), keepInUrlWhenDefault: false, valueType: String, defaultValue: '', diff --git a/src/router/storeSync/storeSync.config.js b/src/router/storeSync/storeSync.config.js index 5a7fc29c5..e6be6cc98 100644 --- a/src/router/storeSync/storeSync.config.js +++ b/src/router/storeSync/storeSync.config.js @@ -13,8 +13,8 @@ import SimpleUrlParamConfig from '@/router/storeSync/SimpleUrlParamConfig.class' import ZoomParamConfig from '@/router/storeSync/ZoomParamConfig.class' import { FeatureInfoPositions } from '@/store/modules/ui.store.js' import allCoordinateSystems from '@/utils/coordinates/coordinateSystems' - -import TimeSliderParamConfig from './TimeSliderParamConfig.class' +import TimeSliderParamConfig from '@/router/storeSync/TimeSliderParamConfig.class' +import SearchAutoSelectConfig from '@/router/storeSync/SearchAutoSelectConfig.class' /** * Configuration for all URL parameters of this app that need syncing with the store (and @@ -23,6 +23,8 @@ import TimeSliderParamConfig from './TimeSliderParamConfig.class' * @type Array */ const storeSyncConfig = [ + // SearchAutoSelectConfig should be processed before SearchParamConfig to avoid a bug where the autoselect would not be set + new SearchAutoSelectConfig(), new SimpleUrlParamConfig({ urlParamName: 'lang', mutationsToWatch: ['setLang'], diff --git a/src/router/storeSync/storeSync.routerPlugin.js b/src/router/storeSync/storeSync.routerPlugin.js index 39c6d11cf..a8ecfc1bc 100644 --- a/src/router/storeSync/storeSync.routerPlugin.js +++ b/src/router/storeSync/storeSync.routerPlugin.js @@ -126,6 +126,8 @@ function urlQueryWatcher(store, to, from) { } if ( + // to call afterSetValuesInStore in SearchAutoSelectConfig if it was set to false in the URL to remove the parameter from the URL + queryValue === false || // when the query value is an empty string, queryValue is false. (queryValue || queryValue === '') && queryValue !== storeValue && diff --git a/src/store/modules/search.store.js b/src/store/modules/search.store.js index 72c352daf..aa56961ef 100644 --- a/src/store/modules/search.store.js +++ b/src/store/modules/search.store.js @@ -30,24 +30,33 @@ const state = { * @type {SearchResult[]} */ results: [], + + /** + * If true, the first search result will be automatically selected + * + * @type {Boolean} + */ + swisssearchAutoSelect: false, } const getters = {} -function extractLimitNumber(query) { - const regex = / limit: \d+/ - const match = query.match(regex) - - if (match) { - return { - limit: parseInt(match[0].split(':')[1].trim()), - extractedQuery: query.replace(match[0], ''), - } +function getResultForAutoselect(results) { + if (results.length === 1) { + return results[0]; } - return { limit: 0, extractedQuery: query } + // Try to find a result with resultType LOCATION + const locationResult = results.find((result) => result.resultType === SearchResultTypes.LOCATION); + + // If a location result is found, return it; otherwise, return the first result + return locationResult || results[0]; } const actions = { + setSwisssearchAutoSelect: ({ commit }, { value = false, dispatcher }) => { + commit('setSwisssearchAutoSelect', { value, dispatcher }) + }, + /** * @param {vuex} vuex * @param {Object} payload @@ -59,8 +68,6 @@ const actions = { ) => { let results = [] commit('setSearchQuery', { query, dispatcher }) - const { limit, extractedQuery } = extractLimitNumber(query) - query = extractedQuery // only firing search if query is longer than or equal to 2 chars if (query.length >= 2) { const currentProjection = rootState.position.projection @@ -151,12 +158,12 @@ const actions = { queryString: query, lang: rootState.i18n.lang, layersToSearch: getters.visibleLayers, - limit, + limit: state.swisssearchAutoSelect ? 1 : 0, }) - if (originUrlParam && results.length === 1) { + if (originUrlParam && results.length === 1 || originUrlParam && state.swisssearchAutoSelect && results.length >= 1) { dispatch('selectResultEntry', { dispatcher: `${dispatcher}/setSearchQuery`, - entry: results[0], + entry: getResultForAutoselect(results), }) } } catch (error) { @@ -195,6 +202,7 @@ const actions = { queryString: state.query, lang: rootState.i18n.lang, layersToSearch: getters.visibleLayers, + limit: state.swisssearchAutoSelect ? 1 : 0, }) if (resultIncludingLayerFeatures.length > state.results.length) { commit('setSearchResults', { @@ -258,6 +266,12 @@ const actions = { break } + if(state.swisssearchAutoSelect) { + dispatch('setSwisssearchAutoSelect', { + value: false, + dispatcher: dispatcherSelectResultEntry, + }) + } }, } @@ -287,6 +301,7 @@ function createLayerFeature(olFeature, layer) { } const mutations = { + setSwisssearchAutoSelect: (state, { value }) => (state.swisssearchAutoSelect = value), setSearchQuery: (state, { query }) => (state.query = query), setSearchResults: (state, { results }) => (state.results = results ?? []), } diff --git a/src/utils/searchParamUtils.js b/src/utils/searchParamUtils.js new file mode 100644 index 000000000..20a7e78e1 --- /dev/null +++ b/src/utils/searchParamUtils.js @@ -0,0 +1,23 @@ +/** + * This will remove the query param from the URL It is necessary to do this in vanilla JS because + * the router does not provide a way to remove a query without reloading the page which then removes + * the value from the store. + * + * @param {Object} key The key to remove from the URL + */ +export function removeQueryParamFromHref(key) { + const [baseUrl, queryString] = window.location.href.split('?') + if (!queryString) { + return + } + + const params = new URLSearchParams(queryString) + if (!params.has(key)) { + return + } + params.delete(key) + + const newQueryString = params.toString() + const newUrl = newQueryString ? `${baseUrl}?${newQueryString}` : baseUrl + window.history.replaceState({}, document.title, newUrl) +} \ No newline at end of file diff --git a/tests/cypress/tests-e2e/legacyParamImport.cy.js b/tests/cypress/tests-e2e/legacyParamImport.cy.js index 9cc30701b..fae067bd3 100644 --- a/tests/cypress/tests-e2e/legacyParamImport.cy.js +++ b/tests/cypress/tests-e2e/legacyParamImport.cy.js @@ -286,46 +286,6 @@ describe('Test on legacy param import', () => { }) cy.get('[data-cy="search-results-locations"]').should('not.be.visible') }) - it('limits the swisssearch with legacy parameter limit', () => { - cy.intercept('**/rest/services/ech/SearchServer*?type=layers*', { - body: { results: [] }, - }).as('search-layers') - const coordinates = [2598633.75, 1200386.75] - cy.intercept('**/rest/services/ech/SearchServer*?type=locations*', { - body: { - results: [ - { - attrs: { - detail: '1530 payerne 5822 payerne ch vd', - label: ' 1530 Payerne', - lat: 46.954559326171875, - lon: 7.420684814453125, - y: coordinates[0], - x: coordinates[1], - }, - }, - { - attrs: { - detail: '1530 payerne 5822 payerne ch vd 2', - label: ' 1530 Payerne 2', - lat: 46.954559326171875, - lon: 7.420684814453125, - y: coordinates[0], - x: coordinates[1], - }, - }, - ], - }, - }).as('search-locations') - cy.goToMapView( - { - swisssearch: '1530 Payerne limit: 2', - }, - false - ) - cy.readStoreValue('state.search.query').should('eq', '1530 Payerne limit: 2') - cy.url().should('not.contain', 'swisssearch') - }) it('External WMS layer', () => { const layerName = 'OpenData-AV' const layerId = 'ch.swisstopo-vd.official-survey' diff --git a/tests/cypress/tests-e2e/search/search-results.cy.js b/tests/cypress/tests-e2e/search/search-results.cy.js index 35562163e..62df3681b 100644 --- a/tests/cypress/tests-e2e/search/search-results.cy.js +++ b/tests/cypress/tests-e2e/search/search-results.cy.js @@ -402,4 +402,54 @@ describe('Test the search bar result handling', () => { cy.readStoreValue('state.search.query').should('equal', '') cy.get('@locationSearchResults').should('not.exist') }) + it('autoselects the first swisssearch result when swisssearch_autoselect is true', () => { + cy.intercept('**/rest/services/ech/SearchServer*?type=layers*', { + body: { results: [] }, + }).as('search-layers') + const coordinates = [2598633.75, 1200386.75] + cy.intercept('**/rest/services/ech/SearchServer*?type=locations*', { + body: { + results: [ + { + attrs: { + detail: '1530 payerne 5822 payerne ch vd', + label: ' 1530 Payerne', + lat: 46.954559326171875, + lon: 7.420684814453125, + y: coordinates[0], + x: coordinates[1], + }, + }, + { + attrs: { + detail: '1530 payerne 5822 payerne ch vd 2', + label: ' 1530 Payerne 2', + lat: 46.954559326171875, + lon: 7.420684814453125, + y: coordinates[0], + x: coordinates[1], + }, + }, + ], + }, + }).as('search-locations') + cy.goToMapView( + { + swisssearch: '1530 Payerne', + swisssearch_autoselect: 'true', + }, + false + ) + cy.readStoreValue('state.search.query').should('eq', '1530 Payerne') + cy.url().should('not.contain', 'swisssearch') + cy.url().should('not.contain', 'swisssearch_autoselect') + const acceptableDelta = 0.25 + + cy.readStoreValue('state.map.pinnedLocation').should((feature) => { + expect(feature).to.not.be.null + expect(feature).to.be.a('array').that.is.not.empty + expect(feature[0]).to.be.approximately(coordinates[0], acceptableDelta) + expect(feature[1]).to.be.approximately(coordinates[1], acceptableDelta) + }) + }) })