import { useState, useEffect, useCallback, useRef } from 'react' import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter, DialogDescription } from '@/components/ui/dialog' import { Tabs, TabsList, TabsTrigger, TabsContent } from '@/components/ui/tabs' import { Button } from '@/components/ui/button' import { Input } from '@/components/ui/input' import { Badge } from '@/components/ui/badge' import { invoke } from '@tauri-apps/api/core' import { isTauri, mockInvoke } from '../mock-tauri' import { GitHubDeviceFlow } from './GitHubDeviceFlow' import type { GithubRepo } from '../types' function tauriCall(cmd: string, args: Record): Promise { return isTauri() ? invoke(cmd, args) : mockInvoke(cmd, args) } interface GitHubVaultModalProps { open: boolean githubToken: string | null onClose: () => void onVaultCloned: (path: string, label: string) => void onOpenSettings: () => void onGitHubConnected?: (token: string, username: string) => void } type CloneStatus = 'idle' | 'cloning' | 'success' | 'error' function RepoListItem({ repo, selected, onSelect }: { repo: GithubRepo; selected: boolean; onSelect: () => void }) { return (
{ if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); onSelect() } }} className={`flex flex-col gap-1 rounded-md px-3 py-2.5 cursor-pointer transition-colors ${ selected ? 'bg-accent' : 'hover:bg-accent/50' }`} data-testid={`repo-item-${repo.name}`} >
{repo.full_name} {repo.private ? 'Private' : 'Public'}
{repo.description && ( {repo.description} )}
) } function CloneTab({ repos, loading, search, setSearch, selectedRepo, setSelectedRepo, localPath, setLocalPath, onClone, cloneStatus, cloneError, }: { repos: GithubRepo[] loading: boolean search: string setSearch: (v: string) => void selectedRepo: GithubRepo | null setSelectedRepo: (r: GithubRepo) => void localPath: string setLocalPath: (v: string) => void onClone: () => void cloneStatus: CloneStatus cloneError: string | null }) { const filtered = repos.filter(r => r.full_name.toLowerCase().includes(search.toLowerCase()) || (r.description?.toLowerCase().includes(search.toLowerCase()) ?? false) ) return (
setSearch(e.target.value)} data-testid="github-repo-search" />
{loading ? (
Loading repositories...
) : filtered.length === 0 ? (
{search ? 'No repos match your search' : 'No repositories found'}
) : (
{filtered.map(repo => ( setSelectedRepo(repo)} /> ))}
)}
setLocalPath(e.target.value)} placeholder="~/Vaults/repo-name" data-testid="github-clone-path" />
{cloneError && (

{cloneError}

)} {filtered.length} repo{filtered.length !== 1 ? 's' : ''} found
) } function CreateTab({ repoName, setRepoName, isPrivate, setIsPrivate, localPath, setLocalPath, onCreate, cloneStatus, cloneError, }: { repoName: string setRepoName: (v: string) => void isPrivate: boolean setIsPrivate: (v: boolean) => void localPath: string setLocalPath: (v: string) => void onCreate: () => void cloneStatus: CloneStatus cloneError: string | null }) { return (
setRepoName(e.target.value)} placeholder="my-vault" data-testid="github-repo-name" /> {repoName && ( github.com/you/{repoName} )}
setLocalPath(e.target.value)} placeholder="~/Vaults/my-vault" data-testid="github-create-path" />
{cloneError && (

{cloneError}

)}
) } function CloningProgress({ repoName }: { repoName: string }) { return (

Cloning {repoName}...

This may take a moment for large repositories

) } export function GitHubVaultModal({ open, githubToken, onClose, onVaultCloned, onOpenSettings, onGitHubConnected }: GitHubVaultModalProps) { const [tab, setTab] = useState('clone') const [repos, setRepos] = useState([]) const [loading, setLoading] = useState(false) const [search, setSearch] = useState('') const [selectedRepo, setSelectedRepo] = useState(null) const [clonePath, setClonePath] = useState('') const [repoName, setRepoName] = useState('') const [isPrivate, setIsPrivate] = useState(true) const [createPath, setCreatePath] = useState('') const [cloneStatus, setCloneStatus] = useState('idle') const [cloneError, setCloneError] = useState(null) const loadedRef = useRef(false) const loadRepos = useCallback(async () => { if (!githubToken) return setLoading(true) try { const result = await tauriCall('github_list_repos', { token: githubToken }) setRepos(result) } catch (err) { console.error('Failed to load GitHub repos:', err) setCloneError(`Failed to load repos: ${err}`) } finally { setLoading(false) } }, [githubToken]) useEffect(() => { if (open && githubToken && !loadedRef.current) { loadedRef.current = true loadRepos() } if (!open) { loadedRef.current = false } }, [open, githubToken, loadRepos]) // Auto-fill clone path when repo selected useEffect(() => { if (selectedRepo) { setClonePath(`~/Vaults/${selectedRepo.name}`) } }, [selectedRepo]) // Auto-fill create path when name changes useEffect(() => { if (repoName) { setCreatePath(`~/Vaults/${repoName}`) } }, [repoName]) const resetState = useCallback(() => { setTab('clone') setSearch('') setSelectedRepo(null) setClonePath('') setRepoName('') setIsPrivate(true) setCreatePath('') setCloneStatus('idle') setCloneError(null) }, []) const handleClose = useCallback(() => { resetState() onClose() }, [onClose, resetState]) const handleClone = useCallback(async () => { if (!selectedRepo || !clonePath.trim() || !githubToken) return setCloneStatus('cloning') setCloneError(null) try { await tauriCall('clone_repo', { url: selectedRepo.clone_url, token: githubToken, localPath: clonePath.trim(), }) setCloneStatus('success') onVaultCloned(clonePath.trim(), selectedRepo.name) handleClose() } catch (err) { setCloneStatus('error') setCloneError(`Clone failed: ${err}`) } }, [selectedRepo, clonePath, githubToken, onVaultCloned, handleClose]) const handleCreate = useCallback(async () => { if (!repoName.trim() || !createPath.trim() || !githubToken) return setCloneStatus('cloning') setCloneError(null) try { const repo = await tauriCall('github_create_repo', { token: githubToken, name: repoName.trim(), private: isPrivate, }) await tauriCall('clone_repo', { url: repo.clone_url, token: githubToken, localPath: createPath.trim(), }) setCloneStatus('success') onVaultCloned(createPath.trim(), repo.name) handleClose() } catch (err) { setCloneStatus('error') setCloneError(`${err}`) } }, [repoName, createPath, githubToken, isPrivate, onVaultCloned, handleClose]) if (!githubToken) { return ( { if (!isOpen) handleClose() }}> Connect GitHub Repo Connect your GitHub account to clone or create vaults backed by GitHub repos.
{onGitHubConnected ? ( ) : ( <>

You need to connect your GitHub account first. Add your GitHub token in Settings.

)}
) } const cloningRepoName = tab === 'clone' ? (selectedRepo?.full_name ?? '') : repoName return ( { if (!isOpen) handleClose() }}> Connect GitHub Repo Clone an existing repo or create a new one as a vault. {cloneStatus === 'cloning' ? ( ) : ( Clone Existing Create New )} ) }