From d99f59798816919d73712de000a9147818270448 Mon Sep 17 00:00:00 2001 From: Arunavo Ray Date: Thu, 28 Aug 2025 12:58:58 +0530 Subject: [PATCH] Update the Ignore Repo --- CLAUDE.md | 3 +- src/components/repositories/Repository.tsx | 218 +++++++++++++++++- .../repositories/RepositoryTable.tsx | 168 ++++++++++---- src/pages/api/repositories/[id]/status.ts | 82 +++++++ 4 files changed, 422 insertions(+), 49 deletions(-) create mode 100644 src/pages/api/repositories/[id]/status.ts diff --git a/CLAUDE.md b/CLAUDE.md index f352b35..0673a92 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -234,4 +234,5 @@ Repositories can have the following statuses: ## Security Guidelines - **Confidentiality Guidelines**: - - Dont ever say Claude Code or generated with AI anyhwere. \ No newline at end of file + - Dont ever say Claude Code or generated with AI anyhwere. +- Never commit without the explicict ask \ No newline at end of file diff --git a/src/components/repositories/Repository.tsx b/src/components/repositories/Repository.tsx index c2f565a..efc9204 100644 --- a/src/components/repositories/Repository.tsx +++ b/src/components/repositories/Repository.tsx @@ -18,7 +18,7 @@ import { SelectValue, } from "../ui/select"; import { Button } from "@/components/ui/button"; -import { Search, RefreshCw, FlipHorizontal, RotateCcw, X, Filter } from "lucide-react"; +import { Search, RefreshCw, FlipHorizontal, RotateCcw, X, Filter, Ban, Check } from "lucide-react"; import type { MirrorRepoRequest, MirrorRepoResponse } from "@/types/mirror"; import { Drawer, @@ -210,10 +210,13 @@ export default function Repository() { return; } - // Filter out repositories that are already mirroring to avoid duplicate operations. also filter out mirrored (mirrored can be synced and not mirrored again) + // Filter out repositories that are already mirroring, mirrored, or ignored const eligibleRepos = repositories.filter( (repo) => - repo.status !== "mirroring" && repo.status !== "mirrored" && repo.id //not ignoring failed ones because we want to retry them if not mirrored. if mirrored, gitea fucnion handlers will silently ignore them + repo.status !== "mirroring" && + repo.status !== "mirrored" && + repo.status !== "ignored" && // Skip ignored repositories + repo.id ); if (eligibleRepos.length === 0) { @@ -400,6 +403,80 @@ export default function Repository() { } }; + const handleBulkSkip = async (skip: boolean) => { + if (selectedRepoIds.size === 0) return; + + const selectedRepos = repositories.filter(repo => repo.id && selectedRepoIds.has(repo.id)); + const eligibleRepos = skip + ? selectedRepos.filter(repo => + repo.status !== "ignored" && + repo.status !== "mirroring" && + repo.status !== "syncing" + ) + : selectedRepos.filter(repo => repo.status === "ignored"); + + if (eligibleRepos.length === 0) { + toast.info(`No eligible repositories to ${skip ? "ignore" : "include"} in selection`); + return; + } + + const repoIds = eligibleRepos.map(repo => repo.id as string); + + setLoadingRepoIds(prev => { + const newSet = new Set(prev); + repoIds.forEach(id => newSet.add(id)); + return newSet; + }); + + try { + // Update each repository's status + const newStatus = skip ? "ignored" : "imported"; + const promises = repoIds.map(repoId => + apiRequest<{ success: boolean; repository?: Repository; error?: string }>( + `/repositories/${repoId}/status`, + { + method: "PATCH", + data: { status: newStatus, userId: user?.id }, + } + ) + ); + + const results = await Promise.allSettled(promises); + const successCount = results.filter(r => r.status === "fulfilled" && (r.value as any).success).length; + + if (successCount > 0) { + toast.success(`${successCount} repositories ${skip ? "ignored" : "included"}`); + + // Update local state for successful updates + const successfulRepoIds = new Set(); + results.forEach((result, index) => { + if (result.status === "fulfilled" && (result.value as any).success) { + successfulRepoIds.add(repoIds[index]); + } + }); + + setRepositories(prevRepos => + prevRepos.map(repo => { + if (repo.id && successfulRepoIds.has(repo.id)) { + return { ...repo, status: newStatus as any }; + } + return repo; + }) + ); + + setSelectedRepoIds(new Set()); + } + + if (successCount < repoIds.length) { + toast.error(`Failed to ${skip ? "ignore" : "include"} ${repoIds.length - successCount} repositories`); + } + } catch (error) { + showErrorToast(error, toast); + } finally { + setLoadingRepoIds(new Set()); + } + }; + const handleSyncRepo = async ({ repoId }: { repoId: string }) => { try { if (!user || !user.id) { @@ -440,6 +517,58 @@ export default function Repository() { } }; + const handleSkipRepo = async ({ repoId, skip }: { repoId: string; skip: boolean }) => { + try { + if (!user || !user.id) { + return; + } + + // Check if repository is currently being processed + const repo = repositories.find(r => r.id === repoId); + if (skip && repo && (repo.status === "mirroring" || repo.status === "syncing")) { + toast.warning("Cannot skip repository while it's being processed"); + return; + } + + // Set loading state + setLoadingRepoIds(prev => { + const newSet = new Set(prev); + newSet.add(repoId); + return newSet; + }); + + const newStatus = skip ? "ignored" : "imported"; + + // Update repository status via API + const response = await apiRequest<{ success: boolean; repository?: Repository; error?: string }>( + `/repositories/${repoId}/status`, + { + method: "PATCH", + data: { status: newStatus, userId: user.id }, + } + ); + + if (response.success && response.repository) { + toast.success(`Repository ${skip ? "ignored" : "included"}`); + setRepositories(prevRepos => + prevRepos.map(repo => + repo.id === repoId ? response.repository! : repo + ) + ); + } else { + showErrorToast(response.error || `Error ${skip ? "ignoring" : "including"} repository`, toast); + } + } catch (error) { + showErrorToast(error, toast); + } finally { + setLoadingRepoIds(prev => { + const newSet = new Set(prev); + newSet.delete(repoId); + return newSet; + }); + } + }; + const handleRetryRepoAction = async ({ repoId }: { repoId: string }) => { try { if (!user || !user.id) { @@ -543,7 +672,6 @@ export default function Repository() { if (selectedRepoIds.size === 0) return []; const selectedRepos = repositories.filter(repo => repo.id && selectedRepoIds.has(repo.id)); - const statuses = new Set(selectedRepos.map(repo => repo.status)); const actions = []; @@ -562,10 +690,35 @@ export default function Repository() { actions.push('retry'); } + // Check if any selected repos can be ignored + if (selectedRepos.some(repo => repo.status !== "ignored")) { + actions.push('ignore'); + } + + // Check if any selected repos can be included (unignored) + if (selectedRepos.some(repo => repo.status === "ignored")) { + actions.push('include'); + } + return actions; }; const availableActions = getAvailableActions(); + + // Get counts for eligible repositories for each action + const getActionCounts = () => { + const selectedRepos = repositories.filter(repo => repo.id && selectedRepoIds.has(repo.id)); + + return { + mirror: selectedRepos.filter(repo => repo.status === "imported" || repo.status === "failed").length, + sync: selectedRepos.filter(repo => repo.status === "mirrored" || repo.status === "synced").length, + retry: selectedRepos.filter(repo => repo.status === "failed").length, + ignore: selectedRepos.filter(repo => repo.status !== "ignored").length, + include: selectedRepos.filter(repo => repo.status === "ignored").length, + }; + }; + + const actionCounts = getActionCounts(); // Check if any filters are active const hasActiveFilters = !!(filter.owner || filter.organization || filter.status); @@ -867,7 +1020,7 @@ export default function Repository() { disabled={loadingRepoIds.size > 0} > - Mirror ({selectedRepoIds.size}) + Mirror ({actionCounts.mirror}) )} @@ -879,7 +1032,7 @@ export default function Repository() { disabled={loadingRepoIds.size > 0} > - Sync ({selectedRepoIds.size}) + Sync ({actionCounts.sync}) )} @@ -894,6 +1047,30 @@ export default function Repository() { Retry )} + + {availableActions.includes('ignore') && ( + + )} + + {availableActions.includes('include') && ( + + )} )} @@ -926,7 +1103,7 @@ export default function Repository() { disabled={loadingRepoIds.size > 0} > - Mirror ({selectedRepoIds.size}) + Mirror ({actionCounts.mirror}) )} @@ -938,7 +1115,7 @@ export default function Repository() { disabled={loadingRepoIds.size > 0} > - Sync ({selectedRepoIds.size}) + Sync ({actionCounts.sync}) )} @@ -953,6 +1130,30 @@ export default function Repository() { Retry )} + + {availableActions.includes('ignore') && ( + + )} + + {availableActions.includes('include') && ( + + )} )} @@ -984,6 +1185,7 @@ export default function Repository() { onMirror={handleMirrorRepo} onSync={handleSyncRepo} onRetry={handleRetryRepoAction} + onSkip={handleSkipRepo} loadingRepoIds={loadingRepoIds} selectedRepoIds={selectedRepoIds} onSelectionChange={setSelectedRepoIds} diff --git a/src/components/repositories/RepositoryTable.tsx b/src/components/repositories/RepositoryTable.tsx index 72505d6..c16b9ee 100644 --- a/src/components/repositories/RepositoryTable.tsx +++ b/src/components/repositories/RepositoryTable.tsx @@ -1,7 +1,7 @@ import { useMemo, useRef } from "react"; import Fuse from "fuse.js"; import { useVirtualizer } from "@tanstack/react-virtual"; -import { FlipHorizontal, GitFork, RefreshCw, RotateCcw, Star, Lock } from "lucide-react"; +import { FlipHorizontal, GitFork, RefreshCw, RotateCcw, Star, Lock, Ban, Check, ChevronDown } from "lucide-react"; import { SiGithub, SiGitea } from "react-icons/si"; import type { Repository } from "@/lib/db/schema"; import { Button } from "@/components/ui/button"; @@ -19,6 +19,12 @@ import { import { InlineDestinationEditor } from "./InlineDestinationEditor"; import { Card, CardContent } from "@/components/ui/card"; import { Badge } from "@/components/ui/badge"; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuTrigger, +} from "@/components/ui/dropdown-menu"; interface RepositoryTableProps { repositories: Repository[]; @@ -29,6 +35,7 @@ interface RepositoryTableProps { onMirror: ({ repoId }: { repoId: string }) => Promise; onSync: ({ repoId }: { repoId: string }) => Promise; onRetry: ({ repoId }: { repoId: string }) => Promise; + onSkip: ({ repoId, skip }: { repoId: string; skip: boolean }) => Promise; loadingRepoIds: Set; selectedRepoIds: Set; onSelectionChange: (selectedIds: Set) => void; @@ -44,6 +51,7 @@ export default function RepositoryTable({ onMirror, onSync, onRetry, + onSkip, loadingRepoIds, selectedRepoIds, onSelectionChange, @@ -306,6 +314,31 @@ export default function RepositoryTable({ )} + {/* Ignore/Include button */} + {repo.status === "ignored" ? ( + + ) : ( + + )} + {/* External links */}
{/* Links */} @@ -754,54 +788,108 @@ function RepoActionButton({ onMirror, onSync, onRetry, + onSkip, }: { repo: { id: string; status: string }; isLoading: boolean; onMirror: () => void; onSync: () => void; onRetry: () => void; + onSkip: (skip: boolean) => void; }) { - let label = ""; - let icon = <>; - let onClick = () => {}; - let disabled = isLoading; - - if (repo.status === "failed") { - label = "Retry"; - icon = ; - onClick = onRetry; - } else if (["mirrored", "synced", "syncing"].includes(repo.status)) { - label = "Sync"; - icon = ; - onClick = onSync; - disabled ||= repo.status === "syncing"; - } else if (["imported", "mirroring"].includes(repo.status)) { - label = "Mirror"; - icon = ; - onClick = onMirror; - disabled ||= repo.status === "mirroring"; - } else { - return null; // unsupported status + // For ignored repos, show an "Include" action + if (repo.status === "ignored") { + return ( + + ); } + // For actionable statuses, show action + dropdown for skip + let primaryLabel = ""; + let primaryIcon = <>; + let primaryOnClick = () => {}; + let primaryDisabled = isLoading; + let showPrimaryAction = true; + + if (repo.status === "failed") { + primaryLabel = "Retry"; + primaryIcon = ; + primaryOnClick = onRetry; + } else if (["mirrored", "synced", "syncing"].includes(repo.status)) { + primaryLabel = "Sync"; + primaryIcon = ; + primaryOnClick = onSync; + primaryDisabled ||= repo.status === "syncing"; + } else if (["imported", "mirroring"].includes(repo.status)) { + primaryLabel = "Mirror"; + primaryIcon = ; + primaryOnClick = onMirror; + primaryDisabled ||= repo.status === "mirroring"; + } else { + showPrimaryAction = false; + } + + // If there's no primary action, just show ignore button + if (!showPrimaryAction) { + return ( + + ); + } + + // Show primary action with dropdown for skip option return ( - + +
+ + + + +
+ + onSkip(true)}> + + Ignore Repository + + +
); } \ No newline at end of file diff --git a/src/pages/api/repositories/[id]/status.ts b/src/pages/api/repositories/[id]/status.ts new file mode 100644 index 0000000..1b49343 --- /dev/null +++ b/src/pages/api/repositories/[id]/status.ts @@ -0,0 +1,82 @@ +import type { APIContext } from "astro"; +import { db, repositories } from "@/lib/db"; +import { eq, and } from "drizzle-orm"; +import { createSecureErrorResponse } from "@/lib/utils"; +import { repoStatusEnum } from "@/types/Repository"; + +export async function PATCH({ params, request }: APIContext) { + try { + const { id } = params; + const body = await request.json(); + const { status, userId } = body; + + if (!id || !userId) { + return new Response( + JSON.stringify({ + success: false, + error: "Repository ID and User ID are required", + }), + { + status: 400, + headers: { "Content-Type": "application/json" }, + } + ); + } + + // Validate the status + const validStatuses = repoStatusEnum.options; + if (!validStatuses.includes(status)) { + return new Response( + JSON.stringify({ + success: false, + error: `Invalid status. Must be one of: ${validStatuses.join(", ")}`, + }), + { + status: 400, + headers: { "Content-Type": "application/json" }, + } + ); + } + + // Update the repository status + const [updatedRepo] = await db + .update(repositories) + .set({ + status, + updatedAt: new Date() + }) + .where( + and( + eq(repositories.id, id), + eq(repositories.userId, userId) + ) + ) + .returning(); + + if (!updatedRepo) { + return new Response( + JSON.stringify({ + success: false, + error: "Repository not found or you don't have permission to update it", + }), + { + status: 404, + headers: { "Content-Type": "application/json" }, + } + ); + } + + return new Response( + JSON.stringify({ + success: true, + repository: updatedRepo, + }), + { + status: 200, + headers: { "Content-Type": "application/json" }, + } + ); + } catch (error) { + return createSecureErrorResponse(error); + } +} \ No newline at end of file