mirror of
https://github.com/RayLabsHQ/gitea-mirror.git
synced 2025-12-08 20:46:44 +03:00
Org ignore
This commit is contained in:
@@ -196,6 +196,63 @@ export function Organization() {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleIgnoreOrg = async ({ orgId, ignore }: { orgId: string; ignore: boolean }) => {
|
||||||
|
try {
|
||||||
|
if (!user || !user.id) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const org = organizations.find(o => o.id === orgId);
|
||||||
|
|
||||||
|
// Check if organization is currently being processed
|
||||||
|
if (ignore && org && (org.status === "mirroring")) {
|
||||||
|
toast.warning("Cannot ignore organization while it's being processed");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setLoadingOrgIds((prev) => new Set(prev).add(orgId));
|
||||||
|
|
||||||
|
const newStatus = ignore ? "ignored" : "imported";
|
||||||
|
|
||||||
|
const response = await apiRequest<{ success: boolean; organization?: Organization; error?: string }>(
|
||||||
|
`/organizations/${orgId}/status`,
|
||||||
|
{
|
||||||
|
method: "PATCH",
|
||||||
|
data: {
|
||||||
|
status: newStatus,
|
||||||
|
userId: user.id
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
if (response.success) {
|
||||||
|
toast.success(ignore
|
||||||
|
? `Organization will be ignored in future operations`
|
||||||
|
: `Organization included for mirroring`
|
||||||
|
);
|
||||||
|
|
||||||
|
// Update local state
|
||||||
|
setOrganizations((prevOrgs) =>
|
||||||
|
prevOrgs.map((org) =>
|
||||||
|
org.id === orgId ? { ...org, status: newStatus } : org
|
||||||
|
)
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
toast.error(response.error || `Failed to ${ignore ? 'ignore' : 'include'} organization`);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
toast.error(
|
||||||
|
error instanceof Error ? error.message : `Error ${ignore ? 'ignoring' : 'including'} organization`
|
||||||
|
);
|
||||||
|
} finally {
|
||||||
|
setLoadingOrgIds((prev) => {
|
||||||
|
const newSet = new Set(prev);
|
||||||
|
newSet.delete(orgId);
|
||||||
|
return newSet;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const handleAddOrganization = async ({
|
const handleAddOrganization = async ({
|
||||||
org,
|
org,
|
||||||
role,
|
role,
|
||||||
@@ -248,10 +305,10 @@ export function Organization() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Filter out organizations that are already mirrored to avoid duplicate operations
|
// Filter out organizations that are already mirrored or ignored to avoid duplicate operations
|
||||||
const eligibleOrgs = organizations.filter(
|
const eligibleOrgs = organizations.filter(
|
||||||
(org) =>
|
(org) =>
|
||||||
org.status !== "mirroring" && org.status !== "mirrored" && org.id
|
org.status !== "mirroring" && org.status !== "mirrored" && org.status !== "ignored" && org.id
|
||||||
);
|
);
|
||||||
|
|
||||||
if (eligibleOrgs.length === 0) {
|
if (eligibleOrgs.length === 0) {
|
||||||
@@ -652,6 +709,7 @@ export function Organization() {
|
|||||||
setFilter={setFilter}
|
setFilter={setFilter}
|
||||||
loadingOrgIds={loadingOrgIds}
|
loadingOrgIds={loadingOrgIds}
|
||||||
onMirror={handleMirrorOrg}
|
onMirror={handleMirrorOrg}
|
||||||
|
onIgnore={handleIgnoreOrg}
|
||||||
onAddOrganization={() => setIsDialogOpen(true)}
|
onAddOrganization={() => setIsDialogOpen(true)}
|
||||||
onRefresh={async () => {
|
onRefresh={async () => {
|
||||||
await fetchOrganizations(false);
|
await fetchOrganizations(false);
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { useMemo } from "react";
|
|||||||
import { Card } from "@/components/ui/card";
|
import { Card } from "@/components/ui/card";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { Badge } from "@/components/ui/badge";
|
import { Badge } from "@/components/ui/badge";
|
||||||
import { Plus, RefreshCw, Building2, Check, AlertCircle, Clock } from "lucide-react";
|
import { Plus, RefreshCw, Building2, Check, AlertCircle, Clock, MoreVertical, Ban } from "lucide-react";
|
||||||
import { SiGithub, SiGitea } from "react-icons/si";
|
import { SiGithub, SiGitea } from "react-icons/si";
|
||||||
import type { Organization } from "@/lib/db/schema";
|
import type { Organization } from "@/lib/db/schema";
|
||||||
import type { FilterParams } from "@/types/filter";
|
import type { FilterParams } from "@/types/filter";
|
||||||
@@ -11,6 +11,14 @@ import { Skeleton } from "@/components/ui/skeleton";
|
|||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
import { MirrorDestinationEditor } from "./MirrorDestinationEditor";
|
import { MirrorDestinationEditor } from "./MirrorDestinationEditor";
|
||||||
import { useGiteaConfig } from "@/hooks/useGiteaConfig";
|
import { useGiteaConfig } from "@/hooks/useGiteaConfig";
|
||||||
|
import {
|
||||||
|
DropdownMenu,
|
||||||
|
DropdownMenuContent,
|
||||||
|
DropdownMenuItem,
|
||||||
|
DropdownMenuLabel,
|
||||||
|
DropdownMenuSeparator,
|
||||||
|
DropdownMenuTrigger,
|
||||||
|
} from "@/components/ui/dropdown-menu";
|
||||||
|
|
||||||
interface OrganizationListProps {
|
interface OrganizationListProps {
|
||||||
organizations: Organization[];
|
organizations: Organization[];
|
||||||
@@ -18,6 +26,7 @@ interface OrganizationListProps {
|
|||||||
filter: FilterParams;
|
filter: FilterParams;
|
||||||
setFilter: (filter: FilterParams) => void;
|
setFilter: (filter: FilterParams) => void;
|
||||||
onMirror: ({ orgId }: { orgId: string }) => Promise<void>;
|
onMirror: ({ orgId }: { orgId: string }) => Promise<void>;
|
||||||
|
onIgnore?: ({ orgId, ignore }: { orgId: string; ignore: boolean }) => Promise<void>;
|
||||||
loadingOrgIds: Set<string>;
|
loadingOrgIds: Set<string>;
|
||||||
onAddOrganization?: () => void;
|
onAddOrganization?: () => void;
|
||||||
onRefresh?: () => Promise<void>;
|
onRefresh?: () => Promise<void>;
|
||||||
@@ -34,6 +43,8 @@ const getStatusBadge = (status: string | null) => {
|
|||||||
return { variant: "default" as const, label: "Mirrored", icon: Check };
|
return { variant: "default" as const, label: "Mirrored", icon: Check };
|
||||||
case "failed":
|
case "failed":
|
||||||
return { variant: "destructive" as const, label: "Failed", icon: AlertCircle };
|
return { variant: "destructive" as const, label: "Failed", icon: AlertCircle };
|
||||||
|
case "ignored":
|
||||||
|
return { variant: "outline" as const, label: "Ignored", icon: Ban };
|
||||||
default:
|
default:
|
||||||
return { variant: "secondary" as const, label: "Unknown", icon: null };
|
return { variant: "secondary" as const, label: "Unknown", icon: null };
|
||||||
}
|
}
|
||||||
@@ -45,6 +56,7 @@ export function OrganizationList({
|
|||||||
filter,
|
filter,
|
||||||
setFilter,
|
setFilter,
|
||||||
onMirror,
|
onMirror,
|
||||||
|
onIgnore,
|
||||||
loadingOrgIds,
|
loadingOrgIds,
|
||||||
onAddOrganization,
|
onAddOrganization,
|
||||||
onRefresh,
|
onRefresh,
|
||||||
@@ -296,6 +308,19 @@ export function OrganizationList({
|
|||||||
{/* Mobile Actions */}
|
{/* Mobile Actions */}
|
||||||
<div className="flex flex-col gap-3 sm:hidden">
|
<div className="flex flex-col gap-3 sm:hidden">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
|
{org.status === "ignored" ? (
|
||||||
|
<Button
|
||||||
|
size="default"
|
||||||
|
variant="outline"
|
||||||
|
onClick={() => org.id && onIgnore && onIgnore({ orgId: org.id, ignore: false })}
|
||||||
|
disabled={isLoading}
|
||||||
|
className="w-full h-10"
|
||||||
|
>
|
||||||
|
<Check className="h-4 w-4 mr-2" />
|
||||||
|
Include Organization
|
||||||
|
</Button>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
{org.status === "imported" && (
|
{org.status === "imported" && (
|
||||||
<Button
|
<Button
|
||||||
size="default"
|
size="default"
|
||||||
@@ -352,6 +377,27 @@ export function OrganizationList({
|
|||||||
)}
|
)}
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Dropdown menu for additional actions */}
|
||||||
|
{org.status !== "ignored" && org.status !== "mirroring" && (
|
||||||
|
<DropdownMenu>
|
||||||
|
<DropdownMenuTrigger asChild>
|
||||||
|
<Button variant="ghost" size="icon" disabled={isLoading} className="h-10 w-10">
|
||||||
|
<MoreVertical className="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
</DropdownMenuTrigger>
|
||||||
|
<DropdownMenuContent align="end">
|
||||||
|
<DropdownMenuItem
|
||||||
|
onClick={() => org.id && onIgnore && onIgnore({ orgId: org.id, ignore: true })}
|
||||||
|
>
|
||||||
|
<Ban className="h-4 w-4 mr-2" />
|
||||||
|
Ignore Organization
|
||||||
|
</DropdownMenuItem>
|
||||||
|
</DropdownMenuContent>
|
||||||
|
</DropdownMenu>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex items-center gap-2 justify-center">
|
<div className="flex items-center gap-2 justify-center">
|
||||||
@@ -412,6 +458,18 @@ export function OrganizationList({
|
|||||||
{/* Desktop Actions */}
|
{/* Desktop Actions */}
|
||||||
<div className="hidden sm:flex items-center justify-between mt-4">
|
<div className="hidden sm:flex items-center justify-between mt-4">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
|
{org.status === "ignored" ? (
|
||||||
|
<Button
|
||||||
|
size="default"
|
||||||
|
variant="outline"
|
||||||
|
onClick={() => org.id && onIgnore && onIgnore({ orgId: org.id, ignore: false })}
|
||||||
|
disabled={isLoading}
|
||||||
|
>
|
||||||
|
<Check className="h-4 w-4 mr-2" />
|
||||||
|
Include Organization
|
||||||
|
</Button>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
{org.status === "imported" && (
|
{org.status === "imported" && (
|
||||||
<Button
|
<Button
|
||||||
size="default"
|
size="default"
|
||||||
@@ -466,6 +524,27 @@ export function OrganizationList({
|
|||||||
)}
|
)}
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Dropdown menu for additional actions */}
|
||||||
|
{org.status !== "ignored" && org.status !== "mirroring" && (
|
||||||
|
<DropdownMenu>
|
||||||
|
<DropdownMenuTrigger asChild>
|
||||||
|
<Button variant="ghost" size="icon" disabled={isLoading}>
|
||||||
|
<MoreVertical className="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
</DropdownMenuTrigger>
|
||||||
|
<DropdownMenuContent align="end">
|
||||||
|
<DropdownMenuItem
|
||||||
|
onClick={() => org.id && onIgnore && onIgnore({ orgId: org.id, ignore: true })}
|
||||||
|
>
|
||||||
|
<Ban className="h-4 w-4 mr-2" />
|
||||||
|
Ignore Organization
|
||||||
|
</DropdownMenuItem>
|
||||||
|
</DropdownMenuContent>
|
||||||
|
</DropdownMenu>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
|
|||||||
81
src/pages/api/organizations/[id]/status.ts
Normal file
81
src/pages/api/organizations/[id]/status.ts
Normal file
@@ -0,0 +1,81 @@
|
|||||||
|
import type { APIContext } from "astro";
|
||||||
|
import { db, organizations } from "@/lib/db";
|
||||||
|
import { eq, and } from "drizzle-orm";
|
||||||
|
import { createSecureErrorResponse } from "@/lib/utils";
|
||||||
|
|
||||||
|
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: "Organization ID and User ID are required",
|
||||||
|
}),
|
||||||
|
{
|
||||||
|
status: 400,
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate the status
|
||||||
|
const validStatuses = ["imported", "mirroring", "mirrored", "failed", "ignored"];
|
||||||
|
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 organization status
|
||||||
|
const [updatedOrg] = await db
|
||||||
|
.update(organizations)
|
||||||
|
.set({
|
||||||
|
status,
|
||||||
|
updatedAt: new Date()
|
||||||
|
})
|
||||||
|
.where(
|
||||||
|
and(
|
||||||
|
eq(organizations.id, id),
|
||||||
|
eq(organizations.userId, userId)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.returning();
|
||||||
|
|
||||||
|
if (!updatedOrg) {
|
||||||
|
return new Response(
|
||||||
|
JSON.stringify({
|
||||||
|
success: false,
|
||||||
|
error: "Organization 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,
|
||||||
|
organization: updatedOrg,
|
||||||
|
}),
|
||||||
|
{
|
||||||
|
status: 200,
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
}
|
||||||
|
);
|
||||||
|
} catch (error) {
|
||||||
|
return createSecureErrorResponse(error);
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user