Skip to content

Commit b832033

Browse files
authored
fix(navigation): respect user defined order (#2974)
1 parent 12aef72 commit b832033

File tree

5 files changed

+96
-30
lines changed

5 files changed

+96
-30
lines changed

src/runtime/internal/navigation.ts

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,13 @@ import { pascalCase } from 'scule'
55
* Create NavItem array to be consumed from runtime plugin.
66
*/
77
export async function generateNavigationTree<T extends PageCollectionItemBase>(queryBuilder: CollectionQueryBuilder<T>, extraFields: Array<keyof T> = []) {
8+
// @ts-expect-error -- internal
9+
const params = queryBuilder.__params
10+
if (!params?.orderBy?.length) {
11+
queryBuilder = queryBuilder.order('stem', 'ASC')
12+
}
13+
814
const collecitonItems = await queryBuilder
9-
.order('stem', 'ASC')
1015
.orWhere(group => group
1116
.where('navigation', '<>', 'false')
1217
.where('navigation', 'IS NULL'),
@@ -161,13 +166,13 @@ export async function generateNavigationTree<T extends PageCollectionItemBase>(q
161166
return sortAndClear(nav)
162167
}
163168

164-
const collator = new Intl.Collator(undefined, { numeric: true, sensitivity: 'base' })
169+
// const collator = new Intl.Collator(undefined, { numeric: true, sensitivity: 'base' })
165170

166171
/**
167172
* Sort items by path and clear empty children keys.
168173
*/
169174
function sortAndClear(nav: ContentNavigationItem[]) {
170-
const sorted = nav.sort((a, b) => collator.compare(a.stem!, b.stem!))
175+
const sorted = nav// .sort((a, b) => collator.compare(a.stem!, b.stem!))
171176

172177
for (const item of sorted) {
173178
if (item.children?.length) {

src/runtime/internal/query.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,8 @@ export const collectionQueryBuilder = <T extends keyof Collections>(collection:
8484
}
8585

8686
const query: CollectionQueryBuilder<Collections[T]> = {
87+
// @ts-expect-error -- internal
88+
__params: params,
8789
andWhere(groupFactory: QueryGroupFunction<Collections[T]>) {
8890
const group = groupFactory(collectionQueryGroup(collection))
8991
params.conditions.push(buildGroup(group, 'AND'))

src/runtime/nitro.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,11 +17,11 @@ export const queryCollectionWithEvent = <T extends keyof Collections>(event: H3E
1717
return collectionQueryBuilder<T>(collection, (collection, sql) => fetchQuery(event, collection, sql))
1818
}
1919

20-
export async function queryCollectionNavigationWithEvent<T extends keyof PageCollections>(event: H3Event, collection: T, fields?: Array<keyof PageCollections[T]>) {
20+
export function queryCollectionNavigationWithEvent<T extends keyof PageCollections>(event: H3Event, collection: T, fields?: Array<keyof PageCollections[T]>) {
2121
return chainablePromise(event, collection, qb => generateNavigationTree(qb, fields))
2222
}
2323

24-
export async function queryCollectionItemSurroundingsWithEvent<T extends keyof PageCollections>(event: H3Event, collection: T, path: string, opts?: SurroundOptions<keyof PageCollections[T]>) {
24+
export function queryCollectionItemSurroundingsWithEvent<T extends keyof PageCollections>(event: H3Event, collection: T, path: string, opts?: SurroundOptions<keyof PageCollections[T]>) {
2525
return chainablePromise(event, collection, qb => generateItemSurround(qb, path, opts))
2626
}
2727

test/unit/generateItemSurround.test.ts

Lines changed: 53 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,26 @@ describe('generateItemSurround', () => {
88
where: () => mockQueryBuilder,
99
orWhere: () => mockQueryBuilder,
1010
order: () => mockQueryBuilder,
11-
only: () => mockQueryBuilder,
11+
only: (_field: string, _direction: 'ASC' | 'DESC') => mockQueryBuilder,
1212
select: () => mockQueryBuilder,
1313
all: async () => mockData,
1414
}
15+
const createMockQueryBuilder = (mockData: Array<Record<string, unknown>>) => ({
16+
find: async () => mockData,
17+
where: () => createMockQueryBuilder(mockData),
18+
orWhere: () => createMockQueryBuilder(mockData),
19+
order: (field: string, direction: 'ASC' | 'DESC') => {
20+
return createMockQueryBuilder(mockData.sort((a, b) => {
21+
if (direction === 'ASC') {
22+
return (a[field] as string) < (b[field] as string) ? -1 : 1
23+
}
24+
return (a[field] as string) > (b[field] as string) ? -1 : 1
25+
}))
26+
},
27+
only: () => createMockQueryBuilder(mockData),
28+
select: () => createMockQueryBuilder(mockData),
29+
all: async () => mockData,
30+
})
1531

1632
const mockData: ContentNavigationItem[] = [
1733
{
@@ -270,4 +286,40 @@ describe('generateItemSurround', () => {
270286
expect(result[0]).toMatchObject({ path: '/section' })
271287
expect(result[1]).toMatchObject({ path: '/section/item-4' })
272288
})
289+
290+
it('Respect user order', async () => {
291+
const mockQueryBuilder = createMockQueryBuilder([
292+
// 1.first-article with a date field set to 2024-01-01.
293+
// 2.second-article with a date field set to 2025-01-01.
294+
{
295+
path: '/first-article',
296+
id: '1',
297+
stem: '1-first-article',
298+
date: new Date('2024-01-01'),
299+
},
300+
{
301+
path: '/second-article',
302+
id: '2',
303+
stem: '2-second-article',
304+
date: new Date('2025-01-01'),
305+
},
306+
{
307+
path: '/third-article',
308+
id: '3',
309+
stem: '3-third-article',
310+
date: new Date('2026-01-01'),
311+
},
312+
])
313+
const queryBuilder = mockQueryBuilder.order('date', 'DESC')
314+
// @ts-expect-error -- internal
315+
queryBuilder.__params = {
316+
orderBy: ['"date" DESC'],
317+
}
318+
const result = await generateItemSurround(queryBuilder, '/second-article')
319+
// console.log(result, result[0], !!result[1])
320+
321+
expect(result).toHaveLength(2)
322+
expect(result[0]).toMatchObject({ path: '/third-article' })
323+
expect(result[1]).toMatchObject({ path: '/first-article' })
324+
})
273325
})

test/unit/generateNavigationTree.test.ts

Lines changed: 31 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,21 @@
11
import { describe, it, expect } from 'vitest'
2-
import type { PageCollectionItemBase } from '@nuxt/content'
2+
import type { CollectionQueryBuilder, PageCollectionItemBase } from '@nuxt/content'
33
import { generateNavigationTree } from '../../src/runtime/internal/navigation'
44

55
describe('generateNavigationTree', () => {
66
const mockQueryBuilder = (items: PageCollectionItemBase[]) => ({
7-
order: () => mockQueryBuilder(items),
7+
order: (field: keyof PageCollectionItemBase, direction: 'ASC' | 'DESC') => {
8+
return mockQueryBuilder(items.sort((a, b) => {
9+
if (direction === 'ASC') {
10+
return (a[field] as string) < (b[field] as string) ? -1 : 1
11+
}
12+
return (a[field] as string) > (b[field] as string) ? -1 : 1
13+
}))
14+
},
815
orWhere: () => mockQueryBuilder(items),
916
select: () => mockQueryBuilder(items),
1017
all: async () => items,
11-
})
18+
} as unknown as CollectionQueryBuilder<PageCollectionItemBase>)
1219

1320
it('should generate a basic navigation tree', async () => {
1421
const items = [
@@ -95,18 +102,18 @@ describe('generateNavigationTree', () => {
95102
{
96103
title: 'Guide',
97104
path: '/guide',
98-
stem: 'guide/index',
105+
stem: 'guide',
99106
children: [
100-
{
101-
title: 'Getting Started',
102-
path: '/guide/getting-started',
103-
stem: 'guide/getting-started',
104-
},
105107
{
106108
title: 'Guide',
107109
path: '/guide',
108110
stem: 'guide/index',
109111
},
112+
{
113+
title: 'Getting Started',
114+
path: '/guide/getting-started',
115+
stem: 'guide/getting-started',
116+
},
110117
],
111118
},
112119
{
@@ -265,16 +272,16 @@ describe('generateNavigationTree', () => {
265272
path: '/devenir-benevole',
266273
stem: 'devenir-benevole',
267274
children: [
268-
{
269-
title: 'bourg-en-bresse',
270-
path: '/devenir-benevole/bourg-en-bresse',
271-
stem: 'devenir-benevole/bourg-en-bresse',
272-
},
273275
{
274276
title: 'index',
275277
path: '/devenir-benevole',
276278
stem: 'devenir-benevole/index',
277279
},
280+
{
281+
title: 'bourg-en-bresse',
282+
path: '/devenir-benevole/bourg-en-bresse',
283+
stem: 'devenir-benevole/bourg-en-bresse',
284+
},
278285
],
279286
},
280287
])
@@ -312,16 +319,16 @@ describe('generateNavigationTree', () => {
312319
path: '/devenir-benevole/france/ain',
313320
stem: 'devenir-benevole/france/ain/index',
314321
children: [
315-
{
316-
title: 'bourg-en-bresse',
317-
path: '/devenir-benevole/france/ain/bourg-en-bresse',
318-
stem: 'devenir-benevole/france/ain/bourg-en-bresse',
319-
},
320322
{
321323
title: 'index',
322324
path: '/devenir-benevole/france/ain',
323325
stem: 'devenir-benevole/france/ain/index',
324326
},
327+
{
328+
title: 'bourg-en-bresse',
329+
path: '/devenir-benevole/france/ain/bourg-en-bresse',
330+
stem: 'devenir-benevole/france/ain/bourg-en-bresse',
331+
},
325332
],
326333
},
327334
],
@@ -359,16 +366,16 @@ describe('generateNavigationTree', () => {
359366
path: '/devenir-benevole',
360367
stem: 'devenir-benevole',
361368
children: [
362-
{
363-
title: 'bourg-en-bresse',
364-
path: '/devenir-benevole/bourg-en-bresse',
365-
stem: 'devenir-benevole/bourg-en-bresse',
366-
},
367369
{
368370
title: 'index',
369371
path: '/devenir-benevole',
370372
stem: 'devenir-benevole/index',
371373
},
374+
{
375+
title: 'bourg-en-bresse',
376+
path: '/devenir-benevole/bourg-en-bresse',
377+
stem: 'devenir-benevole/bourg-en-bresse',
378+
},
372379
],
373380
},
374381
])

0 commit comments

Comments
 (0)