diff --git a/libs/designer-v2/package.json b/libs/designer-v2/package.json
index 66946ad40e7..3dcdd364bb1 100644
--- a/libs/designer-v2/package.json
+++ b/libs/designer-v2/package.json
@@ -35,7 +35,7 @@
"react-intl": "6.3.0",
"react-markdown": "8.0.5",
"react-redux": "8.0.2",
- "react-window": "^1.8.11",
+ "react-window": "^2.2.6",
"redux-thunk": "2.4.2",
"reselect": "4.1.8",
"tabster": "8.5.6",
diff --git a/libs/designer-v2/src/lib/ui/panel/recommendation/browse/__test__/connectorBrowse.spec.tsx b/libs/designer-v2/src/lib/ui/panel/recommendation/browse/__test__/connectorBrowse.spec.tsx
new file mode 100644
index 00000000000..23c501ad952
--- /dev/null
+++ b/libs/designer-v2/src/lib/ui/panel/recommendation/browse/__test__/connectorBrowse.spec.tsx
@@ -0,0 +1,263 @@
+// @vitest-environment jsdom
+import '@testing-library/jest-dom/vitest';
+import { describe, test, expect, vi, beforeEach, afterEach } from 'vitest';
+import { render, screen, cleanup } from '@testing-library/react';
+import { Provider } from 'react-redux';
+import { configureStore } from '@reduxjs/toolkit';
+import { IntlProvider } from 'react-intl';
+import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
+import { ConnectorBrowse } from '../connectorBrowse';
+import type { Connector } from '@microsoft/logic-apps-shared';
+
+// --- Mocks ---
+
+const mockDispatch = vi.fn();
+vi.mock('react-redux', async () => {
+ const actual = await vi.importActual('react-redux');
+ return { ...actual, useDispatch: () => mockDispatch };
+});
+
+const mockUseAllConnectors = vi.fn();
+vi.mock('../../../../../core/queries/browse', () => ({
+ useAllConnectors: () => mockUseAllConnectors(),
+}));
+
+vi.mock('../../../../../core/state/panel/panelSelectors', () => ({
+ useDiscoveryPanelRelationshipIds: vi.fn(() => ({
+ graphId: 'root',
+ parentId: undefined,
+ childId: undefined,
+ })),
+}));
+
+vi.mock('../../../../../core/state/designerView/designerViewSelectors', () => ({
+ useIsA2AWorkflow: vi.fn(() => false),
+}));
+
+vi.mock('../../../../../core/state/panel/panelSlice', () => ({
+ selectOperationGroupId: vi.fn((id: string) => ({ type: 'panel/selectOperationGroupId', payload: id })),
+}));
+
+vi.mock('@microsoft/designer-ui', () => ({
+ isBuiltInConnector: vi.fn((c: Connector) => c.id.includes('builtin')),
+ isCustomConnector: vi.fn((c: Connector) => c.id.includes('custom')),
+}));
+
+vi.mock('../connectorCard', () => ({
+ ConnectorCard: vi.fn(({ connector }: { connector: Connector }) => (
+
{connector.properties.displayName}
+ )),
+}));
+
+vi.mock('../styles/ConnectorBrowse.styles', () => ({
+ useConnectorBrowseStyles: vi.fn(() => ({
+ loadingContainer: 'loading-container',
+ emptyStateContainer: 'empty-state-container',
+ })),
+}));
+
+vi.mock('react-window', () => ({
+ List: vi.fn(({ rowCount, rowComponent: Row }: any) => (
+
+ {Array.from({ length: rowCount }, (_, i) => (
+
+ ))}
+
+ )),
+}));
+
+// --- Helpers ---
+
+const makeConnector = (id: string, displayName: string, overrides?: Partial): Connector =>
+ ({
+ id,
+ name: id.split('/').pop() ?? id,
+ type: 'Microsoft.Web/locations/managedApis',
+ properties: {
+ displayName,
+ capabilities: [],
+ ...overrides?.properties,
+ },
+ ...overrides,
+ }) as unknown as Connector;
+
+const createWrapper = () => {
+ const queryClient = new QueryClient({ defaultOptions: { queries: { retry: false } } });
+ const store = configureStore({ reducer: { stub: (s = {}) => s } });
+
+ return ({ children }: { children: React.ReactNode }) => (
+
+
+ {children}
+
+
+ );
+};
+
+// --- Tests ---
+
+describe('ConnectorBrowse', () => {
+ beforeEach(() => {
+ vi.clearAllMocks();
+ });
+
+ afterEach(() => {
+ cleanup();
+ });
+
+ test('renders loading spinner when data is loading', () => {
+ mockUseAllConnectors.mockReturnValue({ data: undefined, isLoading: true });
+
+ render(, { wrapper: createWrapper() });
+
+ expect(screen.getByText('Loading connectors...')).toBeInTheDocument();
+ });
+
+ test('renders empty state when no connectors match', () => {
+ mockUseAllConnectors.mockReturnValue({ data: [], isLoading: false });
+
+ render(, { wrapper: createWrapper() });
+
+ expect(screen.getByText('No connectors found for this category')).toBeInTheDocument();
+ });
+
+ test('renders connector cards when connectors are available', () => {
+ const connectors = [makeConnector('shared/sql', 'SQL'), makeConnector('shared/outlook', 'Outlook')];
+ mockUseAllConnectors.mockReturnValue({ data: connectors, isLoading: false });
+
+ render(, { wrapper: createWrapper() });
+
+ expect(screen.getByText('SQL')).toBeInTheDocument();
+ expect(screen.getByText('Outlook')).toBeInTheDocument();
+ });
+
+ test('filters out agent connector', () => {
+ const connectors = [makeConnector('connectionProviders/agent', 'Agent'), makeConnector('shared/sql', 'SQL')];
+ mockUseAllConnectors.mockReturnValue({ data: connectors, isLoading: false });
+
+ render(, { wrapper: createWrapper() });
+
+ expect(screen.queryByTestId('connector-card-connectionProviders/agent')).not.toBeInTheDocument();
+ expect(screen.getByText('SQL')).toBeInTheDocument();
+ });
+
+ test('filters out ACA session connector', () => {
+ const connectors = [makeConnector('/serviceProviders/acasession', 'ACA Session'), makeConnector('shared/sql', 'SQL')];
+ mockUseAllConnectors.mockReturnValue({ data: connectors, isLoading: false });
+
+ render(, { wrapper: createWrapper() });
+
+ expect(screen.queryByTestId('connector-card-/serviceProviders/acasession')).not.toBeInTheDocument();
+ expect(screen.getByText('SQL')).toBeInTheDocument();
+ });
+
+ test('filters by runtime=inapp to show only built-in connectors', () => {
+ const connectors = [makeConnector('builtin/http', 'HTTP'), makeConnector('shared/sql', 'SQL')];
+ mockUseAllConnectors.mockReturnValue({ data: connectors, isLoading: false });
+
+ render(, { wrapper: createWrapper() });
+
+ expect(screen.getByText('HTTP')).toBeInTheDocument();
+ expect(screen.queryByText('SQL')).not.toBeInTheDocument();
+ });
+
+ test('filters by runtime=custom to show only custom connectors', () => {
+ const connectors = [makeConnector('custom/myConnector', 'My Custom'), makeConnector('shared/sql', 'SQL')];
+ mockUseAllConnectors.mockReturnValue({ data: connectors, isLoading: false });
+
+ render(, { wrapper: createWrapper() });
+
+ expect(screen.getByText('My Custom')).toBeInTheDocument();
+ expect(screen.queryByText('SQL')).not.toBeInTheDocument();
+ });
+
+ test('filters by runtime=shared to exclude built-in and custom connectors', () => {
+ const connectors = [
+ makeConnector('builtin/http', 'HTTP'),
+ makeConnector('custom/myConnector', 'My Custom'),
+ makeConnector('shared/sql', 'SQL'),
+ ];
+ mockUseAllConnectors.mockReturnValue({ data: connectors, isLoading: false });
+
+ render(, { wrapper: createWrapper() });
+
+ expect(screen.queryByText('HTTP')).not.toBeInTheDocument();
+ expect(screen.queryByText('My Custom')).not.toBeInTheDocument();
+ expect(screen.getByText('SQL')).toBeInTheDocument();
+ });
+
+ test('filters by actionType=triggers', () => {
+ const triggersConnector = makeConnector('shared/trigger', 'Trigger Connector', {
+ properties: { displayName: 'Trigger Connector', capabilities: ['triggers'] },
+ } as any);
+ const actionsConnector = makeConnector('shared/action', 'Action Connector', {
+ properties: { displayName: 'Action Connector', capabilities: ['actions'] },
+ } as any);
+
+ mockUseAllConnectors.mockReturnValue({ data: [triggersConnector, actionsConnector], isLoading: false });
+
+ render(, { wrapper: createWrapper() });
+
+ expect(screen.getByText('Trigger Connector')).toBeInTheDocument();
+ expect(screen.queryByText('Action Connector')).not.toBeInTheDocument();
+ });
+
+ test('filters by actionType=actions', () => {
+ const triggersConnector = makeConnector('shared/trigger', 'Trigger Connector', {
+ properties: { displayName: 'Trigger Connector', capabilities: ['triggers'] },
+ } as any);
+ const actionsConnector = makeConnector('shared/action', 'Action Connector', {
+ properties: { displayName: 'Action Connector', capabilities: ['actions'] },
+ } as any);
+
+ mockUseAllConnectors.mockReturnValue({ data: [triggersConnector, actionsConnector], isLoading: false });
+
+ render(, { wrapper: createWrapper() });
+
+ expect(screen.queryByText('Trigger Connector')).not.toBeInTheDocument();
+ expect(screen.getByText('Action Connector')).toBeInTheDocument();
+ });
+
+ test('connectors with no capabilities pass actionType filter', () => {
+ const noCapsConnector = makeConnector('shared/nocaps', 'No Caps', {
+ properties: { displayName: 'No Caps', capabilities: [] },
+ } as any);
+
+ mockUseAllConnectors.mockReturnValue({ data: [noCapsConnector], isLoading: false });
+
+ render(, { wrapper: createWrapper() });
+
+ expect(screen.getByText('No Caps')).toBeInTheDocument();
+ });
+
+ test('sorts priority connectors before others', () => {
+ const regularConnector = makeConnector('shared/random', 'Random');
+ const priorityConnector = makeConnector('shared/managedApis/office365', 'Office 365');
+
+ mockUseAllConnectors.mockReturnValue({ data: [regularConnector, priorityConnector], isLoading: false });
+
+ render(, { wrapper: createWrapper() });
+
+ const cards = screen.getAllByTestId(/connector-card-/);
+ expect(cards[0]).toHaveTextContent('Office 365');
+ expect(cards[1]).toHaveTextContent('Random');
+ });
+
+ test('uses virtualized list for rendering', () => {
+ const connectors = [makeConnector('shared/sql', 'SQL')];
+ mockUseAllConnectors.mockReturnValue({ data: connectors, isLoading: false });
+
+ render(, { wrapper: createWrapper() });
+
+ expect(screen.getAllByTestId('virtualized-list').length).toBeGreaterThan(0);
+ });
+
+ test('does not render loading spinner after data has loaded', () => {
+ const connectors = [makeConnector('shared/sql', 'SQL')];
+ mockUseAllConnectors.mockReturnValue({ data: connectors, isLoading: false });
+
+ render(, { wrapper: createWrapper() });
+
+ expect(screen.queryByText('Loading connectors...')).not.toBeInTheDocument();
+ });
+});
diff --git a/libs/designer-v2/src/lib/ui/panel/recommendation/browse/connectorBrowse.tsx b/libs/designer-v2/src/lib/ui/panel/recommendation/browse/connectorBrowse.tsx
index fc8f2451a4f..59e9a3e7517 100644
--- a/libs/designer-v2/src/lib/ui/panel/recommendation/browse/connectorBrowse.tsx
+++ b/libs/designer-v2/src/lib/ui/panel/recommendation/browse/connectorBrowse.tsx
@@ -1,4 +1,4 @@
-import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
+import { useCallback, useMemo } from 'react';
import { useIntl } from 'react-intl';
import { useDispatch } from 'react-redux';
import { useAllConnectors } from '../../../../core/queries/browse';
@@ -12,8 +12,7 @@ import { ConnectorCard } from './connectorCard';
import { selectOperationGroupId } from '../../../../core/state/panel/panelSlice';
import type { AppDispatch } from '../../../../core';
import { useConnectorBrowseStyles } from './styles/ConnectorBrowse.styles';
-import type { ListChildComponentProps } from 'react-window';
-import { FixedSizeList } from 'react-window';
+import { List, type RowComponentProps } from 'react-window';
import type { ConnectorFilterTypes } from './helper';
export interface ConnectorBrowseProps {
@@ -90,19 +89,6 @@ export const ConnectorBrowse = ({
const isA2AWorkflow = useIsA2AWorkflow();
const isAddingToGraph = useDiscoveryPanelRelationshipIds().graphId === 'root';
- const containerRef = useRef(null);
- const [containerHeight, setContainerHeight] = useState(0);
-
- useEffect(() => {
- if (!containerRef.current) {
- return;
- }
- const updateHeight = () => setContainerHeight(containerRef.current!.clientHeight);
- updateHeight();
- window.addEventListener('resize', updateHeight);
- return () => window.removeEventListener('resize', updateHeight);
- }, []);
-
const { data: allConnectors, isLoading } = useAllConnectors();
const isAgentConnectorAllowed = useCallback((c: Connector) => c.id !== 'connectionProviders/agent', []);
@@ -234,27 +220,18 @@ export const ConnectorBrowse = ({
}
// --- Row Renderer ---
- const Row = ({ index, style }: ListChildComponentProps) => {
- const connector = sortedConnectors[index];
- return (
-
-
-
- );
- };
+ const Row = ({ index, style }: RowComponentProps) => (
+
+
+
+ );
return (
-
- {containerHeight > 0 && (
-
- {Row}
-
- )}
-
+
);
};
diff --git a/libs/designer-v2/src/lib/ui/panel/recommendation/browse/styles/ConnectorBrowse.styles.ts b/libs/designer-v2/src/lib/ui/panel/recommendation/browse/styles/ConnectorBrowse.styles.ts
index 7e74e30fc61..3c59f0e4187 100644
--- a/libs/designer-v2/src/lib/ui/panel/recommendation/browse/styles/ConnectorBrowse.styles.ts
+++ b/libs/designer-v2/src/lib/ui/panel/recommendation/browse/styles/ConnectorBrowse.styles.ts
@@ -11,12 +11,4 @@ export const useConnectorBrowseStyles = makeStyles({
padding: '40px',
color: tokens.colorNeutralForeground2,
},
- connectorGrid: {
- flex: 1,
- display: 'flex',
- flexDirection: 'column',
- overflow: 'hidden',
- minHeight: 0,
- height: 'calc(100% - 120px)',
- },
});
diff --git a/package.json b/package.json
index 755a7d489e6..57b3968d040 100644
--- a/package.json
+++ b/package.json
@@ -16,7 +16,7 @@
"@types/react": "18.3.0",
"@types/react-dom": "18.3.0",
"@types/react-test-renderer": "^18.0.7",
- "@types/react-window": "^1.8.8",
+ "@types/react-window": "^2.0.0",
"@typescript-eslint/eslint-plugin": "^8.15.0",
"@typescript-eslint/parser": "^8.29.1",
"@vitejs/plugin-react": "^4.4.6",
@@ -113,7 +113,7 @@
"find-process": "^1.4.7",
"fs-extra": "^11.2.0",
"happy-dom": "^20.0.2",
- "react-window": "^1.8.11",
+ "react-window": "^2.2.6",
"ts-node": "^10.9.2"
},
"pnpm": {
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 9ed7649d9a3..5d5ccb5843f 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -71,8 +71,8 @@ importers:
specifier: ^20.0.2
version: 20.0.2
react-window:
- specifier: ^1.8.11
- version: 1.8.11(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
+ specifier: ^2.2.6
+ version: 2.2.6(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
ts-node:
specifier: ^10.9.2
version: 10.9.2(@types/node@20.12.7)(typescript@5.7.2)
@@ -123,8 +123,8 @@ importers:
specifier: ^18.0.7
version: 18.0.7
'@types/react-window':
- specifier: ^1.8.8
- version: 1.8.8
+ specifier: ^2.0.0
+ version: 2.0.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
'@typescript-eslint/eslint-plugin':
specifier: ^8.15.0
version: 8.15.0(@typescript-eslint/parser@8.29.1(eslint@9.26.0(jiti@1.21.0))(typescript@5.7.2))(eslint@9.26.0(jiti@1.21.0))(typescript@5.7.2)
@@ -1567,8 +1567,8 @@ importers:
specifier: 8.0.2
version: 8.0.2(@types/react-dom@18.3.0)(@types/react@18.3.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(redux@4.2.1)
react-window:
- specifier: ^1.8.11
- version: 1.8.11(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
+ specifier: ^2.2.6
+ version: 2.2.6(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
redux-thunk:
specifier: 2.4.2
version: 2.4.2(redux@4.2.1)
@@ -3133,7 +3133,7 @@ packages:
engines: {node: '>=6.9.0'}
'@babel/runtime@7.28.6':
- resolution: {integrity: sha512-05WQkdpL9COIMz4LjTxGpPNCdlpyimKppYNoJ5Di5EUObifl8t4tuLuUBBZEpoLYOmfvIWrsp9fCl0HoPRVTdA==}
+ resolution: {integrity: sha1-0mekPLGDbcTRgszpOudbqVTvbSs=}
engines: {node: '>=6.9.0'}
'@babel/template@7.27.2':
@@ -7004,8 +7004,9 @@ packages:
'@types/react-test-renderer@18.0.7':
resolution: {integrity: sha512-1+ANPOWc6rB3IkSnElhjv6VLlKg2dSv/OWClUyZimbLsQyBn8Js9Vtdsi3UICJ2rIQ3k2la06dkB+C92QfhKmg==}
- '@types/react-window@1.8.8':
- resolution: {integrity: sha512-8Ls660bHR1AUA2kuRvVG9D/4XpRC6wjAaPT9dil7Ckc76eP9TKWZwwmgfq8Q1LANX3QNDnoU4Zp48A3w+zK69Q==}
+ '@types/react-window@2.0.0':
+ resolution: {integrity: sha1-n+RQFdb5jsmKBpNiUYyFX8wfqNA=}
+ deprecated: This is a stub types definition. react-window provides its own type definitions, so you do not need this installed.
'@types/react@18.3.0':
resolution: {integrity: sha512-DiUcKjzE6soLyln8NNZmyhcQjVv+WsUIFSqetMN0p8927OztKT4VTfFTqsbAi5oAGIcgOmOajlfBqyptDDjZRw==}
@@ -13128,6 +13129,12 @@ packages:
react: 18.3.1
react-dom: 18.3.1
+ react-window@2.2.6:
+ resolution: {integrity: sha1-AMoXQ0a1FG08M6dS2IgYElDHHZ8=}
+ peerDependencies:
+ react: 18.3.1
+ react-dom: 18.3.1
+
react@18.3.1:
resolution: {integrity: sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==}
engines: {node: '>=0.10.0'}
@@ -23582,9 +23589,12 @@ snapshots:
dependencies:
'@types/react': 18.3.0
- '@types/react-window@1.8.8':
+ '@types/react-window@2.0.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1)':
dependencies:
- '@types/react': 18.3.0
+ react-window: 2.2.6(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
+ transitivePeerDependencies:
+ - react
+ - react-dom
'@types/react@18.3.0':
dependencies:
@@ -31182,6 +31192,11 @@ snapshots:
react: 18.3.1
react-dom: 18.3.1(react@18.3.1)
+ react-window@2.2.6(react-dom@18.3.1(react@18.3.1))(react@18.3.1):
+ dependencies:
+ react: 18.3.1
+ react-dom: 18.3.1(react@18.3.1)
+
react@18.3.1:
dependencies:
loose-envify: 1.4.0