diff options
author | fenn-cs <fenn25.fn@gmail.com> | 2024-01-29 12:20:05 +0100 |
---|---|---|
committer | fenn-cs <fenn25.fn@gmail.com> | 2024-03-06 02:05:50 +0100 |
commit | d35a49caba0ec1b8ff2857b77b9471ea53076838 (patch) | |
tree | 8564e65f858a7127a2fe319c188aaf01c53f2aef | |
parent | 9f1f123990f0186e50c17337b3dda588c0b33f28 (diff) | |
download | nextcloud-server-d35a49caba0ec1b8ff2857b77b9471ea53076838.tar.gz nextcloud-server-d35a49caba0ec1b8ff2857b77b9471ea53076838.zip |
feat(core): create filter-plugin architecture for unified search
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>
-rw-r--r-- | core/src/services/UnifiedSearchService.js | 4 | ||||
-rw-r--r-- | core/src/store/index.js | 11 | ||||
-rw-r--r-- | core/src/store/unified-search-external-filters.js | 42 | ||||
-rw-r--r-- | core/src/unified-search.js | 20 | ||||
-rw-r--r-- | core/src/views/UnifiedSearchModal.vue | 77 |
5 files changed, 144 insertions, 10 deletions
diff --git a/core/src/services/UnifiedSearchService.js b/core/src/services/UnifiedSearchService.js index 9e16fe1880c..54cd19b6a92 100644 --- a/core/src/services/UnifiedSearchService.js +++ b/core/src/services/UnifiedSearchService.js @@ -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 */ @@ -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, }, }) diff --git a/core/src/store/index.js b/core/src/store/index.js new file mode 100644 index 00000000000..d263a5dc407 --- /dev/null +++ b/core/src/store/index.js @@ -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, + }, +}); diff --git a/core/src/store/unified-search-external-filters.js b/core/src/store/unified-search-external-filters.js new file mode 100644 index 00000000000..45d0f4c7090 --- /dev/null +++ b/core/src/store/unified-search-external-filters.js @@ -0,0 +1,42 @@ +/* + * @copyright Copyright (c) 2024 Fon E. Noel NFEBE <opensource@nfebe.com> + * + * @author Fon E. Noel NFEBE <opensource@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, +} diff --git a/core/src/unified-search.js b/core/src/unified-search.js index f9bddff4c68..3d7ead37cbb 100644 --- a/core/src/unified-search.js +++ b/core/src/unified-search.js @@ -1,7 +1,7 @@ /** - * @copyright Copyright (c) 2020 Fon E. Noel NFEBE <fenn25.fn@gmail.com> + * @copyright Copyright (c) 2024 Fon E. Noel NFEBE <opensource@nfebe.com> * - * @author Fon E. Noel NFEBE <fenn25.fn@gmail.com> + * @author Fon E. Noel NFEBE <opensource@nfebe.com> * * @license AGPL-3.0-or-later * @@ -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()) @@ -47,9 +48,24 @@ Vue.mixin({ }, }) +// Register the add/register filter action API globally +window.OCA = window.OCA || {} +window.OCA.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), }) diff --git a/core/src/views/UnifiedSearchModal.vue b/core/src/views/UnifiedSearchModal.vue index 101859f950f..881e49d6405 100644 --- a/core/src/views/UnifiedSearchModal.vue +++ b/core/src/views/UnifiedSearchModal.vue @@ -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=""> @@ -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 { @@ -217,6 +220,9 @@ export default { }, computed: { + ...mapState({ + externalFilters: state => state.search.externalFilters, + }), userContacts() { return this.contacts }, @@ -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) => { @@ -284,6 +295,7 @@ export default { type: provider.id, query, cursor: null, + extraQueries: provider.extraParams, } if (filters.dateFilterIsApplied) { @@ -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) @@ -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() }, @@ -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; } |