Skip to content

Commit b31651e

Browse files
feat(slack): improve source connections and app Home (#7676)
* fix(slack): expose account setup removal in sources * fix(slack): render source citations inline with answers * feat(slack): add personalized sources to app Home * improvement(slack): simplify Home to a persistent connect link
1 parent 4747afb commit b31651e

29 files changed

Lines changed: 1283 additions & 102 deletions

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

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,6 @@ vi.mock('@/app/workspace/[workspaceId]/integrations/components/integrations-show
5454
vi.mock('@/hooks/queries/kb/connectors', () => ({
5555
useSearchSources: mocks.sources,
5656
useSearchSourceOverview: mocks.overview,
57-
searchSourceKeys: { list: (scope: unknown) => ['sources', scope] },
5857
}))
5958
vi.mock('@/hooks/use-member-enrollment', () => ({
6059
CONNECTABLE_MEMBERSHIPS: new Set(['invited', 'not_enrolled', 'needs_reauth']),

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

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -28,12 +28,9 @@ import {
2828
RESOURCE_LIST_STACK,
2929
SettingsResourceRow,
3030
} from '@/app/workspace/[workspaceId]/settings/components/settings-resource-row'
31-
import {
32-
searchSourceKeys,
33-
useSearchSourceOverview,
34-
useSearchSources,
35-
} from '@/hooks/queries/kb/connectors'
31+
import { useSearchSourceOverview, useSearchSources } from '@/hooks/queries/kb/connectors'
3632
import { useSearchIntegrations } from '@/hooks/queries/search-integrations'
33+
import { searchSourceKeys } from '@/hooks/queries/utils/search-source-keys'
3734
import { useDebounce } from '@/hooks/use-debounce'
3835
import { useMemberEnrollment } from '@/hooks/use-member-enrollment'
3936
import { useDesktopOAuthConnectListener, useOAuthReturnRouter } from '@/hooks/use-oauth-return'
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
'use client'
2+
3+
import { ChipConfirmModal, ChipModalError } from '@sim/emcn'
4+
import type { OrganizationAccountsSettings } from '@/lib/api/contracts/organization-accounts'
5+
import { useUpdateOrganizationAccounts } from '@/hooks/queries/organization-accounts'
6+
7+
interface OrganizationSlackAccountRemovalProps {
8+
organizationId: string
9+
group: NonNullable<OrganizationAccountsSettings['credentialGroup']>
10+
onClose: () => void
11+
onRemoved: () => void
12+
}
13+
14+
export function OrganizationSlackAccountRemoval({
15+
organizationId,
16+
group,
17+
onClose,
18+
onRemoved,
19+
}: OrganizationSlackAccountRemovalProps) {
20+
const update = useUpdateOrganizationAccounts()
21+
return (
22+
<ChipConfirmModal
23+
open
24+
onOpenChange={(open) => {
25+
if (!open && !update.isPending) onClose()
26+
}}
27+
title='Remove Slack account setup?'
28+
text='This disconnects your organization’s Slack accounts and clears their saved app configuration. Remove any sources using these accounts first.'
29+
confirm={{
30+
label: 'Remove',
31+
variant: 'destructive',
32+
pending: update.isPending,
33+
onClick: () =>
34+
update.mutate(
35+
{
36+
organizationId,
37+
groupId: group.id,
38+
update: {
39+
options: group.options
40+
.filter((option) => option.provider !== 'slack')
41+
.map(({ id, provider, label, required }) => ({ id, provider, label, required })),
42+
},
43+
},
44+
{ onSuccess: onRemoved }
45+
),
46+
}}
47+
>
48+
<ChipModalError>{update.error?.message}</ChipModalError>
49+
</ChipConfirmModal>
50+
)
51+
}

apps/sim/app/o/[organizationId]/settings/integrations/providers/[connectorType]/provider-detail.test.tsx

Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,9 @@ const mocks = vi.hoisted(() => ({
2121
approvalError: null as Error | null,
2222
availabilityError: null as Error | null,
2323
retryAvailability: vi.fn(),
24+
removeAccounts: vi.fn(),
25+
accountRemovalError: null as Error | null,
26+
accountRemovalPending: false,
2427
}))
2528

2629
vi.mock('next/navigation', () => ({
@@ -60,6 +63,11 @@ vi.mock('@/hooks/queries/kb/connectors', () => ({
6063
}))
6164
vi.mock('@/hooks/queries/organization-accounts', () => ({
6265
useOrganizationAccounts: mocks.accounts,
66+
useUpdateOrganizationAccounts: () => ({
67+
mutate: mocks.removeAccounts,
68+
error: mocks.accountRemovalError,
69+
isPending: mocks.accountRemovalPending,
70+
}),
6371
}))
6472
vi.mock('@/hooks/queries/search-integrations', () => ({
6573
useUpdateSearchIntegration: () => ({
@@ -126,6 +134,8 @@ describe('organization provider management', () => {
126134
mocks.access = { admin: true, members: true }
127135
mocks.approvalError = null
128136
mocks.availabilityError = null
137+
mocks.accountRemovalError = null
138+
mocks.accountRemovalPending = false
129139
mocks.overview.mockReturnValue({ data: { providers: [provider] }, isPending: false })
130140
mocks.sources.mockReturnValue({
131141
data: [source],
@@ -168,6 +178,96 @@ describe('organization provider management', () => {
168178
await act(async () => button!.click())
169179
}
170180

181+
function withSlackAccounts(approved = true, status = 'active') {
182+
mocks.overview.mockReturnValue({
183+
data: { providers: [{ ...provider, connectorType: 'slack', approved }] },
184+
})
185+
mocks.accounts.mockReturnValue({
186+
data: {
187+
credentialGroup: {
188+
...credentialGroup,
189+
options: [
190+
{ ...credentialGroup.options[0], label: 'Google', required: true },
191+
{
192+
id: 'slack-option',
193+
provider: 'slack',
194+
label: 'Slack',
195+
required: false,
196+
status,
197+
configurationStatus: 'ready',
198+
},
199+
],
200+
},
201+
},
202+
})
203+
}
204+
205+
it.each(['active', 'disabled'])(
206+
'removes only Slack account setup after confirmation, including a %s option',
207+
async (status) => {
208+
withSlackAccounts(true, status)
209+
await render('slack')
210+
await click('Remove account setup')
211+
expect(mocks.removeAccounts).not.toHaveBeenCalled()
212+
expect(document.querySelector('[role="dialog"]')).toHaveTextContent('saved app configuration')
213+
await click('Remove')
214+
expect(mocks.removeAccounts).toHaveBeenCalledExactlyOnceWith(
215+
{
216+
organizationId: 'org-one',
217+
groupId: 'accounts-one',
218+
update: {
219+
options: [{ id: 'google-option', provider: 'google', label: 'Google', required: true }],
220+
},
221+
},
222+
{ onSuccess: expect.any(Function) }
223+
)
224+
await act(async () => mocks.removeAccounts.mock.calls[0][1].onSuccess())
225+
expect(document.querySelector('[role="dialog"]')).toBeNull()
226+
}
227+
)
228+
229+
it('offers removal when Slack is deactivated and allows cancelling without a mutation', async () => {
230+
withSlackAccounts(false)
231+
await render('slack')
232+
await click('Remove account setup')
233+
await click('Cancel')
234+
expect(document.querySelector('[role="dialog"]')).toBeNull()
235+
expect(mocks.removeAccounts).not.toHaveBeenCalled()
236+
})
237+
238+
it('keeps connector-dependency errors visible in the removal dialog', async () => {
239+
withSlackAccounts()
240+
mocks.accountRemovalError = new Error('Remove the source using these accounts first.')
241+
await render('slack')
242+
await click('Remove account setup')
243+
await click('Remove')
244+
expect(document.querySelector('[role="dialog"] [role="alert"]')).toHaveTextContent(
245+
'Remove the source using these accounts first.'
246+
)
247+
})
248+
249+
it('passes the removal action to the Slack Accounts tab header', async () => {
250+
withSlackAccounts()
251+
await render('slack', '?view=accounts')
252+
const actions = mocks.people.mock.calls.at(-1)![0].panel.actions
253+
expect(actions).toEqual([
254+
expect.objectContaining({ text: 'Remove account setup', onSelect: expect.any(Function) }),
255+
])
256+
await act(async () => actions[0].onSelect())
257+
expect(document.querySelector('[role="dialog"]')).toHaveTextContent(
258+
'Remove Slack account setup?'
259+
)
260+
})
261+
262+
it('keeps Slack cleanup available even when personal source creation is unavailable', async () => {
263+
withSlackAccounts()
264+
mocks.personal = false
265+
await render('slack')
266+
expect(mocks.accounts).toHaveBeenCalledWith('org-one')
267+
await click('Remove account setup')
268+
expect(document.querySelector('[role="dialog"]')).not.toBeNull()
269+
})
270+
171271
it('uses named source links even when the admin has not reconnected their own account', async () => {
172272
await render()
173273
expect(mocks.sources).toHaveBeenCalledWith(
@@ -450,6 +550,9 @@ describe('organization provider management', () => {
450550
expect(container.textContent).toContain('Set up the Slack app to connect accounts.')
451551
expect(container.querySelector('input[placeholder="Search people..."]')).toHaveValue('alex')
452552
await click('Set up Slack app')
553+
await act(async () => {
554+
await vi.waitFor(() => expect(mocks.updateUrl).toHaveBeenCalled())
555+
})
453556
const query = new URLSearchParams(mocks.updateUrl.mock.calls.at(-1)![0].queryString)
454557
expect(query.get('connectedAccounts')).toBe('slack')
455558
expect(query.get('view')).toBe('accounts')

apps/sim/app/o/[organizationId]/settings/integrations/providers/[connectorType]/provider-detail.tsx

Lines changed: 27 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import {
1919
connectedAccountsParam,
2020
organizationProviderTabParam,
2121
} from '@/app/o/[organizationId]/settings/components/integrations/search-params'
22+
import { OrganizationSlackAccountRemoval } from '@/app/o/[organizationId]/settings/components/integrations/slack-account-removal'
2223
import { OrganizationSlackAccountSetup } from '@/app/o/[organizationId]/settings/components/integrations/slack-account-setup'
2324
import { SearchSourcePagination } from '@/app/workspace/[workspaceId]/search/components/search-source-pagination'
2425
import { SearchSourceSetup } from '@/app/workspace/[workspaceId]/search/components/search-source-setup'
@@ -59,6 +60,7 @@ export function OrganizationProviderDetail({ connectorType }: OrganizationProvid
5960
const [peopleSearch, setPeopleSearch] = useOrganizationAccountPeopleSearch()
6061
const sourceSearch = useDebounce(search.trim(), SEARCH_DEBOUNCE_MS)
6162
const [deactivating, setDeactivating] = useState(false)
63+
const [removingSlackAccounts, setRemovingSlackAccounts] = useState(false)
6264
const scope = { kind: 'organization', organizationId: organization.id } as const
6365
const meta = CONNECTOR_META_REGISTRY[connectorType]
6466
const personal = Boolean(meta && canConnectPersonally(meta) && searchAccess.memberScoped)
@@ -72,7 +74,7 @@ export function OrganizationProviderDetail({ connectorType }: OrganizationProvid
7274
const availability = usePermissionConfig()
7375
const approval = useUpdateSearchIntegration()
7476
const accounts = useOrganizationAccounts(
75-
viewer.isAdmin && personal && (showAccounts || connectorType === 'slack')
77+
viewer.isAdmin && (connectorType === 'slack' || (personal && showAccounts))
7678
? organization.id
7779
: undefined
7880
)
@@ -123,6 +125,20 @@ export function OrganizationProviderDetail({ connectorType }: OrganizationProvid
123125
const option = accounts.data?.credentialGroup?.options.find(
124126
(item) => item.provider === credentialProvider && item.status === 'active'
125127
)
128+
const group = accounts.data?.credentialGroup
129+
const removalActions: SettingsAction[] =
130+
connectorType === 'slack' &&
131+
!accounts.isError &&
132+
group?.options.some((item) => item.provider === 'slack')
133+
? [
134+
{
135+
text: 'Remove account setup',
136+
textTone: 'error',
137+
disabled: accounts.isFetching,
138+
onSelect: () => setRemovingSlackAccounts(true),
139+
},
140+
]
141+
: []
126142
const needsSlackSetup =
127143
connectorType === 'slack' &&
128144
(option?.provider !== 'slack' || option.configurationStatus !== 'ready')
@@ -170,6 +186,7 @@ export function OrganizationProviderDetail({ connectorType }: OrganizationProvid
170186
onSelect: activate,
171187
},
172188
]
189+
actions.push(...removalActions)
173190
if (overview.isError)
174191
return (
175192
<SettingsPanel {...panel}>
@@ -297,7 +314,7 @@ export function OrganizationProviderDetail({ connectorType }: OrganizationProvid
297314
<OrganizationAccountPeople
298315
organizationId={organization.id}
299316
searchConnection={{ optionId: option.id, providerName: meta.name }}
300-
panel={panel}
317+
panel={{ ...panel, actions: removalActions }}
301318
/>
302319
) : (
303320
<SettingsPanel {...panel} actions={actions}>
@@ -335,6 +352,14 @@ export function OrganizationProviderDetail({ connectorType }: OrganizationProvid
335352
mirroredAccessAvailable={searchAccess.sourceMirrored}
336353
/>
337354
<OrganizationSlackAccountSetup />
355+
{removingSlackAccounts && group && (
356+
<OrganizationSlackAccountRemoval
357+
organizationId={organization.id}
358+
group={group}
359+
onClose={() => setRemovingSlackAccounts(false)}
360+
onRemoved={() => setRemovingSlackAccounts(false)}
361+
/>
362+
)}
338363
<ChipConfirmModal
339364
open={deactivating}
340365
onOpenChange={(open) => {

apps/sim/app/workspace/[workspaceId]/search/search.test.tsx

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,6 @@ vi.mock('@/hooks/queries/workspace', () => ({
4040
useWorkspacePermissionsQuery: () => ({ data: { viewer: { isAdmin: mocks.canAdmin } } }),
4141
}))
4242
vi.mock('@/hooks/queries/kb/connectors', () => ({
43-
searchSourceKeys: { list: (id: string) => ['search-sources', id] },
4443
useSearchSources: (id: string, options: { search: string }) => {
4544
mocks.sourceQuery(id, options)
4645
return {

apps/sim/app/workspace/[workspaceId]/search/search.tsx

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -25,11 +25,8 @@ import {
2525
SettingsEmptyState,
2626
SettingsQueryErrorState,
2727
} from '@/app/workspace/[workspaceId]/settings/components/settings-empty-state'
28-
import {
29-
searchSourceKeys,
30-
useSearchSources,
31-
useWorkspaceMemberConnectors,
32-
} from '@/hooks/queries/kb/connectors'
28+
import { useSearchSources, useWorkspaceMemberConnectors } from '@/hooks/queries/kb/connectors'
29+
import { searchSourceKeys } from '@/hooks/queries/utils/search-source-keys'
3330
import { useWorkspacePermissionsQuery } from '@/hooks/queries/workspace'
3431
import { useDebounce } from '@/hooks/use-debounce'
3532
import { useDebouncedSearchSetter } from '@/hooks/use-debounced-search-setter'

apps/sim/ee/credential-groups/components/organization-account-people.tsx

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
import { type ReactNode, useState } from 'react'
44
import { Chip, ChipConfirmModal, ChipModalError, toast } from '@sim/emcn'
55
import { Plus } from '@sim/emcn/icons'
6-
import type { SettingsBackAction } from '@/components/settings/settings-header'
6+
import type { SettingsAction, SettingsBackAction } from '@/components/settings/settings-header'
77
import { SEARCH_DEBOUNCE_MS } from '@/lib/url-state'
88
import { MemberAvatar } from '@/app/workspace/[workspaceId]/settings/components/member-list'
99
import { RowActionsMenu } from '@/app/workspace/[workspaceId]/settings/components/row-actions-menu'
@@ -29,7 +29,13 @@ import { useOrganizationAccountPeopleSearch } from '@/hooks/use-organization-acc
2929
interface OrganizationAccountPeopleProps {
3030
organizationId: string
3131
searchConnection?: { optionId: string; providerName: string }
32-
panel?: { back: SettingsBackAction; title: string; description?: string; docsLink?: string }
32+
panel?: {
33+
back: SettingsBackAction
34+
title: string
35+
description?: string
36+
docsLink?: string
37+
actions?: SettingsAction[]
38+
}
3339
enabled?: boolean
3440
setupFallback?: ReactNode
3541
}
@@ -66,6 +72,7 @@ export function OrganizationAccountPeople({
6672
disabled: pending || awaitingSetup,
6773
onSelect: () => setInviteOpen(true),
6874
},
75+
...(panel?.actions ?? []),
6976
]}
7077
>
7178
{awaitingSetup ? (

apps/sim/hooks/queries/kb/connectors-cache.test.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,13 +6,13 @@ import { act } from 'react'
66
import { QueryClient, QueryClientProvider, type QueryKey } from '@tanstack/react-query'
77
import { createRoot, type Root } from 'react-dom/client'
88
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
9+
import { searchSourceKeys } from '@/hooks/queries/utils/search-source-keys'
910

1011
const mocks = vi.hoisted(() => ({ requestJson: vi.fn() }))
1112

1213
vi.mock('@/lib/api/client/request', () => ({ requestJson: mocks.requestJson }))
1314

1415
import {
15-
searchSourceKeys,
1616
useConnectSimSearchConnector,
1717
useCreateConnector,
1818
useDeleteConnector,

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
*/
44

55
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
6+
import { searchSourceKeys } from '@/hooks/queries/utils/search-source-keys'
67

78
const mocks = vi.hoisted(() => ({
89
requestJson: vi.fn(),
@@ -51,7 +52,6 @@ import {
5152
connectorKeys,
5253
isConnectorSyncingOrPending,
5354
memberConnectorKeys,
54-
searchSourceKeys,
5555
useConnectorDetail,
5656
useConnectorDocuments,
5757
useConnectorList,

0 commit comments

Comments
 (0)