feat: add importedAt-based repository sorting (#226)

* repositories: add importedAt sorting

* repositories: use tanstack table for repo list
This commit is contained in:
ARUNAVO RAY
2026-03-15 08:52:45 +05:30
committed by GitHub
parent 1bca7df5ab
commit 6f53a3ed41
20 changed files with 2370 additions and 40 deletions

View File

@@ -31,6 +31,7 @@
"@radix-ui/react-tabs": "^1.1.13",
"@radix-ui/react-tooltip": "^1.2.8",
"@tailwindcss/vite": "^4.2.1",
"@tanstack/react-table": "^8.21.3",
"@tanstack/react-virtual": "^3.13.19",
"@types/canvas-confetti": "^1.9.0",
"@types/react": "^19.2.14",
@@ -548,8 +549,12 @@
"@tailwindcss/vite": ["@tailwindcss/vite@4.2.1", "", { "dependencies": { "@tailwindcss/node": "4.2.1", "@tailwindcss/oxide": "4.2.1", "tailwindcss": "4.2.1" }, "peerDependencies": { "vite": "^5.2.0 || ^6 || ^7" } }, "sha512-TBf2sJjYeb28jD2U/OhwdW0bbOsxkWPwQ7SrqGf9sVcoYwZj7rkXljroBO9wKBut9XnmQLXanuDUeqQK0lGg/w=="],
"@tanstack/react-table": ["@tanstack/react-table@8.21.3", "", { "dependencies": { "@tanstack/table-core": "8.21.3" }, "peerDependencies": { "react": ">=16.8", "react-dom": ">=16.8" } }, "sha512-5nNMTSETP4ykGegmVkhjcS8tTLW6Vl4axfEGQN3v0zdHYbK4UfoqfPChclTrJ4EoK9QynqAu9oUf8VEmrpZ5Ww=="],
"@tanstack/react-virtual": ["@tanstack/react-virtual@3.13.19", "", { "dependencies": { "@tanstack/virtual-core": "3.13.19" }, "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-KzwmU1IbE0IvCZSm6OXkS+kRdrgW2c2P3Ho3NC+zZXWK6oObv/L+lcV/2VuJ+snVESRlMJ+w/fg4WXI/JzoNGQ=="],
"@tanstack/table-core": ["@tanstack/table-core@8.21.3", "", {}, "sha512-ldZXEhOBb8Is7xLs01fR3YEc3DERiz5silj8tnGkFZytt1abEvl/GhUmCE0PMLaMPTa3Jk4HbKmRlHmu+gCftg=="],
"@tanstack/virtual-core": ["@tanstack/virtual-core@3.13.19", "", {}, "sha512-/BMP7kNhzKOd7wnDeB8NrIRNLwkf5AhCYCvtfZV2GXWbBieFm/el0n6LOAXlTi6ZwHICSNnQcIxRCWHrLzDY+g=="],
"@testing-library/dom": ["@testing-library/dom@10.4.0", "", { "dependencies": { "@babel/code-frame": "^7.10.4", "@babel/runtime": "^7.12.5", "@types/aria-query": "^5.0.1", "aria-query": "5.3.0", "chalk": "^4.1.0", "dom-accessibility-api": "^0.5.9", "lz-string": "^1.5.0", "pretty-format": "^27.0.2" } }, "sha512-pemlzrSESWbdAloYml3bAJMEfNh1Z7EduzqPKprCH5S341frlpYnUEW0H72dLxa6IsYr+mPno20GiSm+h9dEdQ=="],

View File

@@ -0,0 +1,24 @@
ALTER TABLE `repositories` ADD `imported_at` integer DEFAULT (unixepoch()) NOT NULL;--> statement-breakpoint
UPDATE `repositories`
SET `imported_at` = COALESCE(
(
SELECT MIN(`mj`.`timestamp`)
FROM `mirror_jobs` `mj`
WHERE `mj`.`user_id` = `repositories`.`user_id`
AND `mj`.`status` = 'imported'
AND (
(`mj`.`repository_id` IS NOT NULL AND `mj`.`repository_id` = `repositories`.`id`)
OR (
`mj`.`repository_id` IS NULL
AND `mj`.`repository_name` IS NOT NULL
AND (
lower(trim(`mj`.`repository_name`)) = `repositories`.`normalized_full_name`
OR lower(trim(`mj`.`repository_name`)) = lower(trim(`repositories`.`name`))
)
)
)
),
`repositories`.`created_at`,
`imported_at`
);--> statement-breakpoint
CREATE INDEX `idx_repositories_user_imported_at` ON `repositories` (`user_id`,`imported_at`);

File diff suppressed because it is too large Load Diff

View File

@@ -64,6 +64,13 @@
"when": 1761802056073,
"tag": "0008_serious_thena",
"breakpoints": true
},
{
"idx": 9,
"version": "6",
"when": 1773542995732,
"tag": "0009_nervous_tyger_tiger",
"breakpoints": true
}
]
}

View File

@@ -73,6 +73,7 @@
"@radix-ui/react-tabs": "^1.1.13",
"@radix-ui/react-tooltip": "^1.2.8",
"@tailwindcss/vite": "^4.2.1",
"@tanstack/react-table": "^8.21.3",
"@tanstack/react-virtual": "^3.13.19",
"@types/canvas-confetti": "^1.9.0",
"@types/react": "^19.2.14",

View File

@@ -51,6 +51,15 @@ import { useLiveRefresh } from "@/hooks/useLiveRefresh";
import { useConfigStatus } from "@/hooks/useConfigStatus";
import { useNavigation } from "@/components/layout/MainLayout";
const REPOSITORY_SORT_OPTIONS = [
{ value: "imported-desc", label: "Recently Imported" },
{ value: "imported-asc", label: "Oldest Imported" },
{ value: "updated-desc", label: "Recently Updated" },
{ value: "updated-asc", label: "Oldest Updated" },
{ value: "name-asc", label: "Name (A-Z)" },
{ value: "name-desc", label: "Name (Z-A)" },
] as const;
export default function Repository() {
const [repositories, setRepositories] = useState<Repository[]>([]);
const [isInitialLoading, setIsInitialLoading] = useState(true);
@@ -63,6 +72,7 @@ export default function Repository() {
status: "",
organization: "",
owner: "",
sort: "imported-desc",
});
const [isDialogOpen, setIsDialogOpen] = useState<boolean>(false);
const [selectedRepoIds, setSelectedRepoIds] = useState<Set<string>>(new Set());
@@ -999,6 +1009,7 @@ export default function Repository() {
status: "",
organization: "",
owner: "",
sort: filter.sort || "imported-desc",
});
};
@@ -1139,6 +1150,33 @@ export default function Repository() {
</SelectContent>
</Select>
</div>
{/* Sort Filter */}
<div className="space-y-2">
<label className="text-sm font-medium flex items-center gap-2">
<span className="text-muted-foreground">Sort</span>
</label>
<Select
value={filter.sort || "imported-desc"}
onValueChange={(value) =>
setFilter((prev) => ({
...prev,
sort: value,
}))
}
>
<SelectTrigger className="w-full h-10">
<SelectValue placeholder="Sort repositories" />
</SelectTrigger>
<SelectContent>
{REPOSITORY_SORT_OPTIONS.map((option) => (
<SelectItem key={option.value} value={option.value}>
{option.label}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
</div>
<DrawerFooter className="gap-2 px-4 pt-2 pb-4 border-t">
@@ -1241,6 +1279,27 @@ export default function Repository() {
</SelectContent>
</Select>
<Select
value={filter.sort || "imported-desc"}
onValueChange={(value) =>
setFilter((prev) => ({
...prev,
sort: value,
}))
}
>
<SelectTrigger className="w-[190px] h-10">
<SelectValue placeholder="Sort repositories" />
</SelectTrigger>
<SelectContent>
{REPOSITORY_SORT_OPTIONS.map((option) => (
<SelectItem key={option.value} value={option.value}>
{option.label}
</SelectItem>
))}
</SelectContent>
</Select>
<Button
variant="outline"
size="icon"

View File

@@ -1,11 +1,19 @@
import { useMemo, useRef } from "react";
import Fuse from "fuse.js";
import {
getCoreRowModel,
getFilteredRowModel,
getSortedRowModel,
useReactTable,
type ColumnDef,
type ColumnFiltersState,
type SortingState,
} from "@tanstack/react-table";
import { useVirtualizer } from "@tanstack/react-virtual";
import { FlipHorizontal, GitFork, RefreshCw, RotateCcw, Star, Lock, Ban, Check, ChevronDown, Trash2, X } from "lucide-react";
import { SiGithub, SiGitea } from "react-icons/si";
import type { Repository } from "@/lib/db/schema";
import { Button } from "@/components/ui/button";
import { formatDate, formatLastSyncTime, getStatusColor } from "@/lib/utils";
import { formatLastSyncTime } from "@/lib/utils";
import type { FilterParams } from "@/types/filter";
import { Skeleton } from "@/components/ui/skeleton";
import { useGiteaConfig } from "@/hooks/useGiteaConfig";
@@ -46,6 +54,30 @@ interface RepositoryTableProps {
onDismissSync?: ({ repoId }: { repoId: string }) => Promise<void>;
}
function getTimestamp(value: Date | string | null | undefined): number {
if (!value) return 0;
const timestamp = new Date(value).getTime();
return Number.isNaN(timestamp) ? 0 : timestamp;
}
function getTableSorting(sortOrder: string | undefined): SortingState {
switch (sortOrder ?? "imported-desc") {
case "imported-asc":
return [{ id: "importedAt", desc: false }];
case "updated-desc":
return [{ id: "updatedAt", desc: true }];
case "updated-asc":
return [{ id: "updatedAt", desc: false }];
case "name-asc":
return [{ id: "fullName", desc: false }];
case "name-desc":
return [{ id: "fullName", desc: true }];
case "imported-desc":
default:
return [{ id: "importedAt", desc: true }];
}
}
export default function RepositoryTable({
repositories,
isLoading,
@@ -120,40 +152,89 @@ export default function RepositoryTable({
return `${baseUrl}/${repoPath}`;
};
const hasAnyFilter = Object.values(filter).some(
(val) => val?.toString().trim() !== ""
);
const hasAnyFilter = [
filter.searchTerm,
filter.status,
filter.owner,
filter.organization,
].some((val) => val?.toString().trim() !== "");
const filteredRepositories = useMemo(() => {
let result = repositories;
const columnFilters = useMemo<ColumnFiltersState>(() => {
const next: ColumnFiltersState = [];
if (filter.status) {
result = result.filter((repo) => repo.status === filter.status);
next.push({ id: "status", value: filter.status });
}
if (filter.owner) {
result = result.filter((repo) => repo.owner === filter.owner);
next.push({ id: "owner", value: filter.owner });
}
if (filter.organization) {
result = result.filter(
(repo) => repo.organization === filter.organization
);
next.push({ id: "organization", value: filter.organization });
}
if (filter.searchTerm) {
const fuse = new Fuse(result, {
keys: ["name", "fullName", "owner", "organization"],
threshold: 0.3,
});
result = fuse.search(filter.searchTerm).map((res) => res.item);
}
return next;
}, [filter.status, filter.owner, filter.organization]);
return result;
}, [repositories, filter]);
const sorting = useMemo(() => getTableSorting(filter.sort), [filter.sort]);
const columns = useMemo<ColumnDef<Repository>[]>(
() => [
{
id: "fullName",
accessorFn: (row) => row.fullName,
},
{
id: "owner",
accessorFn: (row) => row.owner,
filterFn: "equalsString",
},
{
id: "organization",
accessorFn: (row) => row.organization ?? "",
filterFn: "equalsString",
},
{
id: "status",
accessorFn: (row) => row.status,
filterFn: "equalsString",
},
{
id: "importedAt",
accessorFn: (row) => getTimestamp(row.importedAt),
enableGlobalFilter: false,
enableColumnFilter: false,
},
{
id: "updatedAt",
accessorFn: (row) => getTimestamp(row.updatedAt),
enableGlobalFilter: false,
enableColumnFilter: false,
},
],
[]
);
const table = useReactTable({
data: repositories,
columns,
state: {
globalFilter: filter.searchTerm ?? "",
columnFilters,
sorting,
},
getCoreRowModel: getCoreRowModel(),
getFilteredRowModel: getFilteredRowModel(),
getSortedRowModel: getSortedRowModel(),
});
const visibleRepositories = table
.getRowModel()
.rows.map((row) => row.original);
const rowVirtualizer = useVirtualizer({
count: filteredRepositories.length,
count: visibleRepositories.length,
getScrollElement: () => tableParentRef.current,
estimateSize: () => 65,
overscan: 5,
@@ -162,7 +243,11 @@ export default function RepositoryTable({
// Selection handlers
const handleSelectAll = (checked: boolean) => {
if (checked) {
const allIds = new Set(filteredRepositories.map(repo => repo.id).filter((id): id is string => !!id));
const allIds = new Set(
visibleRepositories
.map((repo) => repo.id)
.filter((id): id is string => !!id)
);
onSelectionChange(allIds);
} else {
onSelectionChange(new Set());
@@ -179,8 +264,9 @@ export default function RepositoryTable({
onSelectionChange(newSelection);
};
const isAllSelected = filteredRepositories.length > 0 &&
filteredRepositories.every(repo => repo.id && selectedRepoIds.has(repo.id));
const isAllSelected =
visibleRepositories.length > 0 &&
visibleRepositories.every((repo) => repo.id && selectedRepoIds.has(repo.id));
const isPartiallySelected = selectedRepoIds.size > 0 && !isAllSelected;
// Mobile card layout for repository
@@ -235,7 +321,7 @@ export default function RepositoryTable({
{/* Status & Last Mirrored */}
<div className="flex items-center justify-between">
<Badge
<Badge
className={`capitalize
${repo.status === 'imported' ? 'bg-yellow-500/10 text-yellow-600 hover:bg-yellow-500/20 dark:text-yellow-400' :
repo.status === 'mirrored' || repo.status === 'synced' ? 'bg-green-500/10 text-green-600 hover:bg-green-500/20 dark:text-green-400' :
@@ -250,7 +336,7 @@ export default function RepositoryTable({
{repo.status}
</Badge>
<span className="text-xs text-muted-foreground">
{formatLastSyncTime(repo.lastMirrored)}
{formatLastSyncTime(repo.lastMirrored ?? null)}
</span>
</div>
</div>
@@ -379,7 +465,7 @@ export default function RepositoryTable({
Ignore Repository
</Button>
)}
{/* External links */}
<div className="flex gap-2">
<Button variant="outline" size="default" className="flex-1 h-10 min-w-0" asChild>
@@ -510,7 +596,7 @@ export default function RepositoryTable({
{hasAnyFilter && (
<div className="mb-4 flex items-center gap-2">
<span className="text-sm text-muted-foreground">
Showing {filteredRepositories.length} of {repositories.length} repositories
Showing {visibleRepositories.length} of {repositories.length} repositories
</span>
<Button
variant="ghost"
@@ -521,6 +607,7 @@ export default function RepositoryTable({
status: "",
organization: "",
owner: "",
sort: filter.sort || "imported-desc",
})
}
>
@@ -529,7 +616,7 @@ export default function RepositoryTable({
</div>
)}
{filteredRepositories.length === 0 ? (
{visibleRepositories.length === 0 ? (
<div className="text-center py-8">
<p className="text-muted-foreground">
{hasAnyFilter
@@ -550,12 +637,12 @@ export default function RepositoryTable({
className="h-5 w-5"
/>
<span className="text-sm font-medium">
Select All ({filteredRepositories.length})
Select All ({visibleRepositories.length})
</span>
</div>
{/* Repository cards */}
{filteredRepositories.map((repo) => (
{visibleRepositories.map((repo) => (
<RepositoryCard key={repo.id} repo={repo} />
))}
</div>
@@ -601,13 +688,14 @@ export default function RepositoryTable({
position: "relative",
}}
>
{rowVirtualizer.getVirtualItems().map((virtualRow, index) => {
const repo = filteredRepositories[virtualRow.index];
{rowVirtualizer.getVirtualItems().map((virtualRow) => {
const repo = visibleRepositories[virtualRow.index];
if (!repo) return null;
const isLoading = loadingRepoIds.has(repo.id ?? "");
return (
<div
key={index}
key={virtualRow.key}
ref={rowVirtualizer.measureElement}
style={{
position: "absolute",
@@ -670,7 +758,7 @@ export default function RepositoryTable({
{/* Last Mirrored */}
<div className="h-full p-3 flex items-center flex-[1]">
<p className="text-sm">
{formatLastSyncTime(repo.lastMirrored)}
{formatLastSyncTime(repo.lastMirrored ?? null)}
</p>
</div>
@@ -680,7 +768,7 @@ export default function RepositoryTable({
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<Badge
<Badge
variant="destructive"
className="cursor-help capitalize"
>
@@ -693,7 +781,7 @@ export default function RepositoryTable({
</Tooltip>
</TooltipProvider>
) : (
<Badge
<Badge
className={`capitalize
${repo.status === 'imported' ? 'bg-yellow-500/10 text-yellow-600 hover:bg-yellow-500/20 dark:text-yellow-400' :
repo.status === 'mirrored' || repo.status === 'synced' ? 'bg-green-500/10 text-green-600 hover:bg-green-500/20 dark:text-green-400' :
@@ -784,7 +872,7 @@ export default function RepositoryTable({
<div className={`h-1.5 w-1.5 rounded-full ${isLiveActive ? 'bg-emerald-500' : 'bg-primary'}`} />
<span className="text-sm font-medium text-foreground">
{hasAnyFilter
? `Showing ${filteredRepositories.length} of ${repositories.length} repositories`
? `Showing ${visibleRepositories.length} of ${repositories.length} repositories`
: `${repositories.length} ${repositories.length === 1 ? 'repository' : 'repositories'} total`}
</span>
</div>

View File

@@ -7,6 +7,7 @@ const FILTER_KEYS: (keyof FilterParams)[] = [
"membershipRole",
"owner",
"organization",
"sort",
"type",
"name",
];

View File

@@ -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),
]);

View File

@@ -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(),
}));

View File

@@ -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(),
};

View File

@@ -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(),
};

View 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"]);
});
});

View 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);
}
});
}

View File

@@ -49,7 +49,7 @@ export const GET: APIRoute = async ({ request, locals }) => {
.select()
.from(repositories)
.where(and(...conditions))
.orderBy(sql`name COLLATE NOCASE`);
.orderBy(sql`${repositories.importedAt} DESC`, sql`name COLLATE NOCASE`);
const response: RepositoryApiResponse = {
success: true,

View File

@@ -90,6 +90,7 @@ export const POST: APIRoute = async ({ request, locals }) => {
status: repo.status,
lastMirrored: repo.lastMirrored ?? null,
errorMessage: repo.errorMessage ?? null,
importedAt: repo.importedAt,
createdAt: repo.createdAt,
updatedAt: repo.updatedAt,
}));

View File

@@ -187,6 +187,7 @@ export const POST: APIRoute = async ({ request, locals }) => {
status: "imported" as RepoStatus,
lastMirrored: null,
errorMessage: null,
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(),
};

View File

@@ -155,6 +155,7 @@ export const POST: APIRoute = async ({ request, locals }) => {
errorMessage: null,
mirroredLocation: "",
destinationOrg: null,
importedAt: new Date(),
createdAt: repoData.created_at
? new Date(repoData.created_at)
: new Date(),

View File

@@ -75,6 +75,7 @@ export interface GitRepo {
lastMirrored?: Date;
errorMessage?: string;
importedAt: Date;
createdAt: Date;
updatedAt: Date;
}

View File

@@ -7,6 +7,7 @@ export interface FilterParams {
membershipRole?: MembershipRole | ""; //membership role in orgs
owner?: string; // owner of the repos
organization?: string; // organization of the repos
sort?: string; // repository sort order
type?: string; //types in activity log
name?: string; // name in activity log
}