mirror of
https://github.com/RayLabsHQ/gitea-mirror.git
synced 2026-04-15 23:47:47 +03:00
feat: add importedAt-based repository sorting (#226)
* repositories: add importedAt sorting * repositories: use tanstack table for repo list
This commit is contained in:
@@ -181,6 +181,7 @@ export const repositorySchema = z.object({
|
||||
errorMessage: z.string().optional().nullable(),
|
||||
destinationOrg: z.string().optional().nullable(),
|
||||
metadata: z.string().optional().nullable(), // JSON string for metadata sync state
|
||||
importedAt: z.coerce.date(),
|
||||
createdAt: z.coerce.date(),
|
||||
updatedAt: z.coerce.date(),
|
||||
});
|
||||
@@ -395,6 +396,9 @@ export const repositories = sqliteTable("repositories", {
|
||||
destinationOrg: text("destination_org"),
|
||||
|
||||
metadata: text("metadata"), // JSON string storing metadata sync state (issues, PRs, releases, etc.)
|
||||
importedAt: integer("imported_at", { mode: "timestamp" })
|
||||
.notNull()
|
||||
.default(sql`(unixepoch())`),
|
||||
|
||||
createdAt: integer("created_at", { mode: "timestamp" })
|
||||
.notNull()
|
||||
@@ -410,6 +414,7 @@ export const repositories = sqliteTable("repositories", {
|
||||
index("idx_repositories_organization").on(table.organization),
|
||||
index("idx_repositories_is_fork").on(table.isForked),
|
||||
index("idx_repositories_is_starred").on(table.isStarred),
|
||||
index("idx_repositories_user_imported_at").on(table.userId, table.importedAt),
|
||||
uniqueIndex("uniq_repositories_user_full_name").on(table.userId, table.fullName),
|
||||
uniqueIndex("uniq_repositories_user_normalized_full_name").on(table.userId, table.normalizedFullName),
|
||||
]);
|
||||
|
||||
@@ -287,6 +287,7 @@ export async function getGithubRepositories({
|
||||
lastMirrored: undefined,
|
||||
errorMessage: undefined,
|
||||
|
||||
importedAt: new Date(),
|
||||
createdAt: repo.created_at ? new Date(repo.created_at) : new Date(),
|
||||
updatedAt: repo.updated_at ? new Date(repo.updated_at) : new Date(),
|
||||
}));
|
||||
@@ -348,6 +349,7 @@ export async function getGithubStarredRepositories({
|
||||
lastMirrored: undefined,
|
||||
errorMessage: undefined,
|
||||
|
||||
importedAt: new Date(),
|
||||
createdAt: repo.created_at ? new Date(repo.created_at) : new Date(),
|
||||
updatedAt: repo.updated_at ? new Date(repo.updated_at) : new Date(),
|
||||
}));
|
||||
@@ -492,6 +494,7 @@ export async function getGithubOrganizationRepositories({
|
||||
lastMirrored: undefined,
|
||||
errorMessage: undefined,
|
||||
|
||||
importedAt: new Date(),
|
||||
createdAt: repo.created_at ? new Date(repo.created_at) : new Date(),
|
||||
updatedAt: repo.updated_at ? new Date(repo.updated_at) : new Date(),
|
||||
}));
|
||||
|
||||
@@ -28,6 +28,7 @@ function sampleRepo(overrides: Partial<GitRepo> = {}): GitRepo {
|
||||
status: 'imported',
|
||||
lastMirrored: undefined,
|
||||
errorMessage: undefined,
|
||||
importedAt: new Date(),
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
};
|
||||
|
||||
@@ -56,6 +56,7 @@ export function normalizeGitRepoToInsert(
|
||||
status: 'imported',
|
||||
lastMirrored: repo.lastMirrored ?? null,
|
||||
errorMessage: repo.errorMessage ?? null,
|
||||
importedAt: repo.importedAt || new Date(),
|
||||
createdAt: repo.createdAt || new Date(),
|
||||
updatedAt: repo.updatedAt || new Date(),
|
||||
};
|
||||
|
||||
68
src/lib/repository-sorting.test.ts
Normal file
68
src/lib/repository-sorting.test.ts
Normal file
@@ -0,0 +1,68 @@
|
||||
import { describe, expect, test } from "bun:test";
|
||||
import type { Repository } from "@/lib/db/schema";
|
||||
import { sortRepositories } from "@/lib/repository-sorting";
|
||||
|
||||
function makeRepo(overrides: Partial<Repository>): Repository {
|
||||
return {
|
||||
id: "id",
|
||||
userId: "user-1",
|
||||
configId: "config-1",
|
||||
name: "repo",
|
||||
fullName: "owner/repo",
|
||||
normalizedFullName: "owner/repo",
|
||||
url: "https://github.com/owner/repo",
|
||||
cloneUrl: "https://github.com/owner/repo.git",
|
||||
owner: "owner",
|
||||
organization: null,
|
||||
mirroredLocation: "",
|
||||
isPrivate: false,
|
||||
isForked: false,
|
||||
forkedFrom: null,
|
||||
hasIssues: true,
|
||||
isStarred: false,
|
||||
isArchived: false,
|
||||
size: 1,
|
||||
hasLFS: false,
|
||||
hasSubmodules: false,
|
||||
language: null,
|
||||
description: null,
|
||||
defaultBranch: "main",
|
||||
visibility: "public",
|
||||
status: "imported",
|
||||
lastMirrored: null,
|
||||
errorMessage: null,
|
||||
destinationOrg: null,
|
||||
metadata: null,
|
||||
importedAt: new Date("2026-01-01T00:00:00.000Z"),
|
||||
createdAt: new Date("2020-01-01T00:00:00.000Z"),
|
||||
updatedAt: new Date("2026-01-01T00:00:00.000Z"),
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
describe("sortRepositories", () => {
|
||||
test("defaults to recently imported first", () => {
|
||||
const repos = [
|
||||
makeRepo({ id: "a", fullName: "owner/a", importedAt: new Date("2026-01-01T00:00:00.000Z") }),
|
||||
makeRepo({ id: "b", fullName: "owner/b", importedAt: new Date("2026-03-01T00:00:00.000Z") }),
|
||||
makeRepo({ id: "c", fullName: "owner/c", importedAt: new Date("2025-12-01T00:00:00.000Z") }),
|
||||
];
|
||||
|
||||
const sorted = sortRepositories(repos, undefined);
|
||||
expect(sorted.map((repo) => repo.id)).toEqual(["b", "a", "c"]);
|
||||
});
|
||||
|
||||
test("supports name and updated sorting", () => {
|
||||
const repos = [
|
||||
makeRepo({ id: "a", fullName: "owner/zeta", updatedAt: new Date("2026-01-01T00:00:00.000Z") }),
|
||||
makeRepo({ id: "b", fullName: "owner/alpha", updatedAt: new Date("2026-03-01T00:00:00.000Z") }),
|
||||
makeRepo({ id: "c", fullName: "owner/middle", updatedAt: new Date("2025-12-01T00:00:00.000Z") }),
|
||||
];
|
||||
|
||||
const nameSorted = sortRepositories(repos, "name-asc");
|
||||
expect(nameSorted.map((repo) => repo.id)).toEqual(["b", "c", "a"]);
|
||||
|
||||
const updatedSorted = sortRepositories(repos, "updated-desc");
|
||||
expect(updatedSorted.map((repo) => repo.id)).toEqual(["b", "a", "c"]);
|
||||
});
|
||||
});
|
||||
40
src/lib/repository-sorting.ts
Normal file
40
src/lib/repository-sorting.ts
Normal file
@@ -0,0 +1,40 @@
|
||||
import type { Repository } from "@/lib/db/schema";
|
||||
|
||||
export type RepositorySortOrder =
|
||||
| "imported-desc"
|
||||
| "imported-asc"
|
||||
| "updated-desc"
|
||||
| "updated-asc"
|
||||
| "name-asc"
|
||||
| "name-desc";
|
||||
|
||||
function getTimestamp(value: Date | string | null | undefined): number {
|
||||
if (!value) return 0;
|
||||
const timestamp = new Date(value).getTime();
|
||||
return Number.isNaN(timestamp) ? 0 : timestamp;
|
||||
}
|
||||
|
||||
export function sortRepositories(
|
||||
repositories: Repository[],
|
||||
sortOrder: string | undefined,
|
||||
): Repository[] {
|
||||
const order = (sortOrder ?? "imported-desc") as RepositorySortOrder;
|
||||
|
||||
return [...repositories].sort((a, b) => {
|
||||
switch (order) {
|
||||
case "imported-asc":
|
||||
return getTimestamp(a.importedAt) - getTimestamp(b.importedAt);
|
||||
case "updated-desc":
|
||||
return getTimestamp(b.updatedAt) - getTimestamp(a.updatedAt);
|
||||
case "updated-asc":
|
||||
return getTimestamp(a.updatedAt) - getTimestamp(b.updatedAt);
|
||||
case "name-asc":
|
||||
return a.fullName.localeCompare(b.fullName, undefined, { sensitivity: "base" });
|
||||
case "name-desc":
|
||||
return b.fullName.localeCompare(a.fullName, undefined, { sensitivity: "base" });
|
||||
case "imported-desc":
|
||||
default:
|
||||
return getTimestamp(b.importedAt) - getTimestamp(a.importedAt);
|
||||
}
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user