mirror of
https://github.com/RayLabsHQ/gitea-mirror.git
synced 2025-12-08 12:36:44 +03:00
Update the Ignore Repo
This commit is contained in:
@@ -235,3 +235,4 @@ Repositories can have the following statuses:
|
|||||||
|
|
||||||
- **Confidentiality Guidelines**:
|
- **Confidentiality Guidelines**:
|
||||||
- Dont ever say Claude Code or generated with AI anyhwere.
|
- Dont ever say Claude Code or generated with AI anyhwere.
|
||||||
|
- Never commit without the explicict ask
|
||||||
@@ -18,7 +18,7 @@ import {
|
|||||||
SelectValue,
|
SelectValue,
|
||||||
} from "../ui/select";
|
} from "../ui/select";
|
||||||
import { Button } from "@/components/ui/button";
|
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 type { MirrorRepoRequest, MirrorRepoResponse } from "@/types/mirror";
|
||||||
import {
|
import {
|
||||||
Drawer,
|
Drawer,
|
||||||
@@ -210,10 +210,13 @@ export default function Repository() {
|
|||||||
return;
|
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(
|
const eligibleRepos = repositories.filter(
|
||||||
(repo) =>
|
(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) {
|
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<string>();
|
||||||
|
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 }) => {
|
const handleSyncRepo = async ({ repoId }: { repoId: string }) => {
|
||||||
try {
|
try {
|
||||||
if (!user || !user.id) {
|
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 }) => {
|
const handleRetryRepoAction = async ({ repoId }: { repoId: string }) => {
|
||||||
try {
|
try {
|
||||||
if (!user || !user.id) {
|
if (!user || !user.id) {
|
||||||
@@ -543,7 +672,6 @@ export default function Repository() {
|
|||||||
if (selectedRepoIds.size === 0) return [];
|
if (selectedRepoIds.size === 0) return [];
|
||||||
|
|
||||||
const selectedRepos = repositories.filter(repo => repo.id && selectedRepoIds.has(repo.id));
|
const selectedRepos = repositories.filter(repo => repo.id && selectedRepoIds.has(repo.id));
|
||||||
const statuses = new Set(selectedRepos.map(repo => repo.status));
|
|
||||||
|
|
||||||
const actions = [];
|
const actions = [];
|
||||||
|
|
||||||
@@ -562,11 +690,36 @@ export default function Repository() {
|
|||||||
actions.push('retry');
|
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;
|
return actions;
|
||||||
};
|
};
|
||||||
|
|
||||||
const availableActions = getAvailableActions();
|
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
|
// Check if any filters are active
|
||||||
const hasActiveFilters = !!(filter.owner || filter.organization || filter.status);
|
const hasActiveFilters = !!(filter.owner || filter.organization || filter.status);
|
||||||
const activeFilterCount = [filter.owner, filter.organization, filter.status].filter(Boolean).length;
|
const activeFilterCount = [filter.owner, filter.organization, filter.status].filter(Boolean).length;
|
||||||
@@ -867,7 +1020,7 @@ export default function Repository() {
|
|||||||
disabled={loadingRepoIds.size > 0}
|
disabled={loadingRepoIds.size > 0}
|
||||||
>
|
>
|
||||||
<FlipHorizontal className="h-4 w-4 mr-2" />
|
<FlipHorizontal className="h-4 w-4 mr-2" />
|
||||||
Mirror ({selectedRepoIds.size})
|
Mirror ({actionCounts.mirror})
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
@@ -879,7 +1032,7 @@ export default function Repository() {
|
|||||||
disabled={loadingRepoIds.size > 0}
|
disabled={loadingRepoIds.size > 0}
|
||||||
>
|
>
|
||||||
<RefreshCw className="h-4 w-4 mr-2" />
|
<RefreshCw className="h-4 w-4 mr-2" />
|
||||||
Sync ({selectedRepoIds.size})
|
Sync ({actionCounts.sync})
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
@@ -894,6 +1047,30 @@ export default function Repository() {
|
|||||||
Retry
|
Retry
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{availableActions.includes('ignore') && (
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="default"
|
||||||
|
onClick={() => handleBulkSkip(true)}
|
||||||
|
disabled={loadingRepoIds.size > 0}
|
||||||
|
>
|
||||||
|
<Ban className="h-4 w-4 mr-2" />
|
||||||
|
Ignore
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{availableActions.includes('include') && (
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="default"
|
||||||
|
onClick={() => handleBulkSkip(false)}
|
||||||
|
disabled={loadingRepoIds.size > 0}
|
||||||
|
>
|
||||||
|
<Check className="h-4 w-4 mr-2" />
|
||||||
|
Include
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
@@ -926,7 +1103,7 @@ export default function Repository() {
|
|||||||
disabled={loadingRepoIds.size > 0}
|
disabled={loadingRepoIds.size > 0}
|
||||||
>
|
>
|
||||||
<FlipHorizontal className="h-4 w-4 mr-2" />
|
<FlipHorizontal className="h-4 w-4 mr-2" />
|
||||||
<span>Mirror </span>({selectedRepoIds.size})
|
<span>Mirror </span>({actionCounts.mirror})
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
@@ -938,7 +1115,7 @@ export default function Repository() {
|
|||||||
disabled={loadingRepoIds.size > 0}
|
disabled={loadingRepoIds.size > 0}
|
||||||
>
|
>
|
||||||
<RefreshCw className="h-4 w-4 mr-2" />
|
<RefreshCw className="h-4 w-4 mr-2" />
|
||||||
<span className="hidden sm:inline">Sync </span>({selectedRepoIds.size})
|
<span className="hidden sm:inline">Sync </span>({actionCounts.sync})
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
@@ -953,6 +1130,30 @@ export default function Repository() {
|
|||||||
Retry
|
Retry
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{availableActions.includes('ignore') && (
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
onClick={() => handleBulkSkip(true)}
|
||||||
|
disabled={loadingRepoIds.size > 0}
|
||||||
|
>
|
||||||
|
<Ban className="h-4 w-4 mr-2" />
|
||||||
|
Ignore
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{availableActions.includes('include') && (
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
onClick={() => handleBulkSkip(false)}
|
||||||
|
disabled={loadingRepoIds.size > 0}
|
||||||
|
>
|
||||||
|
<Check className="h-4 w-4 mr-2" />
|
||||||
|
Include
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@@ -984,6 +1185,7 @@ export default function Repository() {
|
|||||||
onMirror={handleMirrorRepo}
|
onMirror={handleMirrorRepo}
|
||||||
onSync={handleSyncRepo}
|
onSync={handleSyncRepo}
|
||||||
onRetry={handleRetryRepoAction}
|
onRetry={handleRetryRepoAction}
|
||||||
|
onSkip={handleSkipRepo}
|
||||||
loadingRepoIds={loadingRepoIds}
|
loadingRepoIds={loadingRepoIds}
|
||||||
selectedRepoIds={selectedRepoIds}
|
selectedRepoIds={selectedRepoIds}
|
||||||
onSelectionChange={setSelectedRepoIds}
|
onSelectionChange={setSelectedRepoIds}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { useMemo, useRef } from "react";
|
import { useMemo, useRef } from "react";
|
||||||
import Fuse from "fuse.js";
|
import Fuse from "fuse.js";
|
||||||
import { useVirtualizer } from "@tanstack/react-virtual";
|
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 { SiGithub, SiGitea } from "react-icons/si";
|
||||||
import type { Repository } from "@/lib/db/schema";
|
import type { Repository } from "@/lib/db/schema";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
@@ -19,6 +19,12 @@ import {
|
|||||||
import { InlineDestinationEditor } from "./InlineDestinationEditor";
|
import { InlineDestinationEditor } from "./InlineDestinationEditor";
|
||||||
import { Card, CardContent } from "@/components/ui/card";
|
import { Card, CardContent } from "@/components/ui/card";
|
||||||
import { Badge } from "@/components/ui/badge";
|
import { Badge } from "@/components/ui/badge";
|
||||||
|
import {
|
||||||
|
DropdownMenu,
|
||||||
|
DropdownMenuContent,
|
||||||
|
DropdownMenuItem,
|
||||||
|
DropdownMenuTrigger,
|
||||||
|
} from "@/components/ui/dropdown-menu";
|
||||||
|
|
||||||
interface RepositoryTableProps {
|
interface RepositoryTableProps {
|
||||||
repositories: Repository[];
|
repositories: Repository[];
|
||||||
@@ -29,6 +35,7 @@ interface RepositoryTableProps {
|
|||||||
onMirror: ({ repoId }: { repoId: string }) => Promise<void>;
|
onMirror: ({ repoId }: { repoId: string }) => Promise<void>;
|
||||||
onSync: ({ repoId }: { repoId: string }) => Promise<void>;
|
onSync: ({ repoId }: { repoId: string }) => Promise<void>;
|
||||||
onRetry: ({ repoId }: { repoId: string }) => Promise<void>;
|
onRetry: ({ repoId }: { repoId: string }) => Promise<void>;
|
||||||
|
onSkip: ({ repoId, skip }: { repoId: string; skip: boolean }) => Promise<void>;
|
||||||
loadingRepoIds: Set<string>;
|
loadingRepoIds: Set<string>;
|
||||||
selectedRepoIds: Set<string>;
|
selectedRepoIds: Set<string>;
|
||||||
onSelectionChange: (selectedIds: Set<string>) => void;
|
onSelectionChange: (selectedIds: Set<string>) => void;
|
||||||
@@ -44,6 +51,7 @@ export default function RepositoryTable({
|
|||||||
onMirror,
|
onMirror,
|
||||||
onSync,
|
onSync,
|
||||||
onRetry,
|
onRetry,
|
||||||
|
onSkip,
|
||||||
loadingRepoIds,
|
loadingRepoIds,
|
||||||
selectedRepoIds,
|
selectedRepoIds,
|
||||||
onSelectionChange,
|
onSelectionChange,
|
||||||
@@ -306,6 +314,31 @@ export default function RepositoryTable({
|
|||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{/* Ignore/Include button */}
|
||||||
|
{repo.status === "ignored" ? (
|
||||||
|
<Button
|
||||||
|
size="default"
|
||||||
|
variant="outline"
|
||||||
|
onClick={() => repo.id && onSkip({ repoId: repo.id, skip: false })}
|
||||||
|
disabled={isLoading}
|
||||||
|
className="w-full h-10"
|
||||||
|
>
|
||||||
|
<Check className="h-4 w-4 mr-2" />
|
||||||
|
Include Repository
|
||||||
|
</Button>
|
||||||
|
) : (
|
||||||
|
<Button
|
||||||
|
size="default"
|
||||||
|
variant="ghost"
|
||||||
|
onClick={() => repo.id && onSkip({ repoId: repo.id, skip: true })}
|
||||||
|
disabled={isLoading}
|
||||||
|
className="w-full h-10"
|
||||||
|
>
|
||||||
|
<Ban className="h-4 w-4 mr-2" />
|
||||||
|
Ignore Repository
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* External links */}
|
{/* External links */}
|
||||||
<div className="flex gap-2">
|
<div className="flex gap-2">
|
||||||
<Button variant="outline" size="default" className="flex-1 h-10 min-w-0" asChild>
|
<Button variant="outline" size="default" className="flex-1 h-10 min-w-0" asChild>
|
||||||
@@ -645,6 +678,7 @@ export default function RepositoryTable({
|
|||||||
onMirror={() => onMirror({ repoId: repo.id ?? "" })}
|
onMirror={() => onMirror({ repoId: repo.id ?? "" })}
|
||||||
onSync={() => onSync({ repoId: repo.id ?? "" })}
|
onSync={() => onSync({ repoId: repo.id ?? "" })}
|
||||||
onRetry={() => onRetry({ repoId: repo.id ?? "" })}
|
onRetry={() => onRetry({ repoId: repo.id ?? "" })}
|
||||||
|
onSkip={(skip) => onSkip({ repoId: repo.id ?? "", skip })}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
{/* Links */}
|
{/* Links */}
|
||||||
@@ -754,54 +788,108 @@ function RepoActionButton({
|
|||||||
onMirror,
|
onMirror,
|
||||||
onSync,
|
onSync,
|
||||||
onRetry,
|
onRetry,
|
||||||
|
onSkip,
|
||||||
}: {
|
}: {
|
||||||
repo: { id: string; status: string };
|
repo: { id: string; status: string };
|
||||||
isLoading: boolean;
|
isLoading: boolean;
|
||||||
onMirror: () => void;
|
onMirror: () => void;
|
||||||
onSync: () => void;
|
onSync: () => void;
|
||||||
onRetry: () => void;
|
onRetry: () => void;
|
||||||
|
onSkip: (skip: boolean) => void;
|
||||||
}) {
|
}) {
|
||||||
let label = "";
|
// For ignored repos, show an "Include" action
|
||||||
let icon = <></>;
|
if (repo.status === "ignored") {
|
||||||
let onClick = () => {};
|
return (
|
||||||
let disabled = isLoading;
|
<Button
|
||||||
|
variant="outline"
|
||||||
if (repo.status === "failed") {
|
disabled={isLoading}
|
||||||
label = "Retry";
|
onClick={() => onSkip(false)}
|
||||||
icon = <RotateCcw className="h-4 w-4 mr-1" />;
|
className="min-w-[80px] justify-start"
|
||||||
onClick = onRetry;
|
>
|
||||||
} else if (["mirrored", "synced", "syncing"].includes(repo.status)) {
|
<Check className="h-4 w-4 mr-1" />
|
||||||
label = "Sync";
|
Include
|
||||||
icon = <RefreshCw className="h-4 w-4 mr-1" />;
|
</Button>
|
||||||
onClick = onSync;
|
);
|
||||||
disabled ||= repo.status === "syncing";
|
|
||||||
} else if (["imported", "mirroring"].includes(repo.status)) {
|
|
||||||
label = "Mirror";
|
|
||||||
icon = <FlipHorizontal className="h-4 w-4 mr-1" />;
|
|
||||||
onClick = onMirror;
|
|
||||||
disabled ||= repo.status === "mirroring";
|
|
||||||
} else {
|
|
||||||
return null; // unsupported status
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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 = <RotateCcw className="h-4 w-4" />;
|
||||||
|
primaryOnClick = onRetry;
|
||||||
|
} else if (["mirrored", "synced", "syncing"].includes(repo.status)) {
|
||||||
|
primaryLabel = "Sync";
|
||||||
|
primaryIcon = <RefreshCw className="h-4 w-4" />;
|
||||||
|
primaryOnClick = onSync;
|
||||||
|
primaryDisabled ||= repo.status === "syncing";
|
||||||
|
} else if (["imported", "mirroring"].includes(repo.status)) {
|
||||||
|
primaryLabel = "Mirror";
|
||||||
|
primaryIcon = <FlipHorizontal className="h-4 w-4" />;
|
||||||
|
primaryOnClick = onMirror;
|
||||||
|
primaryDisabled ||= repo.status === "mirroring";
|
||||||
|
} else {
|
||||||
|
showPrimaryAction = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If there's no primary action, just show ignore button
|
||||||
|
if (!showPrimaryAction) {
|
||||||
return (
|
return (
|
||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
disabled={disabled}
|
disabled={isLoading}
|
||||||
onClick={onClick}
|
onClick={() => onSkip(true)}
|
||||||
className="min-w-[80px] justify-start"
|
className="min-w-[80px] justify-start"
|
||||||
|
>
|
||||||
|
<Ban className="h-4 w-4 mr-1" />
|
||||||
|
Ignore
|
||||||
|
</Button>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Show primary action with dropdown for skip option
|
||||||
|
return (
|
||||||
|
<DropdownMenu>
|
||||||
|
<div className="flex">
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
disabled={primaryDisabled}
|
||||||
|
onClick={primaryOnClick}
|
||||||
|
className="min-w-[80px] justify-start rounded-r-none"
|
||||||
>
|
>
|
||||||
{isLoading ? (
|
{isLoading ? (
|
||||||
<>
|
<>
|
||||||
<RefreshCw className="h-4 w-4 animate-spin mr-1" />
|
<RefreshCw className="h-4 w-4 animate-spin mr-1" />
|
||||||
{label}
|
{primaryLabel}
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
{icon}
|
{primaryIcon}
|
||||||
{label}
|
<span className="ml-1">{primaryLabel}</span>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</Button>
|
</Button>
|
||||||
|
<DropdownMenuTrigger asChild>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
disabled={isLoading}
|
||||||
|
className="rounded-l-none px-2 border-l"
|
||||||
|
>
|
||||||
|
<ChevronDown className="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
</DropdownMenuTrigger>
|
||||||
|
</div>
|
||||||
|
<DropdownMenuContent align="end">
|
||||||
|
<DropdownMenuItem onClick={() => onSkip(true)}>
|
||||||
|
<Ban className="h-4 w-4 mr-2" />
|
||||||
|
Ignore Repository
|
||||||
|
</DropdownMenuItem>
|
||||||
|
</DropdownMenuContent>
|
||||||
|
</DropdownMenu>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
82
src/pages/api/repositories/[id]/status.ts
Normal file
82
src/pages/api/repositories/[id]/status.ts
Normal file
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user