Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion apps/files/src/components/FileEntry/FileEntryActions.vue
Original file line number Diff line number Diff line change
Expand Up @@ -281,7 +281,7 @@ export default defineComponent({
}

// Make sure we set the node as active
this.activeStore.setActiveNode(this.source)
this.activeStore.activeNode = this.source

// Execute the action
await executeAction(action)
Expand Down
4 changes: 2 additions & 2 deletions apps/files/src/components/FilesListVirtual.vue
Original file line number Diff line number Diff line change
Expand Up @@ -315,7 +315,7 @@ export default defineComponent({
delete query.openfile
delete query.opendetails

this.activeStore.clearActiveNode()
this.activeStore.activeNode = undefined
window.OCP.Files.Router.goToRoute(
null,
{ ...this.$route.params, fileid: String(this.currentFolder.fileid ?? '') },
Expand Down Expand Up @@ -449,7 +449,7 @@ export default defineComponent({
delete query.openfile
delete query.opendetails

this.activeStore.setActiveNode(node)
this.activeStore.activeNode = node

// Silent update of the URL
window.OCP.Files.Router.goToRoute(
Expand Down
42 changes: 3 additions & 39 deletions apps/files/src/components/FilesNavigationSearch.vue
Original file line number Diff line number Diff line change
Expand Up @@ -13,15 +13,10 @@ import NcAppNavigationSearch from '@nextcloud/vue/components/NcAppNavigationSear
import NcIconSvgWrapper from '@nextcloud/vue/components/NcIconSvgWrapper'
import { onBeforeNavigation } from '../composables/useBeforeNavigation.ts'
import { useNavigation } from '../composables/useNavigation.ts'
import { useRouteParameters } from '../composables/useRouteParameters.ts'
import { useFilesStore } from '../store/files.ts'
import { useSearchStore } from '../store/search.ts'
import { VIEW_ID } from '../views/search.ts'

const { currentView } = useNavigation(true)
const { directory } = useRouteParameters()

const filesStore = useFilesStore()
const searchStore = useSearchStore()

/**
Expand Down Expand Up @@ -55,44 +50,19 @@ onBeforeNavigation((to, from, next) => {
*/
const isSearchView = computed(() => currentView.value.id === VIEW_ID)

/**
* Local search is only possible on real DAV resources within the files root
*/
const canSearchLocally = computed(() => {
if (searchStore.base) {
return true
}

const folder = filesStore.getDirectoryByPath(currentView.value.id, directory.value)
return folder?.isDavResource && folder?.root?.startsWith('/files/')
})

/**
* Different searchbox label depending if filtering or searching
*/
const searchLabel = computed(() => {
if (searchStore.scope === 'globally') {
return t('files', 'Search globally by filename …')
} else if (searchStore.scope === 'locally') {
return t('files', 'Search here by filename …')
}
return t('files', 'Filter file names …')
return t('files', 'Search here by filename …')
})

/**
* Update the search value and set the base if needed
* @param value - The new value
*/
function onUpdateSearch(value: string) {
if (searchStore.scope === 'locally' && currentView.value.id !== VIEW_ID) {
searchStore.base = filesStore.getDirectoryByPath(currentView.value.id, directory.value)
}
searchStore.query = value
}
</script>

<template>
<NcAppNavigationSearch :label="searchLabel" :model-value="searchStore.query" @update:modelValue="onUpdateSearch">
<NcAppNavigationSearch v-model="searchStore.query" :label="searchLabel">
<template #actions>
<NcActions :aria-label="t('files', 'Search scope options')" :disabled="isSearchView">
<template #icon>
Expand All @@ -102,13 +72,7 @@ function onUpdateSearch(value: string) {
<template #icon>
<NcIconSvgWrapper :path="mdiMagnify" />
</template>
{{ t('files', 'Filter in current view') }}
</NcActionButton>
<NcActionButton v-if="canSearchLocally" close-after-click @click="searchStore.scope = 'locally'">
<template #icon>
<NcIconSvgWrapper :path="mdiMagnify" />
</template>
{{ t('files', 'Search from this location') }}
{{ t('files', 'Filter and search from this location') }}
</NcActionButton>
<NcActionButton close-after-click @click="searchStore.scope = 'globally'">
<template #icon>
Expand Down
66 changes: 59 additions & 7 deletions apps/files/src/services/Files.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,25 +2,55 @@
* SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
import type { ContentsWithRoot, File, Folder } from '@nextcloud/files'
import type { ContentsWithRoot, File, Folder, Node } from '@nextcloud/files'
import type { FileStat, ResponseDataDetailed } from 'webdav'

import { davGetDefaultPropfind, davResultToNode, davRootPath } from '@nextcloud/files'
import { defaultRootPath, getDefaultPropfind, resultToNode as davResultToNode } from '@nextcloud/files/dav'
import { CancelablePromise } from 'cancelable-promise'
import { join } from 'path'
import { client } from './WebdavClient.ts'
import { searchNodes } from './WebDavSearch.ts'
import { getPinia } from '../store/index.ts'
import { useFilesStore } from '../store/files.ts'
import { useSearchStore } from '../store/search.ts'
import logger from '../logger.ts'

/**
* Slim wrapper over `@nextcloud/files` `davResultToNode` to allow using the function with `Array.map`
* @param stat The result returned by the webdav library
*/
export const resultToNode = (stat: FileStat): File | Folder => davResultToNode(stat)
export const resultToNode = (stat: FileStat): Node => davResultToNode(stat)

export const getContents = (path = '/'): CancelablePromise<ContentsWithRoot> => {
path = join(davRootPath, path)
/**
* Get contents implementation for the files view.
* This also allows to fetch local search results when the user is currently filtering.
*
* @param path - The path to query
*/
export function getContents(path = '/'): CancelablePromise<ContentsWithRoot> {
const controller = new AbortController()
const propfindPayload = davGetDefaultPropfind()
const searchStore = useSearchStore(getPinia())

if (searchStore.query.length >= 3) {
return new CancelablePromise((resolve, reject, cancel) => {
cancel(() => controller.abort())
getLocalSearch(path, searchStore.query, controller.signal)
.then(resolve)
.catch(reject)
})
} else {
return defaultGetContents(path)
}
}

/**
* Generic `getContents` implementation for the users files.
*
* @param path - The path to get the contents
*/
export function defaultGetContents(path: string): CancelablePromise<ContentsWithRoot> {
path = join(defaultRootPath, path)
const controller = new AbortController()
const propfindPayload = getDefaultPropfind()

return new CancelablePromise(async (resolve, reject, onCancel) => {
onCancel(() => controller.abort())
Expand Down Expand Up @@ -56,3 +86,25 @@ export const getContents = (path = '/'): CancelablePromise<ContentsWithRoot> =>
}
})
}

/**
* Get the local search results for the current folder.
*
* @param path - The path
* @param query - The current search query
* @param signal - The aboort signal
*/
async function getLocalSearch(path: string, query: string, signal: AbortSignal): Promise<ContentsWithRoot> {
const filesStore = useFilesStore(getPinia())
let folder = filesStore.getDirectoryByPath('files', path)
if (!folder) {
const rootPath = join(defaultRootPath, path)
const stat = await client.stat(rootPath, { details: true }) as ResponseDataDetailed<FileStat>
folder = resultToNode(stat.data) as Folder
}
const contents = await searchNodes(query, { dir: path, signal })
return {
folder,
contents,
}
}
4 changes: 2 additions & 2 deletions apps/files/src/services/HotKeysService.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,8 +57,8 @@ describe('HotKeysService testing', () => {
})

// Setting the view first as it reset the active node
activeStore.onChangedView(view)
activeStore.setActiveNode(file)
activeStore.activeView = view
activeStore.activeNode = file

window.OCA = { Files: { Sidebar: { open: () => {}, setActiveTab: () => {} } } }
// We only mock what needed, we do not need Files.Router.goTo or Files.Navigation
Expand Down
3 changes: 1 addition & 2 deletions apps/files/src/services/Search.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,12 +21,11 @@ export function getContents(): CancelablePromise<ContentsWithRoot> {
const controller = new AbortController()

const searchStore = useSearchStore(getPinia())
const dir = searchStore.base?.path

return new CancelablePromise<ContentsWithRoot>(async (resolve, reject, cancel) => {
cancel(() => controller.abort())
try {
const contents = await searchNodes(searchStore.query, { dir, signal: controller.signal })
const contents = await searchNodes(searchStore.query, { signal: controller.signal })
resolve({
contents,
folder: new Folder({
Expand Down
112 changes: 61 additions & 51 deletions apps/files/src/store/active.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
}
})
2 changes: 1 addition & 1 deletion apps/files/src/store/files.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,14 +24,14 @@
getters: {
/**
* Get a file or folder by its source
* @param state

Check warning on line 27 in apps/files/src/store/files.ts

View workflow job for this annotation

GitHub Actions / NPM lint

Missing JSDoc @param "state" description
*/
getNode: (state) => (source: FileSource): Node|undefined => state.files[source],

/**
* Get a list of files or folders by their IDs
* Note: does not return undefined values
* @param state

Check warning on line 34 in apps/files/src/store/files.ts

View workflow job for this annotation

GitHub Actions / NPM lint

Missing JSDoc @param "state" description
*/
getNodes: (state) => (sources: FileSource[]): Node[] => sources
.map(source => state.files[source])
Expand Down Expand Up @@ -154,7 +154,7 @@
}

// 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
}
Expand Down
Loading
Loading