Skip to content

Commit 05eb647

Browse files
authored
Merge pull request #45534 from nextcloud/backport/45419/stable29
[stable29] fix(files): Implement `SortingService` to fix sorting of files
2 parents 340db5c + 8814932 commit 05eb647

File tree

7 files changed

+163
-14
lines changed

7 files changed

+163
-14
lines changed
Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
/**
2+
* SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
3+
* SPDX-License-Identifier: AGPL-3.0-or-later
4+
*/
5+
import { describe, expect } from '@jest/globals'
6+
import { orderBy } from './SortingService'
7+
8+
describe('SortingService', () => {
9+
test('By default the identify and ascending order is used', () => {
10+
const array = ['a', 'z', 'b']
11+
expect(orderBy(array)).toEqual(['a', 'b', 'z'])
12+
})
13+
14+
test('Use identifiy but descending', () => {
15+
const array = ['a', 'z', 'b']
16+
expect(orderBy(array, undefined, ['desc'])).toEqual(['z', 'b', 'a'])
17+
})
18+
19+
test('Can set identifier function', () => {
20+
const array = [
21+
{ text: 'a', order: 2 },
22+
{ text: 'z', order: 1 },
23+
{ text: 'b', order: 3 },
24+
] as const
25+
expect(orderBy(array, [(v) => v.order]).map((v) => v.text)).toEqual(['z', 'a', 'b'])
26+
})
27+
28+
test('Can set multiple identifier functions', () => {
29+
const array = [
30+
{ text: 'a', order: 2, secondOrder: 2 },
31+
{ text: 'z', order: 1, secondOrder: 3 },
32+
{ text: 'b', order: 2, secondOrder: 1 },
33+
] as const
34+
expect(orderBy(array, [(v) => v.order, (v) => v.secondOrder]).map((v) => v.text)).toEqual(['z', 'b', 'a'])
35+
})
36+
37+
test('Can set order partially', () => {
38+
const array = [
39+
{ text: 'a', order: 2, secondOrder: 2 },
40+
{ text: 'z', order: 1, secondOrder: 3 },
41+
{ text: 'b', order: 2, secondOrder: 1 },
42+
] as const
43+
44+
expect(
45+
orderBy(
46+
array,
47+
[(v) => v.order, (v) => v.secondOrder],
48+
['desc'],
49+
).map((v) => v.text),
50+
).toEqual(['b', 'a', 'z'])
51+
})
52+
53+
test('Can set order array', () => {
54+
const array = [
55+
{ text: 'a', order: 2, secondOrder: 2 },
56+
{ text: 'z', order: 1, secondOrder: 3 },
57+
{ text: 'b', order: 2, secondOrder: 1 },
58+
] as const
59+
60+
expect(
61+
orderBy(
62+
array,
63+
[(v) => v.order, (v) => v.secondOrder],
64+
['desc', 'desc'],
65+
).map((v) => v.text),
66+
).toEqual(['a', 'b', 'z'])
67+
})
68+
69+
test('Numbers are handled correctly', () => {
70+
const array = [
71+
{ text: '2.3' },
72+
{ text: '2.10' },
73+
{ text: '2.0' },
74+
{ text: '2.2' },
75+
] as const
76+
77+
expect(
78+
orderBy(
79+
array,
80+
[(v) => v.text],
81+
).map((v) => v.text),
82+
).toEqual(['2.0', '2.2', '2.3', '2.10'])
83+
})
84+
85+
test('Numbers with suffixes are handled correctly', () => {
86+
const array = [
87+
{ text: '2024-01-05' },
88+
{ text: '2024-05-01' },
89+
{ text: '2024-01-10' },
90+
{ text: '2024-01-05 Foo' },
91+
] as const
92+
93+
expect(
94+
orderBy(
95+
array,
96+
[(v) => v.text],
97+
).map((v) => v.text),
98+
).toEqual(['2024-01-05', '2024-01-05 Foo', '2024-01-10', '2024-05-01'])
99+
})
100+
})
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
/**
2+
* SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
3+
* SPDX-License-Identifier: AGPL-3.0-or-later
4+
*/
5+
import { getCanonicalLocale, getLanguage } from '@nextcloud/l10n'
6+
7+
type IdentifierFn<T> = (v: T) => unknown
8+
type SortingOrder = 'asc'|'desc'
9+
10+
/**
11+
* Helper to create string representation
12+
* @param value Value to stringify
13+
*/
14+
function stringify(value: unknown) {
15+
// The default representation of Date is not sortable because of the weekday names in front of it
16+
if (value instanceof Date) {
17+
return value.toISOString()
18+
}
19+
return String(value)
20+
}
21+
22+
/**
23+
* Natural order a collection
24+
* You can define identifiers as callback functions, that get the element and return the value to sort.
25+
*
26+
* @param collection The collection to order
27+
* @param identifiers An array of identifiers to use, by default the identity of the element is used
28+
* @param orders Array of orders, by default all identifiers are sorted ascening
29+
*/
30+
export function orderBy<T>(collection: readonly T[], identifiers?: IdentifierFn<T>[], orders?: SortingOrder[]): T[] {
31+
// If not identifiers are set we use the identity of the value
32+
identifiers = identifiers ?? [(value) => value]
33+
// By default sort the collection ascending
34+
orders = orders ?? []
35+
const sorting = identifiers.map((_, index) => (orders[index] ?? 'asc') === 'asc' ? 1 : -1)
36+
37+
const collator = Intl.Collator(
38+
[getLanguage(), getCanonicalLocale()],
39+
{
40+
// handle 10 as ten and not as one-zero
41+
numeric: true,
42+
usage: 'sort',
43+
},
44+
)
45+
46+
return [...collection].sort((a, b) => {
47+
for (const [index, identifier] of identifiers.entries()) {
48+
// Get the local compare of stringified value a and b
49+
const value = collator.compare(stringify(identifier(a)), stringify(identifier(b)))
50+
// If they do not match return the order
51+
if (value !== 0) {
52+
return value * sorting[index]
53+
}
54+
// If they match we need to continue with the next identifier
55+
}
56+
// If all are equal we need to return equality
57+
return 0
58+
})
59+
}

apps/files/src/views/FilesList.vue

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -125,7 +125,6 @@ import { emit, subscribe, unsubscribe } from '@nextcloud/event-bus'
125125
import { Folder, Node, Permission } from '@nextcloud/files'
126126
import { getCapabilities } from '@nextcloud/capabilities'
127127
import { join, dirname } from 'path'
128-
import { orderBy } from 'natural-orderby'
129128
import { Parser } from 'xml2js'
130129
import { showError } from '@nextcloud/dialogs'
131130
import { translate, translatePlural } from '@nextcloud/l10n'
@@ -152,6 +151,7 @@ import { useSelectionStore } from '../store/selection.ts'
152151
import { useUploaderStore } from '../store/uploader.ts'
153152
import { useUserConfigStore } from '../store/userconfig.ts'
154153
import { useViewConfigStore } from '../store/viewConfig.ts'
154+
import { orderBy } from '../services/SortingService.ts'
155155
import BreadCrumbs from '../components/BreadCrumbs.vue'
156156
import FilesListVirtual from '../components/FilesListVirtual.vue'
157157
import filesListWidthMixin from '../mixins/filesListWidth.ts'

dist/files-main.js

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

dist/files-main.js.map

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package-lock.json

Lines changed: 0 additions & 9 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -84,7 +84,6 @@
8484
"marked": "^11.2.0",
8585
"moment": "^2.30.1",
8686
"moment-timezone": "^0.5.45",
87-
"natural-orderby": "^3.0.2",
8887
"nextcloud-vue-collections": "^0.12.0",
8988
"node-vibrant": "^3.1.6",
9089
"p-limit": "^4.0.0",

0 commit comments

Comments
 (0)