Skip to content

fix(navigation): respect user defined order #2974

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Jan 15, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 8 additions & 3 deletions src/runtime/internal/navigation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,13 @@ import { pascalCase } from 'scule'
* Create NavItem array to be consumed from runtime plugin.
*/
export async function generateNavigationTree<T extends PageCollectionItemBase>(queryBuilder: CollectionQueryBuilder<T>, extraFields: Array<keyof T> = []) {
// @ts-expect-error -- internal
const params = queryBuilder.__params
if (!params?.orderBy?.length) {
queryBuilder = queryBuilder.order('stem', 'ASC')
}

const collecitonItems = await queryBuilder
.order('stem', 'ASC')
.orWhere(group => group
.where('navigation', '<>', 'false')
.where('navigation', 'IS NULL'),
Expand Down Expand Up @@ -161,13 +166,13 @@ export async function generateNavigationTree<T extends PageCollectionItemBase>(q
return sortAndClear(nav)
}

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

/**
* Sort items by path and clear empty children keys.
*/
function sortAndClear(nav: ContentNavigationItem[]) {
const sorted = nav.sort((a, b) => collator.compare(a.stem!, b.stem!))
const sorted = nav// .sort((a, b) => collator.compare(a.stem!, b.stem!))

for (const item of sorted) {
if (item.children?.length) {
Expand Down
2 changes: 2 additions & 0 deletions src/runtime/internal/query.ts
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,8 @@ export const collectionQueryBuilder = <T extends keyof Collections>(collection:
}

const query: CollectionQueryBuilder<Collections[T]> = {
// @ts-expect-error -- internal
__params: params,
andWhere(groupFactory: QueryGroupFunction<Collections[T]>) {
const group = groupFactory(collectionQueryGroup(collection))
params.conditions.push(buildGroup(group, 'AND'))
Expand Down
4 changes: 2 additions & 2 deletions src/runtime/nitro.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,11 +17,11 @@ export const queryCollectionWithEvent = <T extends keyof Collections>(event: H3E
return collectionQueryBuilder<T>(collection, (collection, sql) => fetchQuery(event, collection, sql))
}

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

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

Expand Down
54 changes: 53 additions & 1 deletion test/unit/generateItemSurround.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,26 @@ describe('generateItemSurround', () => {
where: () => mockQueryBuilder,
orWhere: () => mockQueryBuilder,
order: () => mockQueryBuilder,
only: () => mockQueryBuilder,
only: (_field: string, _direction: 'ASC' | 'DESC') => mockQueryBuilder,
select: () => mockQueryBuilder,
all: async () => mockData,
}
const createMockQueryBuilder = (mockData: Array<Record<string, unknown>>) => ({
find: async () => mockData,
where: () => createMockQueryBuilder(mockData),
orWhere: () => createMockQueryBuilder(mockData),
order: (field: string, direction: 'ASC' | 'DESC') => {
return createMockQueryBuilder(mockData.sort((a, b) => {
if (direction === 'ASC') {
return (a[field] as string) < (b[field] as string) ? -1 : 1
}
return (a[field] as string) > (b[field] as string) ? -1 : 1
}))
},
only: () => createMockQueryBuilder(mockData),
select: () => createMockQueryBuilder(mockData),
all: async () => mockData,
})

const mockData: ContentNavigationItem[] = [
{
Expand Down Expand Up @@ -270,4 +286,40 @@ describe('generateItemSurround', () => {
expect(result[0]).toMatchObject({ path: '/section' })
expect(result[1]).toMatchObject({ path: '/section/item-4' })
})

it('Respect user order', async () => {
const mockQueryBuilder = createMockQueryBuilder([
// 1.first-article with a date field set to 2024-01-01.
// 2.second-article with a date field set to 2025-01-01.
{
path: '/first-article',
id: '1',
stem: '1-first-article',
date: new Date('2024-01-01'),
},
{
path: '/second-article',
id: '2',
stem: '2-second-article',
date: new Date('2025-01-01'),
},
{
path: '/third-article',
id: '3',
stem: '3-third-article',
date: new Date('2026-01-01'),
},
])
const queryBuilder = mockQueryBuilder.order('date', 'DESC')
// @ts-expect-error -- internal
queryBuilder.__params = {
orderBy: ['"date" DESC'],
}
const result = await generateItemSurround(queryBuilder, '/second-article')
// console.log(result, result[0], !!result[1])

expect(result).toHaveLength(2)
expect(result[0]).toMatchObject({ path: '/third-article' })
expect(result[1]).toMatchObject({ path: '/first-article' })
})
})
55 changes: 31 additions & 24 deletions test/unit/generateNavigationTree.test.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,21 @@
import { describe, it, expect } from 'vitest'
import type { PageCollectionItemBase } from '@nuxt/content'
import type { CollectionQueryBuilder, PageCollectionItemBase } from '@nuxt/content'
import { generateNavigationTree } from '../../src/runtime/internal/navigation'

describe('generateNavigationTree', () => {
const mockQueryBuilder = (items: PageCollectionItemBase[]) => ({
order: () => mockQueryBuilder(items),
order: (field: keyof PageCollectionItemBase, direction: 'ASC' | 'DESC') => {
return mockQueryBuilder(items.sort((a, b) => {
if (direction === 'ASC') {
return (a[field] as string) < (b[field] as string) ? -1 : 1
}
return (a[field] as string) > (b[field] as string) ? -1 : 1
}))
},
orWhere: () => mockQueryBuilder(items),
select: () => mockQueryBuilder(items),
all: async () => items,
})
} as unknown as CollectionQueryBuilder<PageCollectionItemBase>)

it('should generate a basic navigation tree', async () => {
const items = [
Expand Down Expand Up @@ -95,18 +102,18 @@ describe('generateNavigationTree', () => {
{
title: 'Guide',
path: '/guide',
stem: 'guide/index',
stem: 'guide',
children: [
{
title: 'Getting Started',
path: '/guide/getting-started',
stem: 'guide/getting-started',
},
{
title: 'Guide',
path: '/guide',
stem: 'guide/index',
},
{
title: 'Getting Started',
path: '/guide/getting-started',
stem: 'guide/getting-started',
},
],
},
{
Expand Down Expand Up @@ -265,16 +272,16 @@ describe('generateNavigationTree', () => {
path: '/devenir-benevole',
stem: 'devenir-benevole',
children: [
{
title: 'bourg-en-bresse',
path: '/devenir-benevole/bourg-en-bresse',
stem: 'devenir-benevole/bourg-en-bresse',
},
{
title: 'index',
path: '/devenir-benevole',
stem: 'devenir-benevole/index',
},
{
title: 'bourg-en-bresse',
path: '/devenir-benevole/bourg-en-bresse',
stem: 'devenir-benevole/bourg-en-bresse',
},
],
},
])
Expand Down Expand Up @@ -312,16 +319,16 @@ describe('generateNavigationTree', () => {
path: '/devenir-benevole/france/ain',
stem: 'devenir-benevole/france/ain/index',
children: [
{
title: 'bourg-en-bresse',
path: '/devenir-benevole/france/ain/bourg-en-bresse',
stem: 'devenir-benevole/france/ain/bourg-en-bresse',
},
{
title: 'index',
path: '/devenir-benevole/france/ain',
stem: 'devenir-benevole/france/ain/index',
},
{
title: 'bourg-en-bresse',
path: '/devenir-benevole/france/ain/bourg-en-bresse',
stem: 'devenir-benevole/france/ain/bourg-en-bresse',
},
],
},
],
Expand Down Expand Up @@ -359,16 +366,16 @@ describe('generateNavigationTree', () => {
path: '/devenir-benevole',
stem: 'devenir-benevole',
children: [
{
title: 'bourg-en-bresse',
path: '/devenir-benevole/bourg-en-bresse',
stem: 'devenir-benevole/bourg-en-bresse',
},
{
title: 'index',
path: '/devenir-benevole',
stem: 'devenir-benevole/index',
},
{
title: 'bourg-en-bresse',
path: '/devenir-benevole/bourg-en-bresse',
stem: 'devenir-benevole/bourg-en-bresse',
},
],
},
])
Expand Down
Loading