diff options
author | Ferdinand Thiessen <opensource@fthiessen.de> | 2024-06-06 15:32:27 +0200 |
---|---|---|
committer | Ferdinand Thiessen <opensource@fthiessen.de> | 2024-07-25 19:15:38 +0200 |
commit | 968d41241baac08b9a2a09f104f77c755908d477 (patch) | |
tree | abb8e5058eabfe7f98e78d3ed1bf9bdf40aa25fb /apps/files/src | |
parent | ba91f42c8baf320dee94bd1632314657f79054df (diff) | |
download | nextcloud-server-968d41241baac08b9a2a09f104f77c755908d477.tar.gz nextcloud-server-968d41241baac08b9a2a09f104f77c755908d477.zip |
feat(files): Allow to add file list filters
This adds sticky file list filters above the file list.
Those filters are used to filter the directory content and thus filter the file list.
Signed-off-by: Ferdinand Thiessen <opensource@fthiessen.de>
Diffstat (limited to 'apps/files/src')
-rw-r--r-- | apps/files/src/components/FilesListFilter/FilesListFilters.vue | 30 | ||||
-rw-r--r-- | apps/files/src/components/FilesListVirtual.vue | 29 | ||||
-rw-r--r-- | apps/files/src/components/VirtualList.vue | 4 | ||||
-rw-r--r-- | apps/files/src/composables/useFilesFilter.ts | 20 | ||||
-rw-r--r-- | apps/files/src/types.ts | 14 | ||||
-rw-r--r-- | apps/files/src/views/FilesList.vue | 109 |
6 files changed, 138 insertions, 68 deletions
diff --git a/apps/files/src/components/FilesListFilter/FilesListFilters.vue b/apps/files/src/components/FilesListFilter/FilesListFilters.vue new file mode 100644 index 00000000000..b7752e9befe --- /dev/null +++ b/apps/files/src/components/FilesListFilter/FilesListFilters.vue @@ -0,0 +1,30 @@ +<template> + <Fragment> + </Fragment> +</template> + +<script lang="ts"> +import type { View } from '@nextcloud/files' +import type { PropType } from 'vue' + +import { Fragment } from 'vue-frag' +import { defineComponent } from 'vue' + +export default defineComponent({ + + props: { + currentView: { + type: Object as PropType<View>, + required: true, + }, + }, + + watch: { + currentView() { + // Reset all filters on view change + const components = this.$refs.filters as { resetFilter: () => void }[] | undefined + components?.forEach((component) => component.resetFilter()) + }, + }, +}) +</script> diff --git a/apps/files/src/components/FilesListVirtual.vue b/apps/files/src/components/FilesListVirtual.vue index 4bac5f84db6..b1f619fb971 100644 --- a/apps/files/src/components/FilesListVirtual.vue +++ b/apps/files/src/components/FilesListVirtual.vue @@ -16,6 +16,10 @@ }" :scroll-to-index="scrollToIndex" :caption="caption"> + <template #filters> + <FilesListFilters :current-view="currentView" /> + </template> + <template v-if="!isNoneSelected" #header-overlay> <span class="files-list__selected">{{ t('files', '{count} selected', { count: selectedNodes.length }) }}</span> <FilesListTableHeaderActions :current-view="currentView" @@ -78,11 +82,13 @@ import filesListWidthMixin from '../mixins/filesListWidth.ts' import VirtualList from './VirtualList.vue' import logger from '../logger.js' import FilesListTableHeaderActions from './FilesListTableHeaderActions.vue' +import FilesListFilters from './FilesListFilter/FilesListFilters.vue' export default defineComponent({ name: 'FilesListVirtual', components: { + FilesListFilters, FilesListHeader, FilesListTableFooter, FilesListTableHeader, @@ -364,10 +370,29 @@ export default defineComponent({ } } + .files-list__filters { + display: flex; + align-items: baseline; + justify-content: start; + gap: calc(var(--default-grid-baseline, 4px) * 2); + // Pinned on top when scrolling above table header + position: sticky; + top: 0; + // fix size and background + background-color: var(--color-main-background); + padding-inline: var(--row-height) var(--default-grid-baseline, 4px); + height: var(--row-height); + width: 100%; + + > * { + flex: 0 1 fit-content; + } + } + .files-list__thead-overlay { // Pinned on top when scrolling position: sticky; - top: 0; + top: var(--row-height); // Save space for a row checkbox margin-left: var(--row-height); // More than .files-list__thead @@ -396,7 +421,7 @@ export default defineComponent({ // Pinned on top when scrolling position: sticky; z-index: 10; - top: 0; + top: var(--row-height); } // Table footer diff --git a/apps/files/src/components/VirtualList.vue b/apps/files/src/components/VirtualList.vue index daf021e8ed5..6206f86cda5 100644 --- a/apps/files/src/components/VirtualList.vue +++ b/apps/files/src/components/VirtualList.vue @@ -9,6 +9,10 @@ <slot name="before" /> </div> + <div class="files-list__filters"> + <slot name="filters" /> + </div> + <div v-if="!!$scopedSlots['header-overlay']" class="files-list__thead-overlay"> <slot name="header-overlay" /> </div> diff --git a/apps/files/src/composables/useFilesFilter.ts b/apps/files/src/composables/useFilesFilter.ts new file mode 100644 index 00000000000..a8a0c9e8c4d --- /dev/null +++ b/apps/files/src/composables/useFilesFilter.ts @@ -0,0 +1,20 @@ +/** + * SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ +import type { FilesFilter } from '../types' + +import { inject } from 'vue' + +/** + * Provide functions for adding and deleting filters injected by the files list + */ +export default function() { + const addFilter = inject<(filter: FilesFilter) => void>('files:filter:add', () => {}) + const deleteFilter = inject<(id: string) => void>('files:filter:delete', () => {}) + + return { + addFilter, + deleteFilter, + } +} diff --git a/apps/files/src/types.ts b/apps/files/src/types.ts index 9e1ba049697..c5f6a15d6ef 100644 --- a/apps/files/src/types.ts +++ b/apps/files/src/types.ts @@ -105,3 +105,17 @@ export interface TemplateFile { ratio?: number templates?: Record<string, unknown>[] } + +export interface FilesFilter { + /** + * ID of the filter + */ + id: string + + /** + * Filter function callback + * @param node Node to check + * @return True if keep the file false to remove from list + */ + filter: (node: Node) => boolean +} diff --git a/apps/files/src/views/FilesList.vue b/apps/files/src/views/FilesList.vue index 6d9a6cf60a0..19774292e24 100644 --- a/apps/files/src/views/FilesList.vue +++ b/apps/files/src/views/FilesList.vue @@ -116,7 +116,7 @@ import type { Upload } from '@nextcloud/upload' import type { CancelablePromise } from 'cancelable-promise' import type { ComponentPublicInstance } from 'vue' import type { Route } from 'vue-router' -import type { UserConfig } from '../types.ts' +import type { FilesFilter, UserConfig } from '../types.ts' import { getCapabilities } from '@nextcloud/capabilities' import { emit, subscribe, unsubscribe } from '@nextcloud/event-bus' @@ -154,7 +154,6 @@ import filesListWidthMixin from '../mixins/filesListWidth.ts' import filesSortingMixin from '../mixins/filesSorting.ts' import logger from '../logger.js' import DragAndDropNotice from '../components/DragAndDropNotice.vue' -import debounce from 'debounce' const isSharingEnabled = (getCapabilities() as { files_sharing?: boolean })?.files_sharing !== undefined @@ -183,6 +182,20 @@ export default defineComponent({ filesSortingMixin, ], + provide() { + return { + 'files:filter:add': (filter: FilesFilter) => { + this.$set(this.filters, filter.id, filter) + logger.debug('File list filter updated', { filters: [...Object.keys(this.filters)] }) + }, + + 'files:filter:delete': (id: string) => { + this.$delete(this.filters, id) + logger.debug('File list filter removed', { filter: id, filters: [...Object.keys(this.filters)] }) + }, + } + }, + setup() { const filesStore = useFilesStore() const pathsStore = usePathsStore() @@ -197,6 +210,7 @@ export default defineComponent({ return { currentView, + t, filesStore, pathsStore, @@ -214,7 +228,7 @@ export default defineComponent({ data() { return { - filterText: '', + filters: {} as Record<string, FilesFilter>, // TODO: With Vue3 use Map loading: true, promise: null as CancelablePromise<ContentsWithRoot> | Promise<ContentsWithRoot> | null, @@ -224,20 +238,10 @@ export default defineComponent({ computed: { /** - * Handle search event from unified search. - */ - onSearch() { - return debounce((searchEvent: { query: string }) => { - console.debug('Files app handling search event from unified search...', searchEvent) - this.filterText = searchEvent.query - }, 500) - }, - - /** * Get a callback function for the uploader to fetch directory contents for conflict resolution */ getContent() { - const view = this.currentView + const view = this.currentView! return async (path?: string) => { // as the path is allowed to be undefined we need to normalize the path ('//' to '/') const normalizedPath = normalize(`${this.currentFolder?.path ?? ''}/${path ?? ''}`) @@ -290,6 +294,19 @@ export default defineComponent({ return this.filesStore.getNode(source) as Folder }, + dirContents(): Node[] { + return (this.currentFolder?._children || []) + .map(this.getNode) + .filter((node: Node) => !!node) + .filter(this.filterHidden) + }, + + dirContentsFiltered(): Node[] { + const filters = [...Object.values(this.filters)] + return this.dirContents + .filter((node: Node) => filters.every((filter: FilesFilter) => filter.filter(node))) + }, + /** * The current directory contents. */ @@ -298,25 +315,16 @@ export default defineComponent({ return [] } - let filteredDirContent = [...this.dirContents] - // Filter based on the filterText obtained from nextcloud:unified-search.search event. - if (this.filterText) { - filteredDirContent = filteredDirContent.filter(node => { - return node.basename.toLowerCase().includes(this.filterText.toLowerCase()) - }) - console.debug('Files view filtered', filteredDirContent) - } - const customColumn = (this.currentView?.columns || []) .find(column => column.id === this.sortingMode) // Custom column must provide their own sorting methods if (customColumn?.sort && typeof customColumn.sort === 'function') { - const results = [...this.dirContents].sort(customColumn.sort) + const results = [...this.dirContentsFiltered].sort(customColumn.sort) return this.isAscSorting ? results : results.reverse() } - return sortNodes(filteredDirContent, { + return sortNodes(this.dirContentsFiltered, { sortFavoritesFirst: this.userConfig.sort_favorites_first, sortFoldersFirst: this.userConfig.sort_folders_first, sortingMode: this.sortingMode, @@ -324,19 +332,6 @@ export default defineComponent({ }) }, - dirContents(): Node[] { - const showHidden = this.userConfigStore?.userConfig.show_hidden - return (this.currentFolder?._children || []) - .map(this.getNode) - .filter(file => { - if (!showHidden) { - return file && file?.attributes?.hidden !== true && !file?.basename.startsWith('.') - } - - return !!file - }) - }, - /** * The current directory is empty. */ @@ -431,7 +426,6 @@ export default defineComponent({ logger.debug('View changed', { newView, oldView }) this.selectionStore.reset() - this.triggerResetSearch() this.fetchContent() }, @@ -439,7 +433,6 @@ export default defineComponent({ logger.debug('Directory changed', { newDir, oldDir }) // TODO: preserve selection on browsing? this.selectionStore.reset() - this.triggerResetSearch() if (window.OCA.Files.Sidebar?.close) { window.OCA.Files.Sidebar.close() } @@ -463,8 +456,6 @@ export default defineComponent({ subscribe('files:node:deleted', this.onNodeDeleted) subscribe('files:node:updated', this.onUpdatedNode) - subscribe('nextcloud:unified-search:search', this.onSearch) - subscribe('nextcloud:unified-search:reset', this.onResetSearch) // reload on settings change this.unsubscribeStoreCallback = this.userConfigStore.$subscribe(() => this.fetchContent(), { deep: true }) @@ -473,14 +464,9 @@ export default defineComponent({ unmounted() { unsubscribe('files:node:deleted', this.onNodeDeleted) unsubscribe('files:node:updated', this.onUpdatedNode) - unsubscribe('nextcloud:unified-search:search', this.onSearch) - unsubscribe('nextcloud:unified-search:reset', this.onResetSearch) - this.unsubscribeStoreCallback() }, methods: { - t, - async fetchContent() { this.loading = true const dir = this.dir @@ -548,6 +534,15 @@ export default defineComponent({ }, /** + * Whether the node should be shown + * @param node The node to check + */ + filterHidden(node: Node) { + const showHidden = this.userConfigStore?.userConfig.show_hidden + return showHidden || (node.attributes.hidden !== true && !node.basename.startsWith('.')) + }, + + /** * Handle the node deleted event to reset open file * @param node The deleted node */ @@ -555,7 +550,7 @@ export default defineComponent({ if (node.fileid && node.fileid === this.fileId) { if (node.fileid === this.currentFolder?.fileid) { // Handle the edge case that the current directory is deleted - // in this case we neeed to keept the current view but move to the parent directory + // in this case we need to keep the current view but move to the parent directory window.OCP.Files.Router.goToRoute( null, { view: this.$route.params.view }, @@ -645,24 +640,6 @@ export default defineComponent({ } }, - /** - * Handle reset search query event - */ - onResetSearch() { - // Reset debounced calls to not set the query again - this.onSearch.clear() - // Reset filter query - this.filterText = '' - }, - - /** - * Trigger a reset of the local search (part of unified search) - * This is usful to reset the search on directory / view change - */ - triggerResetSearch() { - emit('nextcloud:unified-search:reset') - }, - openSharingSidebar() { if (!this.currentFolder) { logger.debug('No current folder found for opening sharing sidebar') |