Skip to content

Commit

Permalink
Feat: Create filter-plugin architecture for unified search
Browse files Browse the repository at this point in the history
This commit introduces the mechanism for apps out of the call,
to add search filters to the unified search "Places" filter selector.

Signed-off-by: fenn-cs <fenn25.fn@gmail.com>
  • Loading branch information
nfebe committed Feb 12, 2024
1 parent 9192886 commit e25f0ff
Show file tree
Hide file tree
Showing 5 changed files with 123 additions and 9 deletions.
4 changes: 3 additions & 1 deletion core/src/services/UnifiedSearchService.js
Original file line number Diff line number Diff line change
Expand Up @@ -65,9 +65,10 @@ export async function getProviders() {
* @param {string} options.until the search
* @param {string} options.limit the search
* @param {string} options.person the search
* @param options.extraQueries
* @return {object} {request: Promise, cancel: Promise}
*/
export function search({ type, query, cursor, since, until, limit, person }) {
export function search({ type, query, cursor, since, until, limit, person, extraQueries = {} }) {
/**
* Generate an axios cancel token
*/
Expand All @@ -84,6 +85,7 @@ export function search({ type, query, cursor, since, until, limit, person }) {
person,
// Sending which location we're currently at
from: window.location.pathname.replace('/index.php', '') + window.location.search,
...extraQueries,
},
})

Expand Down
11 changes: 11 additions & 0 deletions core/src/store/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import Vue from 'vue';
import Vuex from 'vuex';
import search from './unified-search-external-filters';

Vue.use(Vuex);

export default new Vuex.Store({
modules: {
search,
},
});
42 changes: 42 additions & 0 deletions core/src/store/unified-search-external-filters.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
/*
* @copyright Copyright (c) 2021 Fon E. Noel NFEBE <me@nfebe.com>
*
* @author Fon E. Noel NFEBE <me@nfebe.com>
*
* @license GNU AGPL version 3 or any later version
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
const state = {
externalFilters: [],
}

const mutations = {
registerExternalFilter(state, { id, label, callback, icon }) {
state.externalFilters.push({ id, name: label, callback, icon, isPluginFilter: true })
},
}

const actions = {
registerExternalFilter({ commit }, { id, label, callback, icon }) {
commit('registerExternalFilter', { id, label, callback, icon })
},
}

export default {
state,
mutations,
actions,
}
16 changes: 16 additions & 0 deletions core/src/unified-search.js
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import { translate as t, translatePlural as n } from '@nextcloud/l10n'
import Vue from 'vue'

import UnifiedSearch from './views/UnifiedSearch.vue'
import store from '../src/store/index.js'

// eslint-disable-next-line camelcase
__webpack_nonce__ = btoa(getRequestToken())
Expand All @@ -47,9 +48,24 @@ Vue.mixin({
},
})

// Register the add/register filter action API globally
window.OCA.Core = window.OCA.Core || {}
window.OCA.Core.UnifiedSearch = {
registerFilterAction: ({ id, name, label, callback, icon }) => {
store.dispatch('registerExternalFilter', {
id,
name,
label,
icon,
callback,
})
},
}

export default new Vue({
el: '#unified-search',
// eslint-disable-next-line vue/match-component-file-name
name: 'UnifiedSearchRoot',
store,
render: h => h(UnifiedSearch),
})
59 changes: 51 additions & 8 deletions core/src/views/UnifiedSearchModal.vue
Original file line number Diff line number Diff line change
Expand Up @@ -18,12 +18,14 @@
:label="t('core', 'Search apps, files, tags, messages') + '...'"
@update:value="debouncedFind" />
<div class="unified-search-modal__filters">
<NcActions :menu-name="t('core', 'Apps and Settings')" :open.sync="providerActionMenuIsOpen">
<NcActions :menu-name="t('core', 'Places')" :open.sync="providerActionMenuIsOpen">
<template #icon>
<ListBox :size="20" />
</template>
<!-- Provider id's may be duplicated since, plugin filters could depend on a provider that is already in the defaults.
provider.id concatenated to provider.name is used to create the item id, if same then, there should be an issue. -->
<NcActionButton v-for="provider in providers"
:key="provider.id"
:key="`${provider.id}-${provider.name.replace(/\s/g, '')}`"
@click="addProviderFilter(provider)">
<template #icon>
<img :src="provider.icon" class="filter-button__icon" alt="">
Expand Down Expand Up @@ -150,8 +152,9 @@ import SearchableList from '../components/UnifiedSearch/SearchableList.vue'
import SearchResult from '../components/UnifiedSearch/SearchResult.vue'
import debounce from 'debounce'
import { emit } from '@nextcloud/event-bus'
import { emit, subscribe } from '@nextcloud/event-bus'
import { useBrowserLocation } from '@vueuse/core'
import { mapState } from 'vuex'
import { getProviders, search as unifiedSearch, getContacts } from '../services/UnifiedSearchService.js'
export default {
Expand Down Expand Up @@ -193,6 +196,7 @@ export default {
},
data() {
return {
searchScope: 'everywhere',
providers: [],
providerActionMenuIsOpen: false,
dateActionMenuIsOpen: false,
Expand All @@ -217,6 +221,9 @@ export default {
},
computed: {
...mapState({
externalFilters: state => state.search.externalFilters,
}),
userContacts() {
return this.contacts
},
Expand Down Expand Up @@ -250,8 +257,12 @@ export default {
},
mounted() {
subscribe('nextcloud:unified-search:conversation', this.handlePluginFilter)
getProviders().then((providers) => {
this.providers = providers
this.externalFilters.forEach(filter => {
this.providers.push(filter)
})
console.debug('Search providers', this.providers)
})
getContacts({ searchTerm: '' }).then((contacts) => {
Expand All @@ -267,7 +278,6 @@ export default {
this.searching = false
return
}
// Event should probably be refactored at some point to used nextcloud:unified-search.search
emit('nextcloud:unified-search.search', { query })
const newResults = []
const providersToSearch = this.filteredProviders.length > 0 ? this.filteredProviders : this.providers
Expand All @@ -276,6 +286,7 @@ export default {
type: provider.id,
query,
cursor: null,
extraQueries: provider.extraParams,
}
if (filters.dateFilterIsApplied) {
Expand Down Expand Up @@ -404,12 +415,27 @@ export default {
},
addProviderFilter(providerFilter, loadMoreResultsForProvider = false) {
if (!providerFilter.id) return
if (providerFilter.isPluginFilter) {
providerFilter.callback()
}
this.providerResultLimit = loadMoreResultsForProvider ? this.providerResultLimit : 5
this.providerActionMenuIsOpen = false
const existingFilter = this.filteredProviders.find(existing => existing.id === providerFilter.id)
if (!existingFilter) {
this.filteredProviders.push({ id: providerFilter.id, name: providerFilter.name, icon: providerFilter.icon, type: 'provider', filters: providerFilter.filters })
// With the possibility for other apps to add new filters
// Resulting in a possible id/provider collision
// If a user tries to apply a filter that seems to already exist, we remove the current one and add the new one.
const existingFilterIndex = this.filteredProviders.findIndex(existing => existing.id === providerFilter.id)
if (existingFilterIndex > -1) {
this.filteredProviders.splice(existingFilterIndex, 1)
this.filters = this.syncProviderFilters(this.filters, this.filteredProviders)
}
this.filteredProviders.push({
id: providerFilter.id,
name: providerFilter.name,
icon: providerFilter.icon,
type: providerFilter.type || 'provider',
filters: providerFilter.filters,
isPluginFilter: providerFilter.isPluginFilter || false,
})
this.filters = this.syncProviderFilters(this.filters, this.filteredProviders)
console.debug('Search filters (newly added)', this.filters)
this.debouncedFind(this.searchQuery)
Expand Down Expand Up @@ -527,6 +553,23 @@ export default {
this.dateFilter.text = t('core', `Between ${this.dateFilter.startFrom.toLocaleDateString()} and ${this.dateFilter.endAt.toLocaleDateString()}`)
this.updateDateFilter()
},
handlePluginFilter(addFilterEvent) {
for (const provider of this.filteredProviders) {
if (provider.id === addFilterEvent.id) {
provider.name = addFilterEvent.filterUpdateText
// Filters attached may only make sense with certain providers,
// So, find the provider attached, add apply the exta parameters to those providers only
const compatibleProviderIndex = this.providers.findIndex(provider => provider.id === addFilterEvent.provider)
if (compatibleProviderIndex > -1) {
provider.id = addFilterEvent.id
provider.extraParams = addFilterEvent.filterParams
}
break
}
}
console.debug('Search scope set to conversation', addFilterEvent)
this.debouncedFind(this.searchQuery)
},
focusInput() {
this.$refs.searchInput.$el.children[0].children[0].focus()
},
Expand All @@ -549,7 +592,7 @@ export default {
padding-block: 10px 0;
// inline padding on direct children to make sure the scrollbar is on the modal container
> * {
>* {
padding-inline: 20px;
}
Expand Down

0 comments on commit e25f0ff

Please sign in to comment.