Skip to content
Open
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
29 changes: 29 additions & 0 deletions src/ui/services/repo.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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;
Expand Down
10 changes: 10 additions & 0 deletions src/ui/views/RepoList/Components/RepositoriesSortMenu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
import React from 'react';
import { ActionMenu, ActionList } from '@primer/react';
import {
CalendarIcon,
ClockIcon,
PersonIcon,
PulseIcon,
Expand Down Expand Up @@ -123,6 +124,15 @@ const RepositoriesSortMenu = ({ sort, onSortChange }: RepositoriesSortMenuProps)
</ActionList.LeadingVisual>
Last pushed
</ActionList.Item>
<ActionList.Item
selected={axisMatchesSort('created', sort)}
onSelect={() => onSortChange(repoSortSetAxis(sort, 'created'))}
>
<ActionList.LeadingVisual>
<CalendarIcon />
</ActionList.LeadingVisual>
Created
</ActionList.Item>
</ActionList.Group>
{sort !== 'relevance' && sort !== 'activity' && sort !== 'latestPendingReview' ? (
<>
Expand Down
18 changes: 14 additions & 4 deletions src/ui/views/RepoList/Components/repoSortField.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,19 +18,24 @@ 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';

export const REPO_SORT_VALUES = [
'relevance',
'lastPushed-asc',
'lastPushed-desc',
'created-asc',
'created-desc',
'name-asc',
'name-desc',
'activity',
Expand All @@ -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',
Expand All @@ -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' => {
Expand All @@ -64,15 +72,17 @@ 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 => {
if (axis === 'relevance') return 'relevance';
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));
};
Expand Down
66 changes: 66 additions & 0 deletions test/ui/repo.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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']);
});
});
Loading