import React from 'react'; import { toast } from '@/components/ui'; import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, } from '@/components/ui/dialog'; import { Button } from '@/components/ui/button'; import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger, } from '@/components/ui/dropdown-menu'; import { ContextMenu, ContextMenuContent, ContextMenuItem, ContextMenuTrigger } from '@/components/ui/context-menu'; import { useGitIdentitiesStore, type GitIdentityProfile, type DiscoveredGitCredential } from '@/stores/useGitIdentitiesStore'; import { useShallow } from 'zustand/react/shallow'; import { GitSettings } from '@/components/sections/openchamber/GitSettings'; import { GitHubSettings } from '@/components/sections/openchamber/GitHubSettings'; import { GitLabSettings } from '@/components/sections/openchamber/GitLabSettings'; import { GitIdentityEditorDialog } from './GitIdentityEditorDialog'; import { Icon } from "@/components/icon/Icon"; import type { IconName } from "@/components/icon/icons"; import { cn } from '@/lib/utils'; import { useI18n } from '@/lib/i18n'; import { SettingsPageLayout } from '@/components/sections/shared/SettingsPageLayout'; import { SettingsSection } from '@/components/sections/shared/SettingsSection'; const ICON_MAP: Record = { branch: 'git-branch', briefcase: 'briefcase', house: 'home', graduation: 'graduation-cap', code: 'code', heart: 'heart', }; const COLOR_MAP: Record = { keyword: 'var(--syntax-keyword)', error: 'var(--status-error)', string: 'var(--syntax-string)', function: 'var(--syntax-function)', type: 'var(--syntax-type)', }; export const GitPage: React.FC = () => { const { t } = useI18n(); const { profiles, globalIdentity, defaultGitIdentityId, deleteProfile, loadProfiles, loadGlobalIdentity, loadDiscoveredCredentials, loadDefaultGitIdentityId, setDefaultGitIdentityId, getUnimportedCredentials, } = useGitIdentitiesStore(useShallow((s) => ({ profiles: s.profiles, globalIdentity: s.globalIdentity, defaultGitIdentityId: s.defaultGitIdentityId, deleteProfile: s.deleteProfile, loadProfiles: s.loadProfiles, loadGlobalIdentity: s.loadGlobalIdentity, loadDiscoveredCredentials: s.loadDiscoveredCredentials, loadDefaultGitIdentityId: s.loadDefaultGitIdentityId, setDefaultGitIdentityId: s.setDefaultGitIdentityId, getUnimportedCredentials: s.getUnimportedCredentials, }))); const [editorOpen, setEditorOpen] = React.useState(false); const [editorProfileId, setEditorProfileId] = React.useState(null); const [editorImportData, setEditorImportData] = React.useState<{ host: string; username: string } | null>(null); const [deleteDialogProfile, setDeleteDialogProfile] = React.useState(null); const [isDeletePending, setIsDeletePending] = React.useState(false); React.useEffect(() => { loadProfiles(); loadGlobalIdentity(); loadDiscoveredCredentials(); loadDefaultGitIdentityId(); }, [loadProfiles, loadGlobalIdentity, loadDiscoveredCredentials, loadDefaultGitIdentityId]); const unimportedCredentials = getUnimportedCredentials(); const openEditor = (id: string | null, importData?: { host: string; username: string } | null) => { setEditorProfileId(id); setEditorImportData(importData ?? null); setEditorOpen(true); }; const handleToggleDefault = async (profileId: string) => { const next = defaultGitIdentityId === profileId ? null : profileId; const ok = await setDefaultGitIdentityId(next); if (!ok) { toast.error(t('settings.gitIdentities.page.toast.updateDefaultFailed')); return; } toast.success(next ? t('settings.gitIdentities.page.toast.defaultUpdated') : t('settings.gitIdentities.page.toast.defaultUnset')); }; const handleConfirmDelete = async () => { if (!deleteDialogProfile) return; setIsDeletePending(true); const success = await deleteProfile(deleteDialogProfile.id); if (success) { toast.success(t('settings.gitIdentities.page.toast.profileDeleted', { name: deleteDialogProfile.name })); setDeleteDialogProfile(null); } else { toast.error(t('settings.gitIdentities.page.toast.deleteProfileFailed')); } setIsDeletePending(false); }; return ( <> openEditor('new')}> {t('settings.common.badge.new')} )} settingsItem="git.identities" >
{/* Global identity */} {globalIdentity && ( openEditor('global')} onToggleDefault={() => handleToggleDefault('global')} isReadOnly hasBorder={profiles.length > 0 || unimportedCredentials.length > 0} /> )} {/* Custom profiles */} {profiles.map((profile, i) => ( openEditor(profile.id)} onToggleDefault={() => handleToggleDefault(profile.id)} onDelete={() => setDeleteDialogProfile(profile)} hasBorder={i < profiles.length - 1 || unimportedCredentials.length > 0} /> ))} {/* Empty state */} {!globalIdentity && profiles.length === 0 && unimportedCredentials.length === 0 && (

{t('settings.gitIdentities.page.empty.title')}

{t('settings.gitIdentities.page.empty.description')}

)} {/* Discovered credentials */} {unimportedCredentials.length > 0 && ( <>
{t('settings.gitIdentities.page.discoveredCredentials.title')}
{unimportedCredentials.map((cred, i) => ( openEditor('new', { host: cred.host, username: cred.username })} hasBorder={i < unimportedCredentials.length - 1} /> ))} )}
{/* Editor dialog */} {/* Delete confirmation */} { if (!isDeletePending) { if (!o) setDeleteDialogProfile(null); } }} > {t('settings.gitIdentities.page.deleteDialog.title')} {t('settings.gitIdentities.page.deleteDialog.description', { name: deleteDialogProfile?.name ?? '' })} ); }; // --- Identity row --- interface IdentityRowProps { profile: GitIdentityProfile; isDefault: boolean; onEdit: () => void; onToggleDefault: () => void; onDelete?: () => void; isReadOnly?: boolean; hasBorder?: boolean; } const IdentityRow: React.FC = ({ profile, isDefault, onEdit, onToggleDefault, onDelete, isReadOnly, hasBorder, }) => { const { t } = useI18n(); const [contextMenuOpen, setContextMenuOpen] = React.useState(false); const iconName = ICON_MAP[profile.icon || 'branch'] || 'git-branch'; const iconColor = COLOR_MAP[profile.color || '']; const authType = profile.authType || 'ssh'; const handleKeyDown = (e: React.KeyboardEvent) => { if (e.currentTarget !== e.target) return; if (e.key !== 'Enter' && e.key !== ' ') return; e.preventDefault(); onEdit(); }; const renderMenuItems = (Item: React.ElementType) => ( <> { e.stopPropagation(); onToggleDefault(); }}> {isDefault ? t('settings.gitIdentities.page.actions.unsetDefault') : t('settings.gitIdentities.page.actions.setAsDefault')} {!isReadOnly && onDelete && ( { e.stopPropagation(); onDelete(); }} className="text-destructive focus:text-destructive" > {t('settings.common.actions.delete')} )} ); return ( { event.preventDefault(); setContextMenuOpen(true); }} /> } >
{profile.name} {authType} {isDefault && ( {t('settings.gitIdentities.page.badge.default')} )} {isReadOnly && ( {t('settings.agents.sidebar.badge.system')} )}
{authType === 'token' && profile.host ? profile.host : profile.userEmail}
{renderMenuItems(DropdownMenuItem)}
{renderMenuItems(ContextMenuItem)}
); }; // --- Discovered credential row --- interface DiscoveredRowProps { credential: DiscoveredGitCredential; onImport: () => void; hasBorder?: boolean; } const DiscoveredRow: React.FC = ({ credential, onImport, hasBorder }) => { const { t } = useI18n(); const parts = credential.host.split('/'); const displayName = parts.length >= 3 ? parts[parts.length - 1] : credential.host; const isRepoSpecific = credential.host.includes('/'); return (
{displayName} {isRepoSpecific ? credential.host : credential.username}
); };