From 7a1b03060e278371cb0cd428a3e3c9b8d0026d2b Mon Sep 17 00:00:00 2001 From: zhenyi <434836402@qq.com> Date: Sun, 31 May 2026 13:12:08 +0800 Subject: [PATCH] fix(repo): update repository page components --- src/page/workspace/repo/branches.tsx | 202 +------ src/page/workspace/repo/code.tsx | 632 +--------------------- src/page/workspace/repo/commit-detail.tsx | 8 +- src/page/workspace/repo/commits.tsx | 148 +---- src/page/workspace/repo/contributors.tsx | 68 +-- src/page/workspace/repo/layout.tsx | 35 +- src/page/workspace/repo/pull-detail.tsx | 18 +- src/page/workspace/repo/pulls.tsx | 20 +- src/page/workspace/repo/settings.tsx | 4 +- src/page/workspace/repo/tags.tsx | 115 +--- src/page/workspace/repo/webhooks.tsx | 18 +- 11 files changed, 69 insertions(+), 1199 deletions(-) diff --git a/src/page/workspace/repo/branches.tsx b/src/page/workspace/repo/branches.tsx index da755f5..b5c22c1 100644 --- a/src/page/workspace/repo/branches.tsx +++ b/src/page/workspace/repo/branches.tsx @@ -1,205 +1,7 @@ -import { useState } from "react"; import { useParams } from "react-router"; -import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query"; -import { client } from "@/client"; -import { GitBranch, Search, Plus, Trash2, Pencil } from "lucide-react"; -import { Input } from "@/components/ui/input"; -import { Button } from "@/components/ui/button"; -import { cn } from "@/lib/utils"; +import BranchesPanel from "@/components/repo/branches-panel"; export default function BranchesTab() { const { projectName = "", repoName = "" } = useParams(); - - const { data: repoData } = useQuery({ - queryKey: ["repo", projectName, repoName], - queryFn: async () => { - const res = await client.gitGetRepo(projectName, repoName); - return res.data; - }, - enabled: Boolean(projectName) && Boolean(repoName), - retry: false, - }); - - const defaultBranch = repoData?.default_branch ?? "main"; - - const qc = useQueryClient(); - const [search, setSearch] = useState(""); - const [showCreate, setShowCreate] = useState(false); - const [newBranchName, setNewBranchName] = useState(""); - const [newBranchSource, setNewBranchSource] = useState(defaultBranch); - const [renaming, setRenaming] = useState(null); - const [renameValue, setRenameValue] = useState(""); - - const { data: branches = [], isLoading } = useQuery({ - queryKey: ["repo", projectName, repoName, "branches"], - queryFn: async () => { - const res = await client.gitListBranches(projectName, repoName); - return res.data.branches; - }, - enabled: Boolean(projectName) && Boolean(repoName), - retry: false, - }); - - const createBranch = useMutation({ - mutationFn: async () => { await client.gitForkBranch(projectName, repoName, { name: newBranchName, oid: newBranchSource } as any); }, - onSuccess: () => { setShowCreate(false); setNewBranchName(""); qc.invalidateQueries({ queryKey: ["repo", projectName, repoName, "branches"] }); }, - }); - - const deleteBranch = useMutation({ - mutationFn: async (name: string) => { await client.gitDeleteBranch(projectName, repoName, name); }, - onSuccess: () => qc.invalidateQueries({ queryKey: ["repo", projectName, repoName, "branches"] }), - }); - - const renameBranch = useMutation({ - mutationFn: async ({ oldName, newName }: { oldName: string; newName: string }) => { await (client as any).gitRenameBranch(projectName, repoName, oldName, { new_branch: newName }); }, - onSuccess: () => { setRenaming(null); setRenameValue(""); qc.invalidateQueries({ queryKey: ["repo", projectName, repoName, "branches"] }); }, - }); - - const filtered = search ? branches.filter((b) => b.name.toLowerCase().includes(search.toLowerCase())) : branches; - const defaultBranches = filtered.filter((b) => b.name === defaultBranch); - const otherBranches = filtered.filter((b) => b.name !== defaultBranch); - - return ( -
-
-
- - setSearch(e.target.value)} /> -
- -
- - {showCreate && ( -
- setNewBranchName(e.target.value)} /> - setNewBranchSource(e.target.value)} /> - - -
- )} - -
- {isLoading ? ( -
{Array.from({ length: 3 }).map((_, i) =>
)}
- ) : filtered.length > 0 ? ( -
- {defaultBranches.map((b) => )} - {otherBranches.length > 0 && defaultBranches.length > 0 && ( -
- Other branches -
- )} - {otherBranches.map((b) => )} -
- ) : ( -
-
- -
-

No branches yet

-

- {search ? `No branches matching "${search}"` : "Create a branch to start working on your project."} -

- {!search && ( - - )} -
- )} -
- - {/* Branch comparison */} - -
- ); + return ; } - -function BranchComparison({ projectName, repoName, branches }: { projectName: string; repoName: string; branches: { name: string }[] }) { - const [oldBranch, setOldBranch] = useState(""); - const [newBranch, setNewBranch] = useState(""); - - const { data: diffResult } = useQuery({ - queryKey: ["repo", projectName, repoName, "branch-diff", oldBranch, newBranch], - queryFn: async () => { - const res = await client.gitDiffBranches(projectName, repoName, { old_branch: oldBranch, new_branch: newBranch }); - return res.data; - }, - enabled: Boolean(oldBranch && newBranch && oldBranch !== newBranch), - retry: false, - }); - - return ( -
-
- Compare branches -
-
-
- - - -
- {diffResult?.stats && ( -
- {diffResult.stats.files_changed} files - +{diffResult.stats.insertions} - -{diffResult.stats.deletions} -
- )} -
-
- ); -} - -function BranchRow({ branch, isDefault, onDelete, onRename, renaming, setRenaming, renameValue, setRenameValue }: { - branch: { name: string; oid: string; is_current?: boolean; is_head?: boolean }; - isDefault?: boolean; - onDelete: any; - onRename: any; - renaming: string | null; - setRenaming: any; - renameValue: string; - setRenameValue: any; -}) { - const isEditing = renaming === branch.name; - return ( -
- - {isEditing ? ( -
- setRenameValue(e.target.value)} autoFocus /> - - -
- ) : ( - <> - {branch.name} - {isDefault && Default} -
- {branch.is_head && HEAD} - {branch.oid.slice(0, 7)} - {!isDefault && ( - - )} - {!isDefault && ( - - )} -
- - )} -
- ); -} \ No newline at end of file diff --git a/src/page/workspace/repo/code.tsx b/src/page/workspace/repo/code.tsx index 20130ee..ce0b869 100644 --- a/src/page/workspace/repo/code.tsx +++ b/src/page/workspace/repo/code.tsx @@ -1,631 +1,7 @@ -import { useState, useEffect, useCallback } from "react"; -import { useParams, useSearchParams } from "react-router"; -import { useQuery } from "@tanstack/react-query"; -import { client, type TreeEntryDto, type TreeKindDto } from "@/client"; -import { - Copy, - Check, - GitBranch, - GitCommitHorizontal, - FileText, - Folder, - Package, - ChevronRight, - ChevronDown, - Star, - FolderOpen, - AlertTriangle, - Download, -} from "lucide-react"; -import { Button } from "@/components/ui/button"; -import { cn } from "@/lib/utils"; -import { workspaceColor, workspaceInitial } from "@/components/shell/shared"; - -const LANGUAGE_COLORS: Record = { - Rust: "#dea584", - TypeScript: "#3178c6", - JavaScript: "#f1e05a", - Python: "#3572A5", - Go: "#00ADD8", - Java: "#b07219", - "C++": "#f34b7d", - C: "#555555", - HTML: "#e34c26", - CSS: "#563d7c", - Shell: "#89e051", - Markdown: "#083fa1", - Vue: "#41b883", - Ruby: "#701516", - Swift: "#F05138", - Kotlin: "#A97BFF", - Dart: "#00B4AB", - Scala: "#c22d40", - R: "#198CE7", - Zig: "#ec915c", - Nix: "#7e7eff", - CMake: "#DA3434", - Dockerfile: "#384d54", -}; - -function formatTimeAgo(timeSecs: number) { - const now = Math.floor(Date.now() / 1000); - const diff = now - timeSecs; - if (diff < 60) return "just now"; - if (diff < 3600) return `${Math.floor(diff / 60)}m ago`; - if (diff < 86400) return `${Math.floor(diff / 3600)}h ago`; - if (diff < 2592000) return `${Math.floor(diff / 86400)}d ago`; - return new Date(timeSecs * 1000).toLocaleDateString("en-US", { month: "short", day: "numeric" }); -} - -function decodeBase64(base64: string): string { - try { - return atob(base64); - } catch { - return ""; - } -} - -function formatTimeAgoEntry(ts: string) { - const d = new Date(Number(ts) * 1000); - const now = Date.now(); - const diff = now - d.getTime(); - if (diff < 3600000) return `${Math.floor(diff / 60000)}m`; - if (diff < 86400000) return `${Math.floor(diff / 3600000)}h`; - if (diff < 2592000000) return `${Math.floor(diff / 86400000)}d`; - return d.toLocaleDateString("en-US", { month: "short", day: "numeric" }); -} - -function FileIcon({ kind }: { kind: TreeKindDto }) { - if (kind === "tree") return ; - if (kind === "lfs_pointer") return ; - return ; -} - -function AuthorAvatar({ author }: { author: { name: string } }) { - return ( - - {workspaceInitial(author.name)} - - ); -} +import { useParams } from "react-router"; +import CodePanel from "@/components/repo/code-panel"; export default function CodeTab() { const { projectName = "", repoName = "" } = useParams(); - - // Get default branch - const { data: repoData } = useQuery({ - queryKey: ["repo", projectName, repoName], - queryFn: async () => { - const res = await client.gitGetRepo(projectName, repoName); - return res.data; - }, - enabled: Boolean(projectName) && Boolean(repoName), - retry: false, - }); - - const defaultBranch = repoData?.default_branch ?? "main"; - const [selectedBranch, setSelectedBranch] = useState(defaultBranch); - const [branchOpen, setBranchOpen] = useState(false); - const [copied, setCopied] = useState(false); - const [cloneMethod, setCloneMethod] = useState<"https" | "ssh">("https"); - const [treeStack, setTreeStack] = useState<{ oid: string; name: string }[]>([]); - const [viewingFile, setViewingFile] = useState<{ oid: string; name: string; isBinary: boolean } | null>(null); - const [showBlame, setShowBlame] = useState(false); - - const [searchParams, setSearchParams] = useSearchParams(); - - const origin = typeof window !== "undefined" ? window.location.origin : ""; - const cloneUrl = cloneMethod === "https" - ? `${origin}/${projectName}/${repoName}.git` - : `git@${origin.replace(/^https?:\/\//, "")}:${projectName}/${repoName}.git`; - - const handleCopy = async () => { - try { - await navigator.clipboard.writeText(cloneUrl); - setCopied(true); - setTimeout(() => setCopied(false), 2000); - } catch { /* noop */ } - }; - - // Branch list - const { data: branchList } = useQuery({ - queryKey: ["repo", projectName, repoName, "branches"], - queryFn: async () => { - const res = await client.gitListBranches(projectName, repoName); - return res.data.branches; - }, - enabled: Boolean(projectName) && Boolean(repoName), - retry: false, - }); - - // Branch info → commit OID - const { data: branchInfo } = useQuery({ - queryKey: ["repo", projectName, repoName, "branch-info", selectedBranch], - queryFn: async () => { - const res = await (client as any).gitBranchInfo(projectName, repoName, selectedBranch); - return res.data.branch; - }, - enabled: Boolean(projectName) && Boolean(repoName), - retry: false, - }); - - // Commit info → tree OID - const { data: commitInfo } = useQuery({ - queryKey: ["repo", projectName, repoName, "commit-info", branchInfo?.oid], - queryFn: async () => { - const res = await (client as any).gitCommitInfo(projectName, repoName, branchInfo!.oid); - return res.data.commit; - }, - enabled: Boolean(branchInfo?.oid), - retry: false, - }); - - const rootTreeOid = commitInfo?.tree_id ?? null; - - // Reset navigation when branch changes - - useEffect(() => { - if (rootTreeOid) { - setTreeStack([{ oid: rootTreeOid, name: repoName }]); - setViewingFile(null); - setSearchParams({}, { replace: true }); - } else { - setTreeStack([]); - setViewingFile(null); - } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [rootTreeOid, repoName]); - - // URL → tree stack sync for browser back/forward - const urlDirPath = searchParams.get("path") ?? ""; - - useEffect(() => { - const curPath = treeStack.slice(1).map((s) => s.name).join("/"); - if (!rootTreeOid || urlDirPath === curPath) return; - setViewingFile(null); - if (!urlDirPath) { - setTreeStack([{ oid: rootTreeOid, name: repoName }]); - } else { - (async () => { - const segments = urlDirPath.split("/"); - const stack = [{ oid: rootTreeOid, name: repoName }]; - let curTree = rootTreeOid; - for (const seg of segments) { - try { - const sub = await (client as any).gitTreeEntryByPath(projectName, repoName, curTree, { path: seg }); - if (sub.data.entry && sub.data.entry.kind === "tree") { - stack.push({ oid: sub.data.entry.oid, name: seg }); - curTree = sub.data.entry.oid; - } else break; - } catch { break; } - } - if (stack.length > 1) setTreeStack(stack); - })(); - } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [urlDirPath, rootTreeOid]); - - // Current tree OID from stack top - const currentTreeOid = treeStack.length > 0 ? treeStack[treeStack.length - 1].oid : null; - // Current directory path (skip root: repoName) - const currentDirPath = treeStack.slice(1).map((s) => s.name).join("/"); - - // Fast tree entries (no last commit info) - const { data: fastEntries } = useQuery({ - queryKey: ["repo", projectName, repoName, "tree", currentTreeOid, currentDirPath, "fast"], - queryFn: async () => { - const res = await (client as any).gitTreeEntries(projectName, repoName, currentTreeOid!, { path: currentDirPath || undefined }); - return res.data.entries; - }, - enabled: Boolean(currentTreeOid), - retry: false, - }); - - // Full tree entries (with last commit info, may be async) - const { data: fullEntries } = useQuery({ - queryKey: ["repo", projectName, repoName, "tree", currentTreeOid, currentDirPath, "full"], - queryFn: async () => { - const res = await (client as any).gitTreeEntries(projectName, repoName, currentTreeOid!, { path: currentDirPath || undefined, last: true } as any); - return res.data.entries; - }, - enabled: Boolean(currentTreeOid), - retry: false, - // Backend returns entries immediately but enriches commit messages async. - // Poll until enrichment is done (cached result includes commit messages). - refetchInterval(query) { - const data = query.state.data; - if (!data || data.length === 0) return false; - const hasCommit = data.some((e: any) => e.last_commit_message); - return hasCommit ? false : 2000; - }, - }); - - const displayEntries = (fullEntries ?? fastEntries) ?? []; - - // Latest commit for selected branch - const { data: latestCommit } = useQuery({ - queryKey: ["repo", projectName, repoName, "latest-commit", selectedBranch], - queryFn: async () => { - const res = await client.gitCommitHistory(projectName, repoName, { branch: selectedBranch, limit: 1 }); - return res.data.commits[0] ?? null; - }, - enabled: Boolean(projectName) && Boolean(repoName), - retry: false, - }); - - // Blob content for viewed file - const { data: blobData, isLoading: blobLoading } = useQuery({ - queryKey: ["repo", projectName, repoName, "blob", viewingFile?.oid], - queryFn: async () => { - const res = await (client as any).gitBlobInfo(projectName, repoName, viewingFile!.oid, { path: viewingFile!.name } as any); - return res.data; - }, - enabled: Boolean(viewingFile) && !viewingFile?.isBinary, - retry: false, - }); - - // Blame lines - const { data: blameLines } = useQuery({ - queryKey: ["repo", projectName, repoName, "blame", viewingFile?.name], - queryFn: async () => { - const res: any = await client.gitBlameFile(projectName, repoName, { path: viewingFile!.name, start_line: 1, end_line: 999999 }); - return res.data?.lines as { commit_oid?: string | null; content: string; line_no: number }[] | undefined; - }, - enabled: Boolean(showBlame && viewingFile && !viewingFile?.isBinary), - retry: false, - }); - - // Commit info for blame (unique OIDs) - const blameOids = blameLines ? [...new Set(blameLines.map((l) => l.commit_oid).filter(Boolean))] : []; - const { data: blameCommits } = useQuery({ - queryKey: ["repo", projectName, repoName, "blame-commits", ...blameOids.slice(0, 10)], - queryFn: async () => { - const results: Record = {}; - for (const oid of blameOids.slice(0, 10)) { - if (oid) { - try { - const res = await (client as any).gitCommitInfo(projectName, repoName, oid); - results[oid] = res.data.commit?.author?.name ?? "?"; - } catch { results[oid] = "?"; } - } - } - return results; - }, - enabled: blameOids.length > 0, - retry: false, - }); - - // Language stats - const { data: languages = [] } = useQuery({ - queryKey: ["repo", projectName, repoName, "languages"], - queryFn: async () => { - const res = await client.gitGetLanguages(projectName, repoName); - return res.data; - }, - enabled: Boolean(projectName) && Boolean(repoName), - retry: false, - }); - - const handleBranchSelect = useCallback((name: string) => { - setSelectedBranch(name); - setBranchOpen(false); - }, []); - - const handleTreeNavigate = useCallback((entry: TreeEntryDto) => { - setViewingFile(null); - const path = [...treeStack.slice(1).map((s) => s.name), entry.name].join("/"); - setTreeStack((prev) => [...prev, { oid: entry.oid, name: entry.name }]); - setSearchParams(path ? { path } : {}, { replace: true }); - }, [treeStack, setSearchParams]); - - const handleFileView = useCallback((entry: TreeEntryDto) => { - setViewingFile({ oid: entry.oid, name: entry.name, isBinary: entry.is_binary }); - }, []); - - const handleBreadcrumbClick = useCallback((index: number) => { - setViewingFile(null); - const path = treeStack.slice(1, index + 1).map((s) => s.name).join("/"); - setTreeStack((prev) => prev.slice(0, index + 1)); - setSearchParams(path ? { path } : {}, { replace: true }); - }, [treeStack, setSearchParams]); - - const entries = displayEntries; - - // Stable skeleton widths — computed once at module level - const [skeletonWidths] = useState(() => Array.from({ length: 12 }, () => 30 + Math.floor(Math.random() * 60))); - - return ( -
- {/* Top action bar */} -
- {/* Branch dropdown */} -
- - {branchOpen && ( - <> -
setBranchOpen(false)} /> -
-
- Switch branch -
-
- {branchList && branchList.length > 0 ? branchList.map((b) => ( - - )) : ( -
No branches found
- )} -
-
- - )} -
- - {/* Clone section */} -
-
- - -
- - {cloneUrl} - - -
- - {/* Archive buttons */} - - -
- - {/* Language stats */} - {languages.length > 0 && ( -
- {languages.map((lang) => ( -
- ))} -
- )} - - {/* Latest commit bar */} - {latestCommit && ( -
- -
- - {latestCommit.summary} -
- - {latestCommit.author?.time_secs ? formatTimeAgo(latestCommit.author.time_secs) : ""} - - {latestCommit.oid.slice(0, 7)} -
- )} - - {/* Content area: file tree or file viewer */} -
- {/* Breadcrumb */} -
- {treeStack.map((item, i) => ( - - ))} - {viewingFile && ( - <> - - {viewingFile.name} - - - - )} -
- - {/* File viewer */} - {viewingFile ? ( - viewingFile.isBinary ? ( -
- -

Binary file

-

This file cannot be displayed in the browser.

-
- ) : blobLoading ? ( -
{skeletonWidths.map((w, i) =>
)}
- ) : blobData ? ( -
- {showBlame && blameLines ? ( - - - {blameLines.map((line, i) => ( - - - {line.commit_oid && ( - - )} - - - ))} - -
- {line.line_no} - - {blameCommits?.[line.commit_oid] ?? line.commit_oid.slice(0, 6)} - {line.content}
- ) : ( -
{decodeBase64((blobData as any).blob)}
- )} -
- ) : ( -
-

Could not load file content

-
- ) - ) : entries.length > 0 ? ( - /* File tree */ -
- {entries - .slice() - .sort((a: any, b: any) => { - if (a.kind !== b.kind) return a.kind === "tree" ? -1 : 1; - return a.name.localeCompare(b.name); - }) - .map((entry: any) => ( -
{ - if (entry.kind === "tree") handleTreeNavigate(entry); - else handleFileView(entry); - }} - > - - - {entry.name} - - {entry.is_lfs && ( - - LFS - - )} -
- {entry.last_commit_message && ( - {entry.last_commit_message} - )} - {entry.last_commit_author_name && ( - {entry.last_commit_author_name} - )} - {entry.last_commit_time && ( - {formatTimeAgoEntry(entry.last_commit_time)} - )} -
-
- ))} -
- ) : currentTreeOid ? ( -
-
- -
-

Empty directory

-

This directory contains no files.

-
- ) : ( -
-
- -
-

No content yet

-

- This repository is empty. Push your first commit to get started. -

-
- git push -u origin {selectedBranch} - -
-
- )} -
- - {/* Language legend */} - {languages.length > 1 && ( -
- {languages.map((lang) => ( - - - {lang.language} - {lang.percentage.toFixed(1)}% - - ))} -
- )} - -
- ); -} \ No newline at end of file + return ; +} diff --git a/src/page/workspace/repo/commit-detail.tsx b/src/page/workspace/repo/commit-detail.tsx index 50404e4..c052286 100644 --- a/src/page/workspace/repo/commit-detail.tsx +++ b/src/page/workspace/repo/commit-detail.tsx @@ -103,8 +103,8 @@ export default function CommitDetailPage() { {(diffData as any)?.stats && (
{ (diffData as any).stats.files_changed} files - { (diffData as any).stats.insertions} - { (diffData as any).stats.deletions} + { (diffData as any).stats.insertions} + { (diffData as any).stats.deletions}
)} @@ -150,11 +150,11 @@ export default function CommitDetailPage() { {selectedDelta.lines.map((line: any, j: number) => ( - + - + ))} diff --git a/src/page/workspace/repo/commits.tsx b/src/page/workspace/repo/commits.tsx index 07bcd14..c30833f 100644 --- a/src/page/workspace/repo/commits.tsx +++ b/src/page/workspace/repo/commits.tsx @@ -1,147 +1,7 @@ -import { useState } from "react"; -import { Link, useParams, useSearchParams } from "react-router"; -import { useQuery } from "@tanstack/react-query"; -import { client } from "@/client"; -import { GitCommitHorizontal, Search } from "lucide-react"; -import { Input } from "@/components/ui/input"; -import { cn } from "@/lib/utils"; -import { workspaceColor, workspaceInitial } from "@/components/shell/shared"; -import PaginationBar from "./pagination"; - -function formatTimeAgo(timeSecs: number) { - const now = Math.floor(Date.now() / 1000); - const diff = now - timeSecs; - if (diff < 60) return "just now"; - if (diff < 3600) return `${Math.floor(diff / 60)}m ago`; - if (diff < 86400) return `${Math.floor(diff / 3600)}h ago`; - if (diff < 2592000) return `${Math.floor(diff / 86400)}d ago`; - return new Date(timeSecs * 1000).toLocaleDateString("en-US", { month: "short", day: "numeric" }); -} - -function AuthorAvatar({ author }: { author: { name: string } }) { - return ( - - {workspaceInitial(author.name)} - - ); -} - -const LIMIT = 30; +import { useParams } from "react-router"; +import CommitsPanel from "@/components/repo/commits-panel"; export default function CommitsTab() { const { projectName = "", repoName = "" } = useParams(); - const [commitsSearchParams, setCommitsSearchParams] = useSearchParams(); - const offset = Number(commitsSearchParams.get("offset") ?? "0"); - const setOffset = (n: number) => { - const next = new URLSearchParams(commitsSearchParams); - if (n <= 0) next.delete("offset"); else next.set("offset", String(n)); - setCommitsSearchParams(next, { replace: true }); - }; - - const { data: repoData } = useQuery({ - queryKey: ["repo", projectName, repoName], - queryFn: async () => { - const res = await client.gitGetRepo(projectName, repoName); - return res.data; - }, - enabled: Boolean(projectName) && Boolean(repoName), - retry: false, - }); - - const defaultBranch = repoData?.default_branch ?? "main"; - const [search, setSearch] = useState(""); - const [selectedBranch, setSelectedBranch] = useState(defaultBranch); - - const { data: commits = [], isLoading } = useQuery({ - queryKey: ["repo", projectName, repoName, "commits", selectedBranch, offset], - queryFn: async () => { - const res = await client.gitCommitHistory(projectName, repoName, { branch: selectedBranch, skip: offset, limit: LIMIT }); - return res.data.commits; - }, - enabled: Boolean(projectName) && Boolean(repoName), - retry: false, - }); - - const { data: branches = [] } = useQuery({ - queryKey: ["repo", projectName, repoName, "branches"], - queryFn: async () => { - const res = await client.gitListBranches(projectName, repoName); - return res.data.branches; - }, - enabled: Boolean(projectName) && Boolean(repoName), - retry: false, - }); - - const filtered = search - ? commits.filter((c) => c.message.toLowerCase().includes(search.toLowerCase()) || c.oid.startsWith(search)) - : commits; - - return ( -
-
- -
- - setSearch(e.target.value)} /> -
-
- -
- {isLoading ? ( -
- {Array.from({ length: 8 }).map((_, i) =>
)} -
- ) : filtered.length > 0 ? ( - <> -
- {filtered.map((commit) => ( - - -
- {commit.summary} -
-
- - {commit.author?.name} - {commit.author?.time_secs ? formatTimeAgo(commit.author.time_secs) : ""} - {commit.oid.slice(0, 7)} -
- - ))} -
- = LIMIT} onPrev={() => setOffset(Math.max(0, offset - LIMIT))} onNext={() => setOffset(offset + LIMIT)} /> - - ) : ( -
-
- -
-

- {search ? "No matching commits" : "No commits yet"} -

-

- {search ? `No commits match "${search}"` : "Push your first commit to this branch to get started."} -

-
- )} -
-
- ); -} \ No newline at end of file + return ; +} diff --git a/src/page/workspace/repo/contributors.tsx b/src/page/workspace/repo/contributors.tsx index a015e96..6f3ecfd 100644 --- a/src/page/workspace/repo/contributors.tsx +++ b/src/page/workspace/repo/contributors.tsx @@ -1,67 +1,7 @@ -import { useParams, useSearchParams } from "react-router"; -import { useQuery } from "@tanstack/react-query"; -import { client } from "@/client"; -import { Users, GitCommitHorizontal } from "lucide-react"; -import PaginationBar from "./pagination"; - -const LIMIT = 20; +import { useParams } from "react-router"; +import ContributorsPanel from "@/components/repo/contributors-panel"; export default function ContributorsTab() { const { projectName = "", repoName = "" } = useParams(); - const [contribSearchParams, setContribSearchParams] = useSearchParams(); - const offset = Number(contribSearchParams.get("offset") ?? "0"); - const setOffset = (n: number) => { - const next = new URLSearchParams(contribSearchParams); - if (n <= 0) next.delete("offset"); else next.set("offset", String(n)); - setContribSearchParams(next, { replace: true }); - }; - - const { data: contributors = [], isLoading } = useQuery({ - queryKey: ["repo", projectName, repoName, "contributors", offset], - queryFn: async () => { - const res = await client.gitListContributors(projectName, repoName, { params: { offset, limit: LIMIT } } as any); - return res.data; - }, - enabled: Boolean(projectName) && Boolean(repoName), - retry: false, - }); - - const sorted = [...contributors].sort((a, b) => b.commit_count - a.commit_count); - - return ( -
-
-
- Contributors -
- {isLoading ? ( -
{Array.from({ length: 5 }).map((_, i) =>
)}
- ) : sorted.length > 0 ? ( - <> -
- {sorted.map((c) => ( -
- -
- {c.name} - {c.email} -
-
- - {c.commit_count} commit{c.commit_count !== 1 ? "s" : ""} -
-
- ))} -
- = LIMIT} onPrev={() => setOffset(Math.max(0, offset - LIMIT))} onNext={() => setOffset(offset + LIMIT)} /> - - ) : ( -
- -

No contributors found

-
- )} -
-
- ); -} \ No newline at end of file + return ; +} diff --git a/src/page/workspace/repo/layout.tsx b/src/page/workspace/repo/layout.tsx index 7906fc4..aeaf2f3 100644 --- a/src/page/workspace/repo/layout.tsx +++ b/src/page/workspace/repo/layout.tsx @@ -101,7 +101,6 @@ export default function RepoLayout() { onSuccess: () => qc.invalidateQueries({ queryKey: ["repo", projectName, repoName, "watch"] }), }); - // Check if README exists const { data: readme } = useQuery({ queryKey: ["repo", projectName, repoName, "readme"], queryFn: async () => { @@ -147,24 +146,24 @@ export default function RepoLayout() {

{repo.name}

{repo.visibility === "private" ? ( - - Private + + Private ) : ( - - Public + + Public )} {repo.is_archived && ( - - Archived + + Archived )} {repo.is_mirror && ( - Mirror + Mirror )} {repo.is_template && ( - Template + Template )}
@@ -172,7 +171,7 @@ export default function RepoLayout() {

{repo.description}

)} -
+
default branch: {repo.default_branch} | {formatSize(repo.size_bytes)} @@ -180,33 +179,33 @@ export default function RepoLayout() { created {formatDate(repo.created_at)}
{/* Tab nav */} -
); -} \ No newline at end of file +} diff --git a/src/page/workspace/repo/pull-detail.tsx b/src/page/workspace/repo/pull-detail.tsx index a833d64..7e098ed 100644 --- a/src/page/workspace/repo/pull-detail.tsx +++ b/src/page/workspace/repo/pull-detail.tsx @@ -189,9 +189,9 @@ export default function PullDetailPage() { {pr.merged_at ? ( ) : isOpen ? ( - + ) : ( - + )}

{pr.title}

# {pr.number} @@ -201,7 +201,7 @@ export default function PullDetailPage() { {pr.author.display_name ?? pr.author.username} {pr.merged_at ? "merged" : pr.state === "closed" ? "closed" : "opened"} {formatDate(pr.created_at)} - {pr.draft && Draft} + {pr.draft && Draft} {pr.source_branch} → {pr.target_branch}
@@ -279,8 +279,8 @@ export default function PullDetailPage() {

Files changed

{ (diffData as any).stats.files_changed} files - { (diffData as any).stats.insertions} - { (diffData as any).stats.deletions} + { (diffData as any).stats.insertions} + { (diffData as any).stats.deletions}
{(diffData as any).deltas.map((delta: any, i: number) => { @@ -296,11 +296,11 @@ export default function PullDetailPage() {
{line.old_lineno ?? ""} {line.new_lineno ?? ""} {line.origin}{line.content}{line.content}
{delta.lines.map((line: any, j: number) => ( - + - + ))} @@ -358,9 +358,9 @@ export default function PullDetailPage() { {pr.merged_at ? ( Merged ) : pr.state === "open" ? ( - Open + Open ) : ( - Closed + Closed )} diff --git a/src/page/workspace/repo/pulls.tsx b/src/page/workspace/repo/pulls.tsx index 836cc25..75b52bc 100644 --- a/src/page/workspace/repo/pulls.tsx +++ b/src/page/workspace/repo/pulls.tsx @@ -61,9 +61,9 @@ function PullStateIcon({ pr }: { pr: PullRequestResponse }) { return ; } if (pr.state === "closed") { - return ; + return ; } - return ; + return ; } function PullRow({ pr, projectName, repoName }: { pr: PullRequestResponse; projectName: string; repoName: string }) { @@ -78,7 +78,7 @@ function PullRow({ pr, projectName, repoName }: { pr: PullRequestResponse; proje {pr.title} # {pr.number} {pr.draft && ( - + Draft )} @@ -132,16 +132,16 @@ export default function PullsTab() { return (
-
-
+
+
- + New PR
- setSearch(e.target.value)} /> + setSearch(e.target.value)} />
diff --git a/src/page/workspace/repo/settings.tsx b/src/page/workspace/repo/settings.tsx index a95b427..abb1913 100644 --- a/src/page/workspace/repo/settings.tsx +++ b/src/page/workspace/repo/settings.tsx @@ -239,7 +239,7 @@ export default function RepoSettingsTab() { {p.pattern} {p.require_pull_request && requires PR} {p.required_approvals > 0 ? `${p.required_approvals} approval(s)` : ""} -
@@ -254,7 +254,7 @@ export default function RepoSettingsTab() { {/* Danger Zone */}
-

Danger Zone

+

Danger Zone

Transfer ownership

diff --git a/src/page/workspace/repo/tags.tsx b/src/page/workspace/repo/tags.tsx index 12e9add..d9df741 100644 --- a/src/page/workspace/repo/tags.tsx +++ b/src/page/workspace/repo/tags.tsx @@ -1,114 +1,7 @@ -import { useState } from "react"; -import { useParams, useSearchParams } from "react-router"; -import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query"; -import { client } from "@/client"; -import { Button } from "@/components/ui/button"; -import { Input } from "@/components/ui/input"; -import { Tag, Plus, Trash2 } from "lucide-react"; -import PaginationBar from "./pagination"; - -const LIMIT = 20; +import { useParams } from "react-router"; +import TagsPanel from "@/components/repo/tags-panel"; export default function TagsTab() { const { projectName = "", repoName = "" } = useParams(); - const qc = useQueryClient(); - const [tagsSearchParams, setTagsSearchParams] = useSearchParams(); - const offset = Number(tagsSearchParams.get("offset") ?? "0"); - const setOffset = (n: number) => { - const next = new URLSearchParams(tagsSearchParams); - if (n <= 0) next.delete("offset"); else next.set("offset", String(n)); - setTagsSearchParams(next, { replace: true }); - }; - const [newName, setNewName] = useState(""); - const [newOid, setNewOid] = useState(""); - const [newMessage, setNewMessage] = useState(""); - const [showCreate, setShowCreate] = useState(false); - - const { data: tags = [], isLoading } = useQuery({ - queryKey: ["repo", projectName, repoName, "tags", offset], - queryFn: async () => { - const res = await client.gitListTags(projectName, repoName, { params: { offset, limit: LIMIT } } as any); - return (res.data as any).tags ?? []; - }, - enabled: Boolean(projectName) && Boolean(repoName), - retry: false, - }); - - const createTag = useMutation({ - mutationFn: async () => { - await client.gitInitTag(projectName, repoName, { name: newName, oid: newOid || undefined, message: newMessage || undefined } as any); - }, - onSuccess: () => { setShowCreate(false); setNewName(""); setNewOid(""); setNewMessage(""); qc.invalidateQueries({ queryKey: ["repo", projectName, repoName, "tags"] }); }, - }); - - const deleteTag = useMutation({ - mutationFn: async (name: string) => { await client.gitDeleteTag(projectName, repoName, { name } as any); }, - onSuccess: () => qc.invalidateQueries({ queryKey: ["repo", projectName, repoName, "tags"] }), - }); - - const sorted = [...tags].sort((a, b) => a.name.localeCompare(b.name)); - - return ( -
-
-

{tags.length} tag{tags.length !== 1 ? "s" : ""}

- -
- - {showCreate && ( -
- setNewName(e.target.value)} /> - setNewOid(e.target.value)} /> - setNewMessage(e.target.value)} /> -
- - -
-
- )} - -
- {isLoading ? ( -
{Array.from({ length: 3 }).map((_, i) =>
)}
- ) : sorted.length > 0 ? ( - <> -
- {sorted.map((tag) => ( -
- - {tag.name} - {tag.is_annotated && annotated} -
- {tag.oid.slice(0, 7)} - {tag.tagger && {tag.tagger}} - -
-
- ))} -
- = LIMIT} onPrev={() => setOffset(Math.max(0, offset - LIMIT))} onNext={() => setOffset(offset + LIMIT)} /> - - ) : ( -
-
- -
-

No tags yet

-

- Tags mark specific points in repository history, typically used for releases. -

- -
- )} -
-
- ); -} \ No newline at end of file + return ; +} diff --git a/src/page/workspace/repo/webhooks.tsx b/src/page/workspace/repo/webhooks.tsx index 8485ac9..b96aaeb 100644 --- a/src/page/workspace/repo/webhooks.tsx +++ b/src/page/workspace/repo/webhooks.tsx @@ -78,10 +78,10 @@ export default function WebhooksTab() {
{showForm && ( -
+
{editingId ? "Edit webhook" : "New webhook"} - +
@@ -95,7 +95,7 @@ export default function WebhooksTab() {
{EVENT_OPTIONS.map((event) => ( - ))} @@ -115,7 +115,7 @@ export default function WebhooksTab() {
)} -
+
{isLoading ? (
{Array.from({ length: 2 }).map((_, i) =>
)}
) : webhooks.length > 0 ? ( @@ -126,14 +126,14 @@ export default function WebhooksTab() {
{wh.url}
- + {wh.events.join(", ")} · {formatDate(wh.created_at)}
- - - + + +
{showDeliveries === wh.id && (
@@ -141,7 +141,7 @@ export default function WebhooksTab() { {deliveries.length > 0 ? (
{deliveries.slice(0, 10).map((d: any, i: number) => (
- + {d.status ?? d.response_status ?? "?"} {d.timestamp && {formatDate(d.timestamp)}} {d.http_status && HTTP {d.http_status}}
{line.old_lineno ?? ""} {line.new_lineno ?? ""} {line.origin}{line.content}{line.content}