Skip to content

Commit 77939fa

Browse files
authored
Merge pull request #53826 from nextcloud/feat/search-while-filtering
2 parents f0dd367 + c9d1b9f commit 77939fa

File tree

54 files changed

+281
-234
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

54 files changed

+281
-234
lines changed

apps/files/src/components/FileEntry/FileEntryActions.vue

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -281,7 +281,7 @@ export default defineComponent({
281281
}
282282
283283
// Make sure we set the node as active
284-
this.activeStore.setActiveNode(this.source)
284+
this.activeStore.activeNode = this.source
285285
286286
// Execute the action
287287
await executeAction(action)

apps/files/src/components/FilesListVirtual.vue

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -315,7 +315,7 @@ export default defineComponent({
315315
delete query.openfile
316316
delete query.opendetails
317317
318-
this.activeStore.clearActiveNode()
318+
this.activeStore.activeNode = undefined
319319
window.OCP.Files.Router.goToRoute(
320320
null,
321321
{ ...this.$route.params, fileid: String(this.currentFolder.fileid ?? '') },
@@ -449,7 +449,7 @@ export default defineComponent({
449449
delete query.openfile
450450
delete query.opendetails
451451
452-
this.activeStore.setActiveNode(node)
452+
this.activeStore.activeNode = node
453453
454454
// Silent update of the URL
455455
window.OCP.Files.Router.goToRoute(

apps/files/src/components/FilesNavigationSearch.vue

Lines changed: 3 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -13,15 +13,10 @@ import NcAppNavigationSearch from '@nextcloud/vue/components/NcAppNavigationSear
1313
import NcIconSvgWrapper from '@nextcloud/vue/components/NcIconSvgWrapper'
1414
import { onBeforeNavigation } from '../composables/useBeforeNavigation.ts'
1515
import { useNavigation } from '../composables/useNavigation.ts'
16-
import { useRouteParameters } from '../composables/useRouteParameters.ts'
17-
import { useFilesStore } from '../store/files.ts'
1816
import { useSearchStore } from '../store/search.ts'
1917
import { VIEW_ID } from '../views/search.ts'
2018
2119
const { currentView } = useNavigation(true)
22-
const { directory } = useRouteParameters()
23-
24-
const filesStore = useFilesStore()
2520
const searchStore = useSearchStore()
2621
2722
/**
@@ -55,44 +50,19 @@ onBeforeNavigation((to, from, next) => {
5550
*/
5651
const isSearchView = computed(() => currentView.value.id === VIEW_ID)
5752
58-
/**
59-
* Local search is only possible on real DAV resources within the files root
60-
*/
61-
const canSearchLocally = computed(() => {
62-
if (searchStore.base) {
63-
return true
64-
}
65-
66-
const folder = filesStore.getDirectoryByPath(currentView.value.id, directory.value)
67-
return folder?.isDavResource && folder?.root?.startsWith('/files/')
68-
})
69-
7053
/**
7154
* Different searchbox label depending if filtering or searching
7255
*/
7356
const searchLabel = computed(() => {
7457
if (searchStore.scope === 'globally') {
7558
return t('files', 'Search globally by filename …')
76-
} else if (searchStore.scope === 'locally') {
77-
return t('files', 'Search here by filename …')
7859
}
79-
return t('files', 'Filter file names')
60+
return t('files', 'Search here by filename')
8061
})
81-
82-
/**
83-
* Update the search value and set the base if needed
84-
* @param value - The new value
85-
*/
86-
function onUpdateSearch(value: string) {
87-
if (searchStore.scope === 'locally' && currentView.value.id !== VIEW_ID) {
88-
searchStore.base = filesStore.getDirectoryByPath(currentView.value.id, directory.value)
89-
}
90-
searchStore.query = value
91-
}
9262
</script>
9363

9464
<template>
95-
<NcAppNavigationSearch :label="searchLabel" :model-value="searchStore.query" @update:modelValue="onUpdateSearch">
65+
<NcAppNavigationSearch v-model="searchStore.query" :label="searchLabel">
9666
<template #actions>
9767
<NcActions :aria-label="t('files', 'Search scope options')" :disabled="isSearchView">
9868
<template #icon>
@@ -102,13 +72,7 @@ function onUpdateSearch(value: string) {
10272
<template #icon>
10373
<NcIconSvgWrapper :path="mdiMagnify" />
10474
</template>
105-
{{ t('files', 'Filter in current view') }}
106-
</NcActionButton>
107-
<NcActionButton v-if="canSearchLocally" close-after-click @click="searchStore.scope = 'locally'">
108-
<template #icon>
109-
<NcIconSvgWrapper :path="mdiMagnify" />
110-
</template>
111-
{{ t('files', 'Search from this location') }}
75+
{{ t('files', 'Filter and search from this location') }}
11276
</NcActionButton>
11377
<NcActionButton close-after-click @click="searchStore.scope = 'globally'">
11478
<template #icon>

apps/files/src/services/Files.ts

Lines changed: 59 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,25 +2,55 @@
22
* SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors
33
* SPDX-License-Identifier: AGPL-3.0-or-later
44
*/
5-
import type { ContentsWithRoot, File, Folder } from '@nextcloud/files'
5+
import type { ContentsWithRoot, File, Folder, Node } from '@nextcloud/files'
66
import type { FileStat, ResponseDataDetailed } from 'webdav'
77

8-
import { davGetDefaultPropfind, davResultToNode, davRootPath } from '@nextcloud/files'
8+
import { defaultRootPath, getDefaultPropfind, resultToNode as davResultToNode } from '@nextcloud/files/dav'
99
import { CancelablePromise } from 'cancelable-promise'
1010
import { join } from 'path'
1111
import { client } from './WebdavClient.ts'
12+
import { searchNodes } from './WebDavSearch.ts'
13+
import { getPinia } from '../store/index.ts'
14+
import { useFilesStore } from '../store/files.ts'
15+
import { useSearchStore } from '../store/search.ts'
1216
import logger from '../logger.ts'
13-
1417
/**
1518
* Slim wrapper over `@nextcloud/files` `davResultToNode` to allow using the function with `Array.map`
1619
* @param stat The result returned by the webdav library
1720
*/
18-
export const resultToNode = (stat: FileStat): File | Folder => davResultToNode(stat)
21+
export const resultToNode = (stat: FileStat): Node => davResultToNode(stat)
1922

20-
export const getContents = (path = '/'): CancelablePromise<ContentsWithRoot> => {
21-
path = join(davRootPath, path)
23+
/**
24+
* Get contents implementation for the files view.
25+
* This also allows to fetch local search results when the user is currently filtering.
26+
*
27+
* @param path - The path to query
28+
*/
29+
export function getContents(path = '/'): CancelablePromise<ContentsWithRoot> {
2230
const controller = new AbortController()
23-
const propfindPayload = davGetDefaultPropfind()
31+
const searchStore = useSearchStore(getPinia())
32+
33+
if (searchStore.query.length >= 3) {
34+
return new CancelablePromise((resolve, reject, cancel) => {
35+
cancel(() => controller.abort())
36+
getLocalSearch(path, searchStore.query, controller.signal)
37+
.then(resolve)
38+
.catch(reject)
39+
})
40+
} else {
41+
return defaultGetContents(path)
42+
}
43+
}
44+
45+
/**
46+
* Generic `getContents` implementation for the users files.
47+
*
48+
* @param path - The path to get the contents
49+
*/
50+
export function defaultGetContents(path: string): CancelablePromise<ContentsWithRoot> {
51+
path = join(defaultRootPath, path)
52+
const controller = new AbortController()
53+
const propfindPayload = getDefaultPropfind()
2454

2555
return new CancelablePromise(async (resolve, reject, onCancel) => {
2656
onCancel(() => controller.abort())
@@ -56,3 +86,25 @@ export const getContents = (path = '/'): CancelablePromise<ContentsWithRoot> =>
5686
}
5787
})
5888
}
89+
90+
/**
91+
* Get the local search results for the current folder.
92+
*
93+
* @param path - The path
94+
* @param query - The current search query
95+
* @param signal - The aboort signal
96+
*/
97+
async function getLocalSearch(path: string, query: string, signal: AbortSignal): Promise<ContentsWithRoot> {
98+
const filesStore = useFilesStore(getPinia())
99+
let folder = filesStore.getDirectoryByPath('files', path)
100+
if (!folder) {
101+
const rootPath = join(defaultRootPath, path)
102+
const stat = await client.stat(rootPath, { details: true }) as ResponseDataDetailed<FileStat>
103+
folder = resultToNode(stat.data) as Folder
104+
}
105+
const contents = await searchNodes(query, { dir: path, signal })
106+
return {
107+
folder,
108+
contents,
109+
}
110+
}

apps/files/src/services/HotKeysService.spec.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -57,8 +57,8 @@ describe('HotKeysService testing', () => {
5757
})
5858

5959
// Setting the view first as it reset the active node
60-
activeStore.onChangedView(view)
61-
activeStore.setActiveNode(file)
60+
activeStore.activeView = view
61+
activeStore.activeNode = file
6262

6363
window.OCA = { Files: { Sidebar: { open: () => {}, setActiveTab: () => {} } } }
6464
// We only mock what needed, we do not need Files.Router.goTo or Files.Navigation

apps/files/src/services/Search.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,12 +21,11 @@ export function getContents(): CancelablePromise<ContentsWithRoot> {
2121
const controller = new AbortController()
2222

2323
const searchStore = useSearchStore(getPinia())
24-
const dir = searchStore.base?.path
2524

2625
return new CancelablePromise<ContentsWithRoot>(async (resolve, reject, cancel) => {
2726
cancel(() => controller.abort())
2827
try {
29-
const contents = await searchNodes(searchStore.query, { dir, signal: controller.signal })
28+
const contents = await searchNodes(searchStore.query, { signal: controller.signal })
3029
resolve({
3130
contents,
3231
folder: new Folder({

apps/files/src/store/active.ts

Lines changed: 61 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -3,74 +3,84 @@
33
* SPDX-License-Identifier: AGPL-3.0-or-later
44
*/
55

6-
import type { ActiveStore } from '../types.ts'
7-
import type { FileAction, Node, View } from '@nextcloud/files'
6+
import type { FileAction, View, Node, Folder } from '@nextcloud/files'
87

9-
import { defineStore } from 'pinia'
10-
import { getNavigation } from '@nextcloud/files'
118
import { subscribe } from '@nextcloud/event-bus'
9+
import { getNavigation } from '@nextcloud/files'
10+
import { defineStore } from 'pinia'
11+
import { ref } from 'vue'
1212

1313
import logger from '../logger.ts'
1414

15-
export const useActiveStore = function(...args) {
16-
const store = defineStore('active', {
17-
state: () => ({
18-
_initialized: false,
19-
activeNode: null,
20-
activeView: null,
21-
activeAction: null,
22-
} as ActiveStore),
15+
export const useActiveStore = defineStore('active', () => {
16+
/**
17+
* The currently active action
18+
*/
19+
const activeAction = ref<FileAction>()
2320

24-
actions: {
25-
setActiveNode(node: Node) {
26-
if (!node) {
27-
throw new Error('Use clearActiveNode to clear the active node')
28-
}
29-
logger.debug('Setting active node', { node })
30-
this.activeNode = node
31-
},
21+
/**
22+
* The currently active folder
23+
*/
24+
const activeFolder = ref<Folder>()
3225

33-
clearActiveNode() {
34-
this.activeNode = null
35-
},
26+
/**
27+
* The current active node within the folder
28+
*/
29+
const activeNode = ref<Node>()
3630

37-
onDeletedNode(node: Node) {
38-
if (this.activeNode && this.activeNode.source === node.source) {
39-
this.clearActiveNode()
40-
}
41-
},
31+
/**
32+
* The current active view
33+
*/
34+
const activeView = ref<View>()
4235

43-
setActiveAction(action: FileAction) {
44-
this.activeAction = action
45-
},
36+
initialize()
4637

47-
clearActiveAction() {
48-
this.activeAction = null
49-
},
38+
/**
39+
* Unset the active node if deleted
40+
*
41+
* @param node - The node thats deleted
42+
* @private
43+
*/
44+
function onDeletedNode(node: Node) {
45+
if (activeNode.value && activeNode.value.source === node.source) {
46+
activeNode.value = undefined
47+
}
48+
}
5049

51-
onChangedView(view: View|null = null) {
52-
logger.debug('Setting active view', { view })
53-
this.activeView = view
54-
this.clearActiveNode()
55-
},
56-
},
57-
})
50+
/**
51+
* Callback to update the current active view
52+
*
53+
* @param view - The new active view
54+
* @private
55+
*/
56+
function onChangedView(view: View|null = null) {
57+
logger.debug('Setting active view', { view })
58+
activeView.value = view ?? undefined
59+
activeNode.value = undefined
60+
}
5861

59-
const activeStore = store(...args)
60-
const navigation = getNavigation()
62+
/**
63+
* Initalize the store - connect all event listeners.
64+
* @private
65+
*/
66+
function initialize() {
67+
const navigation = getNavigation()
6168

62-
// Make sure we only register the listeners once
63-
if (!activeStore._initialized) {
64-
subscribe('files:node:deleted', activeStore.onDeletedNode)
69+
// Make sure we only register the listeners once
70+
subscribe('files:node:deleted', onDeletedNode)
6571

66-
activeStore._initialized = true
67-
activeStore.onChangedView(navigation.active)
72+
onChangedView(navigation.active)
6873

6974
// Or you can react to changes of the current active view
7075
navigation.addEventListener('updateActive', (event) => {
71-
activeStore.onChangedView(event.detail)
76+
onChangedView(event.detail)
7277
})
7378
}
7479

75-
return activeStore
76-
}
80+
return {
81+
activeAction,
82+
activeFolder,
83+
activeNode,
84+
activeView,
85+
}
86+
})

apps/files/src/store/files.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -154,7 +154,7 @@ export const useFilesStore = function(...args) {
154154
}
155155

156156
// If we have only one node with the file ID, we can update it directly
157-
if (node.source === nodes[0].source) {
157+
if (nodes.length === 1 && node.source === nodes[0].source) {
158158
this.updateNodes([node])
159159
return
160160
}

0 commit comments

Comments
 (0)