diff options
author | Ferdinand Thiessen <opensource@fthiessen.de> | 2024-07-25 02:09:53 +0200 |
---|---|---|
committer | GitHub <noreply@github.com> | 2024-07-25 02:09:53 +0200 |
commit | d843d671d5dc3ae4023e80ae5e923f7f7b9cf946 (patch) | |
tree | d0bb9d799523b283d3481a700a9be1b8fc2a4766 /apps | |
parent | ca5b746504fe37139b321021345a5640db70e3f6 (diff) | |
parent | fe1aa830575bf5ef88532d894297c557c69baf20 (diff) | |
download | nextcloud-server-d843d671d5dc3ae4023e80ae5e923f7f7b9cf946.tar.gz nextcloud-server-d843d671d5dc3ae4023e80ae5e923f7f7b9cf946.zip |
Merge pull request #46690 from nextcloud/fix/files-displayname
Update `@nextcloud/files` to 3.6.0 and fix display name handling of folders (breadcrumbs and filename)
Diffstat (limited to 'apps')
-rw-r--r-- | apps/files/src/actions/openFolderAction.ts | 2 | ||||
-rw-r--r-- | apps/files/src/components/BreadCrumbs.vue | 6 | ||||
-rw-r--r-- | apps/files/src/components/FileEntry.vue | 2 | ||||
-rw-r--r-- | apps/files/src/components/FileEntry/FileEntryName.vue | 14 | ||||
-rw-r--r-- | apps/files/src/components/FileEntryGrid.vue | 2 | ||||
-rw-r--r-- | apps/files/src/components/FileEntryMixin.ts | 32 | ||||
-rw-r--r-- | apps/files/src/services/Files.ts | 9 | ||||
-rw-r--r-- | apps/files/src/services/SortingService.spec.ts | 100 | ||||
-rw-r--r-- | apps/files/src/services/SortingService.ts | 59 | ||||
-rw-r--r-- | apps/files/src/views/FilesList.vue | 49 |
10 files changed, 54 insertions, 221 deletions
diff --git a/apps/files/src/actions/openFolderAction.ts b/apps/files/src/actions/openFolderAction.ts index ab2d280eb73..8719f7a93fb 100644 --- a/apps/files/src/actions/openFolderAction.ts +++ b/apps/files/src/actions/openFolderAction.ts @@ -10,7 +10,7 @@ export const action = new FileAction({ id: 'open-folder', displayName(files: Node[]) { // Only works on single node - const displayName = files[0].attributes.displayname || files[0].basename + const displayName = files[0].displayname return t('files', 'Open folder {displayName}', { displayName }) }, iconSvgInline: () => FolderSvg, diff --git a/apps/files/src/components/BreadCrumbs.vue b/apps/files/src/components/BreadCrumbs.vue index 02ccac8b669..169bc504ab9 100644 --- a/apps/files/src/components/BreadCrumbs.vue +++ b/apps/files/src/components/BreadCrumbs.vue @@ -158,9 +158,9 @@ export default defineComponent({ return this.$navigation?.active?.name || t('files', 'Home') } - const source: FileSource | null = this.getFileSourceFromPath(path) - const node: Node | undefined = source ? this.getNodeFromSource(source) : undefined - return node?.attributes?.displayname || basename(path) + const source = this.getFileSourceFromPath(path) + const node = source ? this.getNodeFromSource(source) : undefined + return node?.displayname || basename(path) }, onClick(to) { diff --git a/apps/files/src/components/FileEntry.vue b/apps/files/src/components/FileEntry.vue index fdc800b3464..48b6dcfd8a0 100644 --- a/apps/files/src/components/FileEntry.vue +++ b/apps/files/src/components/FileEntry.vue @@ -34,7 +34,7 @@ @click.native="execDefaultAction" /> <FileEntryName ref="name" - :display-name="displayName" + :basename="basename" :extension="extension" :files-list-width="filesListWidth" :nodes="nodes" diff --git a/apps/files/src/components/FileEntry/FileEntryName.vue b/apps/files/src/components/FileEntry/FileEntryName.vue index 4fb907dd005..875c0892a72 100644 --- a/apps/files/src/components/FileEntry/FileEntryName.vue +++ b/apps/files/src/components/FileEntry/FileEntryName.vue @@ -29,8 +29,8 @@ v-bind="linkTo.params"> <!-- File name --> <span class="files-list__row-name-text"> - <!-- Keep the displayName stuck to the extension to avoid whitespace rendering issues--> - <span class="files-list__row-name-" v-text="displayName" /> + <!-- Keep the filename stuck to the extension to avoid whitespace rendering issues--> + <span class="files-list__row-name-" v-text="basename" /> <span class="files-list__row-name-ext" v-text="extension" /> </span> </component> @@ -64,10 +64,16 @@ export default defineComponent({ }, props: { - displayName: { + /** + * The filename without extension + */ + basename: { type: String, required: true, }, + /** + * The extension of the filename + */ extension: { type: String, required: true, @@ -155,7 +161,7 @@ export default defineComponent({ params: { download: this.source.basename, href: this.source.source, - title: t('files', 'Download file {name}', { name: this.displayName }), + title: t('files', 'Download file {name}', { name: `${this.basename}${this.extension}` }), tabindex: '0', }, } diff --git a/apps/files/src/components/FileEntryGrid.vue b/apps/files/src/components/FileEntryGrid.vue index ed8175fcda7..1f0992bc851 100644 --- a/apps/files/src/components/FileEntryGrid.vue +++ b/apps/files/src/components/FileEntryGrid.vue @@ -36,7 +36,7 @@ @click.native="execDefaultAction" /> <FileEntryName ref="name" - :display-name="displayName" + :basename="basename" :extension="extension" :files-list-width="filesListWidth" :grid-mode="true" diff --git a/apps/files/src/components/FileEntryMixin.ts b/apps/files/src/components/FileEntryMixin.ts index 6c0b278c61b..da9b93107c7 100644 --- a/apps/files/src/components/FileEntryMixin.ts +++ b/apps/files/src/components/FileEntryMixin.ts @@ -74,19 +74,31 @@ export default defineComponent({ return this.source.status === NodeStatus.LOADING }, - extension() { - if (this.source.attributes?.displayname) { - return extname(this.source.attributes.displayname) + /** + * The display name of the current node + * Either the nodes filename or a custom display name (e.g. for shares) + */ + displayName() { + return this.source.displayname + }, + /** + * The display name without extension + */ + basename() { + if (this.extension === '') { + return this.displayName } - return this.source.extension || '' + return this.displayName.slice(0, 0 - this.extension.length) }, - displayName() { - const ext = this.extension - const name = String(this.source.attributes.displayname - || this.source.basename) + /** + * The extension of the file + */ + extension() { + if (this.source.type === FileType.Folder) { + return '' + } - // Strip extension from name if defined - return !ext ? name : name.slice(0, 0 - ext.length) + return extname(this.displayName) }, draggingFiles() { diff --git a/apps/files/src/services/Files.ts b/apps/files/src/services/Files.ts index dc83f16187b..10e553592fe 100644 --- a/apps/files/src/services/Files.ts +++ b/apps/files/src/services/Files.ts @@ -14,7 +14,14 @@ import logger from '../logger.js' * Slim wrapper over `@nextcloud/files` `davResultToNode` to allow using the function with `Array.map` * @param node The node returned by the webdav library */ -export const resultToNode = (node: FileStat): File | Folder => davResultToNode(node) +export const resultToNode = (node: FileStat): File | Folder => { + // TODO remove this hack with nextcloud-files v3.7 + // just needed because of a bug in the webdav client + if (node.props?.displayname !== undefined) { + node.props.displayname = String(node.props.displayname) + } + return davResultToNode(node) +} export const getContents = (path = '/'): CancelablePromise<ContentsWithRoot> => { const controller = new AbortController() diff --git a/apps/files/src/services/SortingService.spec.ts b/apps/files/src/services/SortingService.spec.ts deleted file mode 100644 index 5d20c43ed0a..00000000000 --- a/apps/files/src/services/SortingService.spec.ts +++ /dev/null @@ -1,100 +0,0 @@ -/** - * SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors - * SPDX-License-Identifier: AGPL-3.0-or-later - */ -import { describe, expect } from '@jest/globals' -import { orderBy } from './SortingService' - -describe('SortingService', () => { - test('By default the identify and ascending order is used', () => { - const array = ['a', 'z', 'b'] - expect(orderBy(array)).toEqual(['a', 'b', 'z']) - }) - - test('Use identifiy but descending', () => { - const array = ['a', 'z', 'b'] - expect(orderBy(array, undefined, ['desc'])).toEqual(['z', 'b', 'a']) - }) - - test('Can set identifier function', () => { - const array = [ - { text: 'a', order: 2 }, - { text: 'z', order: 1 }, - { text: 'b', order: 3 }, - ] as const - expect(orderBy(array, [(v) => v.order]).map((v) => v.text)).toEqual(['z', 'a', 'b']) - }) - - test('Can set multiple identifier functions', () => { - const array = [ - { text: 'a', order: 2, secondOrder: 2 }, - { text: 'z', order: 1, secondOrder: 3 }, - { text: 'b', order: 2, secondOrder: 1 }, - ] as const - expect(orderBy(array, [(v) => v.order, (v) => v.secondOrder]).map((v) => v.text)).toEqual(['z', 'b', 'a']) - }) - - test('Can set order partially', () => { - const array = [ - { text: 'a', order: 2, secondOrder: 2 }, - { text: 'z', order: 1, secondOrder: 3 }, - { text: 'b', order: 2, secondOrder: 1 }, - ] as const - - expect( - orderBy( - array, - [(v) => v.order, (v) => v.secondOrder], - ['desc'], - ).map((v) => v.text), - ).toEqual(['b', 'a', 'z']) - }) - - test('Can set order array', () => { - const array = [ - { text: 'a', order: 2, secondOrder: 2 }, - { text: 'z', order: 1, secondOrder: 3 }, - { text: 'b', order: 2, secondOrder: 1 }, - ] as const - - expect( - orderBy( - array, - [(v) => v.order, (v) => v.secondOrder], - ['desc', 'desc'], - ).map((v) => v.text), - ).toEqual(['a', 'b', 'z']) - }) - - test('Numbers are handled correctly', () => { - const array = [ - { text: '2.3' }, - { text: '2.10' }, - { text: '2.0' }, - { text: '2.2' }, - ] as const - - expect( - orderBy( - array, - [(v) => v.text], - ).map((v) => v.text), - ).toEqual(['2.0', '2.2', '2.3', '2.10']) - }) - - test('Numbers with suffixes are handled correctly', () => { - const array = [ - { text: '2024-01-05' }, - { text: '2024-05-01' }, - { text: '2024-01-10' }, - { text: '2024-01-05 Foo' }, - ] as const - - expect( - orderBy( - array, - [(v) => v.text], - ).map((v) => v.text), - ).toEqual(['2024-01-05', '2024-01-05 Foo', '2024-01-10', '2024-05-01']) - }) -}) diff --git a/apps/files/src/services/SortingService.ts b/apps/files/src/services/SortingService.ts deleted file mode 100644 index 392f35efc9f..00000000000 --- a/apps/files/src/services/SortingService.ts +++ /dev/null @@ -1,59 +0,0 @@ -/** - * SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors - * SPDX-License-Identifier: AGPL-3.0-or-later - */ -import { getCanonicalLocale, getLanguage } from '@nextcloud/l10n' - -type IdentifierFn<T> = (v: T) => unknown -type SortingOrder = 'asc'|'desc' - -/** - * Helper to create string representation - * @param value Value to stringify - */ -function stringify(value: unknown) { - // The default representation of Date is not sortable because of the weekday names in front of it - if (value instanceof Date) { - return value.toISOString() - } - return String(value) -} - -/** - * Natural order a collection - * You can define identifiers as callback functions, that get the element and return the value to sort. - * - * @param collection The collection to order - * @param identifiers An array of identifiers to use, by default the identity of the element is used - * @param orders Array of orders, by default all identifiers are sorted ascening - */ -export function orderBy<T>(collection: readonly T[], identifiers?: IdentifierFn<T>[], orders?: SortingOrder[]): T[] { - // If not identifiers are set we use the identity of the value - identifiers = identifiers ?? [(value) => value] - // By default sort the collection ascending - orders = orders ?? [] - const sorting = identifiers.map((_, index) => (orders[index] ?? 'asc') === 'asc' ? 1 : -1) - - const collator = Intl.Collator( - [getLanguage(), getCanonicalLocale()], - { - // handle 10 as ten and not as one-zero - numeric: true, - usage: 'sort', - }, - ) - - return [...collection].sort((a, b) => { - for (const [index, identifier] of identifiers.entries()) { - // Get the local compare of stringified value a and b - const value = collator.compare(stringify(identifier(a)), stringify(identifier(b))) - // If they do not match return the order - if (value !== 0) { - return value * sorting[index] - } - // If they match we need to continue with the next identifier - } - // If all are equal we need to return equality - return 0 - }) -} diff --git a/apps/files/src/views/FilesList.vue b/apps/files/src/views/FilesList.vue index 4fe33c61375..6d9a6cf60a0 100644 --- a/apps/files/src/views/FilesList.vue +++ b/apps/files/src/views/FilesList.vue @@ -120,7 +120,7 @@ import type { UserConfig } from '../types.ts' import { getCapabilities } from '@nextcloud/capabilities' import { emit, subscribe, unsubscribe } from '@nextcloud/event-bus' -import { Folder, Node, Permission } from '@nextcloud/files' +import { Folder, Node, Permission, sortNodes } from '@nextcloud/files' import { translate as t } from '@nextcloud/l10n' import { join, dirname, normalize } from 'path' import { showError, showWarning } from '@nextcloud/dialogs' @@ -148,7 +148,6 @@ import { useSelectionStore } from '../store/selection.ts' import { useUploaderStore } from '../store/uploader.ts' import { useUserConfigStore } from '../store/userconfig.ts' import { useViewConfigStore } from '../store/viewConfig.ts' -import { orderBy } from '../services/SortingService.ts' import BreadCrumbs from '../components/BreadCrumbs.vue' import FilesListVirtual from '../components/FilesListVirtual.vue' import filesListWidthMixin from '../mixins/filesListWidth.ts' @@ -292,43 +291,9 @@ export default defineComponent({ }, /** - * Directory content sorting parameters - * Provided by an extra computed property for caching - */ - sortingParameters() { - const identifiers = [ - // 1: Sort favorites first if enabled - ...(this.userConfig.sort_favorites_first ? [v => v.attributes?.favorite !== 1] : []), - // 2: Sort folders first if sorting by name - ...(this.userConfig.sort_folders_first ? [v => v.type !== 'folder'] : []), - // 3: Use sorting mode if NOT basename (to be able to use displayName too) - ...(this.sortingMode !== 'basename' ? [v => v[this.sortingMode]] : []), - // 4: Use displayname if available, fallback to name - v => v.attributes?.displayname || v.basename, - // 5: Finally, use basename if all previous sorting methods failed - v => v.basename, - ] - const orders = [ - // (for 1): always sort favorites before normal files - ...(this.userConfig.sort_favorites_first ? ['asc'] : []), - // (for 2): always sort folders before files - ...(this.userConfig.sort_folders_first ? ['asc'] : []), - // (for 3): Reverse if sorting by mtime as mtime higher means edited more recent -> lower - ...(this.sortingMode === 'mtime' ? [this.isAscSorting ? 'desc' : 'asc'] : []), - // (also for 3 so make sure not to conflict with 2 and 3) - ...(this.sortingMode !== 'mtime' && this.sortingMode !== 'basename' ? [this.isAscSorting ? 'asc' : 'desc'] : []), - // for 4: use configured sorting direction - this.isAscSorting ? 'asc' : 'desc', - // for 5: use configured sorting direction - this.isAscSorting ? 'asc' : 'desc', - ] as ('asc'|'desc')[] - return [identifiers, orders] as const - }, - - /** * The current directory contents. */ - dirContentsSorted(): Node[] { + dirContentsSorted() { if (!this.currentView) { return [] } @@ -351,10 +316,12 @@ export default defineComponent({ return this.isAscSorting ? results : results.reverse() } - return orderBy( - filteredDirContent, - ...this.sortingParameters, - ) + return sortNodes(filteredDirContent, { + sortFavoritesFirst: this.userConfig.sort_favorites_first, + sortFoldersFirst: this.userConfig.sort_folders_first, + sortingMode: this.sortingMode, + sortingOrder: this.isAscSorting ? 'asc' : 'desc', + }) }, dirContents(): Node[] { |