diff options
Diffstat (limited to 'apps/files/src/views')
-rw-r--r-- | apps/files/src/views/FilesList.vue | 30 | ||||
-rw-r--r-- | apps/files/src/views/Navigation.cy.ts | 25 | ||||
-rw-r--r-- | apps/files/src/views/Navigation.vue | 16 | ||||
-rw-r--r-- | apps/files/src/views/SearchEmptyView.vue | 57 | ||||
-rw-r--r-- | apps/files/src/views/files.ts | 9 | ||||
-rw-r--r-- | apps/files/src/views/search.ts | 51 |
6 files changed, 162 insertions, 26 deletions
diff --git a/apps/files/src/views/FilesList.vue b/apps/files/src/views/FilesList.vue index 60791a2b527..89d9fed6ce5 100644 --- a/apps/files/src/views/FilesList.vue +++ b/apps/files/src/views/FilesList.vue @@ -160,6 +160,7 @@ import { showError, showSuccess, showWarning } from '@nextcloud/dialogs' import { ShareType } from '@nextcloud/sharing' import { UploadPicker, UploadStatus } from '@nextcloud/upload' import { loadState } from '@nextcloud/initial-state' +import { useThrottleFn } from '@vueuse/core' import { defineComponent } from 'vue' import NcAppContent from '@nextcloud/vue/components/NcAppContent' @@ -325,16 +326,7 @@ export default defineComponent({ return } - if (this.directory === '/') { - return this.filesStore.getRoot(this.currentView.id) - } - - const source = this.pathsStore.getPath(this.currentView.id, this.directory) - if (source === undefined) { - return - } - - return this.filesStore.getNode(source) as Folder + return this.filesStore.getDirectoryByPath(this.currentView.id, this.directory) }, dirContents(): Node[] { @@ -479,6 +471,10 @@ export default defineComponent({ const hidden = this.dirContents.length - this.dirContentsFiltered.length return getSummaryFor(this.dirContentsFiltered, hidden) }, + + debouncedFetchContent() { + return useThrottleFn(this.fetchContent, 800, true) + }, }, watch: { @@ -540,14 +536,16 @@ export default defineComponent({ // filter content if filter were changed subscribe('files:filters:changed', this.filterDirContent) + subscribe('files:search:updated', this.onUpdateSearch) + // Finally, fetch the current directory contents await this.fetchContent() if (this.fileId) { // If we have a fileId, let's check if the file exists - const node = this.dirContents.find(node => node.fileid.toString() === this.fileId.toString()) + const node = this.dirContents.find(node => node.fileid?.toString() === this.fileId?.toString()) // If the file isn't in the current directory nor if // the current directory is the file, we show an error - if (!node && this.currentFolder.fileid.toString() !== this.fileId.toString()) { + if (!node && this.currentFolder?.fileid?.toString() !== this.fileId.toString()) { showError(t('files', 'The file could not be found')) } } @@ -557,9 +555,17 @@ export default defineComponent({ unsubscribe('files:node:deleted', this.onNodeDeleted) unsubscribe('files:node:updated', this.onUpdatedNode) unsubscribe('files:config:updated', this.fetchContent) + unsubscribe('files:filters:changed', this.filterDirContent) + unsubscribe('files:search:updated', this.onUpdateSearch) }, methods: { + onUpdateSearch({ query, scope }) { + if (query && scope !== 'filter') { + this.debouncedFetchContent() + } + }, + async fetchContent() { this.loading = true this.error = null diff --git a/apps/files/src/views/Navigation.cy.ts b/apps/files/src/views/Navigation.cy.ts index a88878e2d3a..6b03efa4f5f 100644 --- a/apps/files/src/views/Navigation.cy.ts +++ b/apps/files/src/views/Navigation.cy.ts @@ -10,7 +10,8 @@ import NavigationView from './Navigation.vue' import { useViewConfigStore } from '../store/viewConfig' import { Folder, View, getNavigation } from '@nextcloud/files' -import router from '../router/router' +import router from '../router/router.ts' +import RouterService from '../services/RouterService' const resetNavigation = () => { const nav = getNavigation() @@ -27,9 +28,18 @@ const createView = (id: string, name: string, parent?: string) => new View({ parent, }) +function mockWindow() { + window.OCP ??= {} + window.OCP.Files ??= {} + window.OCP.Files.Router = new RouterService(router) +} + describe('Navigation renders', () => { - before(() => { + before(async () => { delete window._nc_navigation + mockWindow() + getNavigation().register(createView('files', 'Files')) + await router.replace({ name: 'filelist', params: { view: 'files' } }) cy.mockInitialState('files', 'storageStats', { used: 1000 * 1000 * 1000, @@ -41,6 +51,7 @@ describe('Navigation renders', () => { it('renders', () => { cy.mount(NavigationView, { + router, global: { plugins: [createTestingPinia({ createSpy: cy.spy, @@ -60,6 +71,7 @@ describe('Navigation API', () => { before(async () => { delete window._nc_navigation Navigation = getNavigation() + mockWindow() await router.replace({ name: 'filelist', params: { view: 'files' } }) }) @@ -152,14 +164,18 @@ describe('Navigation API', () => { }) describe('Quota rendering', () => { - before(() => { + before(async () => { delete window._nc_navigation + mockWindow() + getNavigation().register(createView('files', 'Files')) + await router.replace({ name: 'filelist', params: { view: 'files' } }) }) afterEach(() => cy.unmockInitialState()) it('Unknown quota', () => { cy.mount(NavigationView, { + router, global: { plugins: [createTestingPinia({ createSpy: cy.spy, @@ -177,6 +193,7 @@ describe('Quota rendering', () => { }) cy.mount(NavigationView, { + router, global: { plugins: [createTestingPinia({ createSpy: cy.spy, @@ -197,6 +214,7 @@ describe('Quota rendering', () => { }) cy.mount(NavigationView, { + router, global: { plugins: [createTestingPinia({ createSpy: cy.spy, @@ -219,6 +237,7 @@ describe('Quota rendering', () => { }) cy.mount(NavigationView, { + router, global: { plugins: [createTestingPinia({ createSpy: cy.spy, diff --git a/apps/files/src/views/Navigation.vue b/apps/files/src/views/Navigation.vue index 3147268f34d..c424a0d74b8 100644 --- a/apps/files/src/views/Navigation.vue +++ b/apps/files/src/views/Navigation.vue @@ -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, diff --git a/apps/files/src/views/SearchEmptyView.vue b/apps/files/src/views/SearchEmptyView.vue new file mode 100644 index 00000000000..0553e416caf --- /dev/null +++ b/apps/files/src/views/SearchEmptyView.vue @@ -0,0 +1,57 @@ +<!-- + - SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors + - SPDX-License-Identifier: AGPL-3.0-or-later +--> + +<script setup lang="ts"> +import { mdiMagnifyClose } from '@mdi/js' +import { t } from '@nextcloud/l10n' +import debounce from 'debounce' +import NcButton from '@nextcloud/vue/components/NcButton' +import NcEmptyContent from '@nextcloud/vue/components/NcEmptyContent' +import NcIconSvgWrapper from '@nextcloud/vue/components/NcIconSvgWrapper' +import NcInputField from '@nextcloud/vue/components/NcInputField' +import { getPinia } from '../store/index.ts' +import { useSearchStore } from '../store/search.ts' + +const searchStore = useSearchStore(getPinia()) +const debouncedUpdate = debounce((value: string) => { + searchStore.query = value +}, 500) +</script> + +<template> + <NcEmptyContent :name="t('files', 'No search results for “{query}”', { query: searchStore.query })"> + <template #icon> + <NcIconSvgWrapper :path="mdiMagnifyClose" /> + </template> + <template #action> + <div class="search-empty-view__wrapper"> + <NcInputField class="search-empty-view__input" + :label="t('files', 'Search for files')" + :model-value="searchStore.query" + type="search" + @update:model-value="debouncedUpdate" /> + <NcButton v-if="searchStore.scope === 'locally'" @click="searchStore.scope = 'globally'"> + {{ t('files', 'Search globally') }} + </NcButton> + </div> + </template> + </NcEmptyContent> +</template> + +<style scoped lang="scss"> +.search-empty-view { + &__input { + flex: 0 1; + min-width: min(400px, 50vw); + } + + &__wrapper { + display: flex; + flex-wrap: wrap; + gap: 10px; + align-items: baseline; + } +} +</style> diff --git a/apps/files/src/views/files.ts b/apps/files/src/views/files.ts index a49a13f91e1..699e173de63 100644 --- a/apps/files/src/views/files.ts +++ b/apps/files/src/views/files.ts @@ -8,10 +8,15 @@ import FolderSvg from '@mdi/svg/svg/folder.svg?raw' import { getContents } from '../services/Files' import { View, getNavigation } from '@nextcloud/files' -export default () => { +export const VIEW_ID = 'files' + +/** + * Register the files view to the navigation + */ +export function registerFilesView() { const Navigation = getNavigation() Navigation.register(new View({ - id: 'files', + id: VIEW_ID, name: t('files', 'All files'), caption: t('files', 'List of your files and folders.'), diff --git a/apps/files/src/views/search.ts b/apps/files/src/views/search.ts new file mode 100644 index 00000000000..a30f732163c --- /dev/null +++ b/apps/files/src/views/search.ts @@ -0,0 +1,51 @@ +/** + * SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +import type { ComponentPublicInstanceConstructor } from 'vue/types/v3-component-public-instance' + +import { View, getNavigation } from '@nextcloud/files' +import { t } from '@nextcloud/l10n' +import { getContents } from '../services/Search.ts' +import { VIEW_ID as FILES_VIEW_ID } from './files.ts' +import MagnifySvg from '@mdi/svg/svg/magnify.svg?raw' +import Vue from 'vue' + +export const VIEW_ID = 'search' + +/** + * Register the search-in-files view + */ +export function registerSearchView() { + let instance: Vue + let view: ComponentPublicInstanceConstructor + + const Navigation = getNavigation() + Navigation.register(new View({ + id: VIEW_ID, + name: t('files', 'Search'), + caption: t('files', 'Search results within your files.'), + + async emptyView(el) { + if (!view) { + view = (await import('./SearchEmptyView.vue')).default + } else { + instance.$destroy() + } + instance = new Vue(view) + instance.$mount(el) + }, + + icon: MagnifySvg, + order: 10, + + parent: FILES_VIEW_ID, + // it should be shown expanded + expanded: true, + // this view is hidden by default and only shown when active + hidden: true, + + getContents, + })) +} |