feat(triage): GAP-33 clickable duplicate links and close-as-duplicate action
Make duplicate issue number a clickable button (text-primary hover:underline) via onNavigateToIssue prop. Add Close as Duplicate button via onCloseAsDuplicate prop, shown only for pending items. i18n keys added for both EN and FR. 5 new tests, 13 total. Co-Authored-By: Claude Opus 4.6 <[email protected]>
This commit is contained in:
co-authored by
Claude Opus 4.6
parent
1503e722e3
commit
3cd8eee3d9
+34
-6
@@ -16,9 +16,11 @@ interface TriageResultCardProps {
|
||||
item: TriageReviewItem;
|
||||
onAccept: (issueNumber: number) => void;
|
||||
onReject: (issueNumber: number) => void;
|
||||
onNavigateToIssue?: (issueNumber: number) => void;
|
||||
onCloseAsDuplicate?: (issueNumber: number, duplicateOf: number) => void;
|
||||
}
|
||||
|
||||
export function TriageResultCard({ item, onAccept, onReject }: TriageResultCardProps) {
|
||||
export function TriageResultCard({ item, onAccept, onReject, onNavigateToIssue, onCloseAsDuplicate }: TriageResultCardProps) {
|
||||
const { t } = useTranslation(['common']);
|
||||
const { result } = item;
|
||||
const level = getConfidenceLevel(result.confidence);
|
||||
@@ -58,11 +60,37 @@ export function TriageResultCard({ item, onAccept, onReject }: TriageResultCardP
|
||||
</div>
|
||||
|
||||
{/* Duplicate */}
|
||||
{result.isDuplicate && result.duplicateOf && (
|
||||
<div className="text-xs text-foreground/50">
|
||||
{t('common:aiTriage.duplicateOf')} <span className="font-medium">#{result.duplicateOf}</span>
|
||||
</div>
|
||||
)}
|
||||
{result.isDuplicate && result.duplicateOf && (() => {
|
||||
const dupOf = result.duplicateOf;
|
||||
return (
|
||||
<div className="flex items-center gap-2 text-xs text-foreground/50">
|
||||
<span>
|
||||
{t('common:aiTriage.duplicateOf')}{' '}
|
||||
{onNavigateToIssue ? (
|
||||
<button
|
||||
type="button"
|
||||
className="font-medium text-primary hover:underline cursor-pointer"
|
||||
onClick={() => onNavigateToIssue(dupOf)}
|
||||
>
|
||||
#{dupOf}
|
||||
</button>
|
||||
) : (
|
||||
<span className="font-medium">#{dupOf}</span>
|
||||
)}
|
||||
</span>
|
||||
{onCloseAsDuplicate && isPending && (
|
||||
<button
|
||||
type="button"
|
||||
className="text-xs px-2 py-0.5 rounded border border-border bg-card hover:bg-accent"
|
||||
aria-label={t('common:aiTriage.closeAsDuplicate')}
|
||||
onClick={() => onCloseAsDuplicate(item.issueNumber, dupOf)}
|
||||
>
|
||||
{t('common:aiTriage.closeAsDuplicate')}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})()}
|
||||
|
||||
{/* Status / Actions */}
|
||||
{item.status === 'accepted' && (
|
||||
|
||||
+48
@@ -94,4 +94,52 @@ describe('TriageResultCard', () => {
|
||||
render(<TriageResultCard item={createItem({ status: 'accepted' })} onAccept={vi.fn()} onReject={vi.fn()} />);
|
||||
expect(screen.queryByRole('button', { name: /accept/i })).toBeNull();
|
||||
});
|
||||
|
||||
it('makes duplicate issue number clickable when onNavigateToIssue provided', () => {
|
||||
const onNavigate = vi.fn();
|
||||
const item = createItem();
|
||||
item.result.isDuplicate = true;
|
||||
item.result.duplicateOf = 10;
|
||||
render(
|
||||
<TriageResultCard item={item} onAccept={vi.fn()} onReject={vi.fn()} onNavigateToIssue={onNavigate} />,
|
||||
);
|
||||
const link = screen.getByRole('button', { name: '#10' });
|
||||
expect(link).toBeDefined();
|
||||
fireEvent.click(link);
|
||||
expect(onNavigate).toHaveBeenCalledWith(10);
|
||||
});
|
||||
|
||||
it('renders duplicate number as static text when onNavigateToIssue absent', () => {
|
||||
const item = createItem();
|
||||
item.result.isDuplicate = true;
|
||||
item.result.duplicateOf = 10;
|
||||
render(<TriageResultCard item={item} onAccept={vi.fn()} onReject={vi.fn()} />);
|
||||
// Should have #10 text but not as a button
|
||||
expect(screen.getByText(/#10/)).toBeDefined();
|
||||
expect(screen.queryByRole('button', { name: '#10' })).toBeNull();
|
||||
});
|
||||
|
||||
it('shows Close as Duplicate button when onCloseAsDuplicate provided and pending', () => {
|
||||
const onClose = vi.fn();
|
||||
const item = createItem();
|
||||
item.result.isDuplicate = true;
|
||||
item.result.duplicateOf = 10;
|
||||
render(
|
||||
<TriageResultCard item={item} onAccept={vi.fn()} onReject={vi.fn()} onCloseAsDuplicate={onClose} />,
|
||||
);
|
||||
const btn = screen.getByRole('button', { name: 'common:aiTriage.closeAsDuplicate' });
|
||||
expect(btn).toBeDefined();
|
||||
fireEvent.click(btn);
|
||||
expect(onClose).toHaveBeenCalledWith(42, 10);
|
||||
});
|
||||
|
||||
it('hides Close as Duplicate for already-accepted items', () => {
|
||||
const item = createItem({ status: 'accepted' });
|
||||
item.result.isDuplicate = true;
|
||||
item.result.duplicateOf = 10;
|
||||
render(
|
||||
<TriageResultCard item={item} onAccept={vi.fn()} onReject={vi.fn()} onCloseAsDuplicate={vi.fn()} />,
|
||||
);
|
||||
expect(screen.queryByRole('button', { name: 'common:aiTriage.closeAsDuplicate' })).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -564,7 +564,8 @@
|
||||
"improveButton": "Improve Issue",
|
||||
"splitButton": "Split Issue",
|
||||
"triageAllButton": "Triage All Untriaged",
|
||||
"retry": "Retry"
|
||||
"retry": "Retry",
|
||||
"closeAsDuplicate": "Close as Duplicate"
|
||||
},
|
||||
"batchReview": {
|
||||
"title": "Batch Triage Review",
|
||||
|
||||
@@ -564,7 +564,8 @@
|
||||
"improveButton": "Améliorer l'issue",
|
||||
"splitButton": "Diviser l'issue",
|
||||
"triageAllButton": "Trier tous les non triés",
|
||||
"retry": "Réessayer"
|
||||
"retry": "Réessayer",
|
||||
"closeAsDuplicate": "Fermer comme doublon"
|
||||
},
|
||||
"batchReview": {
|
||||
"title": "Revue de triage par lots",
|
||||
|
||||
+7
-7
@@ -3,7 +3,7 @@
|
||||
**Branch:** `terminal/enhancement-issues-tab`
|
||||
**Created:** 2026-02-12
|
||||
**Total Gaps:** 41 confirmed (from triple-verified audit)
|
||||
**Status:** 25 / 41 complete
|
||||
**Status:** 26 / 41 complete
|
||||
|
||||
---
|
||||
|
||||
@@ -405,16 +405,16 @@ Each gap has: ID, description, status, files to modify, doc reference, test stat
|
||||
- **Commit:** pending
|
||||
|
||||
### GAP-33: Duplicate detection display-only
|
||||
- **Status:** `PENDING`
|
||||
- **Status:** `DONE`
|
||||
- **Priority:** SHOULD-FIX
|
||||
- **Scope:** Medium
|
||||
- **Doc ref:** Phase 3 PRD > US-7 > AC7.2, AC7.3
|
||||
- **Files to modify:** `renderer/components/github-issues/components/TriageResultCard.tsx`
|
||||
- **Fix:** Make #{duplicateOf} a clickable button with onNavigateToIssue prop. Add "Close as Duplicate" button with onCloseAsDuplicate prop.
|
||||
- **Tests:** Click duplicate link → onNavigateToIssue called; Close as Duplicate → onCloseAsDuplicate called
|
||||
- **Test status:** `PENDING`
|
||||
- **Files modified:** `TriageResultCard.tsx`, `TriageResultCard.test.tsx`, `en/common.json`, `fr/common.json`
|
||||
- **Fix:** Added `onNavigateToIssue` and `onCloseAsDuplicate` props. Duplicate number rendered as clickable button (text-primary hover:underline) when onNavigateToIssue provided, else static span. "Close as Duplicate" button shown when onCloseAsDuplicate provided and item is pending. i18n key: aiTriage.closeAsDuplicate (EN+FR).
|
||||
- **Tests:** 5 new tests: clickable duplicate navigates, static when absent, close-as-duplicate fires callback, hidden when accepted. 13 total.
|
||||
- **Test status:** `PASS`
|
||||
- **Depends on:** None
|
||||
- **Commit:** —
|
||||
- **Commit:** pending
|
||||
|
||||
### GAP-34: No batch triage confirmation dialog or cost estimate
|
||||
- **Status:** `PENDING`
|
||||
|
||||
Reference in New Issue
Block a user