Skip to content

Commit 4f26e55

Browse files
authored
Merge pull request #2540 from trycompai/sale-28-vendor-integrations-priority
SALE-28: Prioritize vendor-listed integrations
2 parents f21436a + 1b5365b commit 4f26e55

File tree

2 files changed

+167
-26
lines changed

2 files changed

+167
-26
lines changed

apps/app/src/app/(app)/[orgId]/integrations/components/PlatformIntegrations.test.tsx

Lines changed: 114 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -18,35 +18,21 @@ vi.mock('@/hooks/use-permissions', () => ({
1818

1919
// Mock integration platform hooks
2020
const mockStartOAuth = vi.fn();
21+
const mockUseIntegrationProviders = vi.fn();
22+
const mockUseIntegrationConnections = vi.fn();
2123
vi.mock('@/hooks/use-integration-platform', () => ({
22-
useIntegrationProviders: () => ({
23-
providers: [
24-
{
25-
id: 'github',
26-
name: 'GitHub',
27-
description: 'Code hosting',
28-
category: 'Development',
29-
logoUrl: '/github.png',
30-
authType: 'oauth2',
31-
oauthConfigured: true,
32-
isActive: true,
33-
requiredVariables: [],
34-
mappedTasks: [],
35-
supportsMultipleConnections: false,
36-
},
37-
],
38-
isLoading: false,
39-
}),
40-
useIntegrationConnections: () => ({
41-
connections: [],
42-
isLoading: false,
43-
refresh: vi.fn(),
44-
}),
24+
useIntegrationProviders: mockUseIntegrationProviders,
25+
useIntegrationConnections: mockUseIntegrationConnections,
4526
useIntegrationMutations: () => ({
4627
startOAuth: mockStartOAuth,
4728
}),
4829
}));
4930

31+
const mockUseVendors = vi.fn();
32+
vi.mock('@/hooks/use-vendors', () => ({
33+
useVendors: mockUseVendors,
34+
}));
35+
5036
// Mock integrations data
5137
vi.mock('../data/integrations', () => ({
5238
CATEGORIES: ['Development'],
@@ -159,6 +145,38 @@ const defaultProps = {
159145
describe('PlatformIntegrations', () => {
160146
beforeEach(() => {
161147
vi.clearAllMocks();
148+
mockUseIntegrationProviders.mockReturnValue({
149+
providers: [
150+
{
151+
id: 'github',
152+
name: 'GitHub',
153+
description: 'Code hosting',
154+
category: 'Development',
155+
logoUrl: '/github.png',
156+
authType: 'oauth2',
157+
oauthConfigured: true,
158+
isActive: true,
159+
requiredVariables: [],
160+
mappedTasks: [],
161+
supportsMultipleConnections: false,
162+
},
163+
],
164+
isLoading: false,
165+
});
166+
mockUseIntegrationConnections.mockReturnValue({
167+
connections: [],
168+
isLoading: false,
169+
refresh: vi.fn(),
170+
});
171+
mockUseVendors.mockReturnValue({
172+
data: {
173+
data: {
174+
data: [],
175+
count: 0,
176+
},
177+
status: 200,
178+
},
179+
});
162180
});
163181

164182
describe('Permission gating', () => {
@@ -329,4 +347,77 @@ describe('PlatformIntegrations', () => {
329347
expect(toast.info).not.toHaveBeenCalled();
330348
});
331349
});
350+
351+
describe('Vendor-prioritized ordering', () => {
352+
it('shows integrations from vendor list before non-vendor integrations', () => {
353+
mockUseIntegrationProviders.mockReturnValue({
354+
providers: [
355+
{
356+
id: 'github',
357+
name: 'GitHub',
358+
description: 'Code hosting',
359+
category: 'Development',
360+
logoUrl: '/github.png',
361+
authType: 'oauth2',
362+
oauthConfigured: true,
363+
isActive: true,
364+
requiredVariables: [],
365+
mappedTasks: [],
366+
supportsMultipleConnections: false,
367+
},
368+
{
369+
id: 'slack',
370+
name: 'Slack',
371+
description: 'Team communication',
372+
category: 'Communication',
373+
logoUrl: '/slack.png',
374+
authType: 'api_key',
375+
isActive: true,
376+
requiredVariables: [],
377+
mappedTasks: [],
378+
supportsMultipleConnections: false,
379+
},
380+
],
381+
isLoading: false,
382+
});
383+
mockUseIntegrationConnections.mockReturnValue({
384+
connections: [
385+
{
386+
id: 'conn-1',
387+
providerSlug: 'github',
388+
status: 'active',
389+
variables: {},
390+
},
391+
],
392+
isLoading: false,
393+
refresh: vi.fn(),
394+
});
395+
mockUseVendors.mockReturnValue({
396+
data: {
397+
data: {
398+
data: [
399+
{
400+
id: 'vnd-1',
401+
name: 'Slack',
402+
},
403+
],
404+
count: 1,
405+
},
406+
status: 200,
407+
},
408+
});
409+
410+
setMockPermissions(ADMIN_PERMISSIONS);
411+
412+
render(<PlatformIntegrations {...defaultProps} />);
413+
414+
const integrationTitles = screen
415+
.getAllByRole('heading', { level: 3 })
416+
.map((heading) => heading.textContent?.trim())
417+
.filter(Boolean);
418+
419+
expect(integrationTitles[0]).toBe('Slack');
420+
expect(integrationTitles[1]).toBe('GitHub');
421+
});
422+
});
332423
});

apps/app/src/app/(app)/[orgId]/integrations/components/PlatformIntegrations.tsx

Lines changed: 53 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import { ConnectIntegrationDialog } from '@/components/integrations/ConnectIntegrationDialog';
44
import { ManageIntegrationDialog } from '@/components/integrations/ManageIntegrationDialog';
55
import { usePermissions } from '@/hooks/use-permissions';
6+
import { useVendors } from '@/hooks/use-vendors';
67
import {
78
ConnectionListItem,
89
IntegrationProvider,
@@ -66,6 +67,16 @@ const providerNeedsConfiguration = (
6667
return requiredVariables.some((varId) => !currentVars[varId]);
6768
};
6869

70+
const normalizeIntegrationName = (value: string): string => {
71+
return value
72+
.toLowerCase()
73+
.replace(/\s*\([^)]*\)\s*$/, '')
74+
.replace(/[_-]+/g, ' ')
75+
.replace(/[^a-z0-9 ]+/g, '')
76+
.replace(/\s+/g, ' ')
77+
.trim();
78+
};
79+
6980
interface RelevantTask {
7081
taskId: string; // Actual task ID for navigation
7182
taskTemplateId: string;
@@ -96,6 +107,7 @@ export function PlatformIntegrations({ className, taskTemplates }: PlatformInteg
96107
const { hasPermission } = usePermissions();
97108
const canCreate = hasPermission('integration', 'create');
98109
const { startOAuth } = useIntegrationMutations();
110+
const { data: vendorsResponse } = useVendors();
99111

100112
const [searchQuery, setSearchQuery] = useState('');
101113
const [selectedCategory, setSelectedCategory] = useState<IntegrationCategory | 'All'>('All');
@@ -183,7 +195,20 @@ export function PlatformIntegrations({ className, taskTemplates }: PlatformInteg
183195
[connections],
184196
);
185197

186-
// Merge and sort: platform first (warnings, then connected, then disconnected), then custom
198+
const vendorNames = useMemo(() => {
199+
const vendors = vendorsResponse?.data?.data;
200+
if (!Array.isArray(vendors)) {
201+
return new Set<string>();
202+
}
203+
204+
return new Set(
205+
vendors
206+
.map((vendor) => normalizeIntegrationName(vendor.name))
207+
.filter((name) => name.length > 0),
208+
);
209+
}, [vendorsResponse]);
210+
211+
// Merge/sort integrations, then prioritize entries matching vendors in the org's vendor list.
187212
const unifiedIntegrations = useMemo<UnifiedIntegration[]>(() => {
188213
const platformIntegrations: UnifiedIntegration[] = (providers?.filter((p) => p.isActive) || [])
189214
.map((provider) => ({
@@ -214,8 +239,33 @@ export function PlatformIntegrations({ className, taskTemplates }: PlatformInteg
214239
integration,
215240
}));
216241

217-
return [...platformIntegrations, ...customIntegrations];
218-
}, [providers, connectionsByProvider]);
242+
const allIntegrations = [...platformIntegrations, ...customIntegrations];
243+
if (vendorNames.size === 0) {
244+
return allIntegrations;
245+
}
246+
247+
const vendorListedIntegrations: UnifiedIntegration[] = [];
248+
const otherIntegrations: UnifiedIntegration[] = [];
249+
250+
allIntegrations.forEach((integration) => {
251+
const candidateNames =
252+
integration.type === 'platform'
253+
? [integration.provider.name, integration.provider.id]
254+
: [integration.integration.name, integration.integration.id];
255+
256+
const isVendorListed = candidateNames
257+
.map((candidateName) => normalizeIntegrationName(candidateName))
258+
.some((normalizedCandidateName) => vendorNames.has(normalizedCandidateName));
259+
260+
if (isVendorListed) {
261+
vendorListedIntegrations.push(integration);
262+
} else {
263+
otherIntegrations.push(integration);
264+
}
265+
});
266+
267+
return [...vendorListedIntegrations, ...otherIntegrations];
268+
}, [providers, connectionsByProvider, vendorNames]);
219269

220270
// Get all unique categories
221271
const allCategories = useMemo(() => {

0 commit comments

Comments
 (0)