fix(hunk-actions): use native menu items, drop hardcoded label
- Render each hunk action as a DropdownMenuItem (Stage/Unstage/Discard hunk N with +/- counts) so Base UI keyboard navigation, highlight, Escape, and close-on-activate apply. Plain div/button rows are gone. - Row labels reuse the already-translated diffView.hunk.stageTitle / unstageTitle / discardTitle strings; no new i18n keys, no hardcoded English. - Disable the file-level actions while a hunk action is in flight for consistency (server serializes per directory regardless).
This commit is contained in:
@@ -1005,7 +1005,7 @@ const MultiFileDiffEntry = React.memo<MultiFileDiffEntryProps>(({
|
|||||||
filePath={file.path}
|
filePath={file.path}
|
||||||
staged={staged}
|
staged={staged}
|
||||||
busyAction={fileAction}
|
busyAction={fileAction}
|
||||||
disabled={fileAction !== null}
|
disabled={fileAction !== null || hunkAction !== null}
|
||||||
onAction={handleFileAction}
|
onAction={handleFileAction}
|
||||||
/>
|
/>
|
||||||
) : null}
|
) : null}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import React, { useMemo } from 'react';
|
|||||||
import {
|
import {
|
||||||
DropdownMenu,
|
DropdownMenu,
|
||||||
DropdownMenuContent,
|
DropdownMenuContent,
|
||||||
|
DropdownMenuItem,
|
||||||
DropdownMenuLabel,
|
DropdownMenuLabel,
|
||||||
DropdownMenuSeparator,
|
DropdownMenuSeparator,
|
||||||
DropdownMenuTrigger,
|
DropdownMenuTrigger,
|
||||||
@@ -27,7 +28,6 @@ interface HunkActionsProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
interface HunkSummary {
|
interface HunkSummary {
|
||||||
patch: string;
|
|
||||||
insertions: number;
|
insertions: number;
|
||||||
deletions: number;
|
deletions: number;
|
||||||
}
|
}
|
||||||
@@ -41,9 +41,29 @@ const summarizeHunks = (patch: string): HunkSummary[] =>
|
|||||||
if (line.startsWith('+')) insertions += 1;
|
if (line.startsWith('+')) insertions += 1;
|
||||||
else if (line.startsWith('-')) deletions += 1;
|
else if (line.startsWith('-')) deletions += 1;
|
||||||
}
|
}
|
||||||
return { patch: hunkPatch, insertions, deletions };
|
return { insertions, deletions };
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const HunkCounts = React.memo<{ insertions: number; deletions: number }>(function HunkCounts({
|
||||||
|
insertions,
|
||||||
|
deletions,
|
||||||
|
}) {
|
||||||
|
if (insertions === 0 && deletions === 0) return null;
|
||||||
|
return (
|
||||||
|
<span className="ml-auto pl-3 typography-micro">
|
||||||
|
{insertions > 0 ? (
|
||||||
|
<span style={{ color: 'var(--status-success)' }}>+{insertions}</span>
|
||||||
|
) : null}
|
||||||
|
{insertions > 0 && deletions > 0 ? (
|
||||||
|
<span className="mx-0.5 text-muted-foreground">/</span>
|
||||||
|
) : null}
|
||||||
|
{deletions > 0 ? (
|
||||||
|
<span style={{ color: 'var(--status-error)' }}>-{deletions}</span>
|
||||||
|
) : null}
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
export const HunkActions = React.memo<HunkActionsProps>(function HunkActions({
|
export const HunkActions = React.memo<HunkActionsProps>(function HunkActions({
|
||||||
filePath,
|
filePath,
|
||||||
patch,
|
patch,
|
||||||
@@ -84,51 +104,31 @@ export const HunkActions = React.memo<HunkActionsProps>(function HunkActions({
|
|||||||
const displayIndex = index + 1;
|
const displayIndex = index + 1;
|
||||||
const busyPrimary = busyHunk?.index === index && busyHunk.action === primaryAction;
|
const busyPrimary = busyHunk?.index === index && busyHunk.action === primaryAction;
|
||||||
const busyDiscard = busyHunk?.index === index && busyHunk.action === 'discard';
|
const busyDiscard = busyHunk?.index === index && busyHunk.action === 'discard';
|
||||||
const rowBusy = busyPrimary || busyDiscard;
|
|
||||||
const primaryTitle = staged
|
const primaryTitle = staged
|
||||||
? t('diffView.hunk.unstageTitle', { index: displayIndex })
|
? t('diffView.hunk.unstageTitle', { index: displayIndex })
|
||||||
: t('diffView.hunk.stageTitle', { index: displayIndex });
|
: t('diffView.hunk.stageTitle', { index: displayIndex });
|
||||||
const discardTitle = t('diffView.hunk.discardTitle', { index: displayIndex });
|
const discardTitle = t('diffView.hunk.discardTitle', { index: displayIndex });
|
||||||
return (
|
return (
|
||||||
<div
|
<React.Fragment key={index}>
|
||||||
key={index}
|
{index > 0 ? <DropdownMenuSeparator /> : null}
|
||||||
className="flex items-center gap-2 px-2 py-1.5"
|
<DropdownMenuItem
|
||||||
aria-label={primaryTitle}
|
disabled={disabled || busyDiscard}
|
||||||
>
|
onSelect={() => onAction(index, primaryAction)}
|
||||||
<span className="min-w-0 flex-1 truncate typography-ui-label text-foreground">
|
|
||||||
Hunk {displayIndex}
|
|
||||||
<span className="ml-1.5 typography-micro">
|
|
||||||
{hunk.insertions > 0 ? (
|
|
||||||
<span style={{ color: 'var(--status-success)' }}>+{hunk.insertions}</span>
|
|
||||||
) : null}
|
|
||||||
{hunk.insertions > 0 && hunk.deletions > 0 ? (
|
|
||||||
<span className="mx-0.5 text-muted-foreground">/</span>
|
|
||||||
) : null}
|
|
||||||
{hunk.deletions > 0 ? (
|
|
||||||
<span style={{ color: 'var(--status-error)' }}>-{hunk.deletions}</span>
|
|
||||||
) : null}
|
|
||||||
</span>
|
|
||||||
</span>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
disabled={disabled || rowBusy}
|
|
||||||
onClick={() => onAction(index, primaryAction)}
|
|
||||||
className="flex size-6 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 disabled:cursor-not-allowed disabled:opacity-50"
|
|
||||||
aria-label={primaryTitle}
|
aria-label={primaryTitle}
|
||||||
title={primaryTitle}
|
title={primaryTitle}
|
||||||
>
|
>
|
||||||
{busyPrimary ? (
|
{busyPrimary ? (
|
||||||
<Icon name="loader-4" className="size-3.5 animate-spin" />
|
<Icon name="loader-4" className="size-3.5 animate-spin" />
|
||||||
) : (
|
) : (
|
||||||
<Icon name="add" className="size-3.5" />
|
<Icon name={staged ? 'arrow-go-back' : 'add'} className="size-3.5" />
|
||||||
)}
|
)}
|
||||||
</button>
|
<span className="min-w-0 flex-1 truncate">{primaryTitle}</span>
|
||||||
|
<HunkCounts insertions={hunk.insertions} deletions={hunk.deletions} />
|
||||||
|
</DropdownMenuItem>
|
||||||
{!staged ? (
|
{!staged ? (
|
||||||
<button
|
<DropdownMenuItem
|
||||||
type="button"
|
disabled={disabled || busyPrimary}
|
||||||
disabled={disabled || rowBusy}
|
onSelect={() => onAction(index, 'discard')}
|
||||||
onClick={() => onAction(index, 'discard')}
|
|
||||||
className="flex size-6 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 disabled:cursor-not-allowed disabled:opacity-50"
|
|
||||||
aria-label={discardTitle}
|
aria-label={discardTitle}
|
||||||
title={discardTitle}
|
title={discardTitle}
|
||||||
>
|
>
|
||||||
@@ -137,9 +137,10 @@ export const HunkActions = React.memo<HunkActionsProps>(function HunkActions({
|
|||||||
) : (
|
) : (
|
||||||
<Icon name="arrow-go-back" className="size-3.5" />
|
<Icon name="arrow-go-back" className="size-3.5" />
|
||||||
)}
|
)}
|
||||||
</button>
|
<span className="min-w-0 flex-1 truncate">{discardTitle}</span>
|
||||||
|
</DropdownMenuItem>
|
||||||
) : null}
|
) : null}
|
||||||
</div>
|
</React.Fragment>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</DropdownMenuContent>
|
</DropdownMenuContent>
|
||||||
|
|||||||
Reference in New Issue
Block a user