aboutsummaryrefslogtreecommitdiffstats
path: root/apps/files/src/store
diff options
context:
space:
mode:
Diffstat (limited to 'apps/files/src/store')
-rw-r--r--apps/files/src/store/active.ts112
-rw-r--r--apps/files/src/store/files.ts25
-rw-r--r--apps/files/src/store/search.ts153
-rw-r--r--apps/files/src/store/userconfig.ts8
4 files changed, 238 insertions, 60 deletions
diff --git a/apps/files/src/store/active.ts b/apps/files/src/store/active.ts
index e261e817f3d..1303a157b08 100644
--- a/apps/files/src/store/active.ts
+++ b/apps/files/src/store/active.ts
@@ -3,74 +3,84 @@
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
-import type { ActiveStore } from '../types.ts'
-import type { FileAction, Node, View } from '@nextcloud/files'
+import type { FileAction, View, Node, Folder } from '@nextcloud/files'
-import { defineStore } from 'pinia'
-import { getNavigation } from '@nextcloud/files'
import { subscribe } from '@nextcloud/event-bus'
+import { getNavigation } from '@nextcloud/files'
+import { defineStore } from 'pinia'
+import { ref } from 'vue'
import logger from '../logger.ts'
-export const useActiveStore = function(...args) {
- const store = defineStore('active', {
- state: () => ({
- _initialized: false,
- activeNode: null,
- activeView: null,
- activeAction: null,
- } as ActiveStore),
+export const useActiveStore = defineStore('active', () => {
+ /**
+ * The currently active action
+ */
+ const activeAction = ref<FileAction>()
- actions: {
- setActiveNode(node: Node) {
- if (!node) {
- throw new Error('Use clearActiveNode to clear the active node')
- }
- logger.debug('Setting active node', { node })
- this.activeNode = node
- },
+ /**
+ * The currently active folder
+ */
+ const activeFolder = ref<Folder>()
- clearActiveNode() {
- this.activeNode = null
- },
+ /**
+ * The current active node within the folder
+ */
+ const activeNode = ref<Node>()
- onDeletedNode(node: Node) {
- if (this.activeNode && this.activeNode.source === node.source) {
- this.clearActiveNode()
- }
- },
+ /**
+ * The current active view
+ */
+ const activeView = ref<View>()
- setActiveAction(action: FileAction) {
- this.activeAction = action
- },
+ initialize()
- clearActiveAction() {
- this.activeAction = null
- },
+ /**
+ * Unset the active node if deleted
+ *
+ * @param node - The node thats deleted
+ * @private
+ */
+ function onDeletedNode(node: Node) {
+ if (activeNode.value && activeNode.value.source === node.source) {
+ activeNode.value = undefined
+ }
+ }
- onChangedView(view: View|null = null) {
- logger.debug('Setting active view', { view })
- this.activeView = view
- this.clearActiveNode()
- },
- },
- })
+ /**
+ * Callback to update the current active view
+ *
+ * @param view - The new active view
+ * @private
+ */
+ function onChangedView(view: View|null = null) {
+ logger.debug('Setting active view', { view })
+ activeView.value = view ?? undefined
+ activeNode.value = undefined
+ }
- const activeStore = store(...args)
- const navigation = getNavigation()
+ /**
+ * Initalize the store - connect all event listeners.
+ * @private
+ */
+ function initialize() {
+ const navigation = getNavigation()
- // Make sure we only register the listeners once
- if (!activeStore._initialized) {
- subscribe('files:node:deleted', activeStore.onDeletedNode)
+ // Make sure we only register the listeners once
+ subscribe('files:node:deleted', onDeletedNode)
- activeStore._initialized = true
- activeStore.onChangedView(navigation.active)
+ onChangedView(navigation.active)
// Or you can react to changes of the current active view
navigation.addEventListener('updateActive', (event) => {
- activeStore.onChangedView(event.detail)
+ onChangedView(event.detail)
})
}
- return activeStore
-}
+ return {
+ activeAction,
+ activeFolder,
+ activeNode,
+ activeView,
+ }
+})
diff --git a/apps/files/src/store/files.ts b/apps/files/src/store/files.ts
index 295704c880b..0bcf4ce9350 100644
--- a/apps/files/src/store/files.ts
+++ b/apps/files/src/store/files.ts
@@ -54,13 +54,13 @@ export const useFilesStore = function(...args) {
actions: {
/**
- * Get cached child nodes within a given path
+ * Get cached directory matching a given path
*
- * @param service The service (files view)
- * @param path The path relative within the service
- * @return Array of cached nodes within the path
+ * @param service - The service (files view)
+ * @param path - The path relative within the service
+ * @return The folder if found
*/
- getNodesByPath(service: string, path?: string): Node[] {
+ getDirectoryByPath(service: string, path?: string): Folder | undefined {
const pathsStore = usePathsStore()
let folder: Folder | undefined
@@ -74,6 +74,19 @@ export const useFilesStore = function(...args) {
}
}
+ return folder
+ },
+
+ /**
+ * Get cached child nodes within a given path
+ *
+ * @param service - The service (files view)
+ * @param path - The path relative within the service
+ * @return Array of cached nodes within the path
+ */
+ getNodesByPath(service: string, path?: string): Node[] {
+ const folder = this.getDirectoryByPath(service, path)
+
// If we found a cache entry and the cache entry was already loaded (has children) then use it
return (folder?._children ?? [])
.map((source: string) => this.getNode(source))
@@ -141,7 +154,7 @@ export const useFilesStore = function(...args) {
}
// If we have only one node with the file ID, we can update it directly
- if (node.source === nodes[0].source) {
+ if (nodes.length === 1 && node.source === nodes[0].source) {
this.updateNodes([node])
return
}
diff --git a/apps/files/src/store/search.ts b/apps/files/src/store/search.ts
new file mode 100644
index 00000000000..43e01f35b92
--- /dev/null
+++ b/apps/files/src/store/search.ts
@@ -0,0 +1,153 @@
+/*!
+ * SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+import type { View } from '@nextcloud/files'
+import type RouterService from '../services/RouterService.ts'
+import type { SearchScope } from '../types.ts'
+
+import { emit, subscribe } from '@nextcloud/event-bus'
+import debounce from 'debounce'
+import { defineStore } from 'pinia'
+import { ref, watch } from 'vue'
+import { VIEW_ID } from '../views/search.ts'
+import logger from '../logger.ts'
+
+export const useSearchStore = defineStore('search', () => {
+ /**
+ * The current search query
+ */
+ const query = ref('')
+
+ /**
+ * Scope of the search.
+ * Scopes:
+ * - filter: only filter current file list
+ * - globally: search everywhere
+ */
+ const scope = ref<SearchScope>('filter')
+
+ // reset the base if query is cleared
+ watch(scope, 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) => {
+ const router = window.OCP.Files.Router as RouterService
+ router.goToRoute(
+ undefined,
+ {
+ view: VIEW_ID,
+ },
+ {
+ 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
+ }
+
+ const isSearch = router.params.view === VIEW_ID
+
+ logger.debug('Update route for updated search query', { query: query.value, isSearch })
+ updateRouter(isSearch)
+ }
+
+ /**
+ * 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 {
+ query,
+ scope,
+ }
+})
diff --git a/apps/files/src/store/userconfig.ts b/apps/files/src/store/userconfig.ts
index d136561c2e5..54e9a75eb8b 100644
--- a/apps/files/src/store/userconfig.ts
+++ b/apps/files/src/store/userconfig.ts
@@ -12,13 +12,15 @@ import { ref, set } from 'vue'
import axios from '@nextcloud/axios'
const initialUserConfig = loadState<UserConfig>('files', 'config', {
- show_hidden: false,
crop_image_previews: true,
- sort_favorites_first: true,
- sort_folders_first: true,
+ default_view: 'files',
grid_view: false,
+ show_hidden: false,
show_mime_column: true,
+ sort_favorites_first: true,
+ sort_folders_first: true,
+ show_dialog_deletion: false,
show_dialog_file_extension: true,
})