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, 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"; import Fuse from "fuse.js"; 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[]; isLoading: boolean; 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; } // Helper function to get status badge variant and icon const getStatusBadge = (status: string | null) => { switch (status) { case "imported": return { variant: "secondary" as const, label: "Not Mirrored", icon: null }; case "mirroring": return { variant: "outline" as const, label: "Mirroring", icon: Clock }; case "mirrored": 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 }; } }; export function OrganizationList({ organizations, isLoading, filter, setFilter, onMirror, onIgnore, loadingOrgIds, onAddOrganization, onRefresh, }: OrganizationListProps) { const { giteaConfig } = useGiteaConfig(); // Helper function to construct Gitea organization URL const getGiteaOrgUrl = (organization: Organization): string | null => { if (!giteaConfig?.url) { return null; } // Only provide Gitea links for organizations that have been mirrored const validStatuses = ['mirroring', 'mirrored']; if (!validStatuses.includes(organization.status || '')) { return null; } // Use destinationOrg if available, otherwise use the organization name const orgName = organization.destinationOrg || organization.name; if (!orgName) { return null; } // Ensure the base URL doesn't have a trailing slash const baseUrl = giteaConfig.url.endsWith('/') ? giteaConfig.url.slice(0, -1) : giteaConfig.url; return `${baseUrl}/${orgName}`; }; const handleUpdateDestination = async (orgId: string, newDestination: string | null) => { // Call API to update organization destination const response = await fetch(`/api/organizations/${orgId}`, { method: "PATCH", headers: { "Content-Type": "application/json", }, body: JSON.stringify({ destinationOrg: newDestination, }), }); if (!response.ok) { const errorData = await response.json(); throw new Error(errorData.error || "Failed to update organization"); } // Refresh organizations data if (onRefresh) { await onRefresh(); } }; const hasAnyFilter = Object.values(filter).some( (val) => val?.toString().trim() !== "" ); const filteredOrganizations = useMemo(() => { let result = organizations; if (filter.membershipRole) { result = result.filter((org) => org.membershipRole === filter.membershipRole); } if (filter.status) { result = result.filter((org) => org.status === filter.status); } if (filter.searchTerm) { const fuse = new Fuse(result, { keys: ["name", "type"], threshold: 0.3, }); result = fuse.search(filter.searchTerm).map((res) => res.item); } return result; }, [organizations, filter]); return isLoading ? (
{Array.from({ length: 5 }).map((_, i) => ( ))}
) : filteredOrganizations.length === 0 ? (

No organizations found

{hasAnyFilter ? "Try adjusting your search or filter criteria." : "Add GitHub organizations to mirror their repositories."}

{hasAnyFilter ? ( ) : ( )}
) : (
{filteredOrganizations.map((org, index) => { const isLoading = loadingOrgIds.has(org.id ?? ""); const statusBadge = getStatusBadge(org.status); const StatusIcon = statusBadge.icon; return ( {/* Mobile Layout */}
{/* Header with org name and badges */}
{StatusIcon && } {statusBadge.label}
{org.membershipRole}
{org.repositoryCount} repos {/* Repository breakdown for mobile - only show non-zero counts */} {(() => { const parts = []; if (org.publicRepositoryCount && org.publicRepositoryCount > 0) { parts.push(`${org.publicRepositoryCount} pub`); } if (org.privateRepositoryCount && org.privateRepositoryCount > 0) { parts.push(`${org.privateRepositoryCount} priv`); } if (org.forkRepositoryCount && org.forkRepositoryCount > 0) { parts.push(`${org.forkRepositoryCount} fork`); } return parts.length > 0 ? ( ({parts.join(' | ')}) ) : null; })()}
{/* Destination override section */}
handleUpdateDestination(org.id!, newDestination)} isUpdating={isLoading} />
{/* Desktop Layout */}
{/* Header with org icon, name, role badge and status */}
{org.name} {org.membershipRole}
{/* Status badge */} {StatusIcon && } {statusBadge.label}
{/* Destination override section */}
handleUpdateDestination(org.id!, newDestination)} isUpdating={isLoading} />
{/* Repository statistics */}
{org.repositoryCount} {org.repositoryCount === 1 ? "repository" : "repositories"}
{/* Repository breakdown - only show non-zero counts */} {(() => { const counts = []; if (org.publicRepositoryCount && org.publicRepositoryCount > 0) { counts.push(`${org.publicRepositoryCount} public`); } if (org.privateRepositoryCount && org.privateRepositoryCount > 0) { counts.push(`${org.privateRepositoryCount} private`); } if (org.forkRepositoryCount && org.forkRepositoryCount > 0) { counts.push(`${org.forkRepositoryCount} ${org.forkRepositoryCount === 1 ? 'fork' : 'forks'}`); } return counts.length > 0 ? (
{counts.map((count, index) => ( 0 ? "border-l pl-3" : ""}> {count} ))}
) : null; })()}
{/* Mobile Actions */}
{org.status === "ignored" ? ( ) : ( <> {org.status === "imported" && ( )} {org.status === "mirroring" && ( )} {org.status === "mirrored" && ( )} {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 )}
{(() => { const giteaUrl = getGiteaOrgUrl(org); // Determine tooltip based on status and configuration let tooltip: string; if (!giteaConfig?.url) { tooltip = "Gitea not configured"; } else if (org.status === 'imported') { tooltip = "Organization not yet mirrored to Gitea"; } else if (org.status === 'failed') { tooltip = "Organization mirroring failed"; } else if (org.status === 'mirroring') { tooltip = "Organization is being mirrored to Gitea"; } else if (giteaUrl) { tooltip = "View on Gitea"; } else { tooltip = "Gitea organization not available"; } return giteaUrl ? ( ) : ( ); })()}
{/* Desktop Actions */}
{org.status === "ignored" ? ( ) : ( <> {org.status === "imported" && ( )} {org.status === "mirroring" && ( )} {org.status === "mirrored" && ( )} {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 )}
{(() => { const giteaUrl = getGiteaOrgUrl(org); // Determine tooltip based on status and configuration let tooltip: string; if (!giteaConfig?.url) { tooltip = "Gitea not configured"; } else if (org.status === 'imported') { tooltip = "Organization not yet mirrored to Gitea"; } else if (org.status === 'failed') { tooltip = "Organization mirroring failed"; } else if (org.status === 'mirroring') { tooltip = "Organization is being mirrored to Gitea"; } else if (giteaUrl) { tooltip = "View on Gitea"; } else { tooltip = "Gitea organization not available"; } return (
); })()}
); })}
); }