Skip to content

Commit bd0579f

Browse files
committed
fix(desktop): preserve enrollment mode and completion state
1 parent d7898ac commit bd0579f

11 files changed

Lines changed: 93 additions & 14 deletions

File tree

‎.github/workflows/desktop-e2e.yml‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,9 @@ on:
1717
- 'apps/sim/app/desktop/connect/**'
1818
- 'apps/sim/app/credential-groups/**'
1919
- 'apps/sim/hooks/queries/slack-search.ts'
20+
- 'apps/sim/hooks/use-github-installation-setup.ts'
21+
- 'apps/sim/app/o/**/integrations/indexed/use-member-enrollment.ts'
22+
- 'apps/sim/lib/api/contracts/desktop-source-connect.ts'
2023
- 'apps/sim/scripts/fixtures/desktop-source-connect.tsx'
2124
- 'apps/sim/app/workspace/**/browser-session/**'
2225
- 'apps/sim/app/_styles/**'

‎apps/desktop/e2e/source-connect.spec.ts‎

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,19 @@ test('source authorization returns to its desktop screen and refreshes live', as
113113
redirect(`/credential-groups/slack-complete?state=${state}&ok=${ok}`)
114114
return
115115
}
116+
if (
117+
path ===
118+
'/api/knowledge/00000000-0000-4000-8000-000000000001/connectors/fixture-connector/enroll'
119+
) {
120+
if (url.searchParams.has('oauthCompletionId'))
121+
json({ error: 'Direct account connection requires a Search source' }, 400)
122+
else
123+
json({
124+
success: true,
125+
data: { url: `${origin}/credential-groups/enroll/fixture-invitation` },
126+
})
127+
return
128+
}
116129
if (path === '/api/knowledge/github/setup') {
117130
if (request.method === 'POST') {
118131
const { setupId } = await body()
@@ -367,6 +380,15 @@ test('source authorization returns to its desktop screen and refreshes live', as
367380
expect(page.url()).toBe(`${origin}/home`)
368381
}
369382
)
383+
await check('ordinary knowledge-base enrollment preserves its invitation step', async () => {
384+
await page.getByRole('button', { name: 'Connect invited source' }).click()
385+
await expect.poll(async () => (await opened()).length).toBe(7)
386+
await external.goto((await opened())[6])
387+
await external.getByRole('link', { name: 'Authorize invited source' }).click()
388+
await expect(page.getByLabel('Enrollment pending')).toHaveText('false')
389+
await expect(page.getByLabel('Enrollment error')).toHaveText('')
390+
expect(page.url()).toBe(`${origin}/home`)
391+
})
370392
await page.screenshot({ path: test.info().outputPath('source-connect-desktop.png') })
371393
} finally {
372394
mkdirSync(dirname(reportPath), { recursive: true })

‎apps/sim/app/api/knowledge/slack/oauth/callback/route.ts‎

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import { createLogger } from '@sim/logger'
2+
import { describeError } from '@sim/utils/errors'
13
import { NextResponse } from 'next/server'
24
import { slackSearchOAuthCallbackContract } from '@/lib/api/contracts/knowledge/slack'
35
import { parseRequest } from '@/lib/api/server'
@@ -15,6 +17,8 @@ import { completeSlackSearchSetup } from '@/lib/knowledge/application/slack-sear
1517
import { slackSearchInstallPath } from '@/lib/slack-search/install-link'
1618
import { authenticateSlackPublicInstallation } from '@/lib/slack-search/public-install-auth'
1719

20+
const logger = createLogger('SlackSearchOAuthCallback')
21+
1822
/** OAuth is a redirect protocol; protected configuration remains in the application use case. */
1923
export const GET = withRouteHandler(async (request) => {
2024
let callbackState: string | undefined
@@ -64,6 +68,12 @@ export const GET = withRouteHandler(async (request) => {
6468
})
6569
} catch (error) {
6670
if (callbackState) {
71+
const projected = internalOrchestrationErrorPolicy.project(error)
72+
if (
73+
!(error instanceof InternalUnauthenticatedError) &&
74+
(!projected || projected.status >= 500)
75+
)
76+
logger.error('Slack authorization callback failed', { error: describeError(error) })
6777
const url = new URL('/credential-groups/slack-complete', getBaseUrl())
6878
url.searchParams.set('state', callbackState)
6979
url.searchParams.set('ok', 'false')

‎apps/sim/app/o/[organizationId]/integrations/indexed/use-member-enrollment.ts‎

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -108,7 +108,10 @@ export function useMemberEnrollment({
108108
nativeAbort.current = controller
109109
return connectDesktopSource(request, controller.signal)
110110
},
111-
onSettled: () => refreshMemberships(),
111+
onSettled: () =>
112+
Promise.all(
113+
membershipQueryKeys.map((queryKey) => queryClient.invalidateQueries({ queryKey }))
114+
),
112115
onError: (error) => onConnectionError?.(error.message),
113116
onSuccess: () => setSetupConnector(null),
114117
})
@@ -286,7 +289,7 @@ export function useMemberEnrollment({
286289
nativeConnection.mutate({
287290
kind: 'member-enrollment',
288291
params: { id: knowledgeBaseId, connectorId },
289-
completionId: generateId(),
292+
...(directOAuth ? { completionId: generateId() } : {}),
290293
})
291294
return
292295
}
@@ -321,7 +324,7 @@ export function useMemberEnrollment({
321324
nativeConnection.mutate({
322325
kind: 'search-source',
323326
body: { ...resourceScopeFields(scope), connectorType, sourceConfig },
324-
completionId: generateId(),
327+
...(directOAuth ? { completionId: generateId() } : {}),
325328
})
326329
return
327330
}

‎apps/sim/components/integrations/slack-search-setup-wizard.tsx‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -156,7 +156,7 @@ export function SlackSearchSetupWizard({
156156
return (
157157
<ChipModal
158158
open
159-
dismissDisabled={busy && !isDesktopApp()}
159+
dismissDisabled={connect.isPending || (oauth.isPending && !isDesktopApp())}
160160
onOpenChange={(open) => {
161161
if (!open) close()
162162
}}
@@ -212,7 +212,7 @@ export function SlackSearchSetupWizard({
212212
return (
213213
<ChipModal
214214
open
215-
dismissDisabled={busy && !isDesktopApp()}
215+
dismissDisabled={connect.isPending || (oauth.isPending && !isDesktopApp())}
216216
onOpenChange={(open) => {
217217
if (!open) close()
218218
}}

‎apps/sim/hooks/queries/kb/connectors.test.ts‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,13 @@ import {
22
apiClientRequestMock,
33
apiClientRequestMockFns,
44
} from '@sim/testing/mocks/api-client-request.mock'
5+
import { emcnMock } from '@sim/testing/mocks/emcn.mock'
56
import { reactQueryMock, reactQueryMockFns } from '@sim/testing/mocks/react-query.mock'
67
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
78
import { searchSourceKeys } from '@/hooks/queries/utils/search-source-keys'
89

910
vi.mock('react', () => ({ useEffect: vi.fn() }))
11+
vi.mock('@sim/emcn', () => emcnMock)
1012

1113
vi.mock('@tanstack/react-query', () => reactQueryMock)
1214

‎apps/sim/hooks/queries/personal-search-integrations.ts‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ export function useConnectPersonalSearchIntegration() {
4545
await connectDesktopSource({ kind: 'personal-search', body }, signal)
4646
return null
4747
}
48-
return (await requestJson(connectPersonalSearchIntegrationContract, { body })).data
48+
return (await requestJson(connectPersonalSearchIntegrationContract, { body, signal })).data
4949
},
5050
onSettled: (_data, _error, body) =>
5151
Promise.all([

‎apps/sim/lib/api/contracts/desktop-source-connect.ts‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,12 +25,12 @@ export const desktopSourceRequestSchema = z.discriminatedUnion('kind', [
2525
z.object({
2626
kind: z.literal('member-enrollment'),
2727
params: knowledgeConnectorParamsSchema,
28-
completionId: z.string().uuid(),
28+
completionId: z.string().uuid().optional(),
2929
}),
3030
z.object({
3131
kind: z.literal('search-source'),
3232
body: connectSimSearchConnectorBodySchema,
33-
completionId: z.string().uuid(),
33+
completionId: z.string().uuid().optional(),
3434
}),
3535
z.object({
3636
kind: z.literal('slack-managed-users'),

‎apps/sim/lib/desktop/source-browser.ts‎

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -106,13 +106,23 @@ async function startRequest(
106106
params: request.params,
107107
query: { oauthCompletionId: request.completionId },
108108
})
109-
return { url: result.data.url, match: { kind: 'completion', id: request.completionId } }
109+
return {
110+
url: result.data.url,
111+
match: request.completionId
112+
? { kind: 'completion', id: request.completionId }
113+
: enrollmentMatch(result.data.url),
114+
}
110115
}
111116
case 'search-source': {
112117
const result = await requestJson(connectSimSearchConnectorContract, {
113118
body: { ...request.body, oauthCompletionId: request.completionId },
114119
})
115-
return { url: result.data.url, match: { kind: 'completion', id: request.completionId } }
120+
return {
121+
url: result.data.url,
122+
match: request.completionId
123+
? { kind: 'completion', id: request.completionId }
124+
: enrollmentMatch(result.data.url),
125+
}
116126
}
117127
case 'slack-managed-users': {
118128
const { owner, body, credentialGroupId } = request

‎apps/sim/lib/desktop/source-request.integration.ts‎

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,11 @@ import { sleep } from '@sim/utils/helpers'
33
import { generateId, generateShortId } from '@sim/utils/id'
44
import { afterAll, describe, expect, it, vi } from 'vitest'
55

6-
await vi.hoisted(async () => {
6+
const { redisUrl } = await vi.hoisted(async () => {
77
const { readTestRedisUrl } = await import('@sim/db/testing/test-infrastructure')
8-
process.env.REDIS_URL = readTestRedisUrl()
8+
const redisUrl = readTestRedisUrl()
9+
if (redisUrl) process.env.REDIS_URL = redisUrl
10+
return { redisUrl }
911
})
1012

1113
import { closeRedisConnection, getRedisClient } from '@/lib/core/config/redis'
@@ -18,7 +20,7 @@ import {
1820
} from '@/lib/desktop/application/source-requests'
1921

2022
/** Real Redis proves cross-session ownership and atomic consumption, without provider credentials. */
21-
describe('desktop source request transport', () => {
23+
describe.skipIf(!redisUrl)('desktop source request transport', () => {
2224
it('allows the same user in another session, without letting another user consume the request', async () => {
2325
const userId = generateId()
2426
const created = await createDesktopSourceRequest.execute({

0 commit comments

Comments
 (0)