mirror of
https://github.com/nextcloud/server.git
synced 2026-04-15 22:11:17 -04:00
feat(files): add search scope toggle and logic
Signed-off-by: Ferdinand Thiessen <opensource@fthiessen.de>
This commit is contained in:
parent
2c65bd2f4b
commit
32dfd34099
4 changed files with 149 additions and 56 deletions
122
apps/files/src/components/FilesNavigationSearch.vue
Normal file
122
apps/files/src/components/FilesNavigationSearch.vue
Normal file
|
|
@ -0,0 +1,122 @@
|
|||
<!--
|
||||
- SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
|
||||
- SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
-->
|
||||
|
||||
<script setup lang="ts">
|
||||
import { mdiMagnify, mdiSearchWeb } from '@mdi/js'
|
||||
import { t } from '@nextcloud/l10n'
|
||||
import { computed } from 'vue'
|
||||
import NcActions from '@nextcloud/vue/components/NcActions'
|
||||
import NcActionButton from '@nextcloud/vue/components/NcActionButton'
|
||||
import NcAppNavigationSearch from '@nextcloud/vue/components/NcAppNavigationSearch'
|
||||
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()
|
||||
|
||||
/**
|
||||
* When the route is changed from search view to something different
|
||||
* we need to clear the search box.
|
||||
*/
|
||||
onBeforeNavigation((to, from, next) => {
|
||||
if (to.params.view !== VIEW_ID && from.params.view === VIEW_ID) {
|
||||
// we are leaving the search view so unset the query
|
||||
searchStore.query = ''
|
||||
searchStore.scope = 'filter'
|
||||
} else if (to.params.view === VIEW_ID && from.params.view === VIEW_ID) {
|
||||
// fix the query if the user refreshed the view
|
||||
if (searchStore.query && !to.query.query) {
|
||||
// @ts-expect-error This is a weird issue with vue-router v4 and will be fixed in v5 (vue 3)
|
||||
return next({
|
||||
...to,
|
||||
query: {
|
||||
...to.query,
|
||||
query: searchStore.query,
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
next()
|
||||
})
|
||||
|
||||
/**
|
||||
* Are we currently on the search view.
|
||||
* Needed to disable the action menu (we cannot change the search mode there)
|
||||
*/
|
||||
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 …')
|
||||
})
|
||||
|
||||
/**
|
||||
* 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">
|
||||
<template #actions>
|
||||
<NcActions :aria-label="t('files', 'Search scope options')" :disabled="isSearchView">
|
||||
<template #icon>
|
||||
<NcIconSvgWrapper :path="searchStore.scope === 'globally' ? mdiSearchWeb : mdiMagnify" />
|
||||
</template>
|
||||
<NcActionButton close-after-click @click="searchStore.scope = 'filter'">
|
||||
<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') }}
|
||||
</NcActionButton>
|
||||
<NcActionButton close-after-click @click="searchStore.scope = 'globally'">
|
||||
<template #icon>
|
||||
<NcIconSvgWrapper :path="mdiSearchWeb" />
|
||||
</template>
|
||||
{{ t('files', 'Search globally') }}
|
||||
</NcActionButton>
|
||||
</NcActions>
|
||||
</template>
|
||||
</NcAppNavigationSearch>
|
||||
</template>
|
||||
20
apps/files/src/composables/useBeforeNavigation.ts
Normal file
20
apps/files/src/composables/useBeforeNavigation.ts
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
/**
|
||||
* SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
|
||||
import type { NavigationGuard } from 'vue-router'
|
||||
|
||||
import { onUnmounted } from 'vue'
|
||||
import { useRouter } from 'vue-router/composables'
|
||||
|
||||
/**
|
||||
* Helper until we use Vue-Router v4 (Vue3).
|
||||
*
|
||||
* @param fn - The navigation guard
|
||||
*/
|
||||
export function onBeforeNavigation(fn: NavigationGuard) {
|
||||
const router = useRouter()
|
||||
const remove = router.beforeResolve(fn)
|
||||
onUnmounted(remove)
|
||||
}
|
||||
|
|
@ -1,47 +0,0 @@
|
|||
/*!
|
||||
* SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
|
||||
import { registerFileListFilter, unregisterFileListFilter } from '@nextcloud/files'
|
||||
import { watchThrottled } from '@vueuse/core'
|
||||
import { onMounted, onUnmounted, ref } from 'vue'
|
||||
import { FilenameFilter } from '../filters/FilenameFilter'
|
||||
|
||||
/**
|
||||
* This is for the `Navigation` component to provide a filename filter
|
||||
*/
|
||||
export function useFilenameFilter() {
|
||||
const searchQuery = ref('')
|
||||
const filenameFilter = new FilenameFilter()
|
||||
|
||||
/**
|
||||
* Updating the search query ref from the filter
|
||||
* @param event The update:query event
|
||||
*/
|
||||
function updateQuery(event: CustomEvent) {
|
||||
if (event.type === 'update:query') {
|
||||
searchQuery.value = event.detail
|
||||
event.stopPropagation()
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
filenameFilter.addEventListener('update:query', updateQuery)
|
||||
registerFileListFilter(filenameFilter)
|
||||
})
|
||||
onUnmounted(() => {
|
||||
filenameFilter.removeEventListener('update:query', updateQuery)
|
||||
unregisterFileListFilter(filenameFilter.id)
|
||||
})
|
||||
|
||||
// Update the query on the filter, but throttle to max. every 800ms
|
||||
// This will debounce the filter refresh
|
||||
watchThrottled(searchQuery, () => {
|
||||
filenameFilter.updateQuery(searchQuery.value)
|
||||
}, { throttle: 800 })
|
||||
|
||||
return {
|
||||
searchQuery,
|
||||
}
|
||||
}
|
||||
|
|
@ -7,7 +7,7 @@
|
|||
class="files-navigation"
|
||||
:aria-label="t('files', 'Files')">
|
||||
<template #search>
|
||||
<NcAppNavigationSearch v-model="searchQuery" :label="t('files', 'Filter file names …')" />
|
||||
<FilesNavigationSearch />
|
||||
</template>
|
||||
<template #default>
|
||||
<NcAppNavigationList class="files-navigation__list"
|
||||
|
|
@ -39,24 +39,24 @@
|
|||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { getNavigation, type View } from '@nextcloud/files'
|
||||
import type { View } from '@nextcloud/files'
|
||||
import type { ViewConfig } from '../types.ts'
|
||||
|
||||
import { defineComponent } from 'vue'
|
||||
import { emit, subscribe } from '@nextcloud/event-bus'
|
||||
import { translate as t, getCanonicalLocale, getLanguage } from '@nextcloud/l10n'
|
||||
import { getNavigation } from '@nextcloud/files'
|
||||
import { t, getCanonicalLocale, getLanguage } from '@nextcloud/l10n'
|
||||
import { defineComponent } from 'vue'
|
||||
|
||||
import IconCog from 'vue-material-design-icons/Cog.vue'
|
||||
import NcAppNavigation from '@nextcloud/vue/components/NcAppNavigation'
|
||||
import NcAppNavigationItem from '@nextcloud/vue/components/NcAppNavigationItem'
|
||||
import NcAppNavigationList from '@nextcloud/vue/components/NcAppNavigationList'
|
||||
import NcAppNavigationSearch from '@nextcloud/vue/components/NcAppNavigationSearch'
|
||||
import NavigationQuota from '../components/NavigationQuota.vue'
|
||||
import SettingsModal from './Settings.vue'
|
||||
import FilesNavigationItem from '../components/FilesNavigationItem.vue'
|
||||
import FilesNavigationSearch from '../components/FilesNavigationSearch.vue'
|
||||
|
||||
import { useNavigation } from '../composables/useNavigation'
|
||||
import { useFilenameFilter } from '../composables/useFilenameFilter'
|
||||
import { useFiltersStore } from '../store/filters.ts'
|
||||
import { useViewConfigStore } from '../store/viewConfig.ts'
|
||||
import logger from '../logger.ts'
|
||||
|
|
@ -75,12 +75,12 @@ export default defineComponent({
|
|||
components: {
|
||||
IconCog,
|
||||
FilesNavigationItem,
|
||||
FilesNavigationSearch,
|
||||
|
||||
NavigationQuota,
|
||||
NcAppNavigation,
|
||||
NcAppNavigationItem,
|
||||
NcAppNavigationList,
|
||||
NcAppNavigationSearch,
|
||||
SettingsModal,
|
||||
},
|
||||
|
||||
|
|
@ -88,11 +88,9 @@ export default defineComponent({
|
|||
const filtersStore = useFiltersStore()
|
||||
const viewConfigStore = useViewConfigStore()
|
||||
const { currentView, views } = useNavigation()
|
||||
const { searchQuery } = useFilenameFilter()
|
||||
|
||||
return {
|
||||
currentView,
|
||||
searchQuery,
|
||||
t,
|
||||
views,
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue