diff options
Diffstat (limited to 'core/src/components/UnifiedSearch/UnifiedSearchModal.vue')
-rw-r--r-- | core/src/components/UnifiedSearch/UnifiedSearchModal.vue | 234 |
1 files changed, 157 insertions, 77 deletions
diff --git a/core/src/components/UnifiedSearch/UnifiedSearchModal.vue b/core/src/components/UnifiedSearch/UnifiedSearchModal.vue index 75009ee9be5..e59058bc0f0 100644 --- a/core/src/components/UnifiedSearch/UnifiedSearchModal.vue +++ b/core/src/components/UnifiedSearch/UnifiedSearchModal.vue @@ -9,6 +9,7 @@ dialog-classes="unified-search-modal" :name="t('core', 'Unified search')" :open="open" + size="normal" @update:open="onUpdateOpen"> <!-- Modal for picking custom time range --> <CustomDateRangeModal :is-open="showDateRangeModal" @@ -33,6 +34,7 @@ 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}-${provider.name.replace(/\s/g, '')}`" + :disabled="provider.disabled" @click="addProviderFilter(provider)"> <template #icon> <img :src="provider.icon" class="filter-button__icon" alt=""> @@ -84,6 +86,13 @@ <IconFilter :size="20" /> </template> </NcButton> + <NcCheckboxRadioSwitch v-if="hasExternalResources" + v-model="searchExternalResources" + type="switch" + class="unified-search-modal__search-external-resources" + :class="{'unified-search-modal__search-external-resources--aligned': localSearch}"> + {{ t('core', 'Search connected services') }} + </NcCheckboxRadioSwitch> </div> <div class="unified-search-modal__filters-applied"> <FilterChip v-for="filter in filters" @@ -114,10 +123,12 @@ </div> <div v-else class="unified-search-modal__results"> - <h3 class="hidden-visually">{{ t('core', 'Results') }}</h3> + <h3 class="hidden-visually"> + {{ t('core', 'Results') }} + </h3> <div v-for="providerResult in results" :key="providerResult.id" class="result"> <h4 :id="`unified-search-result-${providerResult.id}`" class="result-title"> - {{ providerResult.provider }} + {{ providerResult.name }} </h4> <ul class="result-items" :aria-labelledby="`unified-search-result-${providerResult.id}`"> <SearchResult v-for="(result, index) in providerResult.results" @@ -125,14 +136,14 @@ v-bind="result" /> </ul> <div class="result-footer"> - <NcButton type="tertiary-no-background" @click="loadMoreResultsForProvider(providerResult.id)"> + <NcButton v-if="providerResult.results.length === providerResult.limit" type="tertiary-no-background" @click="loadMoreResultsForProvider(providerResult)"> {{ t('core', 'Load more results') }} <template #icon> <IconDotsHorizontal :size="20" /> </template> </NcButton> <NcButton v-if="providerResult.inAppSearch" alignment="end-reverse" type="tertiary-no-background"> - {{ t('core', 'Search in') }} {{ providerResult.provider }} + {{ t('core', 'Search in') }} {{ providerResult.name }} <template #icon> <IconArrowRight :size="20" /> </template> @@ -155,19 +166,20 @@ import debounce from 'debounce' import { unifiedSearchLogger } from '../../logger' import IconArrowRight from 'vue-material-design-icons/ArrowRight.vue' -import IconAccountGroup from 'vue-material-design-icons/AccountGroup.vue' -import IconCalendarRange from 'vue-material-design-icons/CalendarRange.vue' +import IconAccountGroup from 'vue-material-design-icons/AccountGroupOutline.vue' +import IconCalendarRange from 'vue-material-design-icons/CalendarRangeOutline.vue' import IconDotsHorizontal from 'vue-material-design-icons/DotsHorizontal.vue' import IconFilter from 'vue-material-design-icons/Filter.vue' import IconListBox from 'vue-material-design-icons/ListBox.vue' import IconMagnify from 'vue-material-design-icons/Magnify.vue' -import NcActions from '@nextcloud/vue/dist/Components/NcActions.js' -import NcActionButton from '@nextcloud/vue/dist/Components/NcActionButton.js' -import NcAvatar from '@nextcloud/vue/dist/Components/NcAvatar.js' -import NcButton from '@nextcloud/vue/dist/Components/NcButton.js' -import NcEmptyContent from '@nextcloud/vue/dist/Components/NcEmptyContent.js' -import NcInputField from '@nextcloud/vue/dist/Components/NcInputField.js' -import NcDialog from '@nextcloud/vue/dist/Components/NcDialog.js' +import NcActions from '@nextcloud/vue/components/NcActions' +import NcActionButton from '@nextcloud/vue/components/NcActionButton' +import NcAvatar from '@nextcloud/vue/components/NcAvatar' +import NcButton from '@nextcloud/vue/components/NcButton' +import NcEmptyContent from '@nextcloud/vue/components/NcEmptyContent' +import NcInputField from '@nextcloud/vue/components/NcInputField' +import NcDialog from '@nextcloud/vue/components/NcDialog' +import NcCheckboxRadioSwitch from '@nextcloud/vue/components/NcCheckboxRadioSwitch' import CustomDateRangeModal from './CustomDateRangeModal.vue' import FilterChip from './SearchFilterChip.vue' @@ -194,6 +206,7 @@ export default defineComponent({ NcEmptyContent, NcDialog, NcInputField, + NcCheckboxRadioSwitch, SearchableList, SearchResult, }, @@ -248,11 +261,10 @@ export default defineComponent({ providerResultLimit: 5, dateFilter: { id: 'date', type: 'date', text: '', startFrom: null, endAt: null }, personFilter: { id: 'person', type: 'person', name: '' }, - dateFilterIsApplied: false, - personFilterIsApplied: false, filteredProviders: [], searching: false, searchQuery: '', + lastSearchQuery: '', placessearchTerm: '', dateTimeFilter: null, filters: [], @@ -260,6 +272,8 @@ export default defineComponent({ contacts: [], showDateRangeModal: false, internalIsVisible: this.open, + initialized: false, + searchExternalResources: false, } }, @@ -297,38 +311,57 @@ export default defineComponent({ debouncedFilterContacts() { return debounce(this.filterContacts, 300) }, + + hasExternalResources() { + return this.providers.some(provider => provider.isExternalProvider) + }, }, watch: { open() { // Load results when opened with already filled query - if (this.open && this.searchQuery) { - this.find(this.searchQuery) + if (this.open) { + this.focusInput() + if (!this.initialized) { + Promise.all([getProviders(), getContacts({ searchTerm: '' })]) + .then(([providers, contacts]) => { + this.providers = this.groupProvidersByApp([...providers, ...this.externalFilters]) + this.contacts = this.mapContacts(contacts) + unifiedSearchLogger.debug('Search providers and contacts initialized:', { providers: this.providers, contacts: this.contacts }) + this.initialized = true + }) + .catch((error) => { + unifiedSearchLogger.error(error) + }) + } + if (this.searchQuery) { + this.find(this.searchQuery) + } } }, query: { immediate: true, handler() { - this.searchQuery = this.query.trim() + this.searchQuery = this.query + }, + }, + + searchQuery: { + handler() { + this.$emit('update:query', this.searchQuery) + }, + }, + + searchExternalResources() { + if (this.searchQuery) { + this.find(this.searchQuery) } }, }, 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) - unifiedSearchLogger.debug('Search providers', { providers: this.providers }) - }) - getContacts({ searchTerm: '' }).then((contacts) => { - this.contacts = this.mapContacts(contacts) - unifiedSearchLogger.debug('Contacts', { contacts: this.contacts }) - }) }, methods: { /** @@ -349,56 +382,77 @@ export default defineComponent({ this.$emit('update:query', this.searchQuery) this.$emit('update:open', false) }, - - find(query: string) { + focusInput() { + this.$nextTick(() => { + this.$refs.searchInput?.focus() + }) + }, + find(query: string, providersToSearchOverride = null) { if (query.length === 0) { this.results = [] this.searching = false return } + // Reset the provider result limit when performing a new search + if (query !== this.lastSearchQuery) { + this.providerResultLimit = 5 + } + this.lastSearchQuery = query + this.searching = true const newResults = [] - const providersToSearch = this.filteredProviders.length > 0 ? this.filteredProviders : this.providers - const searchProvider = (provider, filters) => { + const providersToSearch = providersToSearchOverride || (this.filteredProviders.length > 0 ? this.filteredProviders : this.providers) + const searchProvider = (provider) => { const params = { - type: provider.id, + type: provider.searchFrom ?? provider.id, query, cursor: null, extraQueries: provider.extraParams, } - if (filters.dateFilterIsApplied) { - if (provider.filters?.since && provider.filters?.until) { - params.since = this.dateFilter.startFrom - params.until = this.dateFilter.endAt - } else { - // Date filter is applied but provider does not support it, no need to search provider - return - } - } + // This block of filter checks should be dynamic somehow and should be handled in + // nextcloud/search lib + const activeFilters = this.filters.filter(filter => { + return filter.type !== 'provider' && this.providerIsCompatibleWithFilters(provider, [filter.type]) + }) - if (filters.personFilterIsApplied) { - if (provider.filters?.person) { - params.person = this.personFilter.user - } else { - // Person filter is applied but provider does not support it, no need to search provider - return + activeFilters.forEach(filter => { + switch (filter.type) { + case 'date': + if (provider.filters?.since && provider.filters?.until) { + params.since = this.dateFilter.startFrom + params.until = this.dateFilter.endAt + } + break + case 'person': + if (provider.filters?.person) { + params.person = this.personFilter.user + } + break } - } + }) if (this.providerResultLimit > 5) { params.limit = this.providerResultLimit + unifiedSearchLogger.debug('Limiting search to', params.limit) + } + + const shouldSkipSearch = !this.searchExternalResources && provider.isExternalProvider + const wasManuallySelected = this.filteredProviders.some(filteredProvider => filteredProvider.id === provider.id) + // if the provider is an external resource and the user has not manually selected it, skip the search + if (shouldSkipSearch && !wasManuallySelected) { + this.searching = false + return } const request = unifiedSearch(params).request request().then((response) => { newResults.push({ - id: provider.id, - provider: provider.name, - inAppSearch: provider.inAppSearch, + ...provider, results: response.data.ocs.data.entries, + limit: params.limit ?? 5, }) unifiedSearchLogger.debug('Unified search results:', { results: this.results, newResults }) @@ -407,12 +461,8 @@ export default defineComponent({ this.searching = false }) } - providersToSearch.forEach(provider => { - const dateFilterIsApplied = this.dateFilterIsApplied - const personFilterIsApplied = this.personFilterIsApplied - searchProvider(provider, { dateFilterIsApplied, personFilterIsApplied }) - }) + providersToSearch.forEach(searchProvider) }, updateResults(newResults) { let updatedResults = [...this.results] @@ -470,7 +520,7 @@ export default defineComponent({ }) }, applyPersonFilter(person) { - this.personFilterIsApplied = true + const existingPersonFilter = this.filters.findIndex(filter => filter.id === person.id) if (existingPersonFilter === -1) { this.personFilter.id = person.id @@ -483,19 +533,27 @@ export default defineComponent({ this.filters[existingPersonFilter].name = person.displayName } + this.providers.forEach(async (provider, index) => { + this.providers[index].disabled = !(await this.providerIsCompatibleWithFilters(provider, ['person'])) + }) + this.debouncedFind(this.searchQuery) unifiedSearchLogger.debug('Person filter applied', { person }) }, - loadMoreResultsForProvider(providerId) { + async loadMoreResultsForProvider(provider) { this.providerResultLimit += 5 - this.filters = this.filters.filter(filter => filter.type !== 'provider') - const provider = this.providers.find(provider => provider.id === providerId) - this.addProviderFilter(provider, true) + this.find(this.searchQuery, [provider]) }, addProviderFilter(providerFilter, loadMoreResultsForProvider = false) { + unifiedSearchLogger.debug('Applying provider filter', { providerFilter, loadMoreResultsForProvider }) if (!providerFilter.id) return if (providerFilter.isPluginFilter) { - providerFilter.callback() + // There is no way to know what should go into the callback currently + // Here we are passing isProviderFilterApplied (boolean) which is a flag sent to the plugin + // This is sent to the plugin so that depending on whether the filter is applied or not, the plugin can decide what to do + // TODO : In nextcloud/search, this should be a proper interface that the plugin can implement + const isProviderFilterApplied = this.filteredProviders.some(provider => provider.id === providerFilter.id) + providerFilter.callback(!isProviderFilterApplied) } this.providerResultLimit = loadMoreResultsForProvider ? this.providerResultLimit : 5 this.providerActionMenuIsOpen = false @@ -508,11 +566,8 @@ export default defineComponent({ this.filters = this.syncProviderFilters(this.filters, this.filteredProviders) } this.filteredProviders.push({ - id: providerFilter.id, - name: providerFilter.name, - icon: providerFilter.icon, + ...providerFilter, type: providerFilter.type || 'provider', - filters: providerFilter.filters, isPluginFilter: providerFilter.isPluginFilter || false, }) this.filters = this.syncProviderFilters(this.filters, this.filteredProviders) @@ -531,14 +586,11 @@ export default defineComponent({ unifiedSearchLogger.debug('Search filters (recently removed)', { filters: this.filters }) } else { + // Remove non provider filters such as date and person filters for (let i = 0; i < this.filters.length; i++) { - // Remove date and person filter - if (this.filters[i].id === 'date' || this.filters[i].id === filter.id) { - this.dateFilterIsApplied = false + if (this.filters[i].id === filter.id) { this.filters.splice(i, 1) - if (filter.type === 'person') { - this.personFilterIsApplied = false - } + this.enableAllProviders() break } } @@ -576,7 +628,10 @@ export default defineComponent({ } else { this.filters.push(this.dateFilter) } - this.dateFilterIsApplied = true + + this.providers.forEach(async (provider, index) => { + this.providers[index].disabled = !(await this.providerIsCompatibleWithFilters(provider, ['since', 'until'])) + }) this.debouncedFind(this.searchQuery) }, applyQuickDateRange(range) { @@ -633,6 +688,7 @@ export default defineComponent({ this.updateDateFilter() }, handlePluginFilter(addFilterEvent) { + unifiedSearchLogger.debug('Handling plugin filter', { addFilterEvent }) for (let i = 0; i < this.filteredProviders.length; i++) { const provider = this.filteredProviders[i] if (provider.id === addFilterEvent.id) { @@ -667,6 +723,14 @@ export default defineComponent({ return flattenedArray }, + async providerIsCompatibleWithFilters(provider, filterIds) { + return filterIds.every(filterId => provider.filters?.[filterId] !== undefined) + }, + async enableAllProviders() { + this.providers.forEach(async (_, index) => { + this.providers[index].disabled = false + }) + }, }, }) </script> @@ -706,6 +770,21 @@ export default defineComponent({ padding-top: 4px; } + &__search-external-resources { + :deep(span.checkbox-content) { + padding-top: 0; + padding-bottom: 0; + } + + :deep(.checkbox-content__icon) { + margin: auto !important; + } + + &--aligned { + margin-inline-start: auto; + } + } + &__filters-applied { padding-top: 4px; display: flex; @@ -715,7 +794,8 @@ export default defineComponent({ &__no-content { display: flex; align-items: center; - height: 100%; + margin-top: 0.5em; + height: 70%; } &__results { |