diff --git a/web/themes/custom/materiotheme/vuejs/components/Form/SearchForm.vue b/web/themes/custom/materiotheme/vuejs/components/Form/SearchForm.vue
index a95929bd..bdaf404d 100644
--- a/web/themes/custom/materiotheme/vuejs/components/Form/SearchForm.vue
+++ b/web/themes/custom/materiotheme/vuejs/components/Form/SearchForm.vue
@@ -135,16 +135,17 @@ export default {
// let tag_list = n
// }
// remove terms from autocomplete if removed from typed
+ if (typeof n !== 'string') {
+ return
+ }
const r = /,\s?$/ // remove last comma space
let tag_list = n.replace(r,'').split(', ')
console.log('watch typed tag_list', tag_list)
console.log('watch typed autocomplete before', this.autocomplete)
- this.autocomplete.forEach( (term,index,array) => {
- console.log("watch typed autocomplete term", term, index, array)
- if (tag_list.indexOf(term.label) < 0) {
- this.autocomplete.splice(index,1)
- }
- });
+ // Keep only the terms whose label is still present in the typed text.
+ // Filtering into a new array avoids skipping items when splicing during
+ // iteration (which left stale terms in autocomplete, corrupting `terms`).
+ this.autocomplete = this.autocomplete.filter(term => tag_list.indexOf(term.label) >= 0)
console.log('watch typed autocomplete after', this.autocomplete)
},
keys(n, o){
@@ -166,6 +167,20 @@ export default {
mounted(){
// console.log('SearchForm mounted')
Drupal.attachBehaviors(this.$el);
+
+ // Route the native form submission (Enter key) through the same submit()
+ // method as the button (@click.prevent="submit"). Without this, pressing
+ // Enter either triggers a native POST reload or only fills the jQuery UI
+ // autocomplete selection, so keys/terms sent to the backend differ from a
+ // button click and the search results are inconsistent.
+ // Note: when the autocomplete menu is open, jQuery UI handles Enter itself
+ // (selects the highlighted suggestion) and prevents this submit event, which
+ // is the intended behaviour.
+ this.$el.addEventListener('submit', (e) => {
+ e.preventDefault()
+ this.submit()
+ })
+
// get the search input
this.$input = this.$el.querySelector('#edit-search')
// // focus on input
diff --git a/web/themes/custom/materiotheme/vuejs/components/Pages/Base.vue b/web/themes/custom/materiotheme/vuejs/components/Pages/Base.vue
index 1076b0eb..af113df6 100644
--- a/web/themes/custom/materiotheme/vuejs/components/Pages/Base.vue
+++ b/web/themes/custom/materiotheme/vuejs/components/Pages/Base.vue
@@ -11,13 +11,14 @@
{{ $t('default.Loading…') }}
No more results
@@ -48,8 +49,14 @@ export default {
searchinfos: state => state.Search.infos,
count: state => state.Search.count,
noresults: state => state.Search.noresults,
- limit: state => state.Search.limit
- })
+ limit: state => state.Search.limit,
+ searchId: state => state.Search.searchId
+ }),
+ // items is a sparse array (pages placed by offset); drop the holes left by
+ // pages that returned fewer results than the limit before rendering.
+ displayedItems () {
+ return this.items.filter(Boolean)
+ }
},
methods: {
...mapActions({
@@ -98,7 +105,10 @@ export default {
// todo text field of search form is not emptying on clicking on base after a search
// this.$store.commit('Search/setKeys', to.query.keys)
if (to.query.hasOwnProperty('terms')) {
- this.$store.commit('Search/setTerms', to.query.terms)
+ // to.query.terms is a raw JSON string (see SearchForm.submit) and must be
+ // parsed like in created(), otherwise getResults() double-encodes it and
+ // the backend silently drops the terms (less relevant results).
+ this.$store.commit('Search/setTerms', JSON.parse(to.query.terms))
}else{
this.$store.commit('Search/reSetTerms')
}
diff --git a/web/themes/custom/materiotheme/vuejs/store/modules/search.js b/web/themes/custom/materiotheme/vuejs/store/modules/search.js
index 2480e5a6..738c3f4f 100644
--- a/web/themes/custom/materiotheme/vuejs/store/modules/search.js
+++ b/web/themes/custom/materiotheme/vuejs/store/modules/search.js
@@ -25,6 +25,9 @@ export default {
infos: null,
count: 0,
noresults: false,
+ // Incremented on every new search. Async responses (MA + GraphQL) carrying an
+ // outdated id are discarded to avoid mixing results from concurrent searches.
+ searchId: 0,
// infinteState will come from vue-infinite-loading plugin
// implemented in vuejs/components/Content/Base.vue
infiniteLoadingState: null
@@ -35,23 +38,38 @@ export default {
// mutations
mutations: {
- setUuids (state, uuids) {
- state.uuids = state.uuids.concat(uuids)
+ setUuids (state, { uuids, offset }) {
+ // Place the page at its offset position instead of concatenating in
+ // response-arrival order. With infinite-loading firing several pages
+ // concurrently, concat mixed pages when a later page resolved first.
+ if (!uuids) {
+ return
+ }
+ const newUuids = state.uuids.slice()
+ uuids.forEach((uuid, i) => {
+ newUuids[offset + i] = uuid
+ })
+ state.uuids = newUuids
},
resetUuids (state) {
state.uuids = []
},
- setResults (state, items) {
- // console.log('setResults, items', items)
- if (items) { // avoid bug like items = [null]
- state.items = state.items.concat(items)
+ setResults (state, { items, offset }) {
+ // Same as setUuids: position each page by its offset so the display order
+ // always matches the backend relevance order, whatever the network race.
+ if (!items) { // avoid bug like items = [null]
+ return
}
- // console.log('setResults, state.items', state.items)
+ const newItems = state.items.slice()
+ items.forEach((item, i) => {
+ newItems[offset + i] = item
+ })
+ state.items = newItems
},
updateItem (state, item) {
// state.items[data.index] = data.item // this is not triggering vuejs refresh
// find the right index
- const index = state.items.findIndex(i => i.id === item.id)
+ const index = state.items.findIndex(i => i && i.id === item.id)
// update the array
if (index !== -1) {
Vue.set(state.items, index, item)
@@ -107,6 +125,9 @@ export default {
},
setInfiniteState (state, infiniteLoadingstate) {
state.infiniteLoadingState = infiniteLoadingstate
+ },
+ incrementSearchId (state) {
+ state.searchId++
}
},
@@ -120,6 +141,8 @@ export default {
commit('resetNoresults')
commit('resetOffset')
commit('resetInfos')
+ // Invalidate any in-flight response from a previous search.
+ commit('incrementSearchId')
if (state.keys || state.terms.length) {
this.commit('Common/setPagetitle', state.keys.join(', '))
} else {
@@ -134,10 +157,15 @@ export default {
dispatch('getResults')
},
getResults ({ dispatch, commit, state }) {
+ // Capture the search token; a newSearch bumps it and makes this run stale.
+ const searchId = state.searchId
+ // Capture the offset of THIS request so the response is placed correctly
+ // even if a later page (higher offset) resolves before it.
+ const offset = state.offset
const params = {
keys: state.keys.join(', '),
terms: JSON.stringify(state.terms),
- offset: state.offset,
+ offset: offset,
limit: state.limit
}
console.log('search store getResults, params', params)
@@ -153,11 +181,15 @@ export default {
return MA.get('/materio_sapi/getresults?' + q)
.then(({ data }) => {
console.log('search MA getresults data', data, state.terms)
+ // Discard the response if another search was started meanwhile.
+ if (searchId !== state.searchId) {
+ return
+ }
// commit('setItems', data.items)
commit('setInfos', data.infos)
commit('setCount', data.count)
if (data.count) {
- commit('setUuids', data.uuids)
+ commit('setUuids', { uuids: data.uuids, offset })
// loadMaterials is on mixins
// https://github.com/huybuidac/vuex-extensions
// dispatch('loadMaterialsGQL', {
@@ -175,7 +207,11 @@ export default {
MGQ.post('', { query: print(ast) })
.then(resp => {
console.log('loadSearchResultsGQL resp', resp)
- dispatch('loadSearchResultsCallBack', { items: resp.data.data.searchresults })
+ // Discard if a newer search was started while GraphQL was in flight.
+ if (searchId !== state.searchId) {
+ return
+ }
+ dispatch('loadSearchResultsCallBack', { items: resp.data.data.searchresults, offset })
})
.catch(error => {
console.warn('Issue with loadSearchResults', error)
@@ -191,9 +227,9 @@ export default {
Promise.reject(error)
})
},
- loadSearchResultsCallBack ({ commit, state }, { items, callBackArgs }) {
+ loadSearchResultsCallBack ({ commit, state }, { items, offset, callBackArgs }) {
console.log('search loadSearchResultsCallBack', items)
- commit('setResults', items)
+ commit('setResults', { items, offset })
if (state.infiniteLoadingState) {
if (state.offset + state.limit > state.count) {
console.log('Search infinite completed')