diff --git a/src/ui/services/repo.ts b/src/ui/services/repo.ts index 710942ada..7e72c30a2 100644 --- a/src/ui/services/repo.ts +++ b/src/ui/services/repo.ts @@ -79,6 +79,29 @@ const compareLatestPush = (a: RepoView, b: RepoView, direction: 'asc' | 'desc'): return compareRepoName(a, b, 'asc'); }; +const createdAtMs = (repo: RepoView): number | undefined => { + if (!repo.dateCreated) return undefined; + const ms = Date.parse(repo.dateCreated); + return Number.isNaN(ms) ? undefined : ms; +}; + +const compareCreated = (a: RepoView, b: RepoView, direction: 'asc' | 'desc'): number => { + if (direction === 'desc') { + const aMs = createdAtMs(a) ?? Number.NEGATIVE_INFINITY; + const bMs = createdAtMs(b) ?? Number.NEGATIVE_INFINITY; + if (bMs !== aMs) { + return bMs - aMs; + } + } else { + const aMs = createdAtMs(a) ?? Number.POSITIVE_INFINITY; + const bMs = createdAtMs(b) ?? Number.POSITIVE_INFINITY; + if (aMs !== bMs) { + return aMs - bMs; + } + } + return compareRepoName(a, b, 'asc'); +}; + export const sortRepoViews = ( repos: RepoView[], sort: RepoSortField, @@ -101,6 +124,12 @@ export const sortRepoViews = ( case 'lastPushed-asc': next.sort((a, b) => compareLatestPush(a, b, 'asc')); break; + case 'created-desc': + next.sort((a, b) => compareCreated(a, b, 'desc')); + break; + case 'created-asc': + next.sort((a, b) => compareCreated(a, b, 'asc')); + break; case 'name-desc': next.sort((a, b) => compareRepoName(a, b, 'desc')); break; diff --git a/src/ui/views/RepoList/Components/RepositoriesSortMenu.tsx b/src/ui/views/RepoList/Components/RepositoriesSortMenu.tsx index cca303787..52c52b305 100644 --- a/src/ui/views/RepoList/Components/RepositoriesSortMenu.tsx +++ b/src/ui/views/RepoList/Components/RepositoriesSortMenu.tsx @@ -17,6 +17,7 @@ import React from 'react'; import { ActionMenu, ActionList } from '@primer/react'; import { + CalendarIcon, ClockIcon, PersonIcon, PulseIcon, @@ -123,6 +124,15 @@ const RepositoriesSortMenu = ({ sort, onSortChange }: RepositoriesSortMenuProps) Last pushed + onSortChange(repoSortSetAxis(sort, 'created'))} + > + + + + Created + {sort !== 'relevance' && sort !== 'activity' && sort !== 'latestPendingReview' ? ( <> diff --git a/src/ui/views/RepoList/Components/repoSortField.ts b/src/ui/views/RepoList/Components/repoSortField.ts index d38f8ab6d..dcd7f1d2e 100644 --- a/src/ui/views/RepoList/Components/repoSortField.ts +++ b/src/ui/views/RepoList/Components/repoSortField.ts @@ -18,12 +18,15 @@ export type RepoSortField = | 'relevance' | 'lastPushed-asc' | 'lastPushed-desc' + | 'created-asc' + | 'created-desc' | 'name-asc' | 'name-desc' | 'activity' | 'latestPendingReview'; -export type RepoSortAxis = 'relevance' | 'lastPushed' | 'name' | 'activity' | 'latestPendingReview'; +export type RepoSortAxis = + 'relevance' | 'lastPushed' | 'created' | 'name' | 'activity' | 'latestPendingReview'; export const DEFAULT_REPO_SORT: RepoSortField = 'relevance'; @@ -31,6 +34,8 @@ export const REPO_SORT_VALUES = [ 'relevance', 'lastPushed-asc', 'lastPushed-desc', + 'created-asc', + 'created-desc', 'name-asc', 'name-desc', 'activity', @@ -40,6 +45,7 @@ export const REPO_SORT_VALUES = [ export const REPO_SORT_AXIS_LABEL = { relevance: 'Relevance', lastPushed: 'Last pushed', + created: 'Created', name: 'Name', activity: 'Activity', latestPendingReview: 'Pending', @@ -52,7 +58,9 @@ export const repoSortAxis = (sort: RepoSortField): RepoSortAxis => { if (sort === 'relevance') return 'relevance'; if (sort === 'activity') return 'activity'; if (sort === 'latestPendingReview') return 'latestPendingReview'; - return sort.startsWith('lastPushed') ? 'lastPushed' : 'name'; + if (sort.startsWith('lastPushed')) return 'lastPushed'; + if (sort.startsWith('created')) return 'created'; + return 'name'; }; export const repoSortDirection = (sort: RepoSortField): 'asc' | 'desc' => { @@ -64,7 +72,9 @@ export const repoSortCombine = (axis: RepoSortAxis, dir: 'asc' | 'desc'): RepoSo if (axis === 'relevance') return 'relevance'; if (axis === 'activity') return 'activity'; if (axis === 'latestPendingReview') return 'latestPendingReview'; - return axis === 'lastPushed' ? `lastPushed-${dir}` : `name-${dir}`; + if (axis === 'lastPushed') return `lastPushed-${dir}`; + if (axis === 'created') return `created-${dir}`; + return `name-${dir}`; }; export const repoSortSetAxis = (prev: RepoSortField, axis: RepoSortAxis): RepoSortField => { @@ -72,7 +82,7 @@ export const repoSortSetAxis = (prev: RepoSortField, axis: RepoSortAxis): RepoSo if (axis === 'activity') return 'activity'; if (axis === 'latestPendingReview') return 'latestPendingReview'; if (prev === 'relevance' || prev === 'activity' || prev === 'latestPendingReview') { - return repoSortCombine(axis, axis === 'lastPushed' ? 'desc' : 'asc'); + return repoSortCombine(axis, axis === 'lastPushed' || axis === 'created' ? 'desc' : 'asc'); } return repoSortCombine(axis, repoSortDirection(prev)); }; diff --git a/test/ui/repo.test.ts b/test/ui/repo.test.ts index 084a07ae6..d8450bb25 100644 --- a/test/ui/repo.test.ts +++ b/test/ui/repo.test.ts @@ -22,7 +22,9 @@ import { getRepo, addRepo, deleteRepo, + sortRepoViews, } from '../../src/ui/services/repo'; +import type { RepoView } from '../../src/ui/types'; const { axiosMock } = vi.hoisted(() => { const axiosFn = vi.fn() as any; @@ -383,3 +385,67 @@ describe('repo service additional functions', () => { }); }); }); + +describe('sortRepoViews created axis', () => { + const makeRepo = (name: string, dateCreated?: string): RepoView => + ({ + name, + project: 'org', + url: `https://example.com/org/${name}.git`, + users: { canPush: [], canAuthorise: [] }, + proxyURL: 'https://proxy.example.com', + ...(dateCreated ? { dateCreated } : {}), + }) as RepoView; + + it('sorts by created desc: newest created first, repos without a date last', () => { + const repos = [ + makeRepo('no-date'), + makeRepo('older', '2024-01-01T00:00:00.000Z'), + makeRepo('newer', '2025-01-01T00:00:00.000Z'), + ]; + + const result = sortRepoViews(repos, 'created-desc'); + + expect(result.map((r) => r.name)).toEqual(['newer', 'older', 'no-date']); + }); + + it('sorts by created asc: oldest created first, repos without a date last', () => { + const repos = [ + makeRepo('no-date'), + makeRepo('newer', '2025-01-01T00:00:00.000Z'), + makeRepo('older', '2024-01-01T00:00:00.000Z'), + ]; + + const result = sortRepoViews(repos, 'created-asc'); + + expect(result.map((r) => r.name)).toEqual(['older', 'newer', 'no-date']); + }); + + it('falls back to name ascending when created dates are equal', () => { + const repos = [ + makeRepo('zebra', '2025-01-01T00:00:00.000Z'), + makeRepo('alpha', '2025-01-01T00:00:00.000Z'), + ]; + + expect(sortRepoViews(repos, 'created-desc').map((r) => r.name)).toEqual(['alpha', 'zebra']); + expect(sortRepoViews(repos, 'created-asc').map((r) => r.name)).toEqual(['alpha', 'zebra']); + }); + + it('treats an unparseable created date as missing and sorts it last', () => { + const repos = [makeRepo('broken', 'not-a-date'), makeRepo('valid', '2024-01-01T00:00:00.000Z')]; + + expect(sortRepoViews(repos, 'created-desc').map((r) => r.name)).toEqual(['valid', 'broken']); + expect(sortRepoViews(repos, 'created-asc').map((r) => r.name)).toEqual(['valid', 'broken']); + }); + + it('does not mutate the input array', () => { + const repos = [ + makeRepo('older', '2024-01-01T00:00:00.000Z'), + makeRepo('newer', '2025-01-01T00:00:00.000Z'), + ]; + + sortRepoViews(repos, 'created-desc'); + + expect(repos.map((r) => r.name)).toEqual(['older', 'newer']); + }); +});