📭 fix: Detect Agent List Pages in useHasData (#15156)

* 🐛 fix: Detect `AgentListResponse` data in `useHasData`

The marketplace agent queries return `AgentListResponse` pages whose
agents live under the `data` field, but `useHasData` only checked for
a non-existent `agents` field, so it always returned `false` for real
agent list pages. Check the `data` field first so cached list pages are
recognized as meaningful data.

* fix: preserve SmartLoader type narrowing

* fix: retain cached agents during refetch

---------

Co-authored-by: Danny Avila <danny@librechat.ai>
This commit is contained in:
JOJO 2026-08-24 15:45:59 +09:00 committed by GitHub
parent c7e8b45419
commit 092bc583a8
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 70 additions and 12 deletions

View file

@ -225,7 +225,7 @@ const AgentGrid: React.FC<AgentGridProps> = ({
</div>
);
if (isLoading || (isFetching && !isFetchingNextPage)) {
if ((isLoading || (isFetching && !isFetchingNextPage)) && !hasData) {
return loadingSpinner;
}
return mainContent;

View file

@ -1,5 +1,5 @@
import React, { useState, useEffect } from 'react';
import { AgentListResponse } from 'librechat-data-provider';
import type { AgentListResponse } from 'librechat-data-provider';
interface SmartLoaderProps {
/** Whether the content is currently loading */
@ -73,6 +73,12 @@ export const useHasData = (data: AgentListResponse | undefined): boolean => {
// Type guard for object data
if (typeof data === 'object' && data !== null) {
// Check for agent list data (AgentListResponse shape, e.g. marketplace pages)
const agents = data.data;
if (Array.isArray(agents)) {
return agents.length > 0;
}
// Check for agent list data
if ('agents' in data) {
const agents = (data as any).agents;

View file

@ -225,7 +225,7 @@ const VirtualizedAgentGrid: React.FC<VirtualizedAgentGridProps> = ({
}
// Handle loading state
if (isLoading || (isFetching && !isFetchingNextPage)) {
if ((isLoading || (isFetching && !isFetchingNextPage)) && !hasData) {
return loadingSpinner;
}

View file

@ -18,11 +18,6 @@ jest.mock('~/hooks/Agents', () => ({
})),
}));
// Mock SmartLoader
jest.mock('../SmartLoader', () => ({
useHasData: jest.fn(() => true),
}));
// Mock useLocalize hook
jest.mock('~/hooks/useLocalize', () => () => (key: string, options?: any) => {
const mockTranslations: Record<string, string> = {
@ -362,6 +357,23 @@ describe('AgentGrid Integration with useGetMarketplaceAgentsQuery', () => {
expect(spinner).toBeInTheDocument();
});
it('should retain cached agents while refetching', () => {
mockUseMarketplaceAgentsInfiniteQuery.mockReturnValue({
...defaultMockQueryResult,
isFetching: true,
});
const Wrapper = createWrapper();
render(
<Wrapper>
<AgentGrid category="finance" searchQuery="" onSelectAgent={mockOnSelectAgent} />
</Wrapper>,
);
expect(screen.getByTestId('agent-card-1')).toBeInTheDocument();
expect(screen.getByTestId('agent-card-2')).toBeInTheDocument();
});
it('should show empty state when no agents are available', () => {
mockUseMarketplaceAgentsInfiniteQuery.mockReturnValue({
...defaultMockQueryResult,

View file

@ -313,6 +313,35 @@ describe('useHasData', () => {
expect(screen.getByTestId('result')).toHaveTextContent('no-data');
});
it('detects empty data array (AgentListResponse) as no data', () => {
render(
<TestComponent
data={{ object: 'list', data: [], first_id: '', last_id: '', has_more: false }}
/>,
);
expect(screen.getByTestId('result')).toHaveTextContent('no-data');
});
it('detects non-empty data array (AgentListResponse) as has data', () => {
render(
<TestComponent
data={{
object: 'list',
data: [{ id: 'agent_1', name: 'Test Agent' }],
first_id: 'agent_1',
last_id: 'agent_1',
has_more: false,
}}
/>,
);
expect(screen.getByTestId('result')).toHaveTextContent('has-data');
});
it('detects invalid data property as no data', () => {
render(<TestComponent data={{ data: 'not-array' }} />);
expect(screen.getByTestId('result')).toHaveTextContent('no-data');
});
it('detects empty agents array as no data', () => {
render(<TestComponent data={{ agents: [] }} />);
expect(screen.getByTestId('result')).toHaveTextContent('no-data');

View file

@ -160,10 +160,6 @@ jest.mock('~/hooks', () => ({
},
}));
jest.mock('../SmartLoader', () => ({
useHasData: () => true,
}));
jest.mock('../AgentCard', () => {
return function MockAgentCard({
agent,
@ -266,6 +262,21 @@ describe('VirtualizedAgentGrid', () => {
expect(spinner).toHaveClass('h-8 w-8 text-text-primary');
});
it('retains cached agents while refetching', () => {
const useMarketplaceAgentsInfiniteQuery = (
jest.requireMock('~/data-provider/Agents') as MarketplaceAgentsMock
).useMarketplaceAgentsInfiniteQuery;
useMarketplaceAgentsInfiniteQuery.mockImplementation(() =>
createMockInfiniteQuery({ isFetching: true }),
);
renderComponent();
expect(screen.getByTestId('virtual-list')).toBeInTheDocument();
expect(screen.getByTestId('agent-card-1')).toBeInTheDocument();
expect(screen.getByTestId('agent-card-2')).toBeInTheDocument();
});
it('has proper accessibility attributes', () => {
renderComponent({ category: 'productivity' });