)}
{step === 'credentials' && (
@@ -268,13 +273,22 @@ export function SlackSearchSetupWizard({
/>
>
)}
- {step === 'install' && (
-
- {installationId
- ? 'Approve the updated permissions for'
- : 'Choose your workspace and approve'}{' '}
- {name} in Slack.
-
+ {step === 'token' && (
+ <>
+
+ Copy the Bot User OAuth Token from OAuth & Permissions in your installed Slack
+ app. If Slack requests updated permissions, approve them there first.
+
+
+ >
)}
{error?.message}
@@ -319,19 +333,22 @@ export function SlackSearchSetupWizard({
disabled: busy,
onClick: () => {
oauth.reset()
- setStep(step === 'install' ? 'credentials' : 'manifest')
+ connect.reset()
+ setStep(step === 'token' ? 'credentials' : 'manifest')
},
}
}
primaryAction={{
- label: busy ? 'Connecting…' : step === 'install' ? title : 'Continue',
+ label: busy ? 'Connecting…' : step === 'token' ? 'Connect app' : 'Continue',
onClick: advance,
disabled:
busy ||
(step === 'manifest'
? Boolean(configuredAppId && !configurationCopied)
- : !installationId &&
- (!clientId.trim() || !clientSecret.trim() || !signingSecret.trim())),
+ : step === 'token'
+ ? !botToken.trim()
+ : !installationId &&
+ (!clientId.trim() || !clientSecret.trim() || !signingSecret.trim())),
}}
/>
diff --git a/apps/sim/ee/credential-groups/components/slack-managed-users-access.test.tsx b/apps/sim/ee/credential-groups/components/slack-managed-users-access.test.tsx
index 1b07c233ca3..03eb34d9bf3 100644
--- a/apps/sim/ee/credential-groups/components/slack-managed-users-access.test.tsx
+++ b/apps/sim/ee/credential-groups/components/slack-managed-users-access.test.tsx
@@ -15,6 +15,7 @@ const mocks = vi.hoisted(() => ({
refetchApps: vi.fn(),
manifest: vi.fn(),
install: vi.fn(),
+ connect: vi.fn(),
accounts: vi.fn(),
refetchAccounts: vi.fn(),
}))
@@ -31,6 +32,7 @@ vi.mock('@/hooks/queries/scoped-credentials', () => ({
}))
vi.mock('@/hooks/queries/slack-search', () => ({
+ useConnectCustomSlackSearch: () => ({ mutate: mocks.connect, isPending: false, reset: vi.fn() }),
useSlackSearchInstallations: mocks.apps,
useSlackSearchManifest: mocks.manifest,
useStartSlackSearchOAuth: () => ({ mutate: mocks.install, isPending: false, reset: vi.fn() }),
@@ -389,6 +391,61 @@ describe('Slack member access selection', () => {
expect(mocks.onOpenChange).not.toHaveBeenCalled()
})
+ it('finishes manifest setup using the installed bot token without installing the app again', async () => {
+ await render(undefined, [], 'org-1')
+ await clickButton('Install Sim Search')
+ expect(document.querySelector('a')?.href).toBe('https://api.slack.com/apps')
+ await clickButton('Continue')
+ await fill('Paste your Slack app’s client ID', 'fixture-client')
+ await fill('Paste your Slack app’s client secret', 'fixture-secret')
+ await fill('Paste your Slack app’s signing secret', 'fixture-signing')
+ await clickButton('Continue')
+ expect(document.body.textContent).toContain('Connect installed Slack app')
+ expect(document.body.textContent).not.toContain('Install in Slack')
+ await fill('xoxb-...', 'xoxb-already-installed')
+ await clickButton('Connect app')
+ expect(mocks.connect).toHaveBeenCalledExactlyOnceWith(
+ {
+ organizationId: 'org-1',
+ installationId: undefined,
+ name: 'Sim Search',
+ description: expect.any(String),
+ clientId: 'fixture-client',
+ clientSecret: 'fixture-secret',
+ signingSecret: 'fixture-signing',
+ botToken: 'xoxb-already-installed',
+ },
+ expect.any(Object)
+ )
+ /** The mutation refreshes installations before closing the nested wizard. */
+ mocks.apps.mockReturnValue({
+ isSuccess: true,
+ isPending: false,
+ error: null,
+ data: {
+ installations: [
+ {
+ id: 'installed',
+ appId: 'A1',
+ teamId: 'T1',
+ teamName: 'Test workspace',
+ appKind: 'custom',
+ enabled: true,
+ needsValidation: false,
+ },
+ ],
+ bots: [],
+ },
+ })
+ await act(async () => mocks.connect.mock.calls[0][1].onSuccess())
+ expect(document.body.textContent).toContain('Installed in Test workspace')
+ expect(document.body.textContent).not.toContain('Install Sim Search first')
+ expect(document.body.textContent).not.toContain('Connect installed Slack app')
+ expect(mocks.install).not.toHaveBeenCalled()
+ expect(mocks.start).not.toHaveBeenCalled()
+ expect(window.open).not.toHaveBeenCalled()
+ })
+
it('waits for the installed app lookup instead of offering a duplicate installation', async () => {
mocks.apps.mockReturnValue({ isPending: true, isSuccess: false, data: undefined, error: null })
await render(undefined, [], 'org-1')
diff --git a/apps/sim/hooks/queries/organization-accounts.test.tsx b/apps/sim/hooks/queries/organization-accounts.test.tsx
index 9752436158c..4367515e13c 100644
--- a/apps/sim/hooks/queries/organization-accounts.test.tsx
+++ b/apps/sim/hooks/queries/organization-accounts.test.tsx
@@ -24,10 +24,10 @@ import {
useRevokeOrganizationAccountEnrollment,
useUpdateOrganizationAccounts,
} from '@/hooks/queries/organization-accounts'
-import { slackSearchKeys } from '@/hooks/queries/slack-search'
import { knowledgeKeys } from '@/hooks/queries/utils/knowledge-keys'
import { searchSourceKeys } from '@/hooks/queries/utils/search-source-keys'
import { selectorKeys, selectorQueryRoots } from '@/hooks/queries/utils/selector-keys'
+import { slackSearchKeys } from '@/hooks/queries/utils/slack-search-keys'
describe('personal account disconnect', () => {
it.each([true, false])(
diff --git a/apps/sim/hooks/queries/organization-accounts.ts b/apps/sim/hooks/queries/organization-accounts.ts
index 4efb5095aea..2973aeef112 100644
--- a/apps/sim/hooks/queries/organization-accounts.ts
+++ b/apps/sim/hooks/queries/organization-accounts.ts
@@ -39,11 +39,11 @@ import {
updateOrganizationAccountWorkspaceAccessContract,
} from '@/lib/api/contracts/organization-accounts'
import { personalCredentialKeys } from '@/hooks/queries/personal-credentials'
-import { slackSearchKeys } from '@/hooks/queries/slack-search'
import { mcpKeys } from '@/hooks/queries/utils/mcp-keys'
import { resetOrganizationSearchAccess } from '@/hooks/queries/utils/reset-organization-search-access'
import { searchSourceKeys } from '@/hooks/queries/utils/search-source-keys'
import { invalidateSelectorQueries } from '@/hooks/queries/utils/selector-keys'
+import { slackSearchKeys } from '@/hooks/queries/utils/slack-search-keys'
export const ORGANIZATION_ACCOUNTS_STALE_TIME = 30_000
diff --git a/apps/sim/hooks/queries/slack-search.test.tsx b/apps/sim/hooks/queries/slack-search.test.tsx
new file mode 100644
index 00000000000..c99dd723d66
--- /dev/null
+++ b/apps/sim/hooks/queries/slack-search.test.tsx
@@ -0,0 +1,115 @@
+/** @vitest-environment jsdom */
+import { act } from 'react'
+import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
+import { createRoot, type Root } from 'react-dom/client'
+import { afterEach, beforeEach, expect, it, vi } from 'vitest'
+import {
+ connectCustomSlackSearchContract,
+ listSlackSearchContract,
+ type SlackSearchList,
+} from '@/lib/api/contracts/knowledge/slack'
+
+const mocks = vi.hoisted(() => ({ request: vi.fn() }))
+vi.mock('@/lib/api/client/request', () => ({ requestJson: mocks.request }))
+
+import { organizationAccountsKeys } from '@/hooks/queries/organization-accounts'
+import {
+ useConnectCustomSlackSearch,
+ useSlackSearchInstallations,
+} from '@/hooks/queries/slack-search'
+import { slackSearchKeys } from '@/hooks/queries/utils/slack-search-keys'
+
+let root: Root
+let client: QueryClient
+let connection: ReturnType