import { useEffect, useState } from 'react'; import { Button, Collapse, Drawer, Icon, Intent, NonIdealState, Spinner, Tag, Tooltip } from '@blueprintjs/core'; import { html as diff2htmlHtml } from 'diff2html'; import 'diff2html/bundles/css/diff2html.min.css'; import { getCommitDetail, type CommitDetail, type CommitFile } from '../api/opcApi'; interface Props { hash: string | null; onClose: () => void; } function fileStatusIntent(status: string): Intent { if (status === 'added') return Intent.SUCCESS; if (status === 'deleted') return Intent.DANGER; if (status === 'renamed') return Intent.WARNING; return Intent.NONE; } function fileStatusIcon(status: string): string { if (status === 'added') return 'plus'; if (status === 'deleted') return 'minus'; if (status === 'renamed') return 'arrow-right'; return 'edit'; } function FileDiff({ file }: { file: CommitFile }) { const [open, setOpen] = useState(true); const diffHtml = file.patch ? diff2htmlHtml(file.patch, { drawFileList: false, matching: 'lines', outputFormat: 'line-by-line', renderNothingWhenEmpty: true, }) : ''; const displayPath = file.status === 'renamed' && file.oldPath && file.oldPath !== file.path ? `${file.oldPath} → ${file.path}` : file.path; return (
{diffHtml ?
:
Binary or empty file — no textual diff available.
}
); } export function GitCommitDrawer({ hash, onClose }: Props) { const [detail, setDetail] = useState(null); const [loading, setLoading] = useState(false); const [error, setError] = useState(null); useEffect(() => { if (!hash) { // Delay clearing so the closing animation doesn't flash blank const t = setTimeout(() => { setDetail(null); setError(null); }, 300); return () => clearTimeout(t); } setLoading(true); setError(null); getCommitDetail(hash) .then(d => { setDetail(d); setError(null); }) .catch(e => setError(String(e))) .finally(() => setLoading(false)); }, [hash]); const totalAdds = detail?.files.reduce((a, f) => a + f.additions, 0) ?? 0; const totalDels = detail?.files.reduce((a, f) => a + f.deletions, 0) ?? 0; return ( {detail.shortHash} {detail.subject} ) : 'Commit Diff' } size="70%" position="right" className="git-commit-drawer" > {/* Scrollable body */}
{/* Loading overlay — keeps old content visible while fetching next */} {loading && (
)} {error && ( )} {!error && detail && ( <> {/* Metadata bar */}
navigator.clipboard.writeText(detail.hash)} style={{ cursor: 'pointer' }} > {detail.shortHash} {detail.author} {detail.date}
{totalAdds > 0 && ( +{totalAdds} )} {totalDels > 0 && ( -{totalDels} )} {detail.files.length} file{detail.files.length !== 1 ? 's' : ''}
{/* Extended commit message */} {detail.body.trim() !== detail.subject.trim() && (
{detail.body.trim()}
)} {/* Per-file diffs */} {detail.files.length === 0 ? ( ) : (
{detail.files.map(f => ( ))}
)} )} {!loading && !error && !detail && hash && ( } title="Loading…" /> )}
{/* Footer — sticky at bottom */}
); }