import { useCallback, useEffect, useState } from "react"; import { Button } from "@/components/ui/button"; import { Search, RefreshCw, FlipHorizontal, Filter } from "lucide-react"; import type { MirrorJob, Organization } from "@/lib/db/schema"; import { OrganizationList } from "./OrganizationsList"; import AddOrganizationDialog from "./AddOrganizationDialog"; import { useAuth } from "@/hooks/useAuth"; import { apiRequest, showErrorToast } from "@/lib/utils"; import { membershipRoleEnum, type AddOrganizationApiRequest, type AddOrganizationApiResponse, type MembershipRole, type OrganizationsApiResponse, } from "@/types/organizations"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from "../ui/select"; import type { MirrorOrgRequest, MirrorOrgResponse } from "@/types/mirror"; import { useSSE } from "@/hooks/useSEE"; import { useFilterParams } from "@/hooks/useFilterParams"; import { toast } from "sonner"; import { useConfigStatus } from "@/hooks/useConfigStatus"; import { useNavigation } from "@/components/layout/MainLayout"; import { useLiveRefresh } from "@/hooks/useLiveRefresh"; import { Drawer, DrawerClose, DrawerContent, DrawerDescription, DrawerFooter, DrawerHeader, DrawerTitle, DrawerTrigger, } from "@/components/ui/drawer"; export function Organization() { const [organizations, setOrganizations] = useState([]); const [isLoading, setIsLoading] = useState(true); const [isDialogOpen, setIsDialogOpen] = useState(false); const { user } = useAuth(); const { isGitHubConfigured } = useConfigStatus(); const { navigationKey } = useNavigation(); const { registerRefreshCallback } = useLiveRefresh(); const { filter, setFilter } = useFilterParams({ searchTerm: "", membershipRole: "", status: "", }); const [loadingOrgIds, setLoadingOrgIds] = useState>(new Set()); // this is used when the api actions are performed // Create a stable callback using useCallback const handleNewMessage = useCallback((data: MirrorJob) => { if (data.organizationId) { setOrganizations((prevOrgs) => prevOrgs.map((org) => org.id === data.organizationId ? { ...org, status: data.status, details: data.details } : org ) ); } }, []); // Use the SSE hook const { connected } = useSSE({ userId: user?.id, onMessage: handleNewMessage, }); const fetchOrganizations = useCallback(async (isLiveRefresh = false) => { if (!user?.id) { return false; } // Don't fetch organizations if GitHub is not configured if (!isGitHubConfigured) { if (!isLiveRefresh) { setIsLoading(false); } return false; } try { if (!isLiveRefresh) { setIsLoading(true); } const response = await apiRequest( `/github/organizations?userId=${user.id}`, { method: "GET", } ); if (response.success) { setOrganizations(response.organizations); return true; } else { if (!isLiveRefresh) { toast.error(response.error || "Error fetching organizations"); } return false; } } catch (error) { if (!isLiveRefresh) { toast.error( error instanceof Error ? error.message : "Error fetching organizations" ); } return false; } finally { if (!isLiveRefresh) { setIsLoading(false); } } }, [user?.id, isGitHubConfigured]); // Only depend on user.id, not entire user object useEffect(() => { // Reset loading state when component becomes active setIsLoading(true); fetchOrganizations(false); // Manual refresh, not live }, [fetchOrganizations, navigationKey]); // Include navigationKey to trigger on navigation // Register with global live refresh system useEffect(() => { // Only register for live refresh if GitHub is configured if (!isGitHubConfigured) { return; } const unregister = registerRefreshCallback(() => { fetchOrganizations(true); // Live refresh }); return unregister; }, [registerRefreshCallback, fetchOrganizations, isGitHubConfigured]); const handleRefresh = async () => { const success = await fetchOrganizations(false); if (success) { toast.success("Organizations refreshed successfully."); } }; const handleMirrorOrg = async ({ orgId }: { orgId: string }) => { try { if (!user || !user.id) { return; } setLoadingOrgIds((prev) => new Set(prev).add(orgId)); const reqPayload: MirrorOrgRequest = { userId: user.id, organizationIds: [orgId], }; const response = await apiRequest("/job/mirror-org", { method: "POST", data: reqPayload, }); if (response.success) { toast.success(`Mirroring started for organization ID: ${orgId}`); setOrganizations((prevOrgs) => prevOrgs.map((org) => { const updated = response.organizations.find((o) => o.id === org.id); return updated ? updated : org; }) ); // Refresh organization data to get updated repository breakdown // Use a small delay to allow the backend to process the mirroring request setTimeout(() => { fetchOrganizations(true); }, 1000); } else { toast.error(response.error || "Error starting mirror job"); } } catch (error) { toast.error( error instanceof Error ? error.message : "Error starting mirror job" ); } finally { setLoadingOrgIds((prev) => { const newSet = new Set(prev); newSet.delete(orgId); return newSet; }); } }; 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, }: { org: string; role: MembershipRole; }) => { try { if (!user || !user.id) { return; } const reqPayload: AddOrganizationApiRequest = { userId: user.id, org, role, }; const response = await apiRequest( "/sync/organization", { method: "POST", data: reqPayload, } ); if (response.success) { toast.success(`Organization added successfully`); setOrganizations((prev) => [...prev, response.organization]); await fetchOrganizations(); setFilter((prev) => ({ ...prev, searchTerm: org, })); } else { showErrorToast(response.error || "Error adding organization", toast); } } catch (error) { showErrorToast(error, toast); } finally { setIsLoading(false); } }; const handleMirrorAllOrgs = async () => { try { if (!user || !user.id || organizations.length === 0) { return; } // 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.status !== "ignored" && org.id ); if (eligibleOrgs.length === 0) { toast.info("No eligible organizations to mirror"); return; } // Get all organization IDs const orgIds = eligibleOrgs.map((org) => org.id as string); // Set loading state for all organizations being mirrored setLoadingOrgIds((prev) => { const newSet = new Set(prev); orgIds.forEach((id) => newSet.add(id)); return newSet; }); const reqPayload: MirrorOrgRequest = { userId: user.id, organizationIds: orgIds, }; const response = await apiRequest("/job/mirror-org", { method: "POST", data: reqPayload, }); if (response.success) { toast.success(`Mirroring started for ${orgIds.length} organizations`); setOrganizations((prevOrgs) => prevOrgs.map((org) => { const updated = response.organizations.find((o) => o.id === org.id); return updated ? updated : org; }) ); } else { showErrorToast(response.error || "Error starting mirror jobs", toast); } } catch (error) { showErrorToast(error, toast); } finally { // Reset loading states - we'll let the SSE updates handle status changes setLoadingOrgIds(new Set()); } }; // Check if any filters are active const hasActiveFilters = !!(filter.membershipRole || filter.status); const activeFilterCount = [filter.membershipRole, filter.status].filter(Boolean).length; // Clear all filters const clearFilters = () => { setFilter({ searchTerm: filter.searchTerm, membershipRole: "", status: "", }); }; return (
{/* Search and filters */}
{/* Mobile: Search bar with filter button */}
setFilter((prev) => ({ ...prev, searchTerm: e.target.value })) } />
{/* Mobile Filter Drawer */} Filter Organizations Narrow down your organization list
{/* Active filters summary */} {hasActiveFilters && (
{activeFilterCount} filter{activeFilterCount > 1 ? 's' : ''} active
)} {/* Role Filter */}
{/* Status Filter */}
{/* Desktop: Original layout */}
setFilter((prev) => ({ ...prev, searchTerm: e.target.value })) } />
{/* Filter controls */}
{/* Membership Role Filter */} {/* Status Filter */}
{/* Action buttons */}
setIsDialogOpen(true)} onRefresh={async () => { await fetchOrganizations(false); }} />
); }