Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
1 change: 1 addition & 0 deletions src/components/GlobalChatHistorySheet.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ describe('GlobalChatHistorySheet', () => {
render(<GlobalChatHistorySheet isOpen repositories={repositories} onClose={() => {}} onSelectSession={() => {}} />);

const items = await screen.findAllByTitle(/进入 owner\//);
expect(screen.getByRole('heading', { name: '问答历史 (2)' })).toBeInTheDocument();
expect(items.map((item) => item.textContent)).toEqual([
expect.stringContaining('title-newer'),
expect.stringContaining('title-older'),
Expand Down
1 change: 1 addition & 0 deletions src/components/GlobalChatHistorySheet.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,7 @@ export const GlobalChatHistorySheet: React.FC<GlobalChatHistorySheetProps> = ({
<SheetTitle className="flex items-center gap-2 text-base">
<History className="h-4 w-4 shrink-0" aria-hidden="true" />
{t('globalChatHistorySheet.chat-history')}
{!isLoading && !loadError && <span className="text-sm font-normal text-muted-foreground">({sessions.length})</span>}
</SheetTitle>
<SheetDescription>
{t('globalChatHistorySheet.conversations-across-repositories-select-one-to')}
Expand Down
50 changes: 45 additions & 5 deletions src/components/RepositoryCard.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -210,7 +210,7 @@ describe('RepositoryCard view modes', () => {
actionMocks.actions.vectorSearchAvailable = false;
renderRepositoryCard('list');

expect(screen.queryByRole('button', { name: '更多仓库操作' })).not.toBeInTheDocument();
expect(screen.getByRole('button', { name: '更多仓库操作' })).toBeInTheDocument();
expect(screen.queryByRole('menuitem', { name: '查找同类仓库' })).not.toBeInTheDocument();
expect(screen.queryByText('查找同类')).not.toBeInTheDocument();
} finally {
Expand Down Expand Up @@ -280,17 +280,57 @@ describe('RepositoryCard view modes', () => {
expect(screen.getByTestId('repository-edit-modal')).toBeInTheDocument();
});

it('delegates grid quick actions to the domain Hook without changing their presentation', async () => {
it('keeps Unstar at the bottom of the grid menu and delegates it to the domain Hook', async () => {
const user = userEvent.setup();
renderRepositoryCard('grid');

await user.click(screen.getByTitle('AI分析此仓库'));
await user.click(screen.getByTitle('取消 Star'));
expect(screen.queryByTitle('取消 Star')).not.toBeInTheDocument();
await user.click(screen.getByRole('button', { name: '更多仓库操作' }));
expect(screen.getAllByRole('menuitem').at(-1)).toHaveTextContent('取消 Star');
await user.click(screen.getByRole('menuitem', { name: '取消 Star' }));

expect(actionMocks.actions.analyze).toHaveBeenCalledOnce();
expect(actionMocks.actions.unstar).toHaveBeenCalledOnce();
});

it('does not show a separator above Unstar when it is the only grid menu item', async () => {
const user = userEvent.setup();
renderRepositoryCard('grid');

await user.click(screen.getByRole('button', { name: '更多仓库操作' }));
expect(screen.getAllByRole('menuitem')).toHaveLength(1);
expect(screen.queryByRole('separator')).not.toBeInTheDocument();
});

it('closes the grid action menu without opening README when card whitespace is clicked', async () => {
const user = userEvent.setup();
const { container } = renderRepositoryCard('grid');
const card = container.firstElementChild as HTMLElement;

await user.click(screen.getByRole('button', { name: '更多仓库操作' }));
expect(screen.getByRole('menuitem', { name: '取消 Star' })).toBeInTheDocument();
await act(async () => {
fireEvent.pointerDown(card);
fireEvent.click(card);
});

await waitFor(() => expect(screen.queryByRole('menuitem', { name: '取消 Star' })).not.toBeInTheDocument());
expect(screen.queryByTestId('readme-modal')).not.toBeInTheDocument();
});

it('shows the customized edit tooltip in grid mode', () => {
render(
<DialogProvider>
<TooltipProvider>
<RepositoryCard repository={{ ...repository, custom_description: 'My summary' }} allCategories={[]} viewMode="grid" />
</TooltipProvider>
</DialogProvider>,
);

expect(screen.getByTitle('已自定义,编辑仓库信息')).toBeInTheDocument();
});

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();
Expand Down Expand Up @@ -360,10 +400,10 @@ describe('RepositoryCard view modes', () => {
expect(screen.queryByTestId('readme-modal')).not.toBeInTheDocument();
});

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

expect(screen.queryByRole('button', { name: '更多仓库操作' })).not.toBeInTheDocument();
expect(screen.getByRole('button', { name: '更多仓库操作' })).toBeInTheDocument();
expect(screen.getByTitle('AI分析此仓库')).toBeInTheDocument();
expect(screen.getByTitle('问答此仓库')).toBeInTheDocument();
expect(screen.getByTitle('取消订阅发布')).toBeInTheDocument();
Expand Down
72 changes: 33 additions & 39 deletions src/components/RepositoryCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -198,21 +198,26 @@ const MAX_CACHE_SIZE = 500;
const ACTION_SLOT_WIDTH = 32;
const ACTION_SLOT_GAP = 6;
const ACTION_SLOT_STRIDE = ACTION_SLOT_WIDTH + ACTION_SLOT_GAP;
const BASE_OVERFLOW_ACTION_COUNT = 7;
const BASE_OVERFLOW_ACTION_COUNT = 6;

const highlightCache = new Map<string, React.ReactNode>();

/**
* Counts quick actions that fit beside the persistent More actions button.
* A zero width means the row has not been measured yet, so all primary actions
* remain visible until layout supplies a usable width.
*
* @param width - Available action-row width in pixels.
* @param primaryActionCount - Number of primary quick actions.
* @returns Number of primary actions to render outside the menu.
*/
const countVisibleOverflowActions = (
width: number,
primaryActionCount: number,
hasPersistentMenu: boolean,
): number => {
if (width === 0) return primaryActionCount;
const capacity = Math.max(1, Math.floor((width + ACTION_SLOT_GAP) / ACTION_SLOT_STRIDE));
if (hasPersistentMenu) {
return Math.max(0, Math.min(primaryActionCount, capacity - 1));
}
return capacity >= primaryActionCount ? primaryActionCount : Math.max(0, capacity - 1);
return Math.max(0, Math.min(primaryActionCount, capacity - 1));
};

const mutedIconButtonClass =
Expand Down Expand Up @@ -295,9 +300,9 @@ const OverflowActionRow: React.FC<OverflowActionRowProps> = ({
const showReleases = visibleActionCount >= 4 + askSlot;
const showDocs = visibleActionCount >= 5 + askSlot;
const showGithub = visibleActionCount >= 6 + askSlot;
const showUnstar = visibleActionCount >= 7 + askSlot;
const showFindSimilarInMenu = Boolean(vectorSearchAvailable && onFindSimilar && (persistFindSimilarInMenu || visibleActionCount < primaryActionCount));
const showOverflowMenu = visibleActionCount < primaryActionCount || pluginActions.length > 0 || showFindSimilarInMenu;
const hasActionsAbovePlugins = !showAnalyze || (Boolean(onAskRepository) && !showAsk) || !showSubscribe || !showEdit || !showReleases || !showDocs || !showGithub || showFindSimilarInMenu;
const hasActionsAboveUnstar = hasActionsAbovePlugins || pluginActions.length > 0;
const menuOpenProps = onActionsMenuOpenChange
? { open: isActionsMenuOpen, onOpenChange: onActionsMenuOpenChange }
: {};
Expand Down Expand Up @@ -387,19 +392,7 @@ const OverflowActionRow: React.FC<OverflowActionRowProps> = ({
<ExternalLink className="w-4 h-4" />
</a>
) : null}
{showUnstar ? (
<SelectionAwareButton
onClick={onUnstar}
disabled={unstarring}
selectionMode={selectionMode}
variant="unstar"
title={t('repositoryCard.unstar')}
>
<StarOff className={`w-4 h-4 ${unstarring ? 'animate-pulse' : ''}`} />
</SelectionAwareButton>
) : null}
{showOverflowMenu ? (
<DropdownMenu {...menuOpenProps}>
<DropdownMenu {...menuOpenProps}>
<DropdownMenuTrigger asChild>
<Button
type="button"
Expand Down Expand Up @@ -470,12 +463,6 @@ const OverflowActionRow: React.FC<OverflowActionRowProps> = ({
</a>
</DropdownMenuItem>
)}
{showUnstar ? null : (
<DropdownMenuItem className="text-destructive focus:text-destructive" disabled={unstarring} onSelect={() => void onUnstar()}>
<StarOff className={`mr-2 h-3.5 w-3.5 ${unstarring ? 'animate-pulse' : ''}`} />
{t('repositoryCard.unstar')}
</DropdownMenuItem>
)}
{showFindSimilarInMenu ? (
<DropdownMenuItem disabled={isFindingSimilar} onSelect={() => onFindSimilar?.()}>
{isFindingSimilar ? <Loader2 className="mr-2 h-3.5 w-3.5 animate-spin" /> : <Search className="mr-2 h-3.5 w-3.5" />}
Expand All @@ -484,14 +471,18 @@ const OverflowActionRow: React.FC<OverflowActionRowProps> = ({
) : null}
{pluginActions.length > 0 ? (
<>
<DropdownMenuSeparator />
{hasActionsAbovePlugins ? <DropdownMenuSeparator /> : null}
<DropdownMenuLabel>{t('repositoryCard.plugin-actions')}</DropdownMenuLabel>
<PluginRepositoryActionItems actions={pluginActions} repository={repository} language={language} />
</>
) : null}
{hasActionsAboveUnstar ? <DropdownMenuSeparator /> : null}
<DropdownMenuItem className="text-destructive focus:text-destructive" disabled={unstarring} onSelect={() => void onUnstar()}>
<StarOff className={`mr-2 h-3.5 w-3.5 ${unstarring ? 'animate-pulse' : ''}`} />
{t('repositoryCard.unstar')}
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
) : null}
</DropdownMenu>
</div>
);
};
Expand Down Expand Up @@ -559,9 +550,7 @@ const RepositoryCardComponent: React.FC<RepositoryCardProps> = ({
}, []);

useEffect(() => {
if (viewMode !== 'list' || selectionMode) {
setIsActionsMenuOpen(false);
}
setIsActionsMenuOpen(false);
}, [viewMode, selectionMode]);

useEffect(() => {
Expand All @@ -573,11 +562,8 @@ const RepositoryCardComponent: React.FC<RepositoryCardProps> = ({
setVisibleOverflowActionCount(primaryOverflowActionCount);
return;
}
const hasPersistentMenu =
pluginActions.actions.length > 0 ||
(viewMode === 'list' && vectorSearchAvailable);
setVisibleOverflowActionCount(
countVisibleOverflowActions(width, primaryOverflowActionCount, hasPersistentMenu),
countVisibleOverflowActions(width, primaryOverflowActionCount),
);
};

Expand All @@ -589,7 +575,7 @@ const RepositoryCardComponent: React.FC<RepositoryCardProps> = ({
observer.disconnect();
window.removeEventListener('resize', updateVisibleActionCount);
};
}, [viewMode, primaryOverflowActionCount, pluginActions.actions.length, selectionMode, vectorSearchAvailable]);
}, [viewMode, primaryOverflowActionCount, selectionMode]);

// 高亮搜索关键词的工具函数 - 使用缓存优化
const highlightSearchTerm = useCallback((text: string, searchTerm: string): React.ReactNode => {
Expand Down Expand Up @@ -1233,13 +1219,21 @@ const RepositoryCardComponent: React.FC<RepositoryCardProps> = ({
isSubscribed={isSubscribed}
unstarring={unstarring}
pluginActions={pluginActions.actions}
isActionsMenuOpen={isActionsMenuOpen}
onActionsMenuOpenChange={setIsActionsMenuOpen}
onAnalyze={handleAIAnalyze}
onAsk={onAskRepository ? () => onAskRepository(repository) : undefined}
onToggleReleaseSubscription={toggleReleaseSubscription}
onEdit={() => setEditModalOpen(true)}
editButtonTitle={t('repositoryCard.edit-repository-info')}
editButtonTitle={displayContent.isCustomized ? t('repositoryCard.customized-edit-repository-info') : t('repositoryCard.edit-repository-info')}
onViewReleases={() => setReleaseSheetOpen(true)}
onUnstar={handleUnstar}
menuAlign="start"
onMenuPointerDownOutside={(target) => {
if (cardRef.current?.contains(target)) {
menuDismissedByPointerDownRef.current = true;
}
}}
t={t}
/>
) : null}
Expand Down
4 changes: 2 additions & 2 deletions src/components/RepositoryHealthPanel.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ const release: Release = {
};

async function expandPanel() {
const trigger = screen.getByRole('button', { name: /仓库健康事实/ });
const trigger = screen.getByRole('button', { name: /仓库概况/ });
if (trigger.getAttribute('aria-expanded') !== 'true') {
await userEvent.click(trigger);
}
Expand All @@ -48,7 +48,7 @@ describe('RepositoryHealthPanel', () => {
it('默认折叠,点击标题后展开事实', async () => {
render(<RepositoryHealthPanel repository={makeRepo()} releases={[release]} language="zh" />);

const trigger = screen.getByRole('button', { name: /仓库健康事实/ });
const trigger = screen.getByRole('button', { name: /仓库概况/ });
expect(trigger).toHaveAttribute('aria-expanded', 'false');
expect(screen.queryByText('活跃度')).not.toBeInTheDocument();

Expand Down
36 changes: 0 additions & 36 deletions src/components/SearchBar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@ import { useSearchShortcuts } from '../hooks/useSearchShortcuts';
import { useSearchActions } from '../features/repositories/hooks/useSearchActions';
import { useDialog } from '../hooks/useDialog';
import { isRepoCustomized } from '../utils/repoUtils';
import { repositoryChatStorage } from '../services/repositoryChatStorage';
import { applyRepoFilters, performBasicTextSearch as basicTextSearch, sortRepositories } from '../utils/repoSearch';
import { NO_LICENSE_SENTINEL, normalizeLicense } from '../utils/licenseFilter';
import { NumberInput } from './ui/NumberInput';
Expand Down Expand Up @@ -119,36 +118,6 @@ export const SearchBar: React.FC = () => {
const [availableLicenses, setAvailableLicenses] = useState<string[]>([]);
const [isRealTimeSearch, setIsRealTimeSearch] = useState(false);
const [isComposing, setIsComposing] = useState(false);
// S4 全局问答历史入口:徽标显示已保存会话数。
const [globalHistoryCount, setGlobalHistoryCount] = useState(0);
// 重叠刷新只允许最新请求提交,避免旧数量覆盖新状态。
const globalHistoryRequestRef = useRef(0);

useEffect(() => {
let cancelled = false;
const refreshGlobalHistoryCount = async () => {
const requestId = ++globalHistoryRequestRef.current;
try {
const sessions = await repositoryChatStorage.listRecentSessions(100);
if (!cancelled && requestId === globalHistoryRequestRef.current) setGlobalHistoryCount(sessions.length);
} catch {
if (!cancelled && requestId === globalHistoryRequestRef.current) setGlobalHistoryCount(0);
}
};
void refreshGlobalHistoryCount();
// 删除会话时由历史抽屉广播;打开抽屉与窗口重获焦点时也刷新,
// 覆盖单仓问答内新建会话导致的计数过期。
window.addEventListener('gsm:global-chat-history-changed', refreshGlobalHistoryCount);
window.addEventListener('gsm:open-global-chat-history', refreshGlobalHistoryCount);
window.addEventListener('focus', refreshGlobalHistoryCount);
return () => {
cancelled = true;
window.removeEventListener('gsm:global-chat-history-changed', refreshGlobalHistoryCount);
window.removeEventListener('gsm:open-global-chat-history', refreshGlobalHistoryCount);
window.removeEventListener('focus', refreshGlobalHistoryCount);
};
}, []);

const openGlobalChatHistory = () => {
window.dispatchEvent(new CustomEvent('gsm:open-global-chat-history'));
};
Expand Down Expand Up @@ -873,11 +842,6 @@ export const SearchBar: React.FC = () => {
>
<History className="w-4 h-4" aria-hidden="true" />
<span>{t('searchBar.chat-history')}</span>
{globalHistoryCount > 0 && (
<span className="bg-primary text-primary-foreground rounded-full px-2 py-0.5 text-xs">
{globalHistoryCount > 99 ? '99+' : globalHistoryCount}
</span>
)}
</Button>

<Button
Expand Down
2 changes: 1 addition & 1 deletion src/locales/de/app.json
Original file line number Diff line number Diff line change
Expand Up @@ -339,7 +339,7 @@
"sort-stars": "Nach Stern sortieren",
"sort-starred": "Sortieren nach Sternadditionszeit",
"sync-lists-confirm-body": "Ruft Ihre GitHub-Listen (Sternlisten) ab und wendet sie auf Ihr lokales Repository an:\n\n· Jeder Listenname wird dem entsprechenden Lager als Bezeichnung hinzugefügt (ein Lager kann zu mehreren Listen/Kategorien gehören).\n· Lager mit entsperrten Kategorien wenden die der Liste entsprechende Kategorie an und sind standardmäßig gesperrt\n· Lager mit gesperrten Kategorien bleiben unverändert\n\nSind Sie sicher, dass Sie fortfahren möchten?",
"repository-health-facts": "Repository-Gesundheitsdaten",
"repository-health-facts": "Repository-Überblick",
"show-only-archived-repositories": "Nur archivierte Repositories anzeigen",
"archived": "Archiviert",
"show-only-repositories-pushed-within-the-last-12": "Nur Repositories mit Push in den letzten 12 Monaten anzeigen",
Expand Down
2 changes: 1 addition & 1 deletion src/locales/de/repositories.json
Original file line number Diff line number Diff line change
Expand Up @@ -305,7 +305,7 @@
"no": "Nein",
"v1-year": "{{v1}} / Jahr",
"v1-days-years-years": "{{v1}} Tage (~{{years}} Jahre)",
"repository-health-facts": "Repository-Gesundheitsdaten",
"repository-health-facts": "Repository-Überblick",
"these-are-objective-facts-with-no-overall-score": "Das sind objektive Fakten ohne Gesamtpunktzahl. „Unbekannt“ bedeutet, dass der Fakt noch nicht vorliegt."
}
}
2 changes: 1 addition & 1 deletion src/locales/en/app.json
Original file line number Diff line number Diff line change
Expand Up @@ -339,7 +339,7 @@
"sort-stars": "Sort by Stars",
"sort-starred": "Sort by Starred Time",
"sync-lists-confirm-body": "This will fetch your GitHub Lists and apply them to local repositories:\n\n· Each list name is added as a tag (a repo can belong to multiple lists/categories)\n· Unlocked repos get the list category applied and locked\n· Locked repos are left unchanged\n\nContinue?",
"repository-health-facts": "Repository Health Facts",
"repository-health-facts": "Repository overview",
"show-only-archived-repositories": "Show only archived repositories",
"archived": "Archived",
"show-only-repositories-pushed-within-the-last-12": "Show only repositories pushed within the last 12 months",
Expand Down
2 changes: 1 addition & 1 deletion src/locales/en/repositories.json
Original file line number Diff line number Diff line change
Expand Up @@ -305,7 +305,7 @@
"no": "No",
"v1-year": "{{v1}} / year",
"v1-days-years-years": "{{v1}} days (~{{years}} years)",
"repository-health-facts": "Repository health facts",
"repository-health-facts": "Repository overview",
"these-are-objective-facts-with-no-overall-score": "These are objective facts with no overall score. “Unknown” means the fact has not been obtained yet."
}
}
2 changes: 1 addition & 1 deletion src/locales/es/app.json
Original file line number Diff line number Diff line change
Expand Up @@ -339,7 +339,7 @@
"sort-stars": "Ordenar por estrella",
"sync-lists-confirm-body": "Extraerá sus listas de GitHub (listas destacadas) y las aplicará a su repositorio local:\n\n· Cada nombre de lista se agregará al almacén correspondiente como una etiqueta (un almacén puede pertenecer a varias listas/categorías)\n· Los almacenes con categorías desbloqueadas aplicarán la categoría correspondiente a la lista y estarán bloqueados de forma predeterminada\n· Los almacenes con categorías bloqueadas permanecen sin cambios\n\n¿Estás seguro de que quieres continuar?",
"sort-starred": "Ordenar por tiempo de adición de estrellas",
"repository-health-facts": "Hechos de salud del repositorio",
"repository-health-facts": "Resumen del repositorio",
"show-only-archived-repositories": "Mostrar solo repositorios archivados",
"archived": "Archivado",
"show-only-repositories-pushed-within-the-last-12": "Mostrar solo repositorios con push en los últimos 12 meses",
Expand Down
2 changes: 1 addition & 1 deletion src/locales/es/repositories.json
Original file line number Diff line number Diff line change
Expand Up @@ -305,7 +305,7 @@
"no": "No",
"v1-year": "{{v1}} / año",
"v1-days-years-years": "{{v1}} días (~{{years}} años)",
"repository-health-facts": "Hechos de salud del repositorio",
"repository-health-facts": "Resumen del repositorio",
"these-are-objective-facts-with-no-overall-score": "Son hechos objetivos, sin puntuación global. “Desconocido” significa que el dato aún no se ha obtenido."
}
}
Loading
Loading