aboutsummaryrefslogtreecommitdiffstats
path: root/core
diff options
context:
space:
mode:
authorF. E Noel Nfebe <fenn25.fn@gmail.com>2024-03-06 11:07:23 +0100
committerGitHub <noreply@github.com>2024-03-06 11:07:23 +0100
commit0a64b69ab8e9b4350a40a03ee1b971a093417498 (patch)
tree1e6c09015cc74413a8cab4300434195ecdfe8d52 /core
parent945d97ded1469da0b93285c1817cf5ff4937f88f (diff)
parenta5c207f1e7fefeb2d6f57b2d1d40827e4366e329 (diff)
downloadnextcloud-server-0a64b69ab8e9b4350a40a03ee1b971a093417498.tar.gz
nextcloud-server-0a64b69ab8e9b4350a40a03ee1b971a093417498.zip
Merge pull request #43189 from nextcloud/42914-search-in-single-talk-room
Feat: Create filter-plugin architecture for unified search
Diffstat (limited to 'core')
-rw-r--r--core/src/services/UnifiedSearchService.js4
-rw-r--r--core/src/store/unified-search-external-filters.js36
-rw-r--r--core/src/unified-search.js19
-rw-r--r--core/src/views/UnifiedSearchModal.vue75
4 files changed, 124 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/unified-search-external-filters.js b/core/src/store/unified-search-external-filters.js
new file mode 100644
index 00000000000..3a8cd2aca2d
--- /dev/null
+++ b/core/src/store/unified-search-external-filters.js
@@ -0,0 +1,36 @@
+/*
+ * @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/>.
+ *
+ */
+import { defineStore } from 'pinia'
+
+export const useSearchStore = defineStore({
+ id: 'search',
+
+ state: () => ({
+ externalFilters: [],
+ }),
+
+ actions: {
+ registerExternalFilter({ id, appId, label, callback, icon }) {
+ this.externalFilters.push({ id, appId, name: label, callback, icon, isPluginFilter: true })
+ },
+ },
+})
diff --git a/core/src/unified-search.js b/core/src/unified-search.js
index f9bddff4c68..4be923085d3 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
*
@@ -23,9 +23,11 @@
import { getLoggerBuilder } from '@nextcloud/logger'
import { getRequestToken } from '@nextcloud/auth'
import { translate as t, translatePlural as n } from '@nextcloud/l10n'
+import { createPinia, PiniaVuePlugin } from 'pinia'
import Vue from 'vue'
import UnifiedSearch from './views/UnifiedSearch.vue'
+import { useSearchStore } from '../src/store/unified-search-external-filters.js'
// eslint-disable-next-line camelcase
__webpack_nonce__ = btoa(getRequestToken())
@@ -47,8 +49,21 @@ Vue.mixin({
},
})
+// Register the add/register filter action API globally
+window.OCA = window.OCA || {}
+window.OCA.UnifiedSearch = {
+ registerFilterAction: ({ id, appId, label, callback, icon }) => {
+ const searchStore = useSearchStore()
+ searchStore.registerExternalFilter({ id, appId, label, callback, icon })
+ },
+}
+
+Vue.use(PiniaVuePlugin)
+const pinia = createPinia()
+
export default new Vue({
el: '#unified-search',
+ pinia,
// eslint-disable-next-line vue/match-component-file-name
name: 'UnifiedSearchRoot',
render: h => h(UnifiedSearch),
diff --git a/core/src/views/UnifiedSearchModal.vue b/core/src/views/UnifiedSearchModal.vue
index 101859f950f..b33cebed8e2 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,9 +152,10 @@ 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 { getProviders, search as unifiedSearch, getContacts } from '../services/UnifiedSearchService.js'
+import { useSearchStore } from '../store/unified-search-external-filters.js'
export default {
name: 'UnifiedSearchModal',
@@ -187,8 +190,10 @@ export default {
* Reactive version of window.location
*/
const currentLocation = useBrowserLocation()
+ const searchStore = useSearchStore()
return {
currentLocation,
+ externalFilters: searchStore.externalFilters,
}
},
data() {
@@ -258,8 +263,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 +294,7 @@ export default {
type: provider.id,
query,
cursor: null,
+ extraQueries: provider.extraParams,
}
if (filters.dateFilterIsApplied) {
@@ -412,12 +423,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 +561,41 @@ 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
+ }
+ }
+ 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 +618,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;
}