feat(git): enhance git change row
This commit is contained in:
@@ -1,4 +1,4 @@
|
|||||||
import React from 'react';
|
import React, { useCallback, useMemo } from 'react';
|
||||||
import {
|
import {
|
||||||
RiCheckboxLine,
|
RiCheckboxLine,
|
||||||
RiCheckboxBlankLine,
|
RiCheckboxBlankLine,
|
||||||
@@ -8,6 +8,38 @@ import {
|
|||||||
import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip';
|
import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip';
|
||||||
import type { GitStatus } from '@/lib/api/types';
|
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 {
|
interface ChangeRowProps {
|
||||||
file: GitStatus['files'][number];
|
file: GitStatus['files'][number];
|
||||||
checked: boolean;
|
checked: boolean;
|
||||||
@@ -18,33 +50,7 @@ interface ChangeRowProps {
|
|||||||
stats?: { insertions: number; deletions: number };
|
stats?: { insertions: number; deletions: number };
|
||||||
}
|
}
|
||||||
|
|
||||||
function describeChange(file: GitStatus['files'][number]) {
|
export const ChangeRow = React.memo<ChangeRowProps>(function ChangeRow({
|
||||||
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> = ({
|
|
||||||
file,
|
file,
|
||||||
checked,
|
checked,
|
||||||
onToggle,
|
onToggle,
|
||||||
@@ -52,12 +58,43 @@ export const ChangeRow: React.FC<ChangeRowProps> = ({
|
|||||||
onRevert,
|
onRevert,
|
||||||
isReverting,
|
isReverting,
|
||||||
stats,
|
stats,
|
||||||
}) => {
|
}) {
|
||||||
const descriptor = React.useMemo(() => describeChange(file), [file]);
|
const descriptor = useMemo(() => describeChange(file), [file]);
|
||||||
const indicatorLabel = descriptor.description ?? descriptor.code;
|
const indicatorLabel = descriptor.description;
|
||||||
const insertions = stats?.insertions ?? 0;
|
const insertions = stats?.insertions ?? 0;
|
||||||
const deletions = stats?.deletions ?? 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 (
|
return (
|
||||||
<li>
|
<li>
|
||||||
<div
|
<div
|
||||||
@@ -65,23 +102,11 @@ export const ChangeRow: React.FC<ChangeRowProps> = ({
|
|||||||
role="button"
|
role="button"
|
||||||
tabIndex={0}
|
tabIndex={0}
|
||||||
onClick={onViewDiff}
|
onClick={onViewDiff}
|
||||||
onKeyDown={(event) => {
|
onKeyDown={handleKeyDown}
|
||||||
if (event.key === ' ') {
|
|
||||||
event.preventDefault();
|
|
||||||
onToggle();
|
|
||||||
} else if (event.key === 'Enter') {
|
|
||||||
event.preventDefault();
|
|
||||||
onViewDiff();
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={(event) => {
|
onClick={handleToggleClick}
|
||||||
event.preventDefault();
|
|
||||||
event.stopPropagation();
|
|
||||||
onToggle();
|
|
||||||
}}
|
|
||||||
aria-pressed={checked}
|
aria-pressed={checked}
|
||||||
aria-label={`Select ${file.path}`}
|
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"
|
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>
|
<TooltipTrigger asChild>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={(event) => {
|
onClick={handleRevertClick}
|
||||||
event.preventDefault();
|
|
||||||
event.stopPropagation();
|
|
||||||
onRevert();
|
|
||||||
}}
|
|
||||||
disabled={isReverting}
|
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"
|
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}`}
|
aria-label={`Revert changes for ${file.path}`}
|
||||||
@@ -137,4 +158,4 @@ export const ChangeRow: React.FC<ChangeRowProps> = ({
|
|||||||
</div>
|
</div>
|
||||||
</li>
|
</li>
|
||||||
);
|
);
|
||||||
};
|
});
|
||||||
|
|||||||
@@ -202,9 +202,9 @@ export function getLanguageFromExtension(filePath: string): string | null {
|
|||||||
const languageMap: Record<string, string> = {
|
const languageMap: Record<string, string> = {
|
||||||
|
|
||||||
'js': 'javascript',
|
'js': 'javascript',
|
||||||
'jsx': 'javascript',
|
'jsx': 'jsx',
|
||||||
'ts': 'typescript',
|
'ts': 'typescript',
|
||||||
'tsx': 'typescript',
|
'tsx': 'tsx',
|
||||||
'mjs': 'javascript',
|
'mjs': 'javascript',
|
||||||
'cjs': 'javascript',
|
'cjs': 'javascript',
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user