Skip to content

Commit b02e228

Browse files
authored
Respect number order when sorting assets by title (#12030)
- Respect number order when sorting assets by title - `New Project 1` < `New Project 2` < `New Project 10` rather than lexicographic sort (`1` < `10` < `2`) - Use user's locale when sorting names # Important Notes None
1 parent 4ace1b2 commit b02e228

File tree

6 files changed

+166
-27
lines changed

6 files changed

+166
-27
lines changed

app/gui/integration-test/dashboard/sort.spec.ts

Lines changed: 39 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -45,11 +45,17 @@ test('sort', ({ page }) =>
4545
const date2 = toRfc3339(new Date(START_DATE_EPOCH_MS + 1 * MIN_MS))
4646
const date3 = toRfc3339(new Date(START_DATE_EPOCH_MS + 2 * MIN_MS))
4747
const date4 = toRfc3339(new Date(START_DATE_EPOCH_MS + 3 * MIN_MS))
48+
const date4a = toRfc3339(new Date(START_DATE_EPOCH_MS + 3 * MIN_MS + 1))
49+
const date4b = toRfc3339(new Date(START_DATE_EPOCH_MS + 3 * MIN_MS + 2))
4850
const date5 = toRfc3339(new Date(START_DATE_EPOCH_MS + 4 * MIN_MS))
51+
const date5a = toRfc3339(new Date(START_DATE_EPOCH_MS + 4 * MIN_MS + 1))
4952
const date6 = toRfc3339(new Date(START_DATE_EPOCH_MS + 5 * MIN_MS))
5053
const date7 = toRfc3339(new Date(START_DATE_EPOCH_MS + 6 * MIN_MS))
5154
const date8 = toRfc3339(new Date(START_DATE_EPOCH_MS + 7 * MIN_MS))
52-
api.addDirectory({ modifiedAt: date4, title: 'a directory' })
55+
api.addDirectory({ modifiedAt: date4, title: 'a directory 1' })
56+
api.addDirectory({ modifiedAt: date4a, title: 'a directory 10' })
57+
api.addDirectory({ modifiedAt: date4b, title: 'a directory 2' })
58+
api.addDirectory({ modifiedAt: date5a, title: 'a directory 11' })
5359
api.addDirectory({ modifiedAt: date6, title: 'G directory' })
5460
api.addProject({ modifiedAt: date7, title: 'C project' })
5561
api.addSecret({ modifiedAt: date2, title: 'H secret' })
@@ -61,8 +67,11 @@ test('sort', ({ page }) =>
6167
// b project
6268
// h secret
6369
// f secret
64-
// a directory
70+
// a directory 1
71+
// a directory 10
72+
// a directory 2
6573
// e file
74+
// a directory 11
6675
// g directory
6776
// c project
6877
// d file
@@ -81,7 +90,10 @@ test('sort', ({ page }) =>
8190
// Assets in each group are ordered by insertion order.
8291
await expect(rows).toHaveText([
8392
/^G directory/,
84-
/^a directory/,
93+
/^a directory 11/,
94+
/^a directory 2/,
95+
/^a directory 10/,
96+
/^a directory 1/,
8597
/^C project/,
8698
/^b project/,
8799
/^d file/,
@@ -97,7 +109,10 @@ test('sort', ({ page }) =>
97109
})
98110
.driveTable.withRows(async (rows) => {
99111
await expect(rows).toHaveText([
100-
/^a directory/,
112+
/^a directory 1/,
113+
/^a directory 2/,
114+
/^a directory 10/,
115+
/^a directory 11/,
101116
/^b project/,
102117
/^C project/,
103118
/^d file/,
@@ -121,7 +136,10 @@ test('sort', ({ page }) =>
121136
/^d file/,
122137
/^C project/,
123138
/^b project/,
124-
/^a directory/,
139+
/^a directory 11/,
140+
/^a directory 10/,
141+
/^a directory 2/,
142+
/^a directory 1/,
125143
])
126144
})
127145
// Sorting should be unset.
@@ -136,7 +154,10 @@ test('sort', ({ page }) =>
136154
.driveTable.withRows(async (rows) => {
137155
await expect(rows).toHaveText([
138156
/^G directory/,
139-
/^a directory/,
157+
/^a directory 11/,
158+
/^a directory 2/,
159+
/^a directory 10/,
160+
/^a directory 1/,
140161
/^C project/,
141162
/^b project/,
142163
/^d file/,
@@ -155,8 +176,11 @@ test('sort', ({ page }) =>
155176
/^b project/,
156177
/^H secret/,
157178
/^f secret/,
158-
/^a directory/,
179+
/^a directory 1/,
180+
/^a directory 10/,
181+
/^a directory 2/,
159182
/^e file/,
183+
/^a directory 11/,
160184
/^G directory/,
161185
/^C project/,
162186
/^d file/,
@@ -172,8 +196,11 @@ test('sort', ({ page }) =>
172196
/^d file/,
173197
/^C project/,
174198
/^G directory/,
199+
/^a directory 11/,
175200
/^e file/,
176-
/^a directory/,
201+
/^a directory 2/,
202+
/^a directory 10/,
203+
/^a directory 1/,
177204
/^f secret/,
178205
/^H secret/,
179206
/^b project/,
@@ -191,7 +218,10 @@ test('sort', ({ page }) =>
191218
.driveTable.withRows(async (rows) => {
192219
await expect(rows).toHaveText([
193220
/^G directory/,
194-
/^a directory/,
221+
/^a directory 11/,
222+
/^a directory 2/,
223+
/^a directory 10/,
224+
/^a directory 1/,
195225
/^C project/,
196226
/^b project/,
197227
/^d file/,
Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
/** @file Tests for comparing assets. */
2+
import { Column, type SortableColumn } from '#/components/dashboard/column/columnUtils'
3+
import { assetCompareFunction } from '#/layouts/Drive/compareAssets'
4+
import { SortDirection, type SortInfo } from '#/utilities/sorting'
5+
import * as fc from '@fast-check/vitest'
6+
import { DirectoryId, createPlaceholderFileAsset } from 'enso-common/src/services/Backend'
7+
import { toRfc3339 } from 'enso-common/src/utilities/data/dateTime'
8+
import { merge } from 'enso-common/src/utilities/data/object'
9+
import { expect } from 'vitest'
10+
11+
const SORT_BY_NAME_ASCENDING: SortInfo<SortableColumn> = {
12+
field: Column.name,
13+
direction: SortDirection.ascending,
14+
}
15+
16+
const SORT_BY_NAME_DESCENDING: SortInfo<SortableColumn> = {
17+
field: Column.name,
18+
direction: SortDirection.descending,
19+
}
20+
21+
const SORT_BY_MODIFIED_ASCENDING: SortInfo<SortableColumn> = {
22+
field: Column.modified,
23+
direction: SortDirection.ascending,
24+
}
25+
26+
const SORT_BY_MODIFIED_DESCENDING: SortInfo<SortableColumn> = {
27+
field: Column.modified,
28+
direction: SortDirection.descending,
29+
}
30+
31+
fc.test.prop({
32+
prefix: fc.fc.string(),
33+
numbers: fc.fc.array(fc.fc.integer({ min: 0 })),
34+
})('numbers should be sorted with dictionary sort', ({ prefix, numbers }) => {
35+
const names = numbers.map((number) => `${prefix} ${number}`)
36+
const assets = names.map((name) =>
37+
createPlaceholderFileAsset(name, DirectoryId('directory-'), []),
38+
)
39+
const compareByNameAscending = assetCompareFunction(SORT_BY_NAME_ASCENDING, 'en')
40+
const sorted = assets.sort(compareByNameAscending)
41+
const sortedNames = sorted.map((asset) => asset.title)
42+
const expectedNames = numbers.sort((a, b) => a - b).map((number) => `${prefix} ${number}`)
43+
expect(sortedNames).toStrictEqual(expectedNames)
44+
45+
const compareByNameDescending = assetCompareFunction(SORT_BY_NAME_DESCENDING, 'en')
46+
const sortedDescending = assets.sort(compareByNameDescending)
47+
const sortedDescendingNames = sortedDescending.map((asset) => asset.title)
48+
const expectedDescendingNames = numbers
49+
.sort((a, b) => b - a)
50+
.map((number) => `${prefix} ${number}`)
51+
expect(sortedDescendingNames).toStrictEqual(expectedDescendingNames)
52+
})
53+
54+
fc.test.prop({
55+
names: fc.fc.array(fc.fc.string().map((s) => s.replace(/\d+/g, ''))),
56+
})('sort by name', ({ names }) => {
57+
const assets = names.map((name) =>
58+
createPlaceholderFileAsset(name, DirectoryId('directory-'), []),
59+
)
60+
61+
const compareByModifiedAscending = assetCompareFunction(SORT_BY_NAME_ASCENDING, 'en')
62+
const sorted = assets.sort(compareByModifiedAscending)
63+
const sortedNames = sorted.map((asset) => asset.title)
64+
const expectedNames = names.sort((a, b) => a.localeCompare(b, 'en'))
65+
expect(sortedNames).toStrictEqual(expectedNames)
66+
67+
const compareByModifiedDescending = assetCompareFunction(SORT_BY_NAME_DESCENDING, 'en')
68+
const sortedDescending = assets.sort(compareByModifiedDescending)
69+
const sortedDescendingNames = sortedDescending.map((asset) => asset.title)
70+
const expectedDescendingNames = names.sort((a, b) => -a.localeCompare(b, 'en'))
71+
expect(sortedDescendingNames).toStrictEqual(expectedDescendingNames)
72+
})
73+
74+
fc.test.prop({
75+
dates: fc.fc.array(fc.fc.integer({ min: 0 })).map((numbers) => numbers.map((n) => new Date(n))),
76+
})('sort by modified', ({ dates }) => {
77+
const assets = dates.map((date) =>
78+
merge(createPlaceholderFileAsset('', DirectoryId('directory-'), []), {
79+
modifiedAt: toRfc3339(date),
80+
}),
81+
)
82+
83+
const compareByModifiedAscending = assetCompareFunction(SORT_BY_MODIFIED_ASCENDING, 'en')
84+
const sorted = assets.sort(compareByModifiedAscending)
85+
const sortedDates = sorted.map((asset) => new Date(asset.modifiedAt))
86+
const expectedDates = dates.sort((a, b) => Number(a) - Number(b))
87+
expect(sortedDates).toStrictEqual(expectedDates)
88+
89+
const compareByModifiedDescending = assetCompareFunction(SORT_BY_MODIFIED_DESCENDING, 'en')
90+
const sortedDescending = assets.sort(compareByModifiedDescending)
91+
const sortedDescendingDates = sortedDescending.map((asset) => new Date(asset.modifiedAt))
92+
const expectedDescendingDates = dates.sort((a, b) => Number(b) - Number(a))
93+
expect(sortedDescendingDates).toStrictEqual(expectedDescendingDates)
94+
})

app/gui/src/dashboard/layouts/Drive/assetTreeHooks.tsx renamed to app/gui/src/dashboard/layouts/Drive/assetTreeHooks.ts

File renamed without changes.

app/gui/src/dashboard/layouts/Drive/assetsTableItemsHooks.tsx renamed to app/gui/src/dashboard/layouts/Drive/assetsTableItemsHooks.ts

Lines changed: 5 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -4,14 +4,14 @@ import { AssetType, getAssetPermissionName } from 'enso-common/src/services/Back
44
import { PermissionAction } from 'enso-common/src/utilities/permissions'
55

66
import type { SortableColumn } from '#/components/dashboard/column/columnUtils'
7-
import { Column } from '#/components/dashboard/column/columnUtils'
7+
import { assetCompareFunction } from '#/layouts/Drive/compareAssets'
8+
import { useText } from '#/providers/TextProvider'
89
import type { DirectoryId } from '#/services/ProjectManager'
910
import type AssetQuery from '#/utilities/AssetQuery'
1011
import type { AnyAssetTreeNode } from '#/utilities/AssetTreeNode'
1112
import Visibility from '#/utilities/Visibility'
1213
import { fileExtension } from '#/utilities/fileInfo'
1314
import type { SortInfo } from '#/utilities/sorting'
14-
import { SortDirection } from '#/utilities/sorting'
1515
import { regexEscape } from '#/utilities/string'
1616
import { createStore, useStore } from '#/utilities/zustand.ts'
1717
import invariant from 'tiny-invariant'
@@ -61,6 +61,7 @@ export function useAssetStrict(id: AssetId) {
6161
export function useAssetsTableItems(options: UseAssetsTableOptions) {
6262
const { assetTree, sortInfo, query, expandedDirectoryIds } = options
6363

64+
const { locale } = useText()
6465
const setAssetItems = useStore(ASSET_ITEMS_STORE, (store) => store.setItems)
6566

6667
const filter = (() => {
@@ -212,22 +213,8 @@ export function useAssetsTableItems(options: UseAssetsTableOptions) {
212213

213214
return flatTree
214215
} else {
215-
const multiplier = sortInfo.direction === SortDirection.ascending ? 1 : -1
216-
let compare: (a: AnyAssetTreeNode, b: AnyAssetTreeNode) => number
217-
switch (sortInfo.field) {
218-
case Column.name: {
219-
compare = (a, b) => multiplier * a.item.title.localeCompare(b.item.title, 'en')
220-
break
221-
}
222-
case Column.modified: {
223-
compare = (a, b) => {
224-
const aOrder = Number(new Date(a.item.modifiedAt))
225-
const bOrder = Number(new Date(b.item.modifiedAt))
226-
return multiplier * (aOrder - bOrder)
227-
}
228-
break
229-
}
230-
}
216+
const compareAssets = assetCompareFunction(sortInfo, locale)
217+
const compare = (a: AnyAssetTreeNode, b: AnyAssetTreeNode) => compareAssets(a.item, b.item)
231218
const flatTree = assetTree.preorderTraversal((tree) =>
232219
[...tree]
233220
.filter((child) => expandedDirectoryIds.includes(child.item.parentId))
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
/** @file Functions related to comparing assets. */
2+
import { Column, type SortableColumn } from '#/components/dashboard/column/columnUtils'
3+
import { SortDirection, type SortInfo } from '#/utilities/sorting'
4+
import type { AnyAsset } from 'enso-common/src/services/Backend'
5+
6+
/** Return a function to compare two assets. */
7+
export function assetCompareFunction(
8+
sortInfo: SortInfo<SortableColumn>,
9+
locale: string | undefined,
10+
) {
11+
const multiplier = sortInfo.direction === SortDirection.ascending ? 1 : -1
12+
let compare: (a: AnyAsset, b: AnyAsset) => number
13+
switch (sortInfo.field) {
14+
case Column.name: {
15+
compare = (a, b) => multiplier * a.title.localeCompare(b.title, locale, { numeric: true })
16+
break
17+
}
18+
case Column.modified: {
19+
compare = (a, b) => {
20+
const aOrder = Number(new Date(a.modifiedAt))
21+
const bOrder = Number(new Date(b.modifiedAt))
22+
return multiplier * (aOrder - bOrder)
23+
}
24+
break
25+
}
26+
}
27+
return compare
28+
}

app/gui/src/dashboard/layouts/Drive/directoryIdsHooks.tsx renamed to app/gui/src/dashboard/layouts/Drive/directoryIdsHooks.ts

File renamed without changes.

0 commit comments

Comments
 (0)