Merge pull request #43189 from nextcloud/42914-search-in-single-talk-room

Feat: Create filter-plugin architecture for unified search
pull/44023/head
F. E Noel Nfebe 2024-03-06 11:07:23 +07:00 committed by GitHub
commit 0a64b69ab8
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
13 changed files with 166 additions and 24 deletions

@ -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,
},
})

@ -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 })
},
},
})

@ -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),

@ -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;
}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

@ -1,7 +1,35 @@
/*
* @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/>.
*
*/
/*!
* pinia v2.1.7
* (c) 2023 Eduardo San Martin Morote
* @license MIT
*/
/**
* @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
*

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long