Skip to content

Commit

Permalink
Merge pull request #1098 from geoadmin/feat-pb-877-add-swisssearch-le…
Browse files Browse the repository at this point in the history
…gacy-url-limit-attribute

PB-877: add swisssearch legacy url limit attribute
  • Loading branch information
sommerfe authored Nov 13, 2024
2 parents efbdec3 + 2a822ac commit 82a17fd
Show file tree
Hide file tree
Showing 9 changed files with 207 additions and 37 deletions.
24 changes: 17 additions & 7 deletions src/api/search.api.js
Original file line number Diff line number Diff line change
Expand Up @@ -197,13 +197,14 @@ function parseLocationResult(result, outputProjection) {
}
}

async function searchLayers(queryString, lang, cancelToken) {
async function searchLayers(queryString, lang, cancelToken, limit = null) {
try {
const layerResponse = await generateAxiosSearchRequest(
queryString,
lang,
'layers',
cancelToken.token
cancelToken.token,
{ limit }
)
// checking that there is something of interest to parse
const resultWithAttrs = layerResponse?.data.results?.filter((result) => result.attrs)
Expand All @@ -222,15 +223,17 @@ async function searchLayers(queryString, lang, cancelToken) {
* @param queryString
* @param lang
* @param cancelToken
* @param limit
* @returns {Promise<LocationSearchResult[]>}
*/
async function searchLocation(outputProjection, queryString, lang, cancelToken) {
async function searchLocation(outputProjection, queryString, lang, cancelToken, limit = null) {
try {
const locationResponse = await generateAxiosSearchRequest(
queryString,
lang,
'locations',
cancelToken.token
cancelToken.token,
{ limit }
)
// checking that there is something of interest to parse
const resultWithAttrs = locationResponse?.data.results?.filter((result) => result.attrs)
Expand Down Expand Up @@ -427,10 +430,17 @@ let cancelToken = null
* @param {String} config.lang The lang ISO code in which the search must be conducted
* @param {GeoAdminLayer[]} [config.layersToSearch=[]] List of searchable layers for which to fire
* search requests. Default is `[]`
* @param {number} config.limit The maximum number of results to return
* @returns {Promise<SearchResult[]>}
*/
export default async function search(config) {
const { outputProjection = null, queryString = null, lang = null, layersToSearch = [] } = config
const {
outputProjection = null,
queryString = null,
lang = null,
layersToSearch = [],
limit = null,
} = config
if (!(outputProjection instanceof CoordinateSystem)) {
const errorMessage = `A valid output projection is required to start a search request`
log.error(errorMessage)
Expand All @@ -454,8 +464,8 @@ export default async function search(config) {

/** @type {Promise<SearchResult[]>[]} */
const allRequests = [
searchLayers(queryString, lang, cancelToken),
searchLocation(outputProjection, queryString, lang, cancelToken),
searchLayers(queryString, lang, cancelToken, limit),
searchLocation(outputProjection, queryString, lang, cancelToken, limit),
]

if (layersToSearch.some((layer) => layer.searchable)) {
Expand Down
40 changes: 40 additions & 0 deletions src/router/storeSync/SearchAutoSelectConfig.class.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import AbstractParamConfig, {
STORE_DISPATCHER_ROUTER_PLUGIN,
} from '@/router/storeSync/abstractParamConfig.class'
import { URL_PARAM_NAME_SWISSSEARCH } from '@/router/storeSync/SearchParamConfig.class'
import { removeQueryParamFromHref } from '@/utils/searchParamUtils'

const URL_PARAM_NAME = 'swisssearch_autoselect'
/**
* Dispatches the 'setAutoSelect' action to the store if the URL parameter for swisssearch is
* present.
*
* @param {Object} to - The target route object.
* @param {Object} store - The Vuex store instance.
* @param {string} urlParamValue - The value of the URL parameter to be dispatched.
*/
function dispatchAutoSelect(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('setAutoSelect', {
value: urlParamValue,
dispatcher: STORE_DISPATCHER_ROUTER_PLUGIN,
})
}
}

export default class SearchAutoSelectConfig extends AbstractParamConfig {
constructor() {
super({
urlParamName: URL_PARAM_NAME,
mutationsToWatch: ['setAutoSelect'],
setValuesInStore: dispatchAutoSelect,
afterSetValuesInStore: () => removeQueryParamFromHref(URL_PARAM_NAME),
extractValueFromStore: (store) => store.state.search.autoSelect,
keepInUrlWhenDefault: false,
valueType: Boolean,
defaultValue: false,
validateUrlInput: null,
})
}
}
32 changes: 5 additions & 27 deletions src/router/storeSync/SearchParamConfig.class.js
Original file line number Diff line number Diff line change
@@ -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.
Expand All @@ -18,41 +19,18 @@ function dispatchSearchFromUrl(to, store, urlParamValue) {
query: urlParamValue,
shouldCenter: !(to.query.crosshair && to.query.center),
dispatcher: STORE_DISPATCHER_ROUTER_PLUGIN,
originUrlParam: true,
})
}
}

/**
* 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: '',
Expand Down
6 changes: 4 additions & 2 deletions src/router/storeSync/storeSync.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,21 +8,23 @@ import CompareSliderParamConfig from '@/router/storeSync/CompareSliderParamConfi
import CrossHairParamConfig from '@/router/storeSync/CrossHairParamConfig.class'
import LayerParamConfig from '@/router/storeSync/LayerParamConfig.class'
import PositionParamConfig from '@/router/storeSync/PositionParamConfig.class'
import SearchAutoSelectConfig from '@/router/storeSync/SearchAutoSelectConfig.class'
import SearchParamConfig from '@/router/storeSync/SearchParamConfig.class'
import SimpleUrlParamConfig from '@/router/storeSync/SimpleUrlParamConfig.class'
import TimeSliderParamConfig from '@/router/storeSync/TimeSliderParamConfig.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'

/**
* Configuration for all URL parameters of this app that need syncing with the store (and
* vice-versa)
*
* @type Array<AbstractParamConfig>
*/
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'],
Expand Down
54 changes: 53 additions & 1 deletion src/store/modules/search.store.js
Original file line number Diff line number Diff line change
Expand Up @@ -29,19 +29,53 @@ const state = {
* @type {SearchResult[]}
*/
results: [],

/**
* If true, the first search result will be automatically selected
*
* @type {Boolean}
*/
autoSelect: false,
}

const getters = {}

/**
* Returns the appropriate result for autoselection from a list of search results.
*
* If there is only one result, it returns that result. Otherwise, it tries to find a result with
* the resultType of LOCATION. If such a result is found, it returns that result. If no result with
* resultType LOCATION is found, it returns the first result in the list.
*
* @param {SearchResult[]} results - The list of search results.
* @returns {SearchResult} - The selected search result for autoselection.
*/
function getResultForAutoselect(results) {
if (results.length === 1) {
return results[0]
}
// 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 = {
setAutoSelect: ({ commit }, { value = false, dispatcher }) => {
commit('setAutoSelect', { value, dispatcher })
},

/**
* @param {vuex} vuex
* @param {Object} payload
* @param {String} payload.query
*/
setSearchQuery: async (
{ commit, rootState, dispatch, getters },
{ query = '', shouldCenter = true, dispatcher }
{ query = '', originUrlParam = false, shouldCenter = true, dispatcher }
) => {
let results = []
commit('setSearchQuery', { query, dispatcher })
Expand Down Expand Up @@ -135,7 +169,17 @@ const actions = {
queryString: query,
lang: rootState.i18n.lang,
layersToSearch: getters.visibleLayers,
limit: state.autoSelect ? 1 : null,
})
if (
(originUrlParam && results.length === 1) ||
(originUrlParam && state.autoSelect && results.length >= 1)
) {
dispatch('selectResultEntry', {
dispatcher: `${dispatcher}/setSearchQuery`,
entry: getResultForAutoselect(results),
})
}
} catch (error) {
log.error(`Search failed`, error)
}
Expand Down Expand Up @@ -172,6 +216,7 @@ const actions = {
queryString: state.query,
lang: rootState.i18n.lang,
layersToSearch: getters.visibleLayers,
limit: state.autoSelect ? 1 : null,
})
if (resultIncludingLayerFeatures.length > state.results.length) {
commit('setSearchResults', {
Expand Down Expand Up @@ -235,6 +280,12 @@ const actions = {

break
}
if (state.autoSelect) {
dispatch('setAutoSelect', {
value: false,
dispatcher: dispatcherSelectResultEntry,
})
}
},
}

Expand Down Expand Up @@ -264,6 +315,7 @@ function createLayerFeature(olFeature, layer) {
}

const mutations = {
setAutoSelect: (state, { value }) => (state.autoSelect = value),
setSearchQuery: (state, { query }) => (state.query = query),
setSearchResults: (state, { results }) => (state.results = results ?? []),
}
Expand Down
1 change: 1 addition & 0 deletions src/store/plugins/redo-search-when-needed.plugin.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ const redoSearchWhenNeeded = (store) => {
query: store.state.search.query,
// we don't center on the search query when redoing a search if there is a crosshair
shouldCenter: store.state.position.crossHair === null,
originUrlParam: true, // necessary to select the first result if there is only one else it will not be because this redo search is done every time the page loaded
})
}
}
Expand Down
23 changes: 23 additions & 0 deletions src/utils/searchParamUtils.js
Original file line number Diff line number Diff line change
@@ -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)
}
14 changes: 14 additions & 0 deletions tests/cypress/tests-e2e/legacyParamImport.cy.js
Original file line number Diff line number Diff line change
Expand Up @@ -249,13 +249,18 @@ describe('Test on legacy param import', () => {
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: ' <b>1530 Payerne</b>',
lat: 46.954559326171875,
lon: 7.420684814453125,
y: coordinates[0],
x: coordinates[1],
},
},
],
Expand All @@ -270,6 +275,15 @@ describe('Test on legacy param import', () => {
cy.readStoreValue('state.search.query').should('eq', '1530 Payerne')
cy.url().should('not.contain', 'swisssearch')
cy.get('[data-cy="searchbar"]').click()
const acceptableDelta = 0.25

// selects the result if it is only one
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)
})
cy.get('[data-cy="search-results-locations"]').should('not.be.visible')
})
it('External WMS layer', () => {
Expand Down
Loading

0 comments on commit 82a17fd

Please sign in to comment.