feat(git): enhance git change row

This commit is contained in:
Bohdan Triapitsyn
2025-12-21 02:35:33 +02:00
parent d055117a9c
commit b1a243af1f
2 changed files with 74 additions and 53 deletions
@@ -1,4 +1,4 @@
import React from 'react';
import React, { useCallback, useMemo } from 'react';
import {
RiCheckboxLine,
RiCheckboxBlankLine,
@@ -8,6 +8,38 @@ import {
import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip';
import type { GitStatus } from '@/lib/api/types';
type ChangeDescriptor = {
code: string;
color: string;
description: string;
};
const CHANGE_DESCRIPTORS: Record<string, ChangeDescriptor> = {
'?': { code: '?', color: 'var(--status-info)', description: 'Untracked file' },
A: { code: 'A', color: 'var(--status-success)', description: 'New file' },
D: { code: 'D', color: 'var(--status-error)', description: 'Deleted file' },
R: { code: 'R', color: 'var(--status-info)', description: 'Renamed file' },
C: { code: 'C', color: 'var(--status-info)', description: 'Copied file' },
M: { code: 'M', color: 'var(--status-warning)', description: 'Modified file' },
};
const DEFAULT_DESCRIPTOR = CHANGE_DESCRIPTORS.M;
function getChangeSymbol(file: GitStatus['files'][number]): string {
const indexCode = file.index?.trim();
const workingCode = file.working_dir?.trim();
if (indexCode && indexCode !== '?') return indexCode.charAt(0);
if (workingCode) return workingCode.charAt(0);
return indexCode?.charAt(0) || workingCode?.charAt(0) || 'M';
}
function describeChange(file: GitStatus['files'][number]): ChangeDescriptor {
const symbol = getChangeSymbol(file);
return CHANGE_DESCRIPTORS[symbol] ?? DEFAULT_DESCRIPTOR;
}
interface ChangeRowProps {
file: GitStatus['files'][number];
checked: boolean;
@@ -18,33 +50,7 @@ interface ChangeRowProps {
stats?: { insertions: number; deletions: number };
}
function describeChange(file: GitStatus['files'][number]) {
const rawCode =
file.index && file.index.trim() && file.index.trim() !== '?'
? file.index.trim()
: file.working_dir && file.working_dir.trim()
? file.working_dir.trim()
: file.index || file.working_dir || ' ';
const symbol = rawCode.trim().charAt(0) || rawCode.trim() || '·';
switch (symbol) {
case '?':
return { code: '?', color: 'var(--status-info)', description: 'Untracked file' };
case 'A':
return { code: 'A', color: 'var(--status-success)', description: 'New file' };
case 'D':
return { code: 'D', color: 'var(--status-error)', description: 'Deleted file' };
case 'R':
return { code: 'R', color: 'var(--status-info)', description: 'Renamed file' };
case 'C':
return { code: 'C', color: 'var(--status-info)', description: 'Copied file' };
default:
return { code: 'M', color: 'var(--status-warning)', description: 'Modified file' };
}
}
export const ChangeRow: React.FC<ChangeRowProps> = ({
export const ChangeRow = React.memo<ChangeRowProps>(function ChangeRow({
file,
checked,
onToggle,
@@ -52,12 +58,43 @@ export const ChangeRow: React.FC<ChangeRowProps> = ({
onRevert,
isReverting,
stats,
}) => {
const descriptor = React.useMemo(() => describeChange(file), [file]);
const indicatorLabel = descriptor.description ?? descriptor.code;
}) {
const descriptor = useMemo(() => describeChange(file), [file]);
const indicatorLabel = descriptor.description;
const insertions = stats?.insertions ?? 0;
const deletions = stats?.deletions ?? 0;
const handleKeyDown = useCallback(
(event: React.KeyboardEvent) => {
if (event.key === ' ') {
event.preventDefault();
onToggle();
} else if (event.key === 'Enter') {
event.preventDefault();
onViewDiff();
}
},
[onToggle, onViewDiff]
);
const handleToggleClick = useCallback(
(event: React.MouseEvent) => {
event.preventDefault();
event.stopPropagation();
onToggle();
},
[onToggle]
);
const handleRevertClick = useCallback(
(event: React.MouseEvent) => {
event.preventDefault();
event.stopPropagation();
onRevert();
},
[onRevert]
);
return (
<li>
<div
@@ -65,23 +102,11 @@ export const ChangeRow: React.FC<ChangeRowProps> = ({
role="button"
tabIndex={0}
onClick={onViewDiff}
onKeyDown={(event) => {
if (event.key === ' ') {
event.preventDefault();
onToggle();
} else if (event.key === 'Enter') {
event.preventDefault();
onViewDiff();
}
}}
onKeyDown={handleKeyDown}
>
<button
type="button"
onClick={(event) => {
event.preventDefault();
event.stopPropagation();
onToggle();
}}
onClick={handleToggleClick}
aria-pressed={checked}
aria-label={`Select ${file.path}`}
className="flex size-5 shrink-0 items-center justify-center rounded text-muted-foreground hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary"
@@ -116,11 +141,7 @@ export const ChangeRow: React.FC<ChangeRowProps> = ({
<TooltipTrigger asChild>
<button
type="button"
onClick={(event) => {
event.preventDefault();
event.stopPropagation();
onRevert();
}}
onClick={handleRevertClick}
disabled={isReverting}
className="flex size-5 shrink-0 items-center justify-center rounded text-muted-foreground opacity-0 group-hover:opacity-100 hover:text-foreground focus-visible:opacity-100 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary disabled:cursor-not-allowed disabled:opacity-50 transition-opacity"
aria-label={`Revert changes for ${file.path}`}
@@ -137,4 +158,4 @@ export const ChangeRow: React.FC<ChangeRowProps> = ({
</div>
</li>
);
};
});
+2 -2
View File
@@ -202,9 +202,9 @@ export function getLanguageFromExtension(filePath: string): string | null {
const languageMap: Record<string, string> = {
'js': 'javascript',
'jsx': 'javascript',
'jsx': 'jsx',
'ts': 'typescript',
'tsx': 'typescript',
'tsx': 'tsx',
'mjs': 'javascript',
'cjs': 'javascript',