diff --git a/apps/files/src/components/FileListFilter/FileListFilterToSearch.vue b/apps/files/src/components/FileListFilter/FileListFilterToSearch.vue new file mode 100644 index 00000000000..938be171f6d --- /dev/null +++ b/apps/files/src/components/FileListFilter/FileListFilterToSearch.vue @@ -0,0 +1,47 @@ + + + + + diff --git a/apps/files/src/filters/FilenameFilter.ts b/apps/files/src/filters/FilenameFilter.ts index 7914142f6ca..f86269ccd99 100644 --- a/apps/files/src/filters/FilenameFilter.ts +++ b/apps/files/src/filters/FilenameFilter.ts @@ -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 })) } } diff --git a/apps/files/src/filters/SearchFilter.ts b/apps/files/src/filters/SearchFilter.ts new file mode 100644 index 00000000000..4c7231fd26a --- /dev/null +++ b/apps/files/src/filters/SearchFilter.ts @@ -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 + + 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 + } + + 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()) +} diff --git a/apps/files/src/init.ts b/apps/files/src/init.ts index a9aedb5fb63..710284f691c 100644 --- a/apps/files/src/init.ts +++ b/apps/files/src/init.ts @@ -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() diff --git a/apps/files/src/store/search.ts b/apps/files/src/store/search.ts index 286cad253fc..417f662ca00 100644 --- a/apps/files/src/store/search.ts +++ b/apps/files/src/store/search.ts @@ -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 diff --git a/cypress/e2e/files/search.cy.ts b/cypress/e2e/files/search.cy.ts index eeb08da5a22..be3a7059382 100644 --- a/cypress/e2e/files/search.cy.ts +++ b/cypress/e2e/files/search.cy.ts @@ -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')