diff --git a/src/components/organizations/Organization.tsx b/src/components/organizations/Organization.tsx index 04c5089..7482845 100644 --- a/src/components/organizations/Organization.tsx +++ b/src/components/organizations/Organization.tsx @@ -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 ({ org, role, @@ -248,10 +305,10 @@ export function Organization() { 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( (org) => - org.status !== "mirroring" && org.status !== "mirrored" && org.id + org.status !== "mirroring" && org.status !== "mirrored" && org.status !== "ignored" && org.id ); if (eligibleOrgs.length === 0) { @@ -652,6 +709,7 @@ export function Organization() { setFilter={setFilter} loadingOrgIds={loadingOrgIds} onMirror={handleMirrorOrg} + onIgnore={handleIgnoreOrg} onAddOrganization={() => setIsDialogOpen(true)} onRefresh={async () => { await fetchOrganizations(false); diff --git a/src/components/organizations/OrganizationsList.tsx b/src/components/organizations/OrganizationsList.tsx index 49ba534..d1cf295 100644 --- a/src/components/organizations/OrganizationsList.tsx +++ b/src/components/organizations/OrganizationsList.tsx @@ -2,7 +2,7 @@ import { useMemo } from "react"; import { Card } from "@/components/ui/card"; import { Button } from "@/components/ui/button"; 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 type { Organization } from "@/lib/db/schema"; import type { FilterParams } from "@/types/filter"; @@ -11,6 +11,14 @@ import { Skeleton } from "@/components/ui/skeleton"; import { cn } from "@/lib/utils"; import { MirrorDestinationEditor } from "./MirrorDestinationEditor"; import { useGiteaConfig } from "@/hooks/useGiteaConfig"; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuLabel, + DropdownMenuSeparator, + DropdownMenuTrigger, +} from "@/components/ui/dropdown-menu"; interface OrganizationListProps { organizations: Organization[]; @@ -18,6 +26,7 @@ interface OrganizationListProps { filter: FilterParams; setFilter: (filter: FilterParams) => void; onMirror: ({ orgId }: { orgId: string }) => Promise; + onIgnore?: ({ orgId, ignore }: { orgId: string; ignore: boolean }) => Promise; loadingOrgIds: Set; onAddOrganization?: () => void; onRefresh?: () => Promise; @@ -34,6 +43,8 @@ const getStatusBadge = (status: string | null) => { return { variant: "default" as const, label: "Mirrored", icon: Check }; case "failed": return { variant: "destructive" as const, label: "Failed", icon: AlertCircle }; + case "ignored": + return { variant: "outline" as const, label: "Ignored", icon: Ban }; default: return { variant: "secondary" as const, label: "Unknown", icon: null }; } @@ -45,6 +56,7 @@ export function OrganizationList({ filter, setFilter, onMirror, + onIgnore, loadingOrgIds, onAddOrganization, onRefresh, @@ -296,61 +308,95 @@ export function OrganizationList({ {/* Mobile Actions */}
- {org.status === "imported" && ( + {org.status === "ignored" ? ( - )} - - {org.status === "mirroring" && ( - - )} - - {org.status === "mirrored" && ( - + ) : ( + <> + {org.status === "imported" && ( + + )} + + {org.status === "mirroring" && ( + + )} + + {org.status === "mirrored" && ( + + )} + + {org.status === "failed" && ( + + )} + )} - {org.status === "failed" && ( - + {/* Dropdown menu for additional actions */} + {org.status !== "ignored" && org.status !== "mirroring" && ( + + + + + + org.id && onIgnore && onIgnore({ orgId: org.id, ignore: true })} + > + + Ignore Organization + + + )}
@@ -412,59 +458,92 @@ export function OrganizationList({ {/* Desktop Actions */}
- {org.status === "imported" && ( + {org.status === "ignored" ? ( - )} - - {org.status === "mirroring" && ( - - )} - - {org.status === "mirrored" && ( - + ) : ( + <> + {org.status === "imported" && ( + + )} + + {org.status === "mirroring" && ( + + )} + + {org.status === "mirrored" && ( + + )} + + {org.status === "failed" && ( + + )} + )} - {org.status === "failed" && ( - + {/* Dropdown menu for additional actions */} + {org.status !== "ignored" && org.status !== "mirroring" && ( + + + + + + org.id && onIgnore && onIgnore({ orgId: org.id, ignore: true })} + > + + Ignore Organization + + + )}
diff --git a/src/pages/api/organizations/[id]/status.ts b/src/pages/api/organizations/[id]/status.ts new file mode 100644 index 0000000..6cd3f41 --- /dev/null +++ b/src/pages/api/organizations/[id]/status.ts @@ -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); + } +} \ No newline at end of file