diff --git a/src/components/organizations/MirrorDestinationEditor.tsx b/src/components/organizations/MirrorDestinationEditor.tsx new file mode 100644 index 0000000..a6f6b3f --- /dev/null +++ b/src/components/organizations/MirrorDestinationEditor.tsx @@ -0,0 +1,193 @@ +import { useState } from "react"; +import { ArrowRight, Edit3, RotateCcw, CheckCircle2, XCircle, Building2 } from "lucide-react"; +import { Button } from "@/components/ui/button"; +import { Input } from "@/components/ui/input"; +import { Label } from "@/components/ui/label"; +import { + Popover, + PopoverContent, + PopoverTrigger, +} from "@/components/ui/popover"; +import { Badge } from "@/components/ui/badge"; +import { toast } from "sonner"; +import { cn } from "@/lib/utils"; + +interface MirrorDestinationEditorProps { + organizationId: string; + organizationName: string; + currentDestination?: string; + onUpdate: (newDestination: string | null) => Promise; + isUpdating?: boolean; + className?: string; +} + +export function MirrorDestinationEditor({ + organizationId, + organizationName, + currentDestination, + onUpdate, + isUpdating = false, + className, +}: MirrorDestinationEditorProps) { + const [isOpen, setIsOpen] = useState(false); + const [editValue, setEditValue] = useState(currentDestination || ""); + const [isLoading, setIsLoading] = useState(false); + + const hasOverride = currentDestination && currentDestination !== organizationName; + const effectiveDestination = currentDestination || organizationName; + + const handleSave = async () => { + const trimmedValue = editValue.trim(); + const newDestination = trimmedValue === "" || trimmedValue === organizationName + ? null + : trimmedValue; + + setIsLoading(true); + try { + await onUpdate(newDestination); + setIsOpen(false); + toast.success( + newDestination + ? `Destination updated to: ${newDestination}` + : "Destination reset to default" + ); + } catch (error) { + toast.error("Failed to update destination"); + } finally { + setIsLoading(false); + } + }; + + const handleReset = async () => { + setEditValue(""); + await handleSave(); + }; + + const handleCancel = () => { + setEditValue(currentDestination || ""); + setIsOpen(false); + }; + + return ( +
+
+ + {organizationName} + + + {effectiveDestination} + + {hasOverride && ( + + custom + + )} +
+ + + + + + +
+
+

Mirror Destination

+

+ Customize where this organization's repositories are mirrored to in Gitea. +

+
+ +
+ {/* Visual Preview */} +
+
Preview
+
+
+ + {organizationName} +
+ +
+ + + {editValue.trim() || organizationName} + +
+
+
+ + {/* Input Field */} +
+ + setEditValue(e.target.value)} + placeholder={organizationName} + className="h-8" + disabled={isLoading} + /> +

+ Leave empty to use the default GitHub organization name +

+
+ + {/* Quick Actions */} + {hasOverride && ( + + )} +
+ + {/* Action Buttons */} +
+ + +
+
+
+
+
+ ); +} \ No newline at end of file diff --git a/src/components/organizations/OrganizationsList.tsx b/src/components/organizations/OrganizationsList.tsx index d3a38a9..920c876 100644 --- a/src/components/organizations/OrganizationsList.tsx +++ b/src/components/organizations/OrganizationsList.tsx @@ -1,17 +1,15 @@ -import { useMemo, useState } from "react"; +import { useMemo } from "react"; import { Card } from "@/components/ui/card"; import { Button } from "@/components/ui/button"; import { Badge } from "@/components/ui/badge"; -import { Input } from "@/components/ui/input"; -import { Label } from "@/components/ui/label"; -import { Plus, RefreshCw, Building2, Check, AlertCircle, Clock, Settings, ArrowRight, Edit3, X, Save } from "lucide-react"; -import { toast } from "sonner"; +import { Plus, RefreshCw, Building2, Check, AlertCircle, Clock } from "lucide-react"; import { SiGithub } 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"; interface OrganizationListProps { organizations: Organization[]; @@ -50,60 +48,29 @@ export function OrganizationList({ onAddOrganization, onRefresh, }: OrganizationListProps) { - const [editingOrg, setEditingOrg] = useState(null); - const [editValue, setEditValue] = useState(""); - const [isUpdating, setIsUpdating] = useState(null); + 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, + }), + }); - const handleEditStart = (orgId: string, currentDestination?: string) => { - setEditingOrg(orgId); - setEditValue(currentDestination || ""); - }; + if (!response.ok) { + const errorData = await response.json(); + throw new Error(errorData.error || "Failed to update organization"); + } - const handleEditCancel = () => { - setEditingOrg(null); - setEditValue(""); - }; - - const handleEditSave = async (orgId: string) => { - setIsUpdating(orgId); - try { - // Call API to update organization destination - const response = await fetch(`/api/organizations/${orgId}`, { - method: "PATCH", - headers: { - "Content-Type": "application/json", - }, - body: JSON.stringify({ - destinationOrg: editValue.trim() || null, - }), - }); - - if (!response.ok) { - const errorData = await response.json(); - throw new Error(errorData.error || "Failed to update organization"); - } - - const result = await response.json(); - - // Close edit mode - setEditingOrg(null); - setEditValue(""); - - // Show success message - const destination = result.destinationOrg || "default"; - toast.success(`Organization destination updated to: ${destination}`); - - // Refresh organizations data - if (onRefresh) { - await onRefresh(); - } - } catch (error) { - console.error("Error updating organization:", error); - toast.error(error instanceof Error ? error.message : "Failed to update organization"); - } finally { - setIsUpdating(null); + // Refresh organizations data + if (onRefresh) { + await onRefresh(); } }; + const hasAnyFilter = Object.values(filter).some( (val) => val?.toString().trim() !== "" ); @@ -202,64 +169,13 @@ export function OrganizationList({ {/* Destination override section */}
- {editingOrg === org.id ? ( -
- -
- setEditValue(e.target.value)} - placeholder={org.name} - className="h-7 text-xs" - disabled={isUpdating === org.id} - /> - - -
-
- ) : ( -
-
- - - Mirrors to: - {org.destinationOrg || org.name} - - {org.destinationOrg && ( - (override) - )} - -
- -
- )} + handleUpdateDestination(org.id!, newDestination)} + isUpdating={isLoading} + />