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 { 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"; interface OrganizationListProps { organizations: Organization[]; isLoading: boolean; filter: FilterParams; setFilter: (filter: FilterParams) => void; onMirror: ({ orgId }: { orgId: string }) => 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 }; default: return { variant: "secondary" as const, label: "Unknown", icon: null }; } }; export function OrganizationList({ organizations, isLoading, filter, setFilter, onMirror, 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 (
{org.name} {org.membershipRole}
{/* Destination override section */}
handleUpdateDestination(org.id!, newDestination)} isUpdating={isLoading} />
{StatusIcon && } {statusBadge.label}
{org.repositoryCount}{" "} {org.repositoryCount === 1 ? "repository" : "repositories"}
{/* Always render this section to prevent layout shift */}
{isLoading || (org.status === "mirroring" && org.publicRepositoryCount === undefined) ? ( <> ) : ( <> {org.publicRepositoryCount !== undefined ? (
{org.publicRepositoryCount} public ) : null} {org.privateRepositoryCount !== undefined && org.privateRepositoryCount > 0 ? (
{org.privateRepositoryCount} private ) : null} {org.forkRepositoryCount !== undefined && org.forkRepositoryCount > 0 ? (
{org.forkRepositoryCount} fork{org.forkRepositoryCount !== 1 ? 's' : ''} ) : null} {/* Show a placeholder if no counts are available to maintain height */} {org.publicRepositoryCount === undefined && org.privateRepositoryCount === undefined && org.forkRepositoryCount === undefined && ( Loading counts... )} )}
{org.status === "imported" && ( )} {org.status === "mirroring" && ( )} {org.status === "mirrored" && ( )} {org.status === "failed" && ( )}
{(() => { 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 ? ( ) : ( ); })()}
); })}
); }