From c9997f1e0b08b5bb83df09d544c8e2e7b5c045ba Mon Sep 17 00:00:00 2001 From: Ferdinand Thiessen Date: Tue, 24 Jun 2025 14:59:54 +0200 Subject: [PATCH] feat(files): add `search` store to handle all search related state Signed-off-by: Ferdinand Thiessen --- apps/files/src/eventbus.d.ts | 4 +- apps/files/src/store/search.ts | 170 +++++++++++++++++++++++++++++++++ apps/files/src/types.ts | 5 + 3 files changed, 178 insertions(+), 1 deletion(-) create mode 100644 apps/files/src/store/search.ts diff --git a/apps/files/src/eventbus.d.ts b/apps/files/src/eventbus.d.ts index c9f0fbf86e5..ab8dbb63dfc 100644 --- a/apps/files/src/eventbus.d.ts +++ b/apps/files/src/eventbus.d.ts @@ -3,7 +3,7 @@ * SPDX-License-Identifier: AGPL-3.0-or-later */ -import type { IFileListFilter, Node } from '@nextcloud/files' +import type { IFileListFilter, Node, View } from '@nextcloud/files' import type { SearchScope } from './types' declare module '@nextcloud/event-bus' { @@ -20,6 +20,8 @@ declare module '@nextcloud/event-bus' { // the state of some filters has changed 'files:filters:changed': undefined + 'files:navigation:changed': View + 'files:node:created': Node 'files:node:deleted': Node 'files:node:updated': Node diff --git a/apps/files/src/store/search.ts b/apps/files/src/store/search.ts new file mode 100644 index 00000000000..286cad253fc --- /dev/null +++ b/apps/files/src/store/search.ts @@ -0,0 +1,170 @@ +/*! + * SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +import type { INode, View } from '@nextcloud/files' +import type RouterService from '../services/RouterService' +import type { SearchScope } from '../types' + +import { emit, subscribe } from '@nextcloud/event-bus' +import { defineStore } from 'pinia' +import { ref, watch } from 'vue' +import { VIEW_ID } from '../views/search' +import logger from '../logger' +import debounce from 'debounce' + +export const useSearchStore = defineStore('search', () => { + /** + * The current search query + */ + const query = ref('') + + /** + * Where to start the search + */ + const base = ref() + + /** + * Scope of the search. + * Scopes: + * - filter: only filter current file list + * - locally: search from current location recursivly + * - globally: search everywhere + */ + const scope = ref('filter') + + // reset the base if query is cleared + watch(scope, () => { + if (scope.value !== 'locally') { + base.value = undefined + } + + updateSearch() + }) + + watch(query, (old, current) => { + // skip if only whitespaces changed + if (old.trim() === current.trim()) { + return + } + + updateSearch() + }) + + // initialize the search store + initialize() + + /** + * Debounced update of the current route + * @private + */ + const updateRouter = debounce((isSearch: boolean, fileid?: number) => { + const router = window.OCP.Files.Router as RouterService + router.goToRoute( + undefined, + { + view: VIEW_ID, + ...(fileid === undefined ? {} : { fileid: String(fileid) }), + }, + { + query: query.value, + }, + isSearch, + ) + }) + + /** + * Handle updating the filter if needed. + * Also update the search view by updating the current route if needed. + * + * @private + */ + 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 + if (router.params.view === VIEW_ID && (query.value === '' || scope.value === 'filter')) { + scope.value = 'filter' + return router.goToRoute( + undefined, + { + view: 'files', + }, + { + ...router.query, + query: undefined, + }, + ) + } + + // for the filter scope we do not need to adjust the current route anymore + // also if the query is empty we do not need to do anything + if (scope.value === 'filter' || !query.value) { + return + } + + // we only use the directory if we search locally + const fileid = scope.value === 'locally' ? base.value?.fileid : undefined + const isSearch = router.params.view === VIEW_ID + + logger.debug('Update route for updated search query', { query: query.value, fileid, isSearch }) + updateRouter(isSearch, fileid) + } + + /** + * Event handler that resets the store if the file list view was changed. + * + * @param view - The new view that is active + * @private + */ + function onViewChanged(view: View) { + if (view.id !== VIEW_ID) { + query.value = '' + scope.value = 'filter' + } + } + + /** + * Initialize the store from the router if needed + */ + function initialize() { + subscribe('files:navigation:changed', onViewChanged) + + const router = window.OCP.Files.Router as RouterService + // if we initially load the search view (e.g. hard page refresh) + // then we need to initialize the store from the router + if (router.params.view === VIEW_ID) { + query.value = [router.query.query].flat()[0] ?? '' + + if (query.value) { + scope.value = 'globally' + logger.debug('Directly navigated to search view', { query: query.value }) + } else { + // we do not have any query so we need to move to the files list + logger.info('Directly navigated to search view without any query, redirect to files view.') + router.goToRoute( + undefined, + { + ...router.params, + view: 'files', + }, + { + ...router.query, + query: undefined, + }, + true, + ) + } + } + } + + return { + base, + query, + scope, + } +}) diff --git a/apps/files/src/types.ts b/apps/files/src/types.ts index db3de13d4eb..7e9696d31d6 100644 --- a/apps/files/src/types.ts +++ b/apps/files/src/types.ts @@ -111,6 +111,11 @@ export interface ActiveStore { activeAction: FileAction|null } +/** + * Search scope for the in-files-search + */ +export type SearchScope = 'filter'|'locally'|'globally' + export interface TemplateFile { app: string label: string