diff options
Diffstat (limited to 'core/src/components/UnifiedSearch')
6 files changed, 157 insertions, 153 deletions
diff --git a/core/src/components/UnifiedSearch/CustomDateRangeModal.vue b/core/src/components/UnifiedSearch/CustomDateRangeModal.vue index 332a4286863..d86192d156e 100644 --- a/core/src/components/UnifiedSearch/CustomDateRangeModal.vue +++ b/core/src/components/UnifiedSearch/CustomDateRangeModal.vue @@ -37,9 +37,9 @@ </template> <script> -import NcButton from '@nextcloud/vue/dist/Components/NcButton.js' -import NcDateTimePicker from '@nextcloud/vue/dist/Components/NcDateTimePickerNative.js' -import NcModal from '@nextcloud/vue/dist/Components/NcModal.js' +import NcButton from '@nextcloud/vue/components/NcButton' +import NcDateTimePicker from '@nextcloud/vue/components/NcDateTimePickerNative' +import NcModal from '@nextcloud/vue/components/NcModal' import CalendarRangeIcon from 'vue-material-design-icons/CalendarRange.vue' export default { diff --git a/core/src/components/UnifiedSearch/LegacySearchResult.vue b/core/src/components/UnifiedSearch/LegacySearchResult.vue index 085a6936f2b..4592adf08c9 100644 --- a/core/src/components/UnifiedSearch/LegacySearchResult.vue +++ b/core/src/components/UnifiedSearch/LegacySearchResult.vue @@ -42,7 +42,7 @@ </template> <script> -import NcHighlight from '@nextcloud/vue/dist/Components/NcHighlight.js' +import NcHighlight from '@nextcloud/vue/components/NcHighlight' export default { name: 'LegacySearchResult', diff --git a/core/src/components/UnifiedSearch/SearchResult.vue b/core/src/components/UnifiedSearch/SearchResult.vue index 231ac97642c..4f33fbd54cc 100644 --- a/core/src/components/UnifiedSearch/SearchResult.vue +++ b/core/src/components/UnifiedSearch/SearchResult.vue @@ -3,18 +3,18 @@ - SPDX-License-Identifier: AGPL-3.0-or-later --> <template> - <NcListItem class="result-items__item" + <NcListItem class="result-item" :name="title" :bold="false" :href="resourceUrl" target="_self"> <template #icon> <div aria-hidden="true" - class="result-items__item-icon" + class="result-item__icon" :class="{ - 'result-items__item-icon--rounded': rounded, - 'result-items__item-icon--no-preview': !isValidIconOrPreviewUrl(thumbnailUrl), - 'result-items__item-icon--with-thumbnail': isValidIconOrPreviewUrl(thumbnailUrl), + 'result-item__icon--rounded': rounded, + 'result-item__icon--no-preview': !isValidIconOrPreviewUrl(thumbnailUrl), + 'result-item__icon--with-thumbnail': isValidIconOrPreviewUrl(thumbnailUrl), [icon]: !isValidIconOrPreviewUrl(icon), }" :style="{ @@ -32,7 +32,7 @@ </template> <script> -import NcListItem from '@nextcloud/vue/dist/Components/NcListItem.js' +import NcListItem from '@nextcloud/vue/components/NcListItem' export default { name: 'SearchResult', @@ -101,72 +101,59 @@ export default { </script> <style lang="scss" scoped> -@use "sass:math"; -$clickable-area: 44px; -$margin: 10px; - -.result-items { - &__item:deep { - - a { - border: 2px solid transparent; - border-radius: var(--border-radius-large) !important; - - &--focused { - background-color: var(--color-background-hover); - } - - &:active, - &:hover, - &:focus { - background-color: var(--color-background-hover); - border: 2px solid var(--color-border-maxcontrast); - } +.result-item { + :deep(a) { + border: 2px solid transparent; + border-radius: var(--border-radius-large) !important; + + &:active, + &:hover, + &:focus { + background-color: var(--color-background-hover); + border: 2px solid var(--color-border-maxcontrast); + } - * { - cursor: pointer; - } + * { + cursor: pointer; + } + } + &__icon { + overflow: hidden; + width: var(--default-clickable-area); + height: var(--default-clickable-area); + border-radius: var(--border-radius); + background-repeat: no-repeat; + background-position: center center; + background-size: 32px; + + &--rounded { + border-radius: calc(var(--default-clickable-area) / 2); } - &-icon { - overflow: hidden; - width: $clickable-area; - height: $clickable-area; - border-radius: var(--border-radius); - background-repeat: no-repeat; - background-position: center center; + &--no-preview { background-size: 32px; + } - &--rounded { - border-radius: math.div($clickable-area, 2); - } - - &--no-preview { - background-size: 32px; - } - - &--with-thumbnail { - background-size: cover; - } + &--with-thumbnail { + background-size: cover; + } - &--with-thumbnail:not(&--rounded) { - // compensate for border - max-width: $clickable-area - 2px; - max-height: $clickable-area - 2px; - border: 1px solid var(--color-border); - } + &--with-thumbnail:not(#{&}--rounded) { + border: 1px solid var(--color-border); + // compensate for border + max-height: calc(var(--default-clickable-area) - 2px); + max-width: calc(var(--default-clickable-area) - 2px); + } - img { - // Make sure to keep ratio - width: 100%; - height: 100%; + img { + // Make sure to keep ratio + width: 100%; + height: 100%; - object-fit: cover; - object-position: center; - } + object-fit: cover; + object-position: center; } - } } </style> diff --git a/core/src/components/UnifiedSearch/SearchableList.vue b/core/src/components/UnifiedSearch/SearchableList.vue index b2081c2c5ee..d7abb6ffdbb 100644 --- a/core/src/components/UnifiedSearch/SearchableList.vue +++ b/core/src/components/UnifiedSearch/SearchableList.vue @@ -17,7 +17,7 @@ :show-trailing-button="searchTerm !== ''" @update:value="searchTermChanged" @trailing-button-click="clearSearch"> - <Magnify :size="20" /> + <IconMagnify :size="20" /> </NcTextField> <ul v-if="filteredList.length > 0" class="searchable-list__list"> <li v-for="element in filteredList" @@ -42,7 +42,7 @@ <div v-else class="searchable-list__empty-content"> <NcEmptyContent :name="emptyContentText"> <template #icon> - <AlertCircleOutline /> + <IconAlertCircleOutline /> </template> </NcEmptyContent> </div> @@ -51,22 +51,26 @@ </template> <script> -import { NcPopover, NcTextField, NcAvatar, NcEmptyContent, NcButton } from '@nextcloud/vue' +import NcAvatar from '@nextcloud/vue/components/NcAvatar' +import NcButton from '@nextcloud/vue/components/NcButton' +import NcEmptyContent from '@nextcloud/vue/components/NcEmptyContent' +import NcPopover from '@nextcloud/vue/components/NcPopover' +import NcTextField from '@nextcloud/vue/components/NcTextField' -import AlertCircleOutline from 'vue-material-design-icons/AlertCircleOutline.vue' -import Magnify from 'vue-material-design-icons/Magnify.vue' +import IconAlertCircleOutline from 'vue-material-design-icons/AlertCircleOutline.vue' +import IconMagnify from 'vue-material-design-icons/Magnify.vue' export default { name: 'SearchableList', components: { - NcPopover, - NcTextField, - Magnify, - AlertCircleOutline, + IconMagnify, + IconAlertCircleOutline, NcAvatar, - NcEmptyContent, NcButton, + NcEmptyContent, + NcPopover, + NcTextField, }, props: { diff --git a/core/src/components/UnifiedSearch/UnifiedSearchLocalSearchBar.vue b/core/src/components/UnifiedSearch/UnifiedSearchLocalSearchBar.vue index 67853490d9f..171eada8a06 100644 --- a/core/src/components/UnifiedSearch/UnifiedSearchLocalSearchBar.vue +++ b/core/src/components/UnifiedSearch/UnifiedSearchLocalSearchBar.vue @@ -32,7 +32,7 @@ {{ t('core', 'Search everywhere') }} </template> <template #icon> - <NcIconSvgWrapper :path="mdiCloudSearch" /> + <NcIconSvgWrapper :path="mdiCloudSearchOutline" /> </template> </NcButton> </div> @@ -41,15 +41,15 @@ <script lang="ts" setup> import type { ComponentPublicInstance } from 'vue' -import { mdiCloudSearch, mdiClose } from '@mdi/js' +import { mdiCloudSearchOutline, mdiClose } from '@mdi/js' import { translate as t } from '@nextcloud/l10n' -import { useIsMobile } from '@nextcloud/vue/dist/Composables/useIsMobile.js' +import { useIsMobile } from '@nextcloud/vue/composables/useIsMobile' +import { useElementSize } from '@vueuse/core' import { computed, ref, watchEffect } from 'vue' -import NcButton from '@nextcloud/vue/dist/Components/NcButton.js' -import NcIconSvgWrapper from '@nextcloud/vue/dist/Components/NcIconSvgWrapper.js' -import NcInputField from '@nextcloud/vue/dist/Components/NcInputField.js' -import { useElementSize } from '@vueuse/core' +import NcButton from '@nextcloud/vue/components/NcButton' +import NcIconSvgWrapper from '@nextcloud/vue/components/NcIconSvgWrapper' +import NcInputField from '@nextcloud/vue/components/NcInputField' const props = defineProps<{ query: string, @@ -123,7 +123,7 @@ function clearAndCloseSearch() { // this can break at any time the component library changes :deep(input) { // search global width + close button width - padding-inline-end: calc(v-bind('searchGlobalButtonWidth') + var(--default-clickable-area)); + padding-inline-end: calc(v-bind('searchGlobalButtonCSSWidth') + var(--default-clickable-area)); } } } @@ -132,8 +132,8 @@ function clearAndCloseSearch() { transition: width var(--animation-quick) linear; } -// Make the position absolut during the transition -// this is needed to "hide" the button begind it +// Make the position absolute during the transition +// this is needed to "hide" the button behind it .v-leave-active { position: absolute !important; } @@ -141,7 +141,7 @@ function clearAndCloseSearch() { .v-enter, .v-leave-to { &.local-unified-search { - // Start with only the overlayed button + // Start with only the overlay button --local-search-width: var(--clickable-area-large); } } diff --git a/core/src/components/UnifiedSearch/UnifiedSearchModal.vue b/core/src/components/UnifiedSearch/UnifiedSearchModal.vue index 7400956f96b..b21c65301c4 100644 --- a/core/src/components/UnifiedSearch/UnifiedSearchModal.vue +++ b/core/src/components/UnifiedSearch/UnifiedSearchModal.vue @@ -121,7 +121,7 @@ </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" @@ -129,14 +129,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> @@ -159,19 +159,19 @@ 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 CustomDateRangeModal from './CustomDateRangeModal.vue' import FilterChip from './SearchFilterChip.vue' @@ -252,11 +252,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: [], @@ -264,6 +263,7 @@ export default defineComponent({ contacts: [], showDateRangeModal: false, internalIsVisible: this.open, + initialized: false, } }, @@ -308,6 +308,18 @@ export default defineComponent({ // Load results when opened with already filled query 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) } @@ -317,25 +329,19 @@ export default defineComponent({ query: { immediate: true, handler() { - this.searchQuery = this.query.trim() + this.searchQuery = this.query + }, + }, + + searchQuery: { + handler() { + this.$emit('update:query', 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: { /** @@ -361,19 +367,25 @@ export default defineComponent({ this.$refs.searchInput?.focus() }) }, - find(query: string) { + 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, @@ -381,31 +393,38 @@ export default defineComponent({ // This block of filter checks should be dynamic somehow and should be handled in // nextcloud/search lib - if (filters.dateFilterIsApplied) { - if (provider.filters?.since && provider.filters?.until) { - params.since = this.dateFilter.startFrom - params.until = this.dateFilter.endAt - } - } + 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 + 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 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 }) @@ -414,12 +433,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] @@ -477,7 +492,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 @@ -497,16 +512,20 @@ export default defineComponent({ 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 @@ -519,11 +538,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) @@ -542,14 +558,10 @@ 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 } @@ -588,7 +600,7 @@ 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'])) }) @@ -648,6 +660,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) { |