feat(files): add "search everywhere" button within the filters row

Signed-off-by: Ferdinand Thiessen <opensource@fthiessen.de>
pull/53820/head
Ferdinand Thiessen 2025-07-04 12:14:30 +07:00
parent b8d3e64205
commit 69275cbda5
No known key found for this signature in database
GPG Key ID: 45FAE7268762B400
6 changed files with 153 additions and 3 deletions

@ -0,0 +1,47 @@
<!--
- SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
- SPDX-License-Identifier: AGPL-3.0-or-later
-->
<template>
<NcButton v-show="isVisible" @click="onClick">
{{ t('files', 'Search everywhere') }}
</NcButton>
</template>
<script setup lang="ts">
import { t } from '@nextcloud/l10n'
import { ref } from 'vue'
import NcButton from '@nextcloud/vue/components/NcButton'
import { getPinia } from '../../store/index.ts'
import { useSearchStore } from '../../store/search.ts'
const isVisible = ref(false)
defineExpose({
hideButton,
showButton,
})
/**
* Hide the button - called by the filter class
*/
function hideButton() {
isVisible.value = false
}
/**
* Show the button - called by the filter class
*/
function showButton() {
isVisible.value = true
}
/**
* Button click handler to make the filtering a global search.
*/
function onClick() {
const searchStore = useSearchStore(getPinia())
searchStore.scope = 'globally'
}
</script>

@ -7,6 +7,8 @@ import type { IFileListFilterChip, INode } from '@nextcloud/files'
import { subscribe } from '@nextcloud/event-bus'
import { FileListFilter, registerFileListFilter } from '@nextcloud/files'
import { getPinia } from '../store/index.ts'
import { useSearchStore } from '../store/search.ts'
/**
* Register the filename filter
@ -59,10 +61,14 @@ class FilenameFilter extends FileListFilter {
this.updateQuery('')
},
})
} else {
// make sure to also reset the search store when pressing the "X" on the filter chip
const store = useSearchStore(getPinia())
if (store.scope === 'filter') {
store.query = ''
}
}
this.updateChips(chips)
// Emit the new query as it might have come not from the Navigation
this.dispatchTypedEvent('update:query', new CustomEvent('update:query', { detail: query }))
}
}

@ -0,0 +1,49 @@
/*!
* SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
import type { INode } from '@nextcloud/files'
import type { ComponentPublicInstance } from 'vue'
import { subscribe } from '@nextcloud/event-bus'
import { FileListFilter, registerFileListFilter } from '@nextcloud/files'
import Vue from 'vue'
import FileListFilterToSearch from '../components/FileListFilter/FileListFilterToSearch.vue'
class SearchFilter extends FileListFilter {
private currentInstance?: ComponentPublicInstance<typeof FileListFilterToSearch>
constructor() {
super('files:filter-to-search', 999)
subscribe('files:search:updated', ({ query, scope }) => {
if (query && scope === 'filter') {
this.currentInstance?.showButton()
} else {
this.currentInstance?.hideButton()
}
})
}
public mount(el: HTMLElement) {
if (this.currentInstance) {
this.currentInstance.$destroy()
}
const View = Vue.extend(FileListFilterToSearch)
this.currentInstance = new View().$mount(el) as unknown as ComponentPublicInstance<typeof FileListFilterToSearch>
}
public filter(nodes: INode[]): INode[] {
return nodes
}
}
/**
* Register a file list filter to only show hidden files if enabled by user config
*/
export function registerFilterToSearchToggle() {
registerFileListFilter(new SearchFilter())
}

@ -36,6 +36,7 @@ import { initLivePhotos } from './services/LivePhotos'
import { isPublicShare } from '@nextcloud/sharing/public'
import { registerConvertActions } from './actions/convertAction.ts'
import { registerFilenameFilter } from './filters/FilenameFilter.ts'
import { registerFilterToSearchToggle } from './filters/SearchFilter.ts'
// Register file actions
registerConvertActions()
@ -70,6 +71,7 @@ registerHiddenFilesFilter()
registerTypeFilter()
registerModifiedFilter()
registerFilenameFilter()
registerFilterToSearchToggle()
// Register preview service worker
registerPreviewServiceWorker()

@ -83,7 +83,6 @@ export const useSearchStore = defineStore('search', () => {
function updateSearch() {
// emit the search event to update the filter
emit('files:search:updated', { query: query.value, scope: scope.value })
const router = window.OCP.Files.Router as RouterService
// if we are on the search view and the query was unset or scope was set to 'filter' we need to move back to the files view

@ -75,6 +75,53 @@ describe('files: search', () => {
cy.get('[data-cy-files-list-row-fileid]').should('have.length', 2)
})
it('See "search everywhere" button', () => {
// Not visible initially
cy.get('[data-cy-files-filters]')
.findByRole('button', { name: /Search everywhere/i })
.should('not.to.exist')
// add a filter
navigation.searchInput().type('file')
// see its visible
cy.get('[data-cy-files-filters]')
.findByRole('button', { name: /Search everywhere/i })
.should('be.visible')
// clear the filter
navigation.searchClearButton().click()
// see its not visible again
cy.get('[data-cy-files-filters]')
.findByRole('button', { name: /Search everywhere/i })
.should('not.to.exist')
})
it('can make local search a global search', () => {
navigateToFolder('some folder')
getRowForFile('a file.txt').should('be.visible')
navigation.searchInput().type('file')
// see local results
getRowForFile('a file.txt').should('be.visible')
getRowForFile('a second file.txt').should('be.visible')
cy.get('[data-cy-files-list-row-fileid]').should('have.length', 2)
// toggle global search
cy.get('[data-cy-files-filters]')
.findByRole('button', { name: /Search everywhere/i })
.should('be.visible')
.click()
// see global results
getRowForFile('file.txt').should('be.visible')
getRowForFile('a file.txt').should('be.visible')
getRowForFile('a second file.txt').should('be.visible')
getRowForFile('another file.txt').should('be.visible')
})
it('shows empty content when there are no results', () => {
navigateToFolder('some folder')
getRowForFile('a file.txt').should('be.visible')