Browse Source
fix(files): Implement `SortingService` to fix sorting of files
fix(files): Implement `SortingService` to fix sorting of files
The previously used library was parsing strings to try to detect dates, but for filenames it makes no sense to parse them as dates. Signed-off-by: Ferdinand Thiessen <opensource@fthiessen.de>pull/45419/head
No known key found for this signature in database
GPG Key ID: 45FAE7268762B400
5 changed files with 160 additions and 11 deletions
-
100apps/files/src/services/SortingService.spec.ts
-
59apps/files/src/services/SortingService.ts
-
2apps/files/src/views/FilesList.vue
-
9package-lock.json
-
1package.json
@ -0,0 +1,100 @@ |
|||
/** |
|||
* 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']) |
|||
}) |
|||
}) |
|||
@ -0,0 +1,59 @@ |
|||
/** |
|||
* 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 |
|||
}) |
|||
} |
|||
Write
Preview
Loading…
Cancel
Save
Reference in new issue