import { useState } from "react"; import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query"; import { ExternalLink, Link2, RefreshCw, Search, Trash2, Loader2, Plus, X, } from "lucide-react"; import { listJiraLinks, searchJiraIssues, createJiraLink, syncJiraLink, deleteJiraLink, type JiraLink, type JiraLinkEntityType, type JiraIssueResult, } from "../api/jira"; import { useDebounce } from "../hooks/useDebounce"; interface JiraLinkPanelProps { entityType: JiraLinkEntityType; entityId: string; } const priorityColors: Record = { Highest: "text-red-400", High: "text-orange-400", Medium: "text-yellow-400", Low: "text-cyan-400", Lowest: "text-gray-400", }; const statusColors: Record = { "To Do": "bg-gray-700 text-gray-300", "In Progress": "bg-blue-900/50 text-blue-400", "Done": "bg-green-900/50 text-green-400", }; export default function JiraLinkPanel({ entityType, entityId }: JiraLinkPanelProps) { const queryClient = useQueryClient(); const [showSearch, setShowSearch] = useState(false); const [searchQuery, setSearchQuery] = useState(""); const debouncedQuery = useDebounce(searchQuery, 400); // ── Queries ───────────────────────────────────────────────────── const { data: links = [], isLoading: isLoadingLinks } = useQuery({ queryKey: ["jira-links", entityType, entityId], queryFn: () => listJiraLinks({ entity_type: entityType, entity_id: entityId }), }); const { data: searchResults = [], isFetching: isSearching } = useQuery({ queryKey: ["jira-search", debouncedQuery], queryFn: () => searchJiraIssues(debouncedQuery), enabled: debouncedQuery.length >= 2, }); // ── Mutations ─────────────────────────────────────────────────── const invalidate = () => { queryClient.invalidateQueries({ queryKey: ["jira-links", entityType, entityId], }); }; const linkMutation = useMutation({ mutationFn: (issueKey: string) => createJiraLink({ entity_type: entityType, entity_id: entityId, jira_issue_key: issueKey, }), onSuccess: () => { invalidate(); setShowSearch(false); setSearchQuery(""); }, }); const syncMutation = useMutation({ mutationFn: (linkId: string) => syncJiraLink(linkId), onSuccess: invalidate, }); const deleteMutation = useMutation({ mutationFn: (linkId: string) => deleteJiraLink(linkId), onSuccess: invalidate, }); // ── Render helpers ────────────────────────────────────────────── const formatDate = (dateStr: string | null) => { if (!dateStr) return null; return new Date(dateStr).toLocaleDateString("en-US", { month: "short", day: "numeric", hour: "2-digit", minute: "2-digit", }); }; const getStatusClass = (status: string | null) => { if (!status) return "bg-gray-700 text-gray-400"; return statusColors[status] || "bg-gray-700 text-gray-300"; }; return (

Jira

{/* Search panel */} {showSearch && (
setSearchQuery(e.target.value)} placeholder="Search Jira issues (e.g. SEC-1234 or keyword)..." className="w-full rounded-lg border border-gray-700 bg-gray-900 py-2 pl-10 pr-3 text-sm text-gray-200 placeholder-gray-500 focus:border-blue-500 focus:outline-none" autoFocus /> {isSearching && ( )}
{searchResults.length > 0 && (
{searchResults.map((issue: JiraIssueResult) => ( ))}
)} {debouncedQuery.length >= 2 && !isSearching && searchResults.length === 0 && (

No issues found

)}
)} {/* Linked issues */} {isLoadingLinks ? (
) : links.length === 0 ? (

No Jira issues linked

) : (
{links.map((link: JiraLink) => (
{link.jira_issue_key} {link.jira_status && ( {link.jira_status} )} {link.jira_priority && ( {link.jira_priority} )}
{link.jira_assignee && (

Assignee: {link.jira_assignee}

)} {link.last_synced_at && (

Synced: {formatDate(link.last_synced_at)}

)}
))}
)}
); }