2424 <div class =" unified-search-modal__header" >
2525 <NcInputField
2626 ref =" searchInput"
27- v-model:value =" searchQuery"
27+ v-model =" searchQuery"
2828 data-cy-unified-search-input
2929 type =" text"
3030 :label =" t('core', 'Search apps, files, tags, messages') + '...'"
3131 @update:value =" debouncedFind" />
3232 <div class =" unified-search-modal__filters" data-cy-unified-search-filters >
33- <NcActions v-model :open =" providerActionMenuIsOpen" :menu-name =" t('core', 'Places')" data-cy-unified-search-filter =" places" >
33+ <NcActions :open.sync =" providerActionMenuIsOpen" :menu-name =" t('core', 'Places')" data-cy-unified-search-filter =" places" >
3434 <template #icon >
3535 <IconListBox :size =" 20" />
3636 </template >
4747 {{ provider.name }}
4848 </NcActionButton >
4949 </NcActions >
50- <NcActions v-model :open =" dateActionMenuIsOpen" :menu-name =" t('core', 'Date')" data-cy-unified-search-filter =" date" >
50+ <NcActions :open.sync =" dateActionMenuIsOpen" :menu-name =" t('core', 'Date')" data-cy-unified-search-filter =" date" >
5151 <template #icon >
5252 <IconCalendarRange :size =" 20" />
5353 </template >
135135 <h3 class =" hidden-visually" >
136136 {{ t('core', 'Results') }}
137137 </h3 >
138- <div v-for =" providerResult in results" :key =" providerResult.id" class =" result" >
138+ <!-- Filtered results section -->
139+ <div v-for =" providerResult in filteredResults" :key =" providerResult.id" class =" result" >
139140 <h4 :id =" `unified-search-result-${providerResult.id}`" class =" result-title" >
140141 {{ providerResult.name }}
141142 </h4 >
160161 </NcButton >
161162 </div >
162163 </div >
164+ <!-- Unfiltered results section -->
165+ <template v-if =" unfilteredResults .length > 0 " >
166+ <div class =" unified-search-modal__unfiltered-header" >
167+ <span class =" unified-search-modal__unfiltered-label" >{{ t('core', 'Partial matches') }}</span >
168+ </div >
169+ <div v-for =" providerResult in unfilteredResults" :key =" `unfiltered-${providerResult.id}`" class =" result result--unfiltered" >
170+ <h4 :id =" `unified-search-result-${providerResult.id}`" class =" result-title" >
171+ {{ providerResult.name }}
172+ </h4 >
173+ <ul class =" result-items" :aria-labelledby =" `unified-search-result-${providerResult.id}`" >
174+ <SearchResult
175+ v-for =" (result, index) in providerResult.results"
176+ :key =" index"
177+ v-bind =" result" />
178+ </ul >
179+ <div class =" result-footer" >
180+ <NcButton v-if =" providerResult.results.length === providerResult.limit" variant =" tertiary-no-background" @click =" loadMoreResultsForProvider(providerResult)" >
181+ {{ t('core', 'Load more results') }}
182+ <template #icon >
183+ <IconDotsHorizontal :size =" 20" />
184+ </template >
185+ </NcButton >
186+ <NcButton v-if =" providerResult.inAppSearch" alignment =" end-reverse" variant =" tertiary-no-background" >
187+ {{ t('core', 'Search in') }} {{ providerResult.name }}
188+ <template #icon >
189+ <IconArrowRight :size =" 20" />
190+ </template >
191+ </NcButton >
192+ </div >
193+ </div >
194+ </template >
163195 </div >
164196 </NcDialog >
165197</template >
@@ -342,6 +374,50 @@ export default defineComponent({
342374 hasExternalResources() {
343375 return this .providers .some ((provider ) => provider .isExternalProvider )
344376 },
377+
378+ hasContentFilters() {
379+ return this .filters .some ((filter ) => filter .type === ' date' || filter .type === ' person' )
380+ },
381+
382+ filteredResults() {
383+ const isInFolderAtRoot = (result ) => {
384+ if (result .id !== ' in-folder' ) {
385+ return false
386+ }
387+ const path = result .extraParams ?.path
388+ return ! path || path === ' /' || path === ' '
389+ }
390+
391+ if (! this .hasContentFilters ) {
392+ return this .results .filter ((result ) => ! isInFolderAtRoot (result ))
393+ }
394+ return this .results .filter ((result ) => result .supportsActiveFilters === true && ! isInFolderAtRoot (result ))
395+ },
396+
397+ filteredResultUrls() {
398+ const urls = new Set ()
399+ this .filteredResults .forEach ((provider ) => {
400+ provider .results .forEach ((entry ) => {
401+ if (entry .resourceUrl ) {
402+ urls .add (entry .resourceUrl )
403+ }
404+ })
405+ })
406+ return urls
407+ },
408+
409+ unfilteredResults() {
410+ if (! this .hasContentFilters ) {
411+ return []
412+ }
413+ return this .results
414+ .filter ((result ) => result .supportsActiveFilters === false )
415+ .map ((provider ) => ({
416+ ... provider ,
417+ results: provider .results .filter ((entry ) => ! this .filteredResultUrls .has (entry .resourceUrl )),
418+ }))
419+ .filter ((provider ) => provider .results .length > 0 )
420+ },
345421 },
346422
347423 watch: {
@@ -444,20 +520,30 @@ export default defineComponent({
444520
445521 // This block of filter checks should be dynamic somehow and should be handled in
446522 // nextcloud/search lib
523+ const contentFilterTypes = this .filters
524+ .filter ((f ) => f .type !== ' provider' )
525+ .map ((f ) => f .type )
526+ const supportsActiveFilters = contentFilterTypes .length === 0
527+ || contentFilterTypes .every ((type ) => this .providerIsCompatibleWithFilters (provider , [type ]))
528+
529+ const baseProvider = provider .searchFrom
530+ ? this .providers .find ((p ) => p .id === provider .searchFrom ) ?? provider
531+ : provider
532+
447533 const activeFilters = this .filters .filter ((filter ) => {
448534 return filter .type !== ' provider' && this .providerIsCompatibleWithFilters (provider , [filter .type ])
449535 })
450536
451537 activeFilters .forEach ((filter ) => {
452538 switch (filter .type ) {
453539 case ' date' :
454- if (provider .filters ?.since && provider .filters ?.until ) {
540+ if (baseProvider .filters ?.since && baseProvider .filters ?.until ) {
455541 params .since = this .dateFilter .startFrom
456542 params .until = this .dateFilter .endAt
457543 }
458544 break
459545 case ' person' :
460- if (provider .filters ?.person ) {
546+ if (baseProvider .filters ?.person ) {
461547 params .person = this .personFilter .user
462548 }
463549 break
@@ -484,6 +570,7 @@ export default defineComponent({
484570 ... provider ,
485571 results: response .data .ocs .data .entries ,
486572 limit: params .limit ?? 5 ,
573+ supportsActiveFilters ,
487574 })
488575
489576 unifiedSearchLogger .debug (' Unified search results:' , { results: this .results , newResults })
@@ -766,8 +853,20 @@ export default defineComponent({
766853 return flattenedArray
767854 },
768855
769- async providerIsCompatibleWithFilters(provider , filterIds ) {
770- return filterIds .every ((filterId ) => provider .filters ?.[filterId ] !== undefined )
856+ providerIsCompatibleWithFilters(provider , filterIds ) {
857+ const baseProvider = provider .searchFrom
858+ ? this .providers .find ((p ) => p .id === provider .searchFrom ) ?? provider
859+ : provider
860+ return filterIds .every ((filterId ) => {
861+ switch (filterId ) {
862+ case ' date' :
863+ return baseProvider .filters ?.since !== undefined && baseProvider .filters ?.until !== undefined
864+ case ' person' :
865+ return baseProvider .filters ?.person !== undefined
866+ default :
867+ return baseProvider .filters ?.[filterId ] !== undefined
868+ }
869+ })
771870 },
772871
773872 async enableAllProviders() {
@@ -859,9 +958,27 @@ export default defineComponent({
859958 align-items : center ;
860959 display : flex ;
861960 }
961+
962+ & --unfiltered {
963+ opacity : 0.7 ;
964+ }
862965 }
863966
864967 }
968+
969+ & __unfiltered-header {
970+ display : flex ;
971+ flex-direction : column ;
972+ gap : 2px ;
973+ margin-block : 16px 8px ;
974+ padding-block : 12px 0 ;
975+ border-top : 1px solid var (--color-border );
976+ }
977+
978+ & __unfiltered-label {
979+ font-weight : bold ;
980+ color : var (--color-text-maxcontrast );
981+ }
865982}
866983
867984.filter-button__icon {
0 commit comments