import React from 'react'; import { Icon } from '@/components/icon/Icon'; import { Button } from '@/components/ui/button'; import { ForgeEntityDetailView } from '@/components/views/forge'; import { ForgeCreateIssueDialog } from '@/components/views/forge/actions'; import { buildForgeProvider } from '@/lib/forge'; import { useRuntimeAPIs } from '@/hooks/useRuntimeAPIs'; import { useUIStore } from '@/stores/useUIStore'; import type { GitLabIssueSummary } from '@/lib/api/types'; import type { ForgeIssue } from '@/lib/forge'; import { useI18n } from '@/lib/i18n'; const issueLabelBadgeClass = 'inline-flex items-center rounded border border-border/60 bg-surface-elevated px-1.5 py-px typography-micro text-foreground'; /** * Open GitLab issues for the context panel's MR view. The list is fetched * lazily (the parent only mounts this component while the Issues tab is * active); selecting a row mounts the shared `ForgeEntityDetailView` for the * issue detail. Read-only by design — no create, update, or close actions. */ export const GitLabIssuesSection: React.FC<{ directory: string }> = ({ directory }) => { const { t } = useI18n(); const { gitlab } = useRuntimeAPIs(); const setSettingsDialogOpen = useUIStore((state) => state.setSettingsDialogOpen); const setSettingsPage = useUIStore((state) => state.setSettingsPage); // ---- Open issues list ---------------------------------------------------- const [issues, setIssues] = React.useState([]); const [listPage, setListPage] = React.useState(1); const [listHasMore, setListHasMore] = React.useState(false); const [listLoading, setListLoading] = React.useState(false); const [listLoadingMore, setListLoadingMore] = React.useState(false); const [listError, setListError] = React.useState(null); const [listNotConnected, setListNotConnected] = React.useState(false); const [retryToken, setRetryToken] = React.useState(0); // ---- Selected issue detail ------------------------------------------------ const [selectedNumber, setSelectedNumber] = React.useState(null); const [selectedUrl, setSelectedUrl] = React.useState(null); const issueProvider = React.useMemo(() => (gitlab ? buildForgeProvider('gitlab', { gitlab }) : null), [gitlab]); const [createOpen, setCreateOpen] = React.useState(false); const retry = React.useCallback(() => setRetryToken((value) => value + 1), []); const openGitLabSettings = React.useCallback(() => { setSettingsPage('git'); setSettingsDialogOpen(true); }, [setSettingsDialogOpen, setSettingsPage]); const handleIssueCreated = React.useCallback((issue: ForgeIssue) => { setSelectedNumber(issue.number); setSelectedUrl(issue.url ?? null); setRetryToken((value) => value + 1); }, []); // A different repository invalidates the previously loaded list and detail so // a stale repository's issues never leak into the new one. React.useEffect(() => { setIssues([]); setListPage(1); setListHasMore(false); setListLoading(false); setListLoadingMore(false); setListError(null); setListNotConnected(false); setSelectedNumber(null); setSelectedUrl(null); }, [directory]); React.useEffect(() => { if (!gitlab?.issuesList) { return; } let cancelled = false; setListLoading(true); setListError(null); setListNotConnected(false); void gitlab .issuesList(directory, { page: 1 }) .then((result) => { if (cancelled) { return; } if (result.connected === false) { setListNotConnected(true); return; } setIssues(result.issues ?? []); setListPage(result.page ?? 1); setListHasMore(Boolean(result.hasMore)); }) .catch((error) => { if (!cancelled) { setListError(error instanceof Error ? error.message : String(error)); } }) .finally(() => { if (!cancelled) { setListLoading(false); } }); return () => { cancelled = true; }; }, [directory, gitlab, retryToken]); const loadMore = React.useCallback(async () => { if (!gitlab?.issuesList || listLoadingMore || listLoading || !listHasMore) { return; } setListLoadingMore(true); try { const next = await gitlab.issuesList(directory, { page: listPage + 1 }); if (next.connected === false) { setListNotConnected(true); return; } setIssues((previous) => [...previous, ...(next.issues ?? [])]); setListPage(next.page ?? listPage + 1); setListHasMore(Boolean(next.hasMore)); } catch (error) { setListError(error instanceof Error ? error.message : String(error)); } finally { setListLoadingMore(false); } }, [directory, gitlab, listHasMore, listLoading, listLoadingMore, listPage]); const selectIssue = React.useCallback((item: GitLabIssueSummary) => { setSelectedNumber(item.number); setSelectedUrl(item.url); }, []); const backToIssues = React.useCallback(() => { setSelectedNumber(null); setSelectedUrl(null); }, []); if (selectedNumber !== null) { return (
{selectedUrl ? ( ) : null}
{issueProvider ? ( ) : null}
); } return (
{t('contextPanel.gitlabMr.issues.listSectionTitle')}
{issueProvider?.createIssue ? ( ) : null}
{!gitlab?.issuesList ? (
{t('contextPanel.gitlabMr.issues.empty')}
) : listNotConnected ? (
{t('contextPanel.gitlabMr.error.notConnected')}
) : listLoading ? (
{t('contextPanel.gitlabMr.loading')}
) : listError ? (
{t('contextPanel.gitlabMr.issues.error.loadFailed')}
{listError}
) : issues.length === 0 ? (
{t('contextPanel.gitlabMr.issues.empty')}
) : (
{issues.map((item) => ( ))} {listHasMore ? (
) : null}
)} {issueProvider?.createIssue ? ( ) : null}
); };