Skip to content

Commit 57c1c7c

Browse files
fix(search): expose provider configuration updates in Sources (#7704)
* fix(search): expose provider configuration updates in Sources * chore(search): document provider refresh option mapping
1 parent ccc6f9e commit 57c1c7c

7 files changed

Lines changed: 141 additions & 28 deletions

File tree

apps/docs/content/docs/platform/self-hosting/integrations-oauth.mdx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -203,7 +203,7 @@ Keep **Expire user authorization tokens** enabled so Sim receives the refresh to
203203

204204
Complete the installation through [the GitHub Search source setup](/search/github#add-a-repository).
205205

206-
If you replace a deployment's GitHub App, an organization admin must first open **Settings → Connected accounts → Providers → Update configurations**. This applies the deployment's current App configuration to the existing providers while preserving their saved identities. Accounts whose App configuration changed must reconnect. Then reconnect personal GitHub accounts and connect an installation of the new App. Reconnecting alone cannot update the organization's saved App configuration.
206+
If you replace a deployment's GitHub App, an organization admin must first open **Settings → Sources → Update configurations**. When Search is disabled, this action is under **Settings → Connected accounts → Providers**. This applies the deployment's current App configuration to the existing providers while preserving their saved identities. Accounts whose App configuration changed must reconnect. Then reconnect personal GitHub accounts and connect an installation of the new App. Reconnecting alone cannot update the organization's saved App configuration.
207207

208208
</Step>
209209
</Steps>

apps/docs/content/docs/search/github.mdx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -96,7 +96,7 @@ This is an installation plus personal authorization flow. GitHub Search does not
9696
| Identity verification fails | Verify the email used by your Sim account in GitHub's email settings, then reconnect. A public profile email alone is insufficient. |
9797
| Authorization fails after installation | Return to Sim and start **Connect account** there. Do not enable authorization during installation. |
9898
| Account authorization did not complete | Start the connection again from Sim. If it repeats, contact your organization admin or Sim support. For self-hosted Sim, check the [App callback and credentials](/platform/self-hosting/integrations-oauth#github-search). |
99-
| Update GitHub in Connected accounts before connecting this source | An organization admin must select **Settings → Connected accounts → Providers → Update configurations**, then reconnect GitHub. |
99+
| Update GitHub using Update configurations in organization settings before connecting this source | An organization admin must select **Settings → Sources → Update configurations**, then reconnect GitHub. |
100100
| Indexed files no longer appear | Confirm your own repository access, App repository selection, and connection status. Installation-indexed content is also withheld when GitHub cannot verify current access; retry once GitHub is available. |
101101
| Sync is incomplete | Review the source status. Very large Git trees, file size limits, and unreadable files can limit indexing. |
102102
| Empty repository returns an error | Add an initial commit, then sync again. GitHub does not return a file tree for an uninitialized repository. |

apps/sim/app/o/[organizationId]/settings/components/integrations/organization-integrations-settings.test.tsx

Lines changed: 84 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@ const mocks = vi.hoisted(() => ({
1111
people: vi.fn(),
1212
invite: vi.fn(),
1313
refetch: vi.fn(),
14+
update: vi.fn(),
15+
updatePending: false,
1416
}))
1517
vi.mock('@/app/o/[organizationId]/providers/organization-provider', () => ({
1618
useOrganizationContext: mocks.context,
@@ -21,6 +23,7 @@ vi.mock(
2123
)
2224
vi.mock('@/hooks/queries/organization-accounts', () => ({
2325
useOrganizationAccounts: mocks.accounts,
26+
useUpdateOrganizationAccounts: () => ({ mutate: mocks.update, isPending: mocks.updatePending }),
2427
useOrganizationAccountPeople: mocks.people,
2528
useInviteOrganizationAccountPeople: () => ({ mutateAsync: mocks.invite, reset: vi.fn() }),
2629
useResendOrganizationAccountInvitation: () => ({}),
@@ -37,10 +40,12 @@ describe('organization integration invitations', () => {
3740
beforeEach(() => {
3841
vi.clearAllMocks()
3942
vi.spyOn(toast, 'success').mockReturnValue('toast-id')
43+
vi.spyOn(toast, 'error').mockReturnValue('toast-id')
44+
mocks.updatePending = false
4045
vi.stubGlobal('IS_REACT_ACT_ENVIRONMENT', true)
4146
mocks.context.mockReturnValue({ organization: { id: 'org-a' }, viewer: { isAdmin: true } })
4247
mocks.accounts.mockReturnValue({
43-
data: { credentialGroup: { id: 'group-a' } },
48+
data: { credentialGroup: { id: 'group-a', options: [] } },
4449
error: null,
4550
refetch: mocks.refetch,
4651
})
@@ -93,7 +98,7 @@ describe('organization integration invitations', () => {
9398
it('keeps provider setup as the default and sends manual invitations from People to this org', async () => {
9499
await render()
95100
expect(container.textContent).toContain('Provider setup')
96-
expect(mocks.accounts).toHaveBeenLastCalledWith(undefined)
101+
expect(mocks.accounts).toHaveBeenLastCalledWith('org-a')
97102
expect(mocks.people).not.toHaveBeenCalled()
98103

99104
await click('People')
@@ -119,6 +124,79 @@ describe('organization integration invitations', () => {
119124
expect(document.querySelector('[role="dialog"]')).toBeNull()
120125
})
121126

127+
it('refreshes saved provider identities from Sources and reports the outcome', async () => {
128+
mocks.accounts.mockReturnValue({
129+
data: {
130+
credentialGroup: {
131+
id: 'group-a',
132+
options: [
133+
{
134+
id: 'github-option',
135+
provider: 'github-repositories',
136+
label: 'Engineering',
137+
required: true,
138+
},
139+
{
140+
id: 'slack-option',
141+
provider: 'slack',
142+
label: 'Slack',
143+
required: false,
144+
slackBotCredentialId: 'slack-bot',
145+
requiredScopes: ['search:read'],
146+
},
147+
],
148+
},
149+
},
150+
error: null,
151+
})
152+
mocks.update.mockImplementationOnce((_input, { onSuccess }) => onSuccess())
153+
await render()
154+
await click('Update configurations')
155+
expect(mocks.update).toHaveBeenCalledWith(
156+
{
157+
organizationId: 'org-a',
158+
groupId: 'group-a',
159+
update: {
160+
options: [
161+
{
162+
id: 'github-option',
163+
provider: 'github-repositories',
164+
label: 'Engineering',
165+
required: true,
166+
},
167+
{
168+
id: 'slack-option',
169+
provider: 'slack',
170+
label: 'Slack',
171+
required: false,
172+
slackBotCredentialId: 'slack-bot',
173+
requiredScopes: ['search:read'],
174+
},
175+
],
176+
},
177+
},
178+
expect.any(Object)
179+
)
180+
expect(toast.success).toHaveBeenCalledWith('Provider configurations updated')
181+
182+
mocks.update.mockImplementationOnce((_input, { onError }) =>
183+
onError(new Error('Update denied'))
184+
)
185+
await click('Update configurations')
186+
expect(toast.error).toHaveBeenCalledWith('Update denied')
187+
188+
mocks.updatePending = true
189+
await render()
190+
expect(findButton('Update configurations')).toBeDisabled()
191+
await click('Update configurations')
192+
expect(mocks.update).toHaveBeenCalledTimes(2)
193+
})
194+
195+
it('does not offer a configuration update without saved providers', async () => {
196+
await render()
197+
expect(container.textContent).not.toContain('Update configurations')
198+
})
199+
122200
it('opens People directly from the saved URL', async () => {
123201
await render('?tab=people')
124202
expect(container.textContent).toContain('Request connections')
@@ -137,7 +215,10 @@ describe('organization integration invitations', () => {
137215
await click('Request connections')
138216
expect(document.querySelector('[role="dialog"]')).toBeNull()
139217

140-
mocks.accounts.mockReturnValue({ data: { credentialGroup: { id: 'group-a' } }, error: null })
218+
mocks.accounts.mockReturnValue({
219+
data: { credentialGroup: { id: 'group-a', options: [] } },
220+
error: null,
221+
})
141222
await render('?tab=people')
142223
expect(container.textContent).not.toContain('Loading connected accounts')
143224
expect(findButton('Request connections')).not.toBeDisabled()

apps/sim/app/o/[organizationId]/settings/components/integrations/organization-integrations-settings.tsx

Lines changed: 28 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
'use client'
22

3-
import { Chip, ChipSwitch } from '@sim/emcn'
3+
import { Chip, ChipSwitch, toast } from '@sim/emcn'
44
import { useQueryState } from 'nuqs'
5+
import { getOrganizationAccountUpdateOptions } from '@/lib/credential-groups/organization-account-options'
56
import { useOrganizationContext } from '@/app/o/[organizationId]/providers/organization-provider'
67
import { OrganizationIntegrationsSetup } from '@/app/o/[organizationId]/settings/components/integrations/organization-integrations-setup'
78
import { organizationIntegrationsTabParam } from '@/app/o/[organizationId]/settings/components/integrations/search-params'
@@ -10,17 +11,34 @@ import {
1011
SettingsQueryErrorState,
1112
} from '@/app/workspace/[workspaceId]/settings/components/settings-empty-state'
1213
import { OrganizationAccountPeople } from '@/ee/credential-groups/components/organization-account-people'
13-
import { useOrganizationAccounts } from '@/hooks/queries/organization-accounts'
14+
import {
15+
useOrganizationAccounts,
16+
useUpdateOrganizationAccounts,
17+
} from '@/hooks/queries/organization-accounts'
1418

1519
export function OrganizationIntegrationsSettings() {
1620
const { organization, viewer } = useOrganizationContext()
1721
const [tab, setTab] = useQueryState(
1822
organizationIntegrationsTabParam.key,
1923
organizationIntegrationsTabParam.parser
2024
)
21-
const accounts = useOrganizationAccounts(
22-
viewer.isAdmin && tab === 'people' ? organization.id : undefined
23-
)
25+
const accounts = useOrganizationAccounts(viewer.isAdmin ? organization.id : undefined)
26+
const update = useUpdateOrganizationAccounts()
27+
const group = accounts.data?.credentialGroup
28+
const updateConfigurations = () => {
29+
if (!group || update.isPending) return
30+
update.mutate(
31+
{
32+
organizationId: organization.id,
33+
groupId: group.id,
34+
update: { options: getOrganizationAccountUpdateOptions(group) },
35+
},
36+
{
37+
onSuccess: () => toast.success('Provider configurations updated'),
38+
onError: (error) => toast.error(error.message),
39+
}
40+
)
41+
}
2442
if (!viewer.isAdmin) return null
2543

2644
return (
@@ -35,6 +53,11 @@ export function OrganizationIntegrationsSettings() {
3553
{ value: 'people', label: 'People' },
3654
]}
3755
/>
56+
{tab === 'providers' && !accounts.error && group && group.options.length > 0 && (
57+
<Chip disabled={update.isPending} onClick={updateConfigurations}>
58+
Update configurations
59+
</Chip>
60+
)}
3861
</div>
3962
{tab === 'providers' && <OrganizationIntegrationsSetup />}
4063
{tab === 'people' && (

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

Lines changed: 3 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -12,12 +12,10 @@ import {
1212
} from '@sim/emcn'
1313
import { Plus } from '@sim/emcn/icons'
1414
import { getErrorMessage } from '@sim/utils/errors'
15-
import type {
16-
OrganizationAccountsSettings,
17-
UpdateOrganizationAccountsBody,
18-
} from '@/lib/api/contracts/organization-accounts'
15+
import type { OrganizationAccountsSettings } from '@/lib/api/contracts/organization-accounts'
1916
import { getManagedMcpConnectorIcon } from '@/lib/credential-groups/managed-mcp-connector-icons'
2017
import { MANAGED_MCP_CONNECTORS } from '@/lib/credential-groups/managed-mcp-connectors'
18+
import { getOrganizationAccountUpdateOptions } from '@/lib/credential-groups/organization-account-options'
2119
import {
2220
type CredentialGroupProvider,
2321
getCredentialGroupProviderService,
@@ -60,19 +58,7 @@ export function OrganizationAccountProviders({
6058
const addMcp = useAddOrganizationAccountMcpProvider()
6159
const removeMcp = useRemoveOrganizationAccountMcpProvider()
6260
const pending = update.isPending || addMcp.isPending || removeMcp.isPending
63-
const options: NonNullable<UpdateOrganizationAccountsBody['options']> = group.options.map(
64-
(option) => {
65-
const common = { id: option.id, label: option.label, required: option.required }
66-
return option.provider === 'slack'
67-
? {
68-
...common,
69-
provider: 'slack',
70-
slackBotCredentialId: option.slackBotCredentialId,
71-
requiredScopes: option.requiredScopes,
72-
}
73-
: { ...common, provider: option.provider }
74-
}
75-
)
61+
const options = getOrganizationAccountUpdateOptions(group)
7662
const updateConfigurations = () => {
7763
if (pending) return
7864
update.mutate(
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
import type {
2+
OrganizationAccountsSettings,
3+
UpdateOrganizationAccountsBody,
4+
} from '@/lib/api/contracts/organization-accounts'
5+
6+
/** Preserves option identities and custom Slack scopes while the server refreshes managed OAuth policies. */
7+
export function getOrganizationAccountUpdateOptions(
8+
group: NonNullable<OrganizationAccountsSettings['credentialGroup']>
9+
): NonNullable<UpdateOrganizationAccountsBody['options']> {
10+
return group.options.map((option) => {
11+
const common = { id: option.id, label: option.label, required: option.required }
12+
return option.provider === 'slack'
13+
? {
14+
...common,
15+
provider: 'slack',
16+
slackBotCredentialId: option.slackBotCredentialId,
17+
requiredScopes: option.requiredScopes,
18+
}
19+
: { ...common, provider: option.provider }
20+
})
21+
}

apps/sim/lib/credential-groups/service.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -306,7 +306,9 @@ export async function ensureWorkspaceAccountsGroup(
306306
) {
307307
throw new OrchestrationError(
308308
'validation',
309-
`Update ${preparedOption.label} in Connected accounts before connecting this source`
309+
scope.kind === 'organization'
310+
? `Update ${preparedOption.label} using Update configurations in organization settings before connecting this source`
311+
: `Update ${preparedOption.label} in Connected accounts before connecting this source`
310312
)
311313
}
312314
return existing

0 commit comments

Comments
 (0)