Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 31 additions & 5 deletions src/components/RepositoryCard.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -112,9 +112,12 @@ const storeState = {

const mockUseAppStore = vi.mocked(useAppStore);

const renderRepositoryCard = (viewMode: 'list' | 'grid') => render(
const renderRepositoryCard = (
viewMode: 'list' | 'grid',
options: { onAskRepository?: (repository: Repository) => void; selectionMode?: boolean } = {},
) => render(
<TooltipProvider>
<RepositoryCard repository={repository} allCategories={[]} viewMode={viewMode} />
<RepositoryCard repository={repository} allCategories={[]} viewMode={viewMode} {...options} />
</TooltipProvider>
);

Expand All @@ -138,7 +141,7 @@ beforeEach(() => {
describe('RepositoryCard view modes', () => {
it('moves single-card actions into an accessible more-actions menu in list mode', async () => {
const user = userEvent.setup();
renderRepositoryCard('list');
renderRepositoryCard('list', { onAskRepository: vi.fn() });

const lastPushed = screen.getByText(/最近提交/);
expect(lastPushed).toBeInTheDocument();
Expand All @@ -153,6 +156,7 @@ describe('RepositoryCard view modes', () => {

expect(screen.getByText('仓库操作')).toBeInTheDocument();
expect(screen.getByRole('menuitem', { name: 'AI 分析' })).toBeInTheDocument();
expect(screen.getByRole('menuitem', { name: '问答此仓库' })).toBeInTheDocument();
expect(screen.getByRole('menuitem', { name: '查找同类仓库' })).toBeInTheDocument();
expect(screen.getByRole('menuitem', { name: '查看 Release' })).toBeInTheDocument();
expect(screen.getByRole('menuitem', { name: '取消 Star' })).toBeInTheDocument();
Expand Down Expand Up @@ -250,6 +254,21 @@ describe('RepositoryCard view modes', () => {
expect(actionMocks.actions.unstar).toHaveBeenCalledOnce();
});

it('emits a repository-chat intent from both grid and list controls without invoking a service', async () => {
const user = userEvent.setup();
const onAskRepository = vi.fn();
const { unmount } = renderRepositoryCard('grid', { onAskRepository });

await user.click(screen.getByRole('button', { name: '问答此仓库' }));
expect(onAskRepository).toHaveBeenCalledWith(repository);
unmount();

renderRepositoryCard('list', { onAskRepository });
await user.click(screen.getByRole('button', { name: '更多操作' }));
await user.click(screen.getByRole('menuitem', { name: '问答此仓库' }));
expect(onAskRepository).toHaveBeenCalledTimes(2);
});

it('opens the release sheet from the grid action immediately before the DeepWiki/Zread action', async () => {
const user = userEvent.setup();
renderRepositoryCard('grid');
Expand Down Expand Up @@ -291,10 +310,11 @@ describe('RepositoryCard view modes', () => {
});

it('retains the existing quick action row in grid mode', () => {
renderRepositoryCard('grid');
renderRepositoryCard('grid', { onAskRepository: vi.fn() });

expect(screen.queryByRole('button', { name: '更多操作' })).not.toBeInTheDocument();
expect(screen.getByTitle('AI分析此仓库')).toBeInTheDocument();
expect(screen.getByTitle('问答此仓库')).toBeInTheDocument();
expect(screen.getByTitle('取消订阅发布')).toBeInTheDocument();
expect(screen.getByTitle('编辑仓库信息')).toBeInTheDocument();

Expand All @@ -307,7 +327,7 @@ describe('RepositoryCard view modes', () => {

it('collapses trailing grid actions into a more-actions menu when the card is narrow', async () => {
const user = userEvent.setup();
renderRepositoryCard('grid');
renderRepositoryCard('grid', { onAskRepository: vi.fn() });
const actionRow = screen.getByTestId('grid-action-row');
Object.defineProperty(actionRow, 'clientWidth', { configurable: true, value: 190 });

Expand All @@ -322,5 +342,11 @@ describe('RepositoryCard view modes', () => {
expect(screen.getByRole('menuitem', { name: '在 Zread 中查看' })).toBeInTheDocument();
expect(screen.getByRole('menuitem', { name: '在 GitHub 中查看' })).toBeInTheDocument();
expect(screen.getByRole('menuitem', { name: '取消 Star' })).toBeInTheDocument();

Object.defineProperty(actionRow, 'clientWidth', { configurable: true, value: 76 });
await act(async () => {
window.dispatchEvent(new Event('resize'));
});
expect(screen.getByRole('menuitem', { name: '问答此仓库' })).toBeInTheDocument();
});
});
60 changes: 43 additions & 17 deletions src/components/RepositoryCard.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import React, { Suspense, useState, useRef, useEffect, useMemo, useCallback } from 'react';
import { createPortal } from 'react-dom';
import { GripVertical, Star, StarOff, ExternalLink, Calendar, Bell, BellOff, Bot, Sparkles, Monitor, Smartphone, Globe, Terminal, Package, Edit3, BookOpen, Apple, Square, CheckSquare, Loader2, HelpCircle, Search, Scale, MoreHorizontal, PackageOpen } from 'lucide-react';
import { GripVertical, Star, StarOff, ExternalLink, Calendar, Bell, BellOff, Bot, Sparkles, Monitor, Smartphone, Globe, Terminal, Package, Edit3, BookOpen, Apple, Square, CheckSquare, Loader2, HelpCircle, Search, Scale, MoreHorizontal, PackageOpen, MessageSquareText } from 'lucide-react';
import { Repository, Category } from '../types';
import { useAppStore } from '../store/useAppStore';
import { getAICategory, getDefaultCategory } from '../utils/categoryUtils';
Expand Down Expand Up @@ -128,6 +128,7 @@ interface RepositoryCardProps {
isExitingSelection?: boolean;
allCategories: Category[];
viewMode?: 'grid' | 'list';
onAskRepository?: (repository: Repository) => void;
}

const MAX_CACHE_SIZE = 500;
Expand All @@ -147,7 +148,8 @@ const RepositoryCardComponent: React.FC<RepositoryCardProps> = ({
selectionMode = false,
isExitingSelection = false,
allCategories,
viewMode = 'grid'
viewMode = 'grid',
onAskRepository,
}) => {
const language = useAppStore((state) => state.language);
const {
Expand All @@ -172,7 +174,7 @@ const RepositoryCardComponent: React.FC<RepositoryCardProps> = ({
const menuDismissedByPointerDownRef = useRef(false);
const releaseSheetOutsideDismissedAtRef = useRef<number | null>(null);
const gridActionRowRef = useRef<HTMLDivElement>(null);
const [visibleGridActionCount, setVisibleGridActionCount] = useState(7);
const [visibleGridActionCount, setVisibleGridActionCount] = useState(8);

const restoreReadmeTriggerFocus = useCallback(() => {
cardRef.current?.focus();
Expand All @@ -193,7 +195,7 @@ const RepositoryCardComponent: React.FC<RepositoryCardProps> = ({
// until a real layout measurement is available.
if (width === 0) return;
const capacity = Math.max(1, Math.floor((width + 6) / 38));
setVisibleGridActionCount(capacity >= 7 ? 7 : Math.max(0, capacity - 1));
setVisibleGridActionCount(capacity >= 8 ? 8 : Math.max(0, capacity - 1));
};

updateVisibleActionCount();
Expand Down Expand Up @@ -790,6 +792,12 @@ const RepositoryCardComponent: React.FC<RepositoryCardProps> = ({
{isAnalyzing ? <Loader2 className="mr-2 h-3.5 w-3.5 animate-spin" /> : <Bot className="mr-2 h-3.5 w-3.5" />}
{language === 'zh' ? 'AI 分析' : 'Analyze with AI'}
</DropdownMenuItem>
{onAskRepository && (
<DropdownMenuItem onSelect={() => onAskRepository(repository)}>
<MessageSquareText className="mr-2 h-3.5 w-3.5" />
{language === 'zh' ? '问答此仓库' : 'Ask this repository'}
</DropdownMenuItem>
)}
<DropdownMenuItem onSelect={() => toggleReleaseSubscription()}>
{isSubscribed ? <Bell className="mr-2 h-3.5 w-3.5" /> : <BellOff className="mr-2 h-3.5 w-3.5" />}
{isSubscribed ? (language === 'zh' ? '取消订阅 Release' : 'Unsubscribe from releases') : (language === 'zh' ? '订阅 Release' : 'Subscribe to releases')}
Expand Down Expand Up @@ -883,7 +891,18 @@ const RepositoryCardComponent: React.FC<RepositoryCardProps> = ({
{isAnalyzing ? <Loader2 className="w-4 h-4 animate-spin" /> : <Bot className="w-4 h-4" />}
</SelectionAwareButton>
)}
{visibleGridActionCount >= 2 && (
{onAskRepository && visibleGridActionCount >= 2 && (
<SelectionAwareButton
onClick={() => onAskRepository(repository)}
selectionMode={selectionMode}
className="bg-muted text-muted-foreground transition-colors hover:bg-accent hover:text-accent-foreground"
title={language === 'zh' ? '问答此仓库' : 'Ask this repository'}
aria-label={language === 'zh' ? '问答此仓库' : 'Ask this repository'}
>
<MessageSquareText className="w-4 h-4" />
</SelectionAwareButton>
)}
{visibleGridActionCount >= 3 && (
<SelectionAwareButton
onClick={() => toggleReleaseSubscription()}
selectionMode={selectionMode}
Expand All @@ -896,7 +915,7 @@ const RepositoryCardComponent: React.FC<RepositoryCardProps> = ({
{isSubscribed ? <Bell className="w-4 h-4" /> : <BellOff className="w-4 h-4" />}
</SelectionAwareButton>
)}
{visibleGridActionCount >= 3 && (
{visibleGridActionCount >= 4 && (
<SelectionAwareButton
onClick={() => setEditModalOpen(true)}
selectionMode={selectionMode}
Expand All @@ -906,7 +925,7 @@ const RepositoryCardComponent: React.FC<RepositoryCardProps> = ({
<Edit3 className="w-4 h-4" />
</SelectionAwareButton>
)}
{visibleGridActionCount >= 4 && (
{visibleGridActionCount >= 5 && (
<SelectionAwareButton
onClick={() => setReleaseSheetOpen(true)}
selectionMode={selectionMode}
Expand All @@ -917,7 +936,7 @@ const RepositoryCardComponent: React.FC<RepositoryCardProps> = ({
<PackageOpen className="w-4 h-4" />
</SelectionAwareButton>
)}
{visibleGridActionCount >= 5 && (
{visibleGridActionCount >= 6 && (
<a
href={language === 'zh' ? getZreadUrl(repository.full_name) : getDeepWikiUrl(repository.html_url)}
target="_blank"
Expand All @@ -929,7 +948,7 @@ const RepositoryCardComponent: React.FC<RepositoryCardProps> = ({
<BookOpen className="w-4 h-4" />
</a>
)}
{visibleGridActionCount >= 6 && (
{visibleGridActionCount >= 7 && (
<a
href={repository.html_url}
target="_blank"
Expand All @@ -941,7 +960,7 @@ const RepositoryCardComponent: React.FC<RepositoryCardProps> = ({
<ExternalLink className="w-4 h-4" />
</a>
)}
{visibleGridActionCount >= 7 && (
{visibleGridActionCount >= 8 && (
<SelectionAwareButton
onClick={handleUnstar}
disabled={unstarring}
Expand All @@ -952,7 +971,7 @@ const RepositoryCardComponent: React.FC<RepositoryCardProps> = ({
<StarOff className={`w-4 h-4 ${unstarring ? 'animate-pulse' : ''}`} />
</SelectionAwareButton>
)}
{visibleGridActionCount < 7 && (
{visibleGridActionCount < 8 && (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button
Expand All @@ -974,41 +993,47 @@ const RepositoryCardComponent: React.FC<RepositoryCardProps> = ({
{language === 'zh' ? 'AI 分析' : 'Analyze with AI'}
</DropdownMenuItem>
)}
{visibleGridActionCount < 2 && (
{onAskRepository && visibleGridActionCount < 2 && (
<DropdownMenuItem onSelect={() => onAskRepository(repository)}>
<MessageSquareText className="mr-2 h-3.5 w-3.5" />
{language === 'zh' ? '问答此仓库' : 'Ask this repository'}
</DropdownMenuItem>
)}
{visibleGridActionCount < 3 && (
<DropdownMenuItem onSelect={() => toggleReleaseSubscription()}>
{isSubscribed ? <Bell className="mr-2 h-3.5 w-3.5" /> : <BellOff className="mr-2 h-3.5 w-3.5" />}
{isSubscribed ? (language === 'zh' ? '取消订阅 Release' : 'Unsubscribe from releases') : (language === 'zh' ? '订阅 Release' : 'Subscribe to releases')}
</DropdownMenuItem>
)}
{visibleGridActionCount < 3 && (
{visibleGridActionCount < 4 && (
<DropdownMenuItem onSelect={() => setEditModalOpen(true)}>
<Edit3 className="mr-2 h-3.5 w-3.5" />
{language === 'zh' ? '编辑仓库信息' : 'Edit repository info'}
</DropdownMenuItem>
)}
{visibleGridActionCount < 4 && (
{visibleGridActionCount < 5 && (
<DropdownMenuItem onSelect={() => setReleaseSheetOpen(true)}>
<PackageOpen className="mr-2 h-3.5 w-3.5" />
{language === 'zh' ? '查看 Release' : 'View releases'}
</DropdownMenuItem>
)}
{visibleGridActionCount < 5 && (
{visibleGridActionCount < 6 && (
<DropdownMenuItem asChild>
<a href={language === 'zh' ? getZreadUrl(repository.full_name) : getDeepWikiUrl(repository.html_url)} target="_blank" rel="noopener noreferrer">
<BookOpen className="mr-2 h-3.5 w-3.5" />
{language === 'zh' ? '在 Zread 中查看' : 'View on DeepWiki'}
</a>
</DropdownMenuItem>
)}
{visibleGridActionCount < 6 && (
{visibleGridActionCount < 7 && (
<DropdownMenuItem asChild>
<a href={repository.html_url} target="_blank" rel="noopener noreferrer">
<ExternalLink className="mr-2 h-3.5 w-3.5" />
{language === 'zh' ? '在 GitHub 中查看' : 'View on GitHub'}
</a>
</DropdownMenuItem>
)}
{visibleGridActionCount < 7 && (
{visibleGridActionCount < 8 && (
<DropdownMenuItem className="text-destructive focus:text-destructive" disabled={unstarring} onSelect={() => void handleUnstar()}>
<StarOff className={`mr-2 h-3.5 w-3.5 ${unstarring ? 'animate-pulse' : ''}`} />
{language === 'zh' ? '取消 Star' : 'Unstar'}
Expand Down Expand Up @@ -1297,6 +1322,7 @@ export const RepositoryCard = React.memo(RepositoryCardComponent, (prevProps, ne
prevProps.selectionMode === nextProps.selectionMode &&
prevProps.isExitingSelection === nextProps.isExitingSelection &&
prevProps.viewMode === nextProps.viewMode &&
prevProps.onAskRepository === nextProps.onAskRepository &&
allCategoriesEqual
);
});
Loading