import * as React from 'react'; import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter, } from '@/components/ui/dialog'; import { Input } from '@/components/ui/input'; import { Button } from '@/components/ui/button'; import { Checkbox } from '@/components/ui/checkbox'; import { cn } from '@/lib/utils'; import { getRegisteredRuntimeAPIs } from '@/contexts/runtimeAPIRegistry'; import { useProjectsStore } from '@/stores/useProjectsStore'; import { useUIStore } from '@/stores/useUIStore'; import { useGitLabAuthStore } from '@/stores/useGitLabAuthStore'; import { validateWorktreeCreate } from '@/lib/worktrees/worktreeManager'; import { SortableTabsStrip } from '@/components/ui/sortable-tabs-strip'; import { MobileOverlayPanel } from '@/components/ui/MobileOverlayPanel'; import { Icon } from "@/components/icon/Icon"; import { useDebouncedValue } from '@/hooks/useDebouncedValue'; import type { GitLabIssueSummary, GitLabMergeRequestSummary, } from '@/lib/api/types'; import type { ProjectRef } from '@/lib/worktrees/worktreeManager'; import { useI18n } from '@/lib/i18n'; type GitLabTab = 'issues' | 'mrs'; interface GitLabIntegrationDialogProps { open: boolean; onOpenChange: (open: boolean) => void; onSelect: (result: { type: 'issue'; number: number; title: string; url: string; } | { type: 'mr'; number: number; title: string; url: string; sourceBranch: string; includeDiff: boolean; } | null) => void; } interface ValidationResult { isValid: boolean; error: string | null; } export function GitLabIntegrationDialog({ open, onOpenChange, onSelect, }: GitLabIntegrationDialogProps) { const { t } = useI18n(); const isMobile = useUIStore((state) => state.isMobile); const gitlab = getRegisteredRuntimeAPIs()?.gitlab; const gitlabAuthStatus = useGitLabAuthStore((state) => state.status); const gitlabAuthChecked = useGitLabAuthStore((state) => state.hasChecked); const setSettingsDialogOpen = useUIStore((state) => state.setSettingsDialogOpen); const setSettingsPage = useUIStore((state) => state.setSettingsPage); const activeProject = useProjectsStore((state) => state.getActiveProject()); const projectDirectory = activeProject?.path ?? null; const projectRef: ProjectRef | null = React.useMemo(() => { if (projectDirectory && activeProject) { return { id: activeProject.id, path: projectDirectory }; } return null; }, [activeProject, projectDirectory]); // State const [activeTab, setActiveTab] = React.useState('issues'); const [searchQuery, setSearchQuery] = React.useState(''); const [issues, setIssues] = React.useState([]); const [mrs, setMrs] = React.useState([]); const [loading, setLoading] = React.useState(false); const [loadingMore, setLoadingMore] = React.useState(false); const [error, setError] = React.useState(null); const [selectedIssue, setSelectedIssue] = React.useState(null); const [selectedMr, setSelectedMr] = React.useState(null); const [includeDiff, setIncludeDiff] = React.useState(false); const [validations, setValidations] = React.useState>(new Map()); const [page, setPage] = React.useState(1); const [hasMore, setHasMore] = React.useState(false); const debouncedSearchQuery = useDebouncedValue(searchQuery, 350); const loadData = React.useCallback(async (query?: string) => { if (!projectDirectory || !gitlab) return; if (gitlabAuthChecked && gitlabAuthStatus?.connected === false) return; setLoading(true); setError(null); setPage(1); setHasMore(false); try { if (activeTab === 'issues' && gitlab.issuesList) { const result = await gitlab.issuesList(projectDirectory, { page: 1, query }); if (result.connected === false) { setError(t('session.gitlabIntegration.error.notConnected')); setIssues([]); } else { setIssues(result.issues ?? []); setPage(result.page ?? 1); setHasMore(Boolean(result.hasMore)); } } else if (activeTab === 'mrs' && gitlab.mrsList) { const result = await gitlab.mrsList(projectDirectory, { page: 1, query }); if (result.connected === false) { setError(t('session.gitlabIntegration.error.notConnected')); setMrs([]); } else { setMrs(result.mrs ?? []); setPage(result.page ?? 1); setHasMore(Boolean(result.hasMore)); } } } catch (err) { setError(err instanceof Error ? err.message : t('session.gitlabIntegration.error.loadDataFailed')); } finally { setLoading(false); } }, [projectDirectory, gitlab, gitlabAuthChecked, gitlabAuthStatus, activeTab, t]); React.useEffect(() => { if (!open || !projectDirectory) return; if (gitlabAuthChecked && gitlabAuthStatus?.connected === false) return; if (!gitlab) return; if (!debouncedSearchQuery.trim()) { void loadData(); return; } const controller = new AbortController(); setLoading(true); setError(null); setPage(1); setHasMore(false); const apiCall = activeTab === 'issues' && gitlab.issuesList ? gitlab.issuesList(projectDirectory, { page: 1, query: debouncedSearchQuery.trim() }) : activeTab === 'mrs' && gitlab.mrsList ? gitlab.mrsList(projectDirectory, { page: 1, query: debouncedSearchQuery.trim() }) : null; if (!apiCall) { setLoading(false); return; } apiCall .then((result) => { if (controller.signal.aborted) return; if ('issues' in result) { if (result.connected === false) { setError(t('session.gitlabIntegration.error.notConnected')); setIssues([]); } else { setIssues(result.issues ?? []); setPage(result.page ?? 1); setHasMore(Boolean(result.hasMore)); } } else if ('mrs' in result) { if (result.connected === false) { setError(t('session.gitlabIntegration.error.notConnected')); setMrs([]); } else { setMrs(result.mrs ?? []); setPage(result.page ?? 1); setHasMore(Boolean(result.hasMore)); } } }) .catch((err) => { if (controller.signal.aborted) return; setError(err instanceof Error ? err.message : t('session.gitlabIntegration.error.loadDataFailed')); }) .finally(() => { if (!controller.signal.aborted) setLoading(false); }); return () => controller.abort(); }, [open, projectDirectory, gitlab, gitlabAuthChecked, gitlabAuthStatus, activeTab, debouncedSearchQuery, loadData, t]); const loadMore = React.useCallback(async () => { if (!projectDirectory || !gitlab) return; if (loading || loadingMore) return; if (!hasMore) return; setLoadingMore(true); try { const nextPage = page + 1; if (activeTab === 'issues' && gitlab.issuesList) { const result = debouncedSearchQuery.trim() ? await gitlab.issuesList(projectDirectory, { page: nextPage, query: debouncedSearchQuery.trim() }) : await gitlab.issuesList(projectDirectory, { page: nextPage }); if (result.connected !== false) { setIssues(prev => [...prev, ...(result.issues ?? [])]); setPage(result.page ?? nextPage); setHasMore(Boolean(result.hasMore)); } } else if (activeTab === 'mrs' && gitlab.mrsList) { const result = debouncedSearchQuery.trim() ? await gitlab.mrsList(projectDirectory, { page: nextPage, query: debouncedSearchQuery.trim() }) : await gitlab.mrsList(projectDirectory, { page: nextPage }); if (result.connected !== false) { setMrs(prev => [...prev, ...(result.mrs ?? [])]); setPage(result.page ?? nextPage); setHasMore(Boolean(result.hasMore)); } } } catch { // Silently fail on load more errors } finally { setLoadingMore(false); } }, [projectDirectory, gitlab, activeTab, page, hasMore, loading, loadingMore, debouncedSearchQuery]); // Reset state when dialog opens/closes React.useEffect(() => { if (!open) { setActiveTab('issues'); setSearchQuery(''); setIssues([]); setMrs([]); setSelectedIssue(null); setSelectedMr(null); setIncludeDiff(false); setError(null); setValidations(new Map()); setPage(1); setHasMore(false); return; } void loadData(); }, [open, loadData]); // Validate branches for worktree creation const validateBranch = React.useCallback(async (branchName: string) => { if (!projectRef || !branchName) return; // Check cache first if (validations.has(branchName)) return; try { const result = await validateWorktreeCreate(projectRef, { mode: 'new', branchName, worktreeName: branchName, }); const blockingError = result.errors.find((entry) => entry.code === 'branch_in_use'); setValidations(prev => new Map(prev).set(branchName, { isValid: !blockingError, error: blockingError ? t(blockingError.code === 'branch_exists' ? 'session.gitlabIntegration.validation.branchAlreadyExists' : 'session.gitlabIntegration.validation.branchAlreadyCheckedOut') : null, })); } catch { setValidations(prev => new Map(prev).set(branchName, { isValid: false, error: t('session.gitlabIntegration.validation.failed'), })); } }, [projectRef, validations, t]); // Validate MR branches when loaded React.useEffect(() => { if (!open || activeTab !== 'mrs') return; mrs.forEach(mr => { if (mr.sourceBranch) { void validateBranch(mr.sourceBranch); } }); }, [open, activeTab, mrs, validateBranch]); // GitLab connection check const isGitLabConnected = gitlabAuthChecked && gitlabAuthStatus?.connected === true; const openGitLabSettings = () => { setSettingsPage('git'); setSettingsDialogOpen(true); }; // Handle selection const handleSelectIssue = (issue: GitLabIssueSummary) => { setSelectedIssue(issue); setSelectedMr(null); }; const handleSelectMr = (mr: GitLabMergeRequestSummary) => { setSelectedMr(mr); setSelectedIssue(null); }; const handleConfirm = () => { if (selectedIssue) { onSelect({ type: 'issue', number: selectedIssue.number, title: selectedIssue.title, url: selectedIssue.url, }); } else if (selectedMr) { onSelect({ type: 'mr', number: selectedMr.number, title: selectedMr.title, url: selectedMr.url, sourceBranch: selectedMr.sourceBranch, includeDiff, }); } onOpenChange(false); }; const handleClear = () => { setSelectedIssue(null); setSelectedMr(null); setIncludeDiff(false); }; // Check if selection is valid const canConfirm = selectedIssue || (selectedMr && validations.get(selectedMr.sourceBranch)?.isValid !== false); // Check if MR is blocked const isMrBlocked = (mr: GitLabMergeRequestSummary): boolean => { if (!mr.sourceBranch) return true; const validation = validations.get(mr.sourceBranch); return validation?.isValid === false; }; // Content for the dialog (shared between mobile and desktop) const dialogContent = ( <> {!isGitLabConnected ? (

{t('session.gitlabIntegration.connect.title')}

{t('session.gitlabIntegration.connect.description')}

) : ( <> {/* Search */}
setSearchQuery(e.target.value)} placeholder={activeTab === 'issues' ? t('session.gitlabIntegration.search.issuesPlaceholder') : t('session.gitlabIntegration.search.mrsPlaceholder')} className="h-8 pl-9" />
{/* List Content */}
{/* Loading */} {loading && (
)} {/* Error */} {error && (
{error}
)} {/* Issues List */} {!loading && !error && activeTab === 'issues' && (
{issues.length > 0 ? ( issues.map(issue => ( )) ) : (
{t('session.gitlabIntegration.empty.noIssuesFound')}
)} {hasMore && !loadingMore && (
)} {loadingMore && (
)}
)} {/* MRs List */} {!loading && !error && activeTab === 'mrs' && (
{mrs.length > 0 ? ( mrs.map(mr => { const blocked = isMrBlocked(mr); const validation = mr.sourceBranch ? validations.get(mr.sourceBranch) : undefined; return ( ); }) ) : (
{t('session.gitlabIntegration.empty.noMergeRequestsFound')}
)} {hasMore && !loadingMore && (
)} {loadingMore && (
)}
)}
)} ); // Footer content const footerContent = (
{/* Left side: Selected Item / Checkbox */}
{/* Selected Issue/MR display - hidden on mobile (shown in header instead) */} {!isMobile && (selectedIssue || selectedMr) && (
{selectedIssue ? t('session.gitlabIntegration.selected.issueNumber', { number: selectedIssue.number }) : t('session.gitlabIntegration.selected.mrNumber', { number: selectedMr?.number ?? '' })}
)} {/* Include Diff Checkbox - only show when MR tab is active and MR is selected */} {activeTab === 'mrs' && selectedMr && ( )}
{/* Right side: Buttons */}
); return ( <> {isMobile ? ( onOpenChange(false)} footer={!isGitLabConnected ? undefined : footerContent} renderHeader={(closeButton) => (

{t('session.gitlabIntegration.title')}

{closeButton}
{/* Tabs - using SortableTabsStrip */}
}, { id: 'mrs', label: t('session.gitlabIntegration.tabs.mergeRequests'), icon: }, ]} activeId={activeTab} onSelect={(id) => { setActiveTab(id as GitLabTab); setSearchQuery(''); }} variant="active-pill" layoutMode="fit" />
{/* Selected Item Inline Display */} {(selectedIssue || selectedMr) && (
{selectedIssue ? t('session.gitlabIntegration.selected.issueNumber', { number: selectedIssue.number }) : t('session.gitlabIntegration.selected.mrNumber', { number: selectedMr?.number ?? '' })}
)}
)} > {dialogContent}
) : (
{t('session.gitlabIntegration.title')} {/* Tabs - using SortableTabsStrip */}
}, { id: 'mrs', label: t('session.gitlabIntegration.tabs.mergeRequests'), icon: }, ]} activeId={activeTab} onSelect={(id) => { setActiveTab(id as GitLabTab); setSearchQuery(''); }} variant="active-pill" layoutMode="fit" />
{dialogContent} {/* Footer */} {footerContent}
)} ); }