Skip to content

Commit 38a85ba

Browse files
authored
fix(DesignerV2): Fixed dynamic height issue with Connector Browse panel (#8810)
* Fixed issue * Removed test css * Added test file
1 parent 08d4e5a commit 38a85ba

File tree

6 files changed

+305
-58
lines changed

6 files changed

+305
-58
lines changed

libs/designer-v2/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@
3535
"react-intl": "6.3.0",
3636
"react-markdown": "8.0.5",
3737
"react-redux": "8.0.2",
38-
"react-window": "^1.8.11",
38+
"react-window": "^2.2.6",
3939
"redux-thunk": "2.4.2",
4040
"reselect": "4.1.8",
4141
"tabster": "8.5.6",
Lines changed: 263 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,263 @@
1+
// @vitest-environment jsdom
2+
import '@testing-library/jest-dom/vitest';
3+
import { describe, test, expect, vi, beforeEach, afterEach } from 'vitest';
4+
import { render, screen, cleanup } from '@testing-library/react';
5+
import { Provider } from 'react-redux';
6+
import { configureStore } from '@reduxjs/toolkit';
7+
import { IntlProvider } from 'react-intl';
8+
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
9+
import { ConnectorBrowse } from '../connectorBrowse';
10+
import type { Connector } from '@microsoft/logic-apps-shared';
11+
12+
// --- Mocks ---
13+
14+
const mockDispatch = vi.fn();
15+
vi.mock('react-redux', async () => {
16+
const actual = await vi.importActual('react-redux');
17+
return { ...actual, useDispatch: () => mockDispatch };
18+
});
19+
20+
const mockUseAllConnectors = vi.fn();
21+
vi.mock('../../../../../core/queries/browse', () => ({
22+
useAllConnectors: () => mockUseAllConnectors(),
23+
}));
24+
25+
vi.mock('../../../../../core/state/panel/panelSelectors', () => ({
26+
useDiscoveryPanelRelationshipIds: vi.fn(() => ({
27+
graphId: 'root',
28+
parentId: undefined,
29+
childId: undefined,
30+
})),
31+
}));
32+
33+
vi.mock('../../../../../core/state/designerView/designerViewSelectors', () => ({
34+
useIsA2AWorkflow: vi.fn(() => false),
35+
}));
36+
37+
vi.mock('../../../../../core/state/panel/panelSlice', () => ({
38+
selectOperationGroupId: vi.fn((id: string) => ({ type: 'panel/selectOperationGroupId', payload: id })),
39+
}));
40+
41+
vi.mock('@microsoft/designer-ui', () => ({
42+
isBuiltInConnector: vi.fn((c: Connector) => c.id.includes('builtin')),
43+
isCustomConnector: vi.fn((c: Connector) => c.id.includes('custom')),
44+
}));
45+
46+
vi.mock('../connectorCard', () => ({
47+
ConnectorCard: vi.fn(({ connector }: { connector: Connector }) => (
48+
<div data-testid={`connector-card-${connector.id}`}>{connector.properties.displayName}</div>
49+
)),
50+
}));
51+
52+
vi.mock('../styles/ConnectorBrowse.styles', () => ({
53+
useConnectorBrowseStyles: vi.fn(() => ({
54+
loadingContainer: 'loading-container',
55+
emptyStateContainer: 'empty-state-container',
56+
})),
57+
}));
58+
59+
vi.mock('react-window', () => ({
60+
List: vi.fn(({ rowCount, rowComponent: Row }: any) => (
61+
<div data-testid="virtualized-list">
62+
{Array.from({ length: rowCount }, (_, i) => (
63+
<Row key={i} index={i} style={{}} />
64+
))}
65+
</div>
66+
)),
67+
}));
68+
69+
// --- Helpers ---
70+
71+
const makeConnector = (id: string, displayName: string, overrides?: Partial<Connector>): Connector =>
72+
({
73+
id,
74+
name: id.split('/').pop() ?? id,
75+
type: 'Microsoft.Web/locations/managedApis',
76+
properties: {
77+
displayName,
78+
capabilities: [],
79+
...overrides?.properties,
80+
},
81+
...overrides,
82+
}) as unknown as Connector;
83+
84+
const createWrapper = () => {
85+
const queryClient = new QueryClient({ defaultOptions: { queries: { retry: false } } });
86+
const store = configureStore({ reducer: { stub: (s = {}) => s } });
87+
88+
return ({ children }: { children: React.ReactNode }) => (
89+
<QueryClientProvider client={queryClient}>
90+
<Provider store={store}>
91+
<IntlProvider locale="en">{children}</IntlProvider>
92+
</Provider>
93+
</QueryClientProvider>
94+
);
95+
};
96+
97+
// --- Tests ---
98+
99+
describe('ConnectorBrowse', () => {
100+
beforeEach(() => {
101+
vi.clearAllMocks();
102+
});
103+
104+
afterEach(() => {
105+
cleanup();
106+
});
107+
108+
test('renders loading spinner when data is loading', () => {
109+
mockUseAllConnectors.mockReturnValue({ data: undefined, isLoading: true });
110+
111+
render(<ConnectorBrowse categoryKey="all" />, { wrapper: createWrapper() });
112+
113+
expect(screen.getByText('Loading connectors...')).toBeInTheDocument();
114+
});
115+
116+
test('renders empty state when no connectors match', () => {
117+
mockUseAllConnectors.mockReturnValue({ data: [], isLoading: false });
118+
119+
render(<ConnectorBrowse categoryKey="all" />, { wrapper: createWrapper() });
120+
121+
expect(screen.getByText('No connectors found for this category')).toBeInTheDocument();
122+
});
123+
124+
test('renders connector cards when connectors are available', () => {
125+
const connectors = [makeConnector('shared/sql', 'SQL'), makeConnector('shared/outlook', 'Outlook')];
126+
mockUseAllConnectors.mockReturnValue({ data: connectors, isLoading: false });
127+
128+
render(<ConnectorBrowse categoryKey="all" />, { wrapper: createWrapper() });
129+
130+
expect(screen.getByText('SQL')).toBeInTheDocument();
131+
expect(screen.getByText('Outlook')).toBeInTheDocument();
132+
});
133+
134+
test('filters out agent connector', () => {
135+
const connectors = [makeConnector('connectionProviders/agent', 'Agent'), makeConnector('shared/sql', 'SQL')];
136+
mockUseAllConnectors.mockReturnValue({ data: connectors, isLoading: false });
137+
138+
render(<ConnectorBrowse categoryKey="all" />, { wrapper: createWrapper() });
139+
140+
expect(screen.queryByTestId('connector-card-connectionProviders/agent')).not.toBeInTheDocument();
141+
expect(screen.getByText('SQL')).toBeInTheDocument();
142+
});
143+
144+
test('filters out ACA session connector', () => {
145+
const connectors = [makeConnector('/serviceProviders/acasession', 'ACA Session'), makeConnector('shared/sql', 'SQL')];
146+
mockUseAllConnectors.mockReturnValue({ data: connectors, isLoading: false });
147+
148+
render(<ConnectorBrowse categoryKey="all" />, { wrapper: createWrapper() });
149+
150+
expect(screen.queryByTestId('connector-card-/serviceProviders/acasession')).not.toBeInTheDocument();
151+
expect(screen.getByText('SQL')).toBeInTheDocument();
152+
});
153+
154+
test('filters by runtime=inapp to show only built-in connectors', () => {
155+
const connectors = [makeConnector('builtin/http', 'HTTP'), makeConnector('shared/sql', 'SQL')];
156+
mockUseAllConnectors.mockReturnValue({ data: connectors, isLoading: false });
157+
158+
render(<ConnectorBrowse categoryKey="all" filters={{ runtime: 'inapp' }} />, { wrapper: createWrapper() });
159+
160+
expect(screen.getByText('HTTP')).toBeInTheDocument();
161+
expect(screen.queryByText('SQL')).not.toBeInTheDocument();
162+
});
163+
164+
test('filters by runtime=custom to show only custom connectors', () => {
165+
const connectors = [makeConnector('custom/myConnector', 'My Custom'), makeConnector('shared/sql', 'SQL')];
166+
mockUseAllConnectors.mockReturnValue({ data: connectors, isLoading: false });
167+
168+
render(<ConnectorBrowse categoryKey="all" filters={{ runtime: 'custom' }} />, { wrapper: createWrapper() });
169+
170+
expect(screen.getByText('My Custom')).toBeInTheDocument();
171+
expect(screen.queryByText('SQL')).not.toBeInTheDocument();
172+
});
173+
174+
test('filters by runtime=shared to exclude built-in and custom connectors', () => {
175+
const connectors = [
176+
makeConnector('builtin/http', 'HTTP'),
177+
makeConnector('custom/myConnector', 'My Custom'),
178+
makeConnector('shared/sql', 'SQL'),
179+
];
180+
mockUseAllConnectors.mockReturnValue({ data: connectors, isLoading: false });
181+
182+
render(<ConnectorBrowse categoryKey="all" filters={{ runtime: 'shared' }} />, { wrapper: createWrapper() });
183+
184+
expect(screen.queryByText('HTTP')).not.toBeInTheDocument();
185+
expect(screen.queryByText('My Custom')).not.toBeInTheDocument();
186+
expect(screen.getByText('SQL')).toBeInTheDocument();
187+
});
188+
189+
test('filters by actionType=triggers', () => {
190+
const triggersConnector = makeConnector('shared/trigger', 'Trigger Connector', {
191+
properties: { displayName: 'Trigger Connector', capabilities: ['triggers'] },
192+
} as any);
193+
const actionsConnector = makeConnector('shared/action', 'Action Connector', {
194+
properties: { displayName: 'Action Connector', capabilities: ['actions'] },
195+
} as any);
196+
197+
mockUseAllConnectors.mockReturnValue({ data: [triggersConnector, actionsConnector], isLoading: false });
198+
199+
render(<ConnectorBrowse categoryKey="all" filters={{ actionType: 'triggers' }} />, { wrapper: createWrapper() });
200+
201+
expect(screen.getByText('Trigger Connector')).toBeInTheDocument();
202+
expect(screen.queryByText('Action Connector')).not.toBeInTheDocument();
203+
});
204+
205+
test('filters by actionType=actions', () => {
206+
const triggersConnector = makeConnector('shared/trigger', 'Trigger Connector', {
207+
properties: { displayName: 'Trigger Connector', capabilities: ['triggers'] },
208+
} as any);
209+
const actionsConnector = makeConnector('shared/action', 'Action Connector', {
210+
properties: { displayName: 'Action Connector', capabilities: ['actions'] },
211+
} as any);
212+
213+
mockUseAllConnectors.mockReturnValue({ data: [triggersConnector, actionsConnector], isLoading: false });
214+
215+
render(<ConnectorBrowse categoryKey="all" filters={{ actionType: 'actions' }} />, { wrapper: createWrapper() });
216+
217+
expect(screen.queryByText('Trigger Connector')).not.toBeInTheDocument();
218+
expect(screen.getByText('Action Connector')).toBeInTheDocument();
219+
});
220+
221+
test('connectors with no capabilities pass actionType filter', () => {
222+
const noCapsConnector = makeConnector('shared/nocaps', 'No Caps', {
223+
properties: { displayName: 'No Caps', capabilities: [] },
224+
} as any);
225+
226+
mockUseAllConnectors.mockReturnValue({ data: [noCapsConnector], isLoading: false });
227+
228+
render(<ConnectorBrowse categoryKey="all" filters={{ actionType: 'triggers' }} />, { wrapper: createWrapper() });
229+
230+
expect(screen.getByText('No Caps')).toBeInTheDocument();
231+
});
232+
233+
test('sorts priority connectors before others', () => {
234+
const regularConnector = makeConnector('shared/random', 'Random');
235+
const priorityConnector = makeConnector('shared/managedApis/office365', 'Office 365');
236+
237+
mockUseAllConnectors.mockReturnValue({ data: [regularConnector, priorityConnector], isLoading: false });
238+
239+
render(<ConnectorBrowse categoryKey="all" />, { wrapper: createWrapper() });
240+
241+
const cards = screen.getAllByTestId(/connector-card-/);
242+
expect(cards[0]).toHaveTextContent('Office 365');
243+
expect(cards[1]).toHaveTextContent('Random');
244+
});
245+
246+
test('uses virtualized list for rendering', () => {
247+
const connectors = [makeConnector('shared/sql', 'SQL')];
248+
mockUseAllConnectors.mockReturnValue({ data: connectors, isLoading: false });
249+
250+
render(<ConnectorBrowse categoryKey="all" />, { wrapper: createWrapper() });
251+
252+
expect(screen.getAllByTestId('virtualized-list').length).toBeGreaterThan(0);
253+
});
254+
255+
test('does not render loading spinner after data has loaded', () => {
256+
const connectors = [makeConnector('shared/sql', 'SQL')];
257+
mockUseAllConnectors.mockReturnValue({ data: connectors, isLoading: false });
258+
259+
render(<ConnectorBrowse categoryKey="all" />, { wrapper: createWrapper() });
260+
261+
expect(screen.queryByText('Loading connectors...')).not.toBeInTheDocument();
262+
});
263+
});

libs/designer-v2/src/lib/ui/panel/recommendation/browse/connectorBrowse.tsx

Lines changed: 13 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
1+
import { useCallback, useMemo } from 'react';
22
import { useIntl } from 'react-intl';
33
import { useDispatch } from 'react-redux';
44
import { useAllConnectors } from '../../../../core/queries/browse';
@@ -12,8 +12,7 @@ import { ConnectorCard } from './connectorCard';
1212
import { selectOperationGroupId } from '../../../../core/state/panel/panelSlice';
1313
import type { AppDispatch } from '../../../../core';
1414
import { useConnectorBrowseStyles } from './styles/ConnectorBrowse.styles';
15-
import type { ListChildComponentProps } from 'react-window';
16-
import { FixedSizeList } from 'react-window';
15+
import { List, type RowComponentProps } from 'react-window';
1716
import type { ConnectorFilterTypes } from './helper';
1817

1918
export interface ConnectorBrowseProps {
@@ -90,19 +89,6 @@ export const ConnectorBrowse = ({
9089
const isA2AWorkflow = useIsA2AWorkflow();
9190
const isAddingToGraph = useDiscoveryPanelRelationshipIds().graphId === 'root';
9291

93-
const containerRef = useRef<HTMLDivElement>(null);
94-
const [containerHeight, setContainerHeight] = useState(0);
95-
96-
useEffect(() => {
97-
if (!containerRef.current) {
98-
return;
99-
}
100-
const updateHeight = () => setContainerHeight(containerRef.current!.clientHeight);
101-
updateHeight();
102-
window.addEventListener('resize', updateHeight);
103-
return () => window.removeEventListener('resize', updateHeight);
104-
}, []);
105-
10692
const { data: allConnectors, isLoading } = useAllConnectors();
10793

10894
const isAgentConnectorAllowed = useCallback((c: Connector) => c.id !== 'connectionProviders/agent', []);
@@ -234,27 +220,18 @@ export const ConnectorBrowse = ({
234220
}
235221

236222
// --- Row Renderer ---
237-
const Row = ({ index, style }: ListChildComponentProps) => {
238-
const connector = sortedConnectors[index];
239-
return (
240-
<div style={style}>
241-
<ConnectorCard connector={connector} onClick={handleConnectorSelected} displayRuntimeInfo={displayRuntimeInfo} />
242-
</div>
243-
);
244-
};
223+
const Row = ({ index, style }: RowComponentProps) => (
224+
<div style={style}>
225+
<ConnectorCard connector={sortedConnectors[index]} onClick={handleConnectorSelected} displayRuntimeInfo={displayRuntimeInfo} />
226+
</div>
227+
);
245228

246229
return (
247-
<div ref={containerRef} className={classes.connectorGrid}>
248-
{containerHeight > 0 && (
249-
<FixedSizeList
250-
height={containerHeight}
251-
itemCount={sortedConnectors.length}
252-
itemSize={70} // ConnectorCard height
253-
width="100%"
254-
>
255-
{Row}
256-
</FixedSizeList>
257-
)}
258-
</div>
230+
<List
231+
rowCount={sortedConnectors.length}
232+
rowHeight={70} // ConnectorCard height
233+
rowComponent={Row}
234+
rowProps={{}}
235+
/>
259236
);
260237
};

libs/designer-v2/src/lib/ui/panel/recommendation/browse/styles/ConnectorBrowse.styles.ts

Lines changed: 0 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -11,12 +11,4 @@ export const useConnectorBrowseStyles = makeStyles({
1111
padding: '40px',
1212
color: tokens.colorNeutralForeground2,
1313
},
14-
connectorGrid: {
15-
flex: 1,
16-
display: 'flex',
17-
flexDirection: 'column',
18-
overflow: 'hidden',
19-
minHeight: 0,
20-
height: 'calc(100% - 120px)',
21-
},
2214
});

package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616
"@types/react": "18.3.0",
1717
"@types/react-dom": "18.3.0",
1818
"@types/react-test-renderer": "^18.0.7",
19-
"@types/react-window": "^1.8.8",
19+
"@types/react-window": "^2.0.0",
2020
"@typescript-eslint/eslint-plugin": "^8.15.0",
2121
"@typescript-eslint/parser": "^8.29.1",
2222
"@vitejs/plugin-react": "^4.4.6",
@@ -113,7 +113,7 @@
113113
"find-process": "^1.4.7",
114114
"fs-extra": "^11.2.0",
115115
"happy-dom": "^20.0.2",
116-
"react-window": "^1.8.11",
116+
"react-window": "^2.2.6",
117117
"ts-node": "^10.9.2"
118118
},
119119
"pnpm": {

0 commit comments

Comments
 (0)