diff --git a/CHANGELOG.md b/CHANGELOG.md index 64d5ca1..250f8eb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - **Opt-out flags for auto-generated GitHub links** — `github` and `hybrid` projects accept `useGithubLinkFromRepo` and `useLiveLinkFromRepo` (both default `true`) to suppress the repo-URL `github` link and the repo-homepage `live` link. Explicit `links` in config still win. For the pre-existing `linkOrder`-based `live` suppression, the docs now explain how the two mechanisms interact. Fixes [#24]. +### Fixed + +- **`sortByDate` null-date handling** — Projects with no date (`updatedAt` and `createdAt` both null) previously sorted to the top in `desc` order, and flipped position with sort direction. Null dates are now treated as epoch-equivalent (oldest), so dateless projects sort last in newest-first and first in oldest-first, consistently in both directions. Matches the already-documented behavior. Fixes [#23]. + --- ## [1.5.0] - 2026-08-08 @@ -444,4 +448,7 @@ The following are considered implementation details and may change in any versio [#8]: https://github.com/ManningWorks/Projex/issues/8 [#10]: https://github.com/ManningWorks/Projex/issues/10 [#11]: https://github.com/ManningWorks/Projex/issues/11 +[#23]: https://github.com/ManningWorks/Projex/issues/23 +[#24]: https://github.com/ManningWorks/Projex/issues/24 +[Unreleased]: https://github.com/ManningWorks/Projex/compare/v1.5.0...HEAD [1.5.0]: https://github.com/ManningWorks/Projex/compare/v1.4.0...v1.5.0 diff --git a/packages/core/src/lib/__tests__/sortByDate.test.ts b/packages/core/src/lib/__tests__/sortByDate.test.ts index a35d9a3..9c9bcee 100644 --- a/packages/core/src/lib/__tests__/sortByDate.test.ts +++ b/packages/core/src/lib/__tests__/sortByDate.test.ts @@ -91,50 +91,71 @@ describe('sortByDate', () => { }) describe('handling missing dates', () => { - it('should place projects without dates at the beginning in desc order', () => { + it.each<{ order: SortOrder; position: string; expected: string[] }>([ + { order: 'desc', position: 'end', expected: ['3', '1', '2'] }, + { order: 'asc', position: 'beginning', expected: ['2', '1', '3'] }, + ])('should place projects without dates at the $position in $order order', ({ order, expected }) => { const projects = [ createProject({ id: '1', updatedAt: '2024-01-01' }), createProject({ id: '2', updatedAt: null, createdAt: null }), createProject({ id: '3', updatedAt: '2024-06-01' }), ] - const result = sortByDate(projects, 'desc') + const result = sortByDate(projects, order) - expect(result.map(p => p.id)).toEqual(['2', '3', '1']) + expect(result.map(p => p.id)).toEqual(expected) }) - it('should place projects without dates at the end in asc order', () => { + it('should place projects without dates at the end when order is omitted (default desc)', () => { const projects = [ - createProject({ id: '1', updatedAt: '2024-01-01' }), - createProject({ id: '2', updatedAt: null, createdAt: null }), + createProject({ id: '1', updatedAt: null, createdAt: null }), + createProject({ id: '2', updatedAt: '2024-06-01' }), + ] + + const result = sortByDate(projects) + + expect(result.map(p => p.id)).toEqual(['2', '1']) + }) + + it.each<{ order: SortOrder; position: string; expected: string[] }>([ + { order: 'desc', position: 'after', expected: ['3', '2', '1'] }, + { order: 'asc', position: 'before', expected: ['1', '2', '3'] }, + ])('should sort dateless projects $position createdAt-only projects in $order order', ({ order, expected }) => { + const projects = [ + createProject({ id: '1', updatedAt: null, createdAt: null }), + createProject({ id: '2', updatedAt: null, createdAt: '2024-01-01' }), createProject({ id: '3', updatedAt: '2024-06-01' }), ] - const result = sortByDate(projects, 'asc') + const result = sortByDate(projects, order) - expect(result.map(p => p.id)).toEqual(['1', '3', '2']) + expect(result.map(p => p.id)).toEqual(expected) }) - it('should handle all projects without dates', () => { + it.each<{ order: SortOrder; position: string; expected: string[] }>([ + { order: 'desc', position: 'end', expected: ['2', '1'] }, + { order: 'asc', position: 'beginning', expected: ['1', '2'] }, + ])('should place projects without dates at the $position even against pre-1970 dates in $order order', ({ order, expected }) => { const projects = [ createProject({ id: '1', updatedAt: null, createdAt: null }), - createProject({ id: '2', updatedAt: null, createdAt: null }), + createProject({ id: '2', updatedAt: '1950-06-01' }), ] - const result = sortByDate(projects) + const result = sortByDate(projects, order) - expect(result).toHaveLength(2) + expect(result.map(p => p.id)).toEqual(expected) }) - it('should return 0 for comparison when both dates are missing', () => { + it('should handle all projects without dates', () => { const projects = [ createProject({ id: '1', updatedAt: null, createdAt: null }), createProject({ id: '2', updatedAt: null, createdAt: null }), ] - const result = sortByDate(projects, 'desc') + const result = sortByDate(projects) - expect(result).toHaveLength(2) + // Equal (zero) comparisons keep input order via stable sort + expect(result.map(p => p.id)).toEqual(['1', '2']) }) }) diff --git a/packages/core/src/lib/sortByDate.ts b/packages/core/src/lib/sortByDate.ts index 6a40b26..f32a66a 100644 --- a/packages/core/src/lib/sortByDate.ts +++ b/packages/core/src/lib/sortByDate.ts @@ -13,18 +13,12 @@ export function sortByDate(projects: ProjexProject[], order: SortOrder = 'desc') const dateA = a.updatedAt || a.createdAt || null const dateB = b.updatedAt || b.createdAt || null - if (!dateA && !dateB) { - return 0 - } + // Treat missing dates as older than any real date so dateless projects sort + // last in newest-first order and first in oldest-first order, consistently + // in both directions (issue #23). + const timeA = dateA ? new Date(dateA).getTime() : Number.NEGATIVE_INFINITY + const timeB = dateB ? new Date(dateB).getTime() : Number.NEGATIVE_INFINITY - if (!dateA) { - return 1 * multiplier - } - - if (!dateB) { - return -1 * multiplier - } - - return (new Date(dateA).getTime() - new Date(dateB).getTime()) * multiplier + return (timeA - timeB) * multiplier }) }