import React from 'react'; import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle } from '@/components/ui/dialog'; import { Button } from '@/components/ui/button'; import { Input } from '@/components/ui/input'; import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip'; import { toast } from '@/components/ui'; import { Icon } from "@/components/icon/Icon"; import { useI18n } from '@/lib/i18n'; import { cn } from '@/lib/utils'; import type { GitStashEntry } from '@/lib/api/types'; import { applyGitStash, countGitStashFiles, dropGitStash, listGitStashes, popGitStash, stashGitChanges } from '@/lib/gitApi'; interface StashesDialogProps { open: boolean; onOpenChange: (open: boolean) => void; directory: string | null; hasUncommittedChanges: boolean; hasStagedChanges?: boolean; uncommittedFileCount: number; onChanged?: (change?: { affectsIndex?: boolean }) => void | Promise; } type StashOperation = 'create' | `apply:${string}` | `pop:${string}` | `drop:${string}` | null; export const StashesDialog: React.FC = ({ open, onOpenChange, directory, hasUncommittedChanges, hasStagedChanges = false, uncommittedFileCount, onChanged, }) => { const { t } = useI18n(); const [stashes, setStashes] = React.useState([]); const [isLoading, setIsLoading] = React.useState(false); const [query, setQuery] = React.useState(''); const [message, setMessage] = React.useState(''); const [operation, setOperation] = React.useState(null); const [fileCounts, setFileCounts] = React.useState>({}); const load = React.useCallback(async () => { if (!directory) return; setIsLoading(true); try { const result = await listGitStashes(directory); setStashes(result.stashes); setFileCounts({}); } catch (error) { toast.error(error instanceof Error ? error.message : t('gitView.stashes.toast.loadFailed')); } finally { setIsLoading(false); } }, [directory, t]); React.useEffect(() => { if (open) void load(); }, [load, open]); React.useEffect(() => { if (!open || !directory || stashes.length === 0) return; let cancelled = false; const refs = stashes.map((stash) => stash.ref); void countGitStashFiles(directory, refs).then((result) => { if (!cancelled) setFileCounts(result.counts); }).catch(() => undefined); return () => { cancelled = true; }; }, [directory, open, stashes]); const filtered = React.useMemo(() => { const normalized = query.trim().toLowerCase(); if (!normalized) return stashes; return stashes.filter((stash) => `${stash.ref} ${stash.message} ${stash.relativeTime}`.toLowerCase().includes(normalized)); }, [query, stashes]); const refreshAfterChange = React.useCallback(async (change?: { affectsIndex?: boolean }) => { await load(); await onChanged?.(change); }, [load, onChanged]); const handleCreate = async () => { if (!directory || operation) return; setOperation('create'); try { const result = await stashGitChanges(directory, { message: message.trim() || undefined }); if (result.created) { toast.success(t('gitView.stashes.toast.created')); setMessage(''); } else { toast.info(t('gitView.stashes.toast.noChanges')); } await refreshAfterChange({ affectsIndex: Boolean(result.created && hasStagedChanges) }); } catch (error) { toast.error(error instanceof Error ? error.message : t('gitView.stashes.toast.createFailed')); } finally { setOperation(null); } }; const runStashAction = async (stash: GitStashEntry, kind: 'apply' | 'pop' | 'drop') => { if (!directory || operation) return; if (kind === 'drop' && !window.confirm(t('gitView.stashes.confirm.drop', { ref: stash.ref }))) return; setOperation(`${kind}:${stash.ref}`); try { if (kind === 'apply') await applyGitStash(directory, { ref: stash.ref }); if (kind === 'pop') await popGitStash(directory, { ref: stash.ref }); if (kind === 'drop') await dropGitStash(directory, { ref: stash.ref }); const successKey = kind === 'apply' ? 'gitView.stashes.toast.applySuccess' : kind === 'pop' ? 'gitView.stashes.toast.popSuccess' : 'gitView.stashes.toast.dropSuccess'; toast.success(t(successKey)); await refreshAfterChange({ affectsIndex: kind !== 'drop' }); } catch (error) { const failedKey = kind === 'apply' ? 'gitView.stashes.toast.applyFailed' : kind === 'pop' ? 'gitView.stashes.toast.popFailed' : 'gitView.stashes.toast.dropFailed'; toast.error(error instanceof Error ? error.message : t(failedKey)); await refreshAfterChange({ affectsIndex: kind !== 'drop' }); } finally { setOperation(null); } }; const isOperating = operation !== null; return ( {t('gitView.stashes.title')} {t('gitView.stashes.description')}
setMessage(event.target.value)} placeholder={t('gitView.stashes.messagePlaceholder')} disabled={isOperating || !hasUncommittedChanges} className="flex-1" />

{t('gitView.stashes.includeUntrackedHint')}

setQuery(event.target.value)} placeholder={t('gitView.stashes.searchPlaceholder')} className="pl-9" />
{isLoading ? (
) : filtered.length === 0 ? (
{query ? t('gitView.stashes.empty.search') : t('gitView.stashes.empty.list')}
) : filtered.map((stash, index) => (

{stash.message || t('gitView.stashes.untitled')}

{index === 0 && !query.trim() ? t('gitView.stashes.latestLabel') : t('gitView.stashes.itemNumber', { number: index + 1 })} {stash.ref} {' · '}{stash.relativeTime} · {typeof fileCounts[stash.ref] === 'number' ? t('gitView.stashes.fileCount', { count: fileCounts[stash.ref] }) : t('gitView.stashes.fileCountLoading')}

runStashAction(stash, 'apply')} disabled={isOperating}> runStashAction(stash, 'pop')} disabled={isOperating}> runStashAction(stash, 'drop')} disabled={isOperating} destructive>
))}
); }; const StashIconButton: React.FC<{ label: string; loading: boolean; disabled: boolean; destructive?: boolean; onClick: () => void; children: React.ReactNode }> = ({ label, loading, disabled, destructive, onClick, children }) => ( {label} );