diff --git a/src/runtime/internal/navigation.ts b/src/runtime/internal/navigation.ts index 2d540c137..c4c0ad1be 100644 --- a/src/runtime/internal/navigation.ts +++ b/src/runtime/internal/navigation.ts @@ -5,8 +5,13 @@ import { pascalCase } from 'scule' * Create NavItem array to be consumed from runtime plugin. */ export async function generateNavigationTree(queryBuilder: CollectionQueryBuilder, extraFields: Array = []) { + // @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'), @@ -161,13 +166,13 @@ export async function generateNavigationTree(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) { diff --git a/src/runtime/internal/query.ts b/src/runtime/internal/query.ts index 05e503bc1..e416cbf55 100644 --- a/src/runtime/internal/query.ts +++ b/src/runtime/internal/query.ts @@ -84,6 +84,8 @@ export const collectionQueryBuilder = (collection: } const query: CollectionQueryBuilder = { + // @ts-expect-error -- internal + __params: params, andWhere(groupFactory: QueryGroupFunction) { const group = groupFactory(collectionQueryGroup(collection)) params.conditions.push(buildGroup(group, 'AND')) diff --git a/src/runtime/nitro.ts b/src/runtime/nitro.ts index ebaa7a294..4b73c7527 100644 --- a/src/runtime/nitro.ts +++ b/src/runtime/nitro.ts @@ -17,11 +17,11 @@ export const queryCollectionWithEvent = (event: H3E return collectionQueryBuilder(collection, (collection, sql) => fetchQuery(event, collection, sql)) } -export async function queryCollectionNavigationWithEvent(event: H3Event, collection: T, fields?: Array) { +export function queryCollectionNavigationWithEvent(event: H3Event, collection: T, fields?: Array) { return chainablePromise(event, collection, qb => generateNavigationTree(qb, fields)) } -export async function queryCollectionItemSurroundingsWithEvent(event: H3Event, collection: T, path: string, opts?: SurroundOptions) { +export function queryCollectionItemSurroundingsWithEvent(event: H3Event, collection: T, path: string, opts?: SurroundOptions) { return chainablePromise(event, collection, qb => generateItemSurround(qb, path, opts)) } diff --git a/test/unit/generateItemSurround.test.ts b/test/unit/generateItemSurround.test.ts index 07a245d57..c0da0d1ea 100644 --- a/test/unit/generateItemSurround.test.ts +++ b/test/unit/generateItemSurround.test.ts @@ -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>) => ({ + 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[] = [ { @@ -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' }) + }) }) diff --git a/test/unit/generateNavigationTree.test.ts b/test/unit/generateNavigationTree.test.ts index e2571f55e..02b7ccc47 100644 --- a/test/unit/generateNavigationTree.test.ts +++ b/test/unit/generateNavigationTree.test.ts @@ -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) it('should generate a basic navigation tree', async () => { const items = [ @@ -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', + }, ], }, { @@ -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', + }, ], }, ]) @@ -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', + }, ], }, ], @@ -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', + }, ], }, ])