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 >
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 >
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 >
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