Skip to content
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
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
51 changes: 36 additions & 15 deletions packages/core/src/lib/__tests__/sortByDate.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'])
})
})

Expand Down
18 changes: 6 additions & 12 deletions packages/core/src/lib/sortByDate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
})
}