Skip to content

Commit ad6ced8

Browse files
committed
feat(search): context-aware cmd-k results on the integrations page
When cmd-k is opened on the integrations page, show two new result groups: connected accounts (visible even with empty input) and catalog integrations (appear once the user types). Selecting an OAuth integration deep-links to its detail page with ?connect=oauth so the connect modal auto-opens. Non-OAuth integrations navigate to the plain detail page. Both groups are gated to the integrations page only and respect the hideIntegrationsTab permission. The credentials fetch shares the same React Query cache key as the integrations page itself (no double fetch).
1 parent 59197ad commit ad6ced8

7 files changed

Lines changed: 230 additions & 0 deletions

File tree

apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/search-modal/components/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,10 @@ export {
99
} from './command-items'
1010
export {
1111
BlocksGroup,
12+
ConnectedAccountsGroup,
1213
DocsGroup,
1314
FilesGroup,
15+
IntegrationsGroup,
1416
KnowledgeBasesGroup,
1517
PagesGroup,
1618
TablesGroup,

apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/search-modal/components/search-groups/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
export {
22
BlocksGroup,
3+
ConnectedAccountsGroup,
34
DocsGroup,
45
FilesGroup,
6+
IntegrationsGroup,
57
KnowledgeBasesGroup,
68
PagesGroup,
79
TablesGroup,

apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/search-modal/components/search-groups/search-groups.tsx

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import {
1515
} from '@/app/workspace/[workspaceId]/w/components/sidebar/components/search-modal/components/command-items'
1616
import type {
1717
FileItem,
18+
IntegrationSearchItem,
1819
PageItem,
1920
TaskItem,
2021
WorkflowItem,
@@ -254,6 +255,9 @@ export const PagesGroup = memo(function PagesGroup({
254255
export const TablesGroup = createIconGroup('Tables', 'table', Table)
255256
export const KnowledgeBasesGroup = createIconGroup('Knowledge Bases', 'knowledge-base', Database)
256257

258+
export const ConnectedAccountsGroup = createColoredIconGroup('Connected', 'connected-account')
259+
export const IntegrationsGroup = createColoredIconGroup('Integrations', 'integration')
260+
257261
export const FilesGroup = memo(function FilesGroup({
258262
items,
259263
onSelect,
@@ -277,6 +281,40 @@ export const FilesGroup = memo(function FilesGroup({
277281
)
278282
})
279283

284+
/**
285+
* Factory for groups that render each item with its own brand icon on a
286+
* brand-colored tile (the same `showColoredIcon` pattern used by
287+
* `BlocksGroup` / `ToolsGroup`). Used for integrations and connected accounts
288+
* where every row has a distinct per-item icon and brand color.
289+
*/
290+
function createColoredIconGroup(heading: string, prefix: string) {
291+
return memo(function ColoredIconGroup({
292+
items,
293+
onSelect,
294+
}: {
295+
items: IntegrationSearchItem[]
296+
onSelect: (item: IntegrationSearchItem) => void
297+
}) {
298+
if (items.length === 0) return null
299+
return (
300+
<Command.Group heading={heading} className={GROUP_HEADING_CLASSNAME}>
301+
{items.map((item) => (
302+
<MemoizedCommandItem
303+
key={item.id}
304+
value={`${item.name} ${prefix}-${item.id}`}
305+
onSelect={() => onSelect(item)}
306+
icon={item.icon}
307+
bgColor={item.bgColor}
308+
showColoredIcon
309+
>
310+
{item.name}
311+
</MemoizedCommandItem>
312+
))}
313+
</Command.Group>
314+
)
315+
})
316+
}
317+
280318
function createIconGroup(
281319
heading: string,
282320
prefix: string,
Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
import type { ComponentType } from 'react'
2+
import { blockTypeToIconMap, INTEGRATIONS } from '@/lib/integrations'
3+
import { getServiceConfigByProviderId } from '@/lib/oauth'
4+
import {
5+
CONNECT_MODE,
6+
CONNECT_QUERY_PARAM,
7+
} from '@/app/workspace/[workspaceId]/integrations/connect-route'
8+
import type { IntegrationSearchItem } from '@/app/workspace/[workspaceId]/w/components/sidebar/components/search-modal/utils'
9+
import type { WorkspaceCredential } from '@/hooks/queries/credentials'
10+
11+
/** Fallback brand color for credentials whose integration metadata cannot be resolved. */
12+
const FALLBACK_BG_COLOR = '#6B7280'
13+
14+
/**
15+
* Module-level lookup of integration metadata by OAuth service display name
16+
* (case-insensitive). Mirrors the same map in `integrations.tsx`.
17+
*/
18+
const INTEGRATION_BY_LOWER_NAME = new Map(INTEGRATIONS.map((i) => [i.name.toLowerCase(), i]))
19+
20+
/**
21+
* Module-level base array of resolvable integrations (entries without a
22+
* registered icon are dropped, matching the catalog's `if (!Icon) return null`
23+
* guard). Workspace-independent; `href` is injected per call.
24+
*/
25+
const INTEGRATION_BASES: readonly {
26+
id: string
27+
name: string
28+
icon: ComponentType<{ className?: string }>
29+
bgColor: string
30+
slug: string
31+
authType: string
32+
}[] = INTEGRATIONS.flatMap((integration) => {
33+
const icon = blockTypeToIconMap[integration.type]
34+
if (!icon) return []
35+
return [
36+
{
37+
id: integration.slug,
38+
name: integration.name,
39+
icon,
40+
bgColor: integration.bgColor,
41+
slug: integration.slug,
42+
authType: integration.authType,
43+
},
44+
]
45+
})
46+
47+
/**
48+
* Builds the full integration catalog as search items for a given workspace.
49+
* OAuth integrations link directly to the detail page with `?connect=oauth` so
50+
* the connect modal auto-opens (via the detail page's `useEffect` on
51+
* `CONNECT_QUERY_PARAM`). Non-OAuth integrations link to the plain detail page.
52+
*/
53+
export function buildIntegrationSearchItems(workspaceId: string): IntegrationSearchItem[] {
54+
return INTEGRATION_BASES.map((base) => {
55+
const connectSuffix =
56+
base.authType === 'oauth' ? `?${CONNECT_QUERY_PARAM}=${CONNECT_MODE.oauth}` : ''
57+
return {
58+
id: base.id,
59+
name: base.name,
60+
icon: base.icon,
61+
bgColor: base.bgColor,
62+
href: `/workspace/${workspaceId}/integrations/${base.slug}${connectSuffix}`,
63+
}
64+
})
65+
}
66+
67+
/**
68+
* Builds search items for the user's connected OAuth / service-account
69+
* credentials. Each item links to its credential detail page. Credentials
70+
* without a resolvable OAuth service are silently dropped (same guard as
71+
* `integrations.tsx`'s `connectedItems` memo).
72+
*/
73+
export function buildConnectedAccountSearchItems(
74+
credentials: readonly WorkspaceCredential[],
75+
workspaceId: string
76+
): IntegrationSearchItem[] {
77+
return credentials.flatMap((credential) => {
78+
if (credential.type !== 'oauth' && credential.type !== 'service_account') return []
79+
if (!credential.providerId) return []
80+
81+
const service = getServiceConfigByProviderId(credential.providerId)
82+
if (!service) return []
83+
84+
const integration = INTEGRATION_BY_LOWER_NAME.get(service.name.toLowerCase())
85+
86+
return [
87+
{
88+
id: credential.id,
89+
name: credential.displayName,
90+
icon: service.icon as ComponentType<{ className?: string }>,
91+
bgColor: integration?.bgColor ?? FALLBACK_BG_COLOR,
92+
href: `/workspace/${workspaceId}/integrations/connected/${credential.id}`,
93+
},
94+
]
95+
})
96+
}

apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/search-modal/search-modal.tsx

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,8 +35,10 @@ import type {
3535
} from '@/stores/modals/search/types'
3636
import {
3737
BlocksGroup,
38+
ConnectedAccountsGroup,
3839
DocsGroup,
3940
FilesGroup,
41+
IntegrationsGroup,
4042
KnowledgeBasesGroup,
4143
PagesGroup,
4244
TablesGroup,
@@ -49,6 +51,7 @@ import {
4951
} from './components/search-groups'
5052
import type {
5153
FileItem,
54+
IntegrationSearchItem,
5255
PageItem,
5356
SearchModalProps,
5457
TaskItem,
@@ -68,7 +71,10 @@ export function SearchModal({
6871
tables = [],
6972
files = [],
7073
knowledgeBases = [],
74+
integrations = [],
75+
connectedAccounts = [],
7176
isOnWorkflowPage = false,
77+
isOnIntegrationsPage = false,
7278
}: SearchModalProps) {
7379
const params = useParams()
7480
const router = useRouter()
@@ -378,6 +384,32 @@ export function SearchModal({
378384
[workspaceId]
379385
)
380386

387+
const handleConnectedAccountSelect = useCallback(
388+
(item: IntegrationSearchItem) => {
389+
routerRef.current.push(item.href)
390+
captureEvent(posthogRef.current, 'search_result_selected', {
391+
result_type: 'connected_account',
392+
query_length: deferredSearchRef.current.length,
393+
workspace_id: workspaceId,
394+
})
395+
onOpenChangeRef.current(false)
396+
},
397+
[workspaceId]
398+
)
399+
400+
const handleIntegrationSelect = useCallback(
401+
(item: IntegrationSearchItem) => {
402+
routerRef.current.push(item.href)
403+
captureEvent(posthogRef.current, 'search_result_selected', {
404+
result_type: 'integration',
405+
query_length: deferredSearchRef.current.length,
406+
workspace_id: workspaceId,
407+
})
408+
onOpenChangeRef.current(false)
409+
},
410+
[workspaceId]
411+
)
412+
381413
const handleBlockSelectAsBlock = useCallback(
382414
(block: SearchBlockItem) => handleBlockSelect(block, 'block'),
383415
[handleBlockSelect]
@@ -462,6 +494,22 @@ export function SearchModal({
462494
[pages, deferredSearch]
463495
)
464496

497+
/** Connected accounts: visible on the integrations page even with empty input. */
498+
const filteredConnectedAccounts = useMemo(() => {
499+
if (!isOnIntegrationsPage) return []
500+
return filterAndSort(
501+
connectedAccounts,
502+
(a) => `${a.name} connected-account-${a.id}`,
503+
deferredSearch
504+
)
505+
}, [isOnIntegrationsPage, connectedAccounts, deferredSearch])
506+
507+
/** Catalog integrations: only shown once the user has typed something. */
508+
const filteredIntegrations = useMemo(() => {
509+
if (!isOnIntegrationsPage || !deferredSearch) return []
510+
return filterAndSort(integrations, (i) => `${i.name} integration-${i.id}`, deferredSearch)
511+
}, [isOnIntegrationsPage, deferredSearch, integrations])
512+
465513
if (!mounted) return null
466514

467515
return createPortal(
@@ -513,6 +561,11 @@ export function SearchModal({
513561
No results found.
514562
</Command.Empty>
515563

564+
<ConnectedAccountsGroup
565+
items={filteredConnectedAccounts}
566+
onSelect={handleConnectedAccountSelect}
567+
/>
568+
<IntegrationsGroup items={filteredIntegrations} onSelect={handleIntegrationSelect} />
516569
<BlocksGroup items={filteredBlocks} onSelect={handleBlockSelectAsBlock} />
517570
<ToolsGroup items={filteredTools} onSelect={handleBlockSelectAsTool} />
518571
<TriggersGroup items={filteredTriggers} onSelect={handleBlockSelectAsTrigger} />

apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/search-modal/utils.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,13 @@
11
import type { ComponentType, ReactNode } from 'react'
22

3+
export interface IntegrationSearchItem {
4+
id: string
5+
name: string
6+
href: string
7+
icon: ComponentType<{ className?: string }>
8+
bgColor: string
9+
}
10+
311
export interface TaskItem {
412
id: string
513
name: string
@@ -48,7 +56,10 @@ export interface SearchModalProps {
4856
tables?: TaskItem[]
4957
files?: FileItem[]
5058
knowledgeBases?: TaskItem[]
59+
integrations?: IntegrationSearchItem[]
60+
connectedAccounts?: IntegrationSearchItem[]
5161
isOnWorkflowPage?: boolean
62+
isOnIntegrationsPage?: boolean
5263
}
5364

5465
export interface CommandItemProps {

apps/sim/app/workspace/[workspaceId]/w/components/sidebar/sidebar.tsx

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,10 @@ import {
5858
WorkflowList,
5959
WorkspaceHeader,
6060
} from '@/app/workspace/[workspaceId]/w/components/sidebar/components'
61+
import {
62+
buildConnectedAccountSearchItems,
63+
buildIntegrationSearchItems,
64+
} from '@/app/workspace/[workspaceId]/w/components/sidebar/components/search-modal/integration-search-items'
6165
import { ContextMenu } from '@/app/workspace/[workspaceId]/w/components/sidebar/components/workflow-list/components/context-menu/context-menu'
6266
import { DeleteModal } from '@/app/workspace/[workspaceId]/w/components/sidebar/components/workflow-list/components/delete-modal/delete-modal'
6367
import {
@@ -81,6 +85,7 @@ import {
8185
groupWorkflowsByFolder,
8286
} from '@/app/workspace/[workspaceId]/w/components/sidebar/utils'
8387
import { useImportWorkflow } from '@/app/workspace/[workspaceId]/w/hooks'
88+
import { useWorkspaceCredentials } from '@/hooks/queries/credentials'
8489
import { useFolderMap, useFolders } from '@/hooks/queries/folders'
8590
import { useKnowledgeBasesQuery } from '@/hooks/queries/kb/knowledge'
8691
import { useTablesList } from '@/hooks/queries/tables'
@@ -1010,6 +1015,26 @@ export const Sidebar = memo(function Sidebar() {
10101015
}, [])
10111016

10121017
const isOnSettingsPage = pathname?.startsWith(`/workspace/${workspaceId}/settings`) ?? false
1018+
const isOnIntegrationsPage =
1019+
pathname?.startsWith(`/workspace/${workspaceId}/integrations`) ?? false
1020+
1021+
const { data: fetchedCredentials = [] } = useWorkspaceCredentials({
1022+
workspaceId,
1023+
enabled: isOnIntegrationsPage && !permissionConfig.hideIntegrationsTab,
1024+
})
1025+
1026+
const searchModalIntegrations = useMemo(
1027+
() => (permissionConfig.hideIntegrationsTab ? [] : buildIntegrationSearchItems(workspaceId)),
1028+
[workspaceId, permissionConfig.hideIntegrationsTab]
1029+
)
1030+
1031+
const searchModalConnectedAccounts = useMemo(
1032+
() =>
1033+
permissionConfig.hideIntegrationsTab
1034+
? []
1035+
: buildConnectedAccountSearchItems(fetchedCredentials, workspaceId),
1036+
[fetchedCredentials, workspaceId, permissionConfig.hideIntegrationsTab]
1037+
)
10131038

10141039
const isLoading = workflowsLoading || sessionLoading
10151040
const initialScrollDoneRef = useRef(false)
@@ -1746,7 +1771,10 @@ export const Sidebar = memo(function Sidebar() {
17461771
tables={searchModalTables}
17471772
files={searchModalFiles}
17481773
knowledgeBases={searchModalKnowledgeBases}
1774+
integrations={searchModalIntegrations}
1775+
connectedAccounts={searchModalConnectedAccounts}
17491776
isOnWorkflowPage={!!workflowId}
1777+
isOnIntegrationsPage={isOnIntegrationsPage}
17501778
/>
17511779

17521780
<HelpModal

0 commit comments

Comments
 (0)