diff --git a/src/components/GlobalChatHistorySheet.test.tsx b/src/components/GlobalChatHistorySheet.test.tsx index 5189cbd1..33bc98d6 100644 --- a/src/components/GlobalChatHistorySheet.test.tsx +++ b/src/components/GlobalChatHistorySheet.test.tsx @@ -41,6 +41,7 @@ describe('GlobalChatHistorySheet', () => { render( {}} 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'), diff --git a/src/components/GlobalChatHistorySheet.tsx b/src/components/GlobalChatHistorySheet.tsx index a590b511..3e114fb5 100644 --- a/src/components/GlobalChatHistorySheet.tsx +++ b/src/components/GlobalChatHistorySheet.tsx @@ -115,6 +115,7 @@ export const GlobalChatHistorySheet: React.FC = ({ {t('globalChatHistorySheet.conversations-across-repositories-select-one-to')} diff --git a/src/components/RepositoryCard.test.tsx b/src/components/RepositoryCard.test.tsx index 1bc55150..4cec6d46 100644 --- a/src/components/RepositoryCard.test.tsx +++ b/src/components/RepositoryCard.test.tsx @@ -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 { @@ -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( + + + + + , + ); + + 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(); @@ -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(); diff --git a/src/components/RepositoryCard.tsx b/src/components/RepositoryCard.tsx index eca16a72..d29581ce 100644 --- a/src/components/RepositoryCard.tsx +++ b/src/components/RepositoryCard.tsx @@ -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(); +/** + * 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 = @@ -295,9 +300,9 @@ const OverflowActionRow: React.FC = ({ 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 } : {}; @@ -387,19 +392,7 @@ const OverflowActionRow: React.FC = ({ ) : null} - {showUnstar ? ( - - - - ) : null} - {showOverflowMenu ? ( - +