Skip to content

Commit

Permalink
feat(core): 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 Mar 4, 2024
1 parent 8ae4a32 commit 86da2ad
Show file tree
Hide file tree
Showing 5 changed files with 142 additions and 8 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 {object} options.extraQueries additional queries to filter search results
* @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.OCP = window.OCP || {}
window.OCP.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),
})
77 changes: 70 additions & 7 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 @@ -217,6 +220,9 @@ export default {
},
computed: {
...mapState({
externalFilters: state => state.search.externalFilters,
}),
userContacts() {
return this.contacts
},
Expand Down Expand Up @@ -258,8 +264,13 @@ export default {
},
mounted() {
subscribe('nextcloud:unified-search:add-filter', this.handlePluginFilter)
getProviders().then((providers) => {
this.providers = providers
this.externalFilters.forEach(filter => {
this.providers.push(filter)
})
this.providers = this.groupProvidersByApp(this.providers)
console.debug('Search providers', this.providers)
})
getContacts({ searchTerm: '' }).then((contacts) => {
Expand All @@ -284,6 +295,7 @@ export default {
type: provider.id,
query,
cursor: null,
extraQueries: provider.extraParams,
}
if (filters.dateFilterIsApplied) {
Expand Down Expand Up @@ -412,12 +424,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 @@ -535,6 +562,42 @@ export default {
this.dateFilter.text = t('core', `Between ${this.dateFilter.startFrom.toLocaleDateString()} and ${this.dateFilter.endAt.toLocaleDateString()}`)
this.updateDateFilter()
},
handlePluginFilter(addFilterEvent) {
for (let i = 0; i < this.filteredProviders.length; i++) {
const provider = this.filteredProviders[i]
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 extra parameters to those providers only
const compatibleProviderIndex = this.providers.findIndex(provider => provider.id === addFilterEvent.id)
if (compatibleProviderIndex > -1) {
provider.extraParams = addFilterEvent.filterParams
this.filteredProviders[i] = provider
}
break
}
}
console.debug('Search scope set to conversation', addFilterEvent)
this.debouncedFind(this.searchQuery)
},
groupProvidersByApp(filters) {
const groupedByProviderApp = {}
filters.forEach(filter => {
const provider = filter.appId ? filter.appId : 'general'
if (!groupedByProviderApp[provider]) {
groupedByProviderApp[provider] = []
}
groupedByProviderApp[provider].push(filter)
})
const flattenedArray = []
Object.values(groupedByProviderApp).forEach(group => {
flattenedArray.push(...group)
})
return flattenedArray
},
focusInput() {
this.$refs.searchInput.$el.children[0].children[0].focus()
},
Expand All @@ -557,7 +620,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 86da2ad

Please sign in to comment.