Skip to content

Commit 0a1d322

Browse files
committed
fix(unified-search): Separate filtered and unfiltered results
Show results from providers that don't support active content filters (date/person) in a separate "Additional results" section with a note explaining that some filters may have been ignored. Changes: - Add computed properties to separate filtered/unfiltered results - Track filter compatibility using baseProvider for searchFrom providers - Deduplicate results by resourceUrl across sections - Skip in-folder results when at root to avoid duplicating Files results - Fix providerIsCompatibleWithFilters to check correct filter properties - Add styling for the unfiltered results section Signed-off-by: nfebe <[email protected]>
1 parent 9fa5e6c commit 0a1d322

File tree

1 file changed

+125
-9
lines changed

1 file changed

+125
-9
lines changed

core/src/components/UnifiedSearch/UnifiedSearchModal.vue

Lines changed: 125 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@
2727
:label="t('core', 'Search apps, files, tags, messages') + '...'"
2828
@update:value="debouncedFind" />
2929
<div class="unified-search-modal__filters" data-cy-unified-search-filters>
30-
<NcActions v-model:open="providerActionMenuIsOpen" :menu-name="t('core', 'Places')" data-cy-unified-search-filter="places">
30+
<NcActions :open.sync="providerActionMenuIsOpen" :menu-name="t('core', 'Places')" data-cy-unified-search-filter="places">
3131
<template #icon>
3232
<IconListBox :size="20" />
3333
</template>
@@ -43,7 +43,7 @@
4343
{{ provider.name }}
4444
</NcActionButton>
4545
</NcActions>
46-
<NcActions v-model:open="dateActionMenuIsOpen" :menu-name="t('core', 'Date')" data-cy-unified-search-filter="date">
46+
<NcActions :open.sync="dateActionMenuIsOpen" :menu-name="t('core', 'Date')" data-cy-unified-search-filter="date">
4747
<template #icon>
4848
<IconCalendarRange :size="20" />
4949
</template>
@@ -120,7 +120,8 @@
120120
<h3 class="hidden-visually">
121121
{{ t('core', 'Results') }}
122122
</h3>
123-
<div v-for="providerResult in results" :key="providerResult.id" class="result">
123+
<!-- Filtered results section -->
124+
<div v-for="providerResult in filteredResults" :key="providerResult.id" class="result">
124125
<h4 :id="`unified-search-result-${providerResult.id}`" class="result-title">
125126
{{ providerResult.name }}
126127
</h4>
@@ -144,6 +145,36 @@
144145
</NcButton>
145146
</div>
146147
</div>
148+
<!-- Unfiltered results section -->
149+
<template v-if="unfilteredResults.length > 0">
150+
<div class="unified-search-modal__unfiltered-header">
151+
<span class="unified-search-modal__unfiltered-label">{{ t('core', 'Partial matches') }}</span>
152+
</div>
153+
<div v-for="providerResult in unfilteredResults" :key="`unfiltered-${providerResult.id}`" class="result result--unfiltered">
154+
<h4 :id="`unified-search-result-unfiltered-${providerResult.id}`" class="result-title">
155+
{{ providerResult.name }}
156+
</h4>
157+
<ul class="result-items" :aria-labelledby="`unified-search-result-unfiltered-${providerResult.id}`">
158+
<SearchResult v-for="(result, index) in providerResult.results"
159+
:key="index"
160+
v-bind="result" />
161+
</ul>
162+
<div class="result-footer">
163+
<NcButton v-if="providerResult.results.length === providerResult.limit" variant="tertiary-no-background" @click="loadMoreResultsForProvider(providerResult)">
164+
{{ t('core', 'Load more results') }}
165+
<template #icon>
166+
<IconDotsHorizontal :size="20" />
167+
</template>
168+
</NcButton>
169+
<NcButton v-if="providerResult.inAppSearch" alignment="end-reverse" variant="tertiary-no-background">
170+
{{ t('core', 'Search in') }} {{ providerResult.name }}
171+
<template #icon>
172+
<IconArrowRight :size="20" />
173+
</template>
174+
</NcButton>
175+
</div>
176+
</div>
177+
</template>
147178
</div>
148179
</NcDialog>
149180
</template>
@@ -302,6 +333,50 @@ export default defineComponent({
302333
debouncedFilterContacts() {
303334
return debounce(this.filterContacts, 300)
304335
},
336+
337+
hasContentFilters() {
338+
return this.filters.some((filter) => filter.type === 'date' || filter.type === 'person')
339+
},
340+
341+
filteredResults() {
342+
const isInFolderAtRoot = (result) => {
343+
if (result.id !== 'in-folder') {
344+
return false
345+
}
346+
const path = result.extraParams?.path
347+
return !path || path === '/' || path === ''
348+
}
349+
350+
if (!this.hasContentFilters) {
351+
return this.results.filter((result) => !isInFolderAtRoot(result))
352+
}
353+
return this.results.filter((result) => result.supportsActiveFilters === true && !isInFolderAtRoot(result))
354+
},
355+
356+
filteredResultUrls() {
357+
const urls = new Set()
358+
this.filteredResults.forEach((provider) => {
359+
provider.results.forEach((entry) => {
360+
if (entry.resourceUrl) {
361+
urls.add(entry.resourceUrl)
362+
}
363+
})
364+
})
365+
return urls
366+
},
367+
368+
unfilteredResults() {
369+
if (!this.hasContentFilters) {
370+
return []
371+
}
372+
return this.results
373+
.filter((result) => result.supportsActiveFilters === false)
374+
.map((provider) => ({
375+
...provider,
376+
results: provider.results.filter((entry) => !this.filteredResultUrls.has(entry.resourceUrl)),
377+
}))
378+
.filter((provider) => provider.results.length > 0)
379+
},
305380
},
306381
307382
watch: {
@@ -394,20 +469,30 @@ export default defineComponent({
394469
395470
// This block of filter checks should be dynamic somehow and should be handled in
396471
// nextcloud/search lib
397-
const activeFilters = this.filters.filter(filter => {
472+
const contentFilterTypes = this.filters
473+
.filter((f) => f.type !== 'provider')
474+
.map((f) => f.type)
475+
const supportsActiveFilters = contentFilterTypes.length === 0
476+
|| contentFilterTypes.every((type) => this.providerIsCompatibleWithFilters(provider, [type]))
477+
478+
const baseProvider = provider.searchFrom
479+
? this.providers.find((p) => p.id === provider.searchFrom) ?? provider
480+
: provider
481+
482+
const activeFilters = this.filters.filter((filter) => {
398483
return filter.type !== 'provider' && this.providerIsCompatibleWithFilters(provider, [filter.type])
399484
})
400485
401-
activeFilters.forEach(filter => {
486+
activeFilters.forEach((filter) => {
402487
switch (filter.type) {
403488
case 'date':
404-
if (provider.filters?.since && provider.filters?.until) {
489+
if (baseProvider.filters?.since && baseProvider.filters?.until) {
405490
params.since = this.dateFilter.startFrom
406491
params.until = this.dateFilter.endAt
407492
}
408493
break
409494
case 'person':
410-
if (provider.filters?.person) {
495+
if (baseProvider.filters?.person) {
411496
params.person = this.personFilter.user
412497
}
413498
break
@@ -426,6 +511,7 @@ export default defineComponent({
426511
...provider,
427512
results: response.data.ocs.data.entries,
428513
limit: params.limit ?? 5,
514+
supportsActiveFilters,
429515
})
430516
431517
unifiedSearchLogger.debug('Unified search results:', { results: this.results, newResults })
@@ -688,8 +774,20 @@ export default defineComponent({
688774
689775
return flattenedArray
690776
},
691-
async providerIsCompatibleWithFilters(provider, filterIds) {
692-
return filterIds.every(filterId => provider.filters?.[filterId] !== undefined)
777+
providerIsCompatibleWithFilters(provider, filterIds) {
778+
const baseProvider = provider.searchFrom
779+
? this.providers.find((p) => p.id === provider.searchFrom) ?? provider
780+
: provider
781+
return filterIds.every((filterId) => {
782+
switch (filterId) {
783+
case 'date':
784+
return baseProvider.filters?.since !== undefined && baseProvider.filters?.until !== undefined
785+
case 'person':
786+
return baseProvider.filters?.person !== undefined
787+
default:
788+
return baseProvider.filters?.[filterId] !== undefined
789+
}
790+
})
693791
},
694792
async enableAllProviders() {
695793
this.providers.forEach(async (_, index) => {
@@ -765,9 +863,27 @@ export default defineComponent({
765863
align-items: center;
766864
display: flex;
767865
}
866+
867+
&--unfiltered {
868+
opacity: 0.7;
869+
}
768870
}
769871
770872
}
873+
874+
&__unfiltered-header {
875+
display: flex;
876+
flex-direction: column;
877+
gap: 2px;
878+
margin-block: 16px 8px;
879+
padding-block: 12px 0;
880+
border-top: 1px solid var(--color-border);
881+
}
882+
883+
&__unfiltered-label {
884+
font-weight: bold;
885+
color: var(--color-text-maxcontrast);
886+
}
771887
}
772888
773889
.filter-button__icon {

0 commit comments

Comments
 (0)