Skip to content

Commit 8714297

Browse files
committed
fix(search): restore live integration connection chips
1 parent 266fb26 commit 8714297

25 files changed

Lines changed: 819 additions & 114 deletions

‎apps/sim/app/o/[organizationId]/integrations/integrations.tsx‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ export function OrganizationIntegrations({
4343
)
4444
}
4545
>
46-
{!features.liveEnterpriseSearch && connectionRequest && (
46+
{connectionRequest && (
4747
<SearchIntegrationConnection
4848
organizationId={organization.id}
4949
{...connectionRequest}

‎apps/sim/app/o/[organizationId]/integrations/page.test.tsx‎

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,3 +98,25 @@ describe('integrations page Slack context', () => {
9898
expect(mocks.context).not.toHaveBeenCalled()
9999
})
100100
})
101+
102+
it('preserves a live Slack reconnect chip through navigation and login without an indexed source', async () => {
103+
const query = {
104+
connectorType: 'slack',
105+
connectionMode: 'live',
106+
optionId: 'slack-option',
107+
provider: 'slack',
108+
credentialId: 'own-account',
109+
}
110+
const selected = { ...props, searchParams: Promise.resolve(query) }
111+
const page = await OrganizationIntegrationsPage(selected)
112+
expect(page.props.connectionRequest).toEqual({
113+
userId: 'viewer',
114+
target: { type: 'link', ...query },
115+
})
116+
authMockFns.mockGetSession.mockResolvedValue(null)
117+
await expect(OrganizationIntegrationsPage(selected)).rejects.toThrow('Redirect')
118+
const redirect = new URL(mocks.redirect.mock.calls[0][0], 'https://sim.test')
119+
const callback = new URL(redirect.searchParams.get('callbackUrl')!, 'https://sim.test')
120+
for (const [key, value] of Object.entries(query))
121+
expect(callback.searchParams.get(key)).toBe(value)
122+
})

‎apps/sim/app/o/[organizationId]/integrations/page.tsx‎

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,9 @@ interface OrganizationIntegrationsPageProps {
2323
params: Promise<{ organizationId: string }>
2424
searchParams: Promise<{
2525
slack?: string | string[]
26+
connectionMode?: string | string[]
27+
optionId?: string | string[]
28+
provider?: string | string[]
2629
connectorType?: string | string[]
2730
connectorId?: string | string[]
2831
credentialId?: string | string[]
@@ -39,10 +42,17 @@ export default async function OrganizationIntegrationsPage({
3942
const selection = loadIntegrationConnectionParams(query)
4043
const connector = SEARCH_CONNECTORS.find((entry) => entry.type === selection.connectorType)
4144
const requested =
42-
selection.connectorType || selection.connectorId || selection.credentialId
45+
selection.connectorType ||
46+
selection.connectorId ||
47+
selection.credentialId ||
48+
selection.connectionMode ||
49+
selection.optionId ||
50+
selection.provider
4351
? searchConnectionTargetSchema.safeParse({
4452
type: 'link',
45-
provider: connector?.providerId,
53+
...(selection.connectionMode ? { connectionMode: selection.connectionMode } : {}),
54+
...(selection.optionId ? { optionId: selection.optionId } : {}),
55+
provider: selection.provider || connector?.providerId,
4656
connectorType: selection.connectorType,
4757
...(selection.connectorId ? { connectorId: selection.connectorId } : {}),
4858
...(selection.credentialId ? { credentialId: selection.credentialId } : {}),

‎apps/sim/app/o/[organizationId]/integrations/search-params.ts‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,9 @@ import { createLoader, parseAsString } from 'nuqs/server'
22

33
export const integrationConnectionParams = {
44
connectorType: parseAsString.withDefault(''),
5+
connectionMode: parseAsString.withDefault(''),
6+
optionId: parseAsString.withDefault(''),
7+
provider: parseAsString.withDefault(''),
58
connectorId: parseAsString.withDefault(''),
69
credentialId: parseAsString.withDefault(''),
710
}

‎apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/special-tags/search-integration-connection.tsx‎

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,13 @@ function SearchIntegrationConnectionControl({
6464
? `${name} connection is no longer available`
6565
: `${action} ${name}`
6666
const handleConnect = () => {
67-
if (connector && !connection.connectorId && connector.setupFields.length && !connection.pending)
67+
if (
68+
target.connectionMode !== 'live' &&
69+
connector &&
70+
!connection.connectorId &&
71+
connector.setupFields.length &&
72+
!connection.pending
73+
)
6874
setSetupOpen(true)
6975
else void connection.connect()
7076
}

‎apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/special-tags/special-tags.test.ts‎

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1476,3 +1476,17 @@ describe('source tag', () => {
14761476
expect(hasPendingTag).toBe(true)
14771477
})
14781478
})
1479+
1480+
it('renders a live Slack credential target as a visible Search connection card', () => {
1481+
const item = {
1482+
type: 'link',
1483+
provider: 'slack',
1484+
connectorType: 'slack',
1485+
connectionMode: 'live',
1486+
optionId: 'slack-option',
1487+
} as const
1488+
const text = `Connect your Slack account below.\n<credential>${JSON.stringify(item)}</credential>`
1489+
const parsed = parseLastCredentialTag(text)
1490+
expect(parsed).toEqual([item])
1491+
expect(credentialTagHasVisibleCard(parsed!, false, 'assistant')).toBe(true)
1492+
})

‎apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/special-tags/special-tags.tsx‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -184,6 +184,8 @@ export interface CredentialItemData {
184184
/** Canonical Search source requested by an organization connection control. */
185185
connectorType?: string
186186
connectorId?: string
187+
connectionMode?: 'live'
188+
optionId?: string
187189
}
188190

189191
/**

‎apps/sim/hooks/use-search-integration-connection.test.tsx‎

Lines changed: 37 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,14 @@ const m = vi.hoisted(() => ({
1717
}>,
1818
queryError: null as Error | null,
1919
requestedTarget: undefined as
20-
| { type: 'link'; provider: string; connectorType: string; connectorId?: string }
20+
| {
21+
type: 'link'
22+
provider: string
23+
connectorType: string
24+
connectorId?: string
25+
connectionMode?: 'live'
26+
optionId?: string
27+
}
2128
| undefined,
2229
}))
2330
const target = {
@@ -34,7 +41,9 @@ vi.mock('@/hooks/queries/personal-search-integrations', () => ({
3441
usePersonalSearchIntegrations: (query: { completionId?: string }) => ({
3542
data: {
3643
connections: [{ accounts: m.accounts }],
37-
available: [{ target }],
44+
available: [
45+
{ target: m.requestedTarget?.connectionMode === 'live' ? m.requestedTarget : target },
46+
],
3847
completedCredentialId: m.receipts.get(query.completionId ?? '') ?? null,
3948
},
4049
isSuccess: !m.queryError,
@@ -282,3 +291,29 @@ describe('Search connection card lifecycle', () => {
282291
expect(connection().error).toContain('timed out')
283292
})
284293
})
294+
295+
it('completes a live connection using its receipt without an indexed source', async () => {
296+
m.requestedTarget = {
297+
type: 'link',
298+
provider: 'slack',
299+
connectorType: 'slack',
300+
connectionMode: 'live',
301+
optionId: 'slack-option',
302+
}
303+
m.mutate.mockResolvedValue({ url: 'https://provider.test/authorize' })
304+
render()
305+
expect(connection().available).toBe(true)
306+
await act(async () => {
307+
await connection().connect()
308+
})
309+
const request = m.mutate.mock.calls[0][0]
310+
expect(request.target).toEqual(m.requestedTarget)
311+
expect(connection().connected).toBe(false)
312+
m.accounts = [{ credentialId: 'new-account', status: 'connected' }]
313+
render()
314+
expect(connection().connected).toBe(false)
315+
m.receipts.set(request.oauthCompletionId, 'new-account')
316+
render()
317+
expect(connection().connected).toBe(true)
318+
expect(m.connected).toHaveBeenCalled()
319+
})

‎apps/sim/lib/api/contracts/knowledge/personal-integrations.ts‎

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,8 @@ export const personalSearchIntegrationSchema = z.object({
88
name: z.string().max(200),
99
providerId: z.string().min(1).max(100),
1010
connectorType: z.string().min(1).max(100),
11-
connectorId: z.string().min(1).max(200),
12-
knowledgeBaseId: z.string().min(1).max(200),
11+
connectorId: z.string().min(1).max(200).optional(),
12+
knowledgeBaseId: z.string().min(1).max(200).optional(),
1313
description: z.string().max(240),
1414
accounts: z
1515
.array(
@@ -22,7 +22,9 @@ export const personalSearchIntegrationSchema = z.object({
2222
)
2323
.max(100),
2424
connectionStatus: z.enum(['connected', 'reconnect_needed', 'not_connected', 'unavailable']),
25-
indexingStatus: z.enum(['indexing', 'indexed', 'not_indexed', 'sync_failed', 'paused']),
25+
indexingStatus: z
26+
.enum(['indexing', 'indexed', 'not_indexed', 'sync_failed', 'paused'])
27+
.optional(),
2628
action: searchConnectionTargetSchema.nullable(),
2729
})
2830

@@ -80,8 +82,8 @@ export const connectPersonalSearchIntegrationContract = defineRouteContract({
8082
schema: successResponseSchema(
8183
z.object({
8284
url: z.string().url(),
83-
connectorId: z.string().min(1).max(200),
84-
knowledgeBaseId: z.string().min(1).max(200),
85+
connectorId: z.string().min(1).max(200).optional(),
86+
knowledgeBaseId: z.string().min(1).max(200).optional(),
8587
})
8688
),
8789
},

‎apps/sim/lib/credential-groups/application/organization-accounts.ts‎

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,10 +21,12 @@ import {
2121
} from '@/lib/credential-groups/enrollment-links'
2222
import { CredentialGroupEnrollmentError } from '@/lib/credential-groups/enrollments'
2323
import { ManagedMcpConnectorError } from '@/lib/credential-groups/managed-mcp-service'
24+
import type { CredentialGroupConnectionIntent } from '@/lib/credential-groups/oauth-intent'
2425
import { requireOrganizationAccountsSetup } from '@/lib/credential-groups/organization-setup'
2526
import { listConfiguredCredentialGroupProviders } from '@/lib/credential-groups/provider-availability'
2627
import { isScopedCredentialGroupsAvailable } from '@/lib/credential-groups/scoped-availability'
2728
import { createViewerCredentialGroupEnrollment } from '@/lib/credential-groups/self-enrollment'
29+
import { startViewerCredentialGroupOAuth } from '@/lib/credential-groups/self-enrollment-oauth'
2830
import {
2931
ensureWorkspaceAccountsGroup,
3032
getOrganizationAccountsGroup,
@@ -239,7 +241,11 @@ export const startOrganizationAccountConnection = defineOrganizationAccountsUseC
239241
input,
240242
context,
241243
}: {
242-
input: OrganizationAccountsInput & StartOrganizationAccountConnectionBody
244+
input: OrganizationAccountsInput &
245+
StartOrganizationAccountConnectionBody & {
246+
oauthCompletionId?: string
247+
connectionIntent?: CredentialGroupConnectionIntent
248+
}
243249
context: OrganizationMembershipContext
244250
}) {
245251
const group = await getOrganizationAccountsGroup(context.organizationId)
@@ -263,6 +269,16 @@ export const startOrganizationAccountConnection = defineOrganizationAccountsUseC
263269
) {
264270
throw new OrchestrationError('not_found', 'This account option is no longer available')
265271
}
272+
if (input.oauthCompletionId) {
273+
return startViewerCredentialGroupOAuth({
274+
organizationId: context.organizationId,
275+
userId: context.userId,
276+
credentialGroupId: group.id,
277+
optionId: input.optionId,
278+
completionId: input.oauthCompletionId,
279+
connectionIntent: input.connectionIntent,
280+
})
281+
}
266282
const { invitationLink } = await createViewerCredentialGroupEnrollment({
267283
organizationId: context.organizationId,
268284
userId: context.userId,

0 commit comments

Comments
 (0)