Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -203,7 +203,7 @@ Keep **Expire user authorization tokens** enabled so Sim receives the refresh to

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

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.
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.

</Step>
</Steps>
Expand Down
2 changes: 1 addition & 1 deletion apps/docs/content/docs/search/github.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,7 @@ This is an installation plus personal authorization flow. GitHub Search does not
| 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. |
| Authorization fails after installation | Return to Sim and start **Connect account** there. Do not enable authorization during installation. |
| 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). |
| Update GitHub in Connected accounts before connecting this source | An organization admin must select **Settings → Connected accounts → Providers → Update configurations**, then reconnect GitHub. |
| Update GitHub using Update configurations in organization settings before connecting this source | An organization admin must select **Settings → Sources → Update configurations**, then reconnect GitHub. |
| 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. |
| Sync is incomplete | Review the source status. Very large Git trees, file size limits, and unreadable files can limit indexing. |
| Empty repository returns an error | Add an initial commit, then sync again. GitHub does not return a file tree for an uninitialized repository. |
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ const mocks = vi.hoisted(() => ({
people: vi.fn(),
invite: vi.fn(),
refetch: vi.fn(),
update: vi.fn(),
updatePending: false,
}))
vi.mock('@/app/o/[organizationId]/providers/organization-provider', () => ({
useOrganizationContext: mocks.context,
Expand All @@ -21,6 +23,7 @@ vi.mock(
)
vi.mock('@/hooks/queries/organization-accounts', () => ({
useOrganizationAccounts: mocks.accounts,
useUpdateOrganizationAccounts: () => ({ mutate: mocks.update, isPending: mocks.updatePending }),
useOrganizationAccountPeople: mocks.people,
useInviteOrganizationAccountPeople: () => ({ mutateAsync: mocks.invite, reset: vi.fn() }),
useResendOrganizationAccountInvitation: () => ({}),
Expand All @@ -37,10 +40,12 @@ describe('organization integration invitations', () => {
beforeEach(() => {
vi.clearAllMocks()
vi.spyOn(toast, 'success').mockReturnValue('toast-id')
vi.spyOn(toast, 'error').mockReturnValue('toast-id')
mocks.updatePending = false
vi.stubGlobal('IS_REACT_ACT_ENVIRONMENT', true)
mocks.context.mockReturnValue({ organization: { id: 'org-a' }, viewer: { isAdmin: true } })
mocks.accounts.mockReturnValue({
data: { credentialGroup: { id: 'group-a' } },
data: { credentialGroup: { id: 'group-a', options: [] } },
error: null,
refetch: mocks.refetch,
})
Expand Down Expand Up @@ -93,7 +98,7 @@ describe('organization integration invitations', () => {
it('keeps provider setup as the default and sends manual invitations from People to this org', async () => {
await render()
expect(container.textContent).toContain('Provider setup')
expect(mocks.accounts).toHaveBeenLastCalledWith(undefined)
expect(mocks.accounts).toHaveBeenLastCalledWith('org-a')
expect(mocks.people).not.toHaveBeenCalled()

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

it('refreshes saved provider identities from Sources and reports the outcome', async () => {
mocks.accounts.mockReturnValue({
data: {
credentialGroup: {
id: 'group-a',
options: [
{
id: 'github-option',
provider: 'github-repositories',
label: 'Engineering',
required: true,
},
{
id: 'slack-option',
provider: 'slack',
label: 'Slack',
required: false,
slackBotCredentialId: 'slack-bot',
requiredScopes: ['search:read'],
},
],
},
},
error: null,
})
mocks.update.mockImplementationOnce((_input, { onSuccess }) => onSuccess())
await render()
await click('Update configurations')
expect(mocks.update).toHaveBeenCalledWith(
{
organizationId: 'org-a',
groupId: 'group-a',
update: {
options: [
{
id: 'github-option',
provider: 'github-repositories',
label: 'Engineering',
required: true,
},
{
id: 'slack-option',
provider: 'slack',
label: 'Slack',
required: false,
slackBotCredentialId: 'slack-bot',
requiredScopes: ['search:read'],
},
],
},
},
expect.any(Object)
)
expect(toast.success).toHaveBeenCalledWith('Provider configurations updated')

mocks.update.mockImplementationOnce((_input, { onError }) =>
onError(new Error('Update denied'))
)
await click('Update configurations')
expect(toast.error).toHaveBeenCalledWith('Update denied')

mocks.updatePending = true
await render()
expect(findButton('Update configurations')).toBeDisabled()
await click('Update configurations')
expect(mocks.update).toHaveBeenCalledTimes(2)
})

it('does not offer a configuration update without saved providers', async () => {
await render()
expect(container.textContent).not.toContain('Update configurations')
})

it('opens People directly from the saved URL', async () => {
await render('?tab=people')
expect(container.textContent).toContain('Request connections')
Expand All @@ -137,7 +215,10 @@ describe('organization integration invitations', () => {
await click('Request connections')
expect(document.querySelector('[role="dialog"]')).toBeNull()

mocks.accounts.mockReturnValue({ data: { credentialGroup: { id: 'group-a' } }, error: null })
mocks.accounts.mockReturnValue({
data: { credentialGroup: { id: 'group-a', options: [] } },
error: null,
})
await render('?tab=people')
expect(container.textContent).not.toContain('Loading connected accounts')
expect(findButton('Request connections')).not.toBeDisabled()
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
'use client'

import { Chip, ChipSwitch } from '@sim/emcn'
import { Chip, ChipSwitch, toast } from '@sim/emcn'
import { useQueryState } from 'nuqs'
import { getOrganizationAccountUpdateOptions } from '@/lib/credential-groups/organization-account-options'
import { useOrganizationContext } from '@/app/o/[organizationId]/providers/organization-provider'
import { OrganizationIntegrationsSetup } from '@/app/o/[organizationId]/settings/components/integrations/organization-integrations-setup'
import { organizationIntegrationsTabParam } from '@/app/o/[organizationId]/settings/components/integrations/search-params'
Expand All @@ -10,17 +11,34 @@ import {
SettingsQueryErrorState,
} from '@/app/workspace/[workspaceId]/settings/components/settings-empty-state'
import { OrganizationAccountPeople } from '@/ee/credential-groups/components/organization-account-people'
import { useOrganizationAccounts } from '@/hooks/queries/organization-accounts'
import {
useOrganizationAccounts,
useUpdateOrganizationAccounts,
} from '@/hooks/queries/organization-accounts'

export function OrganizationIntegrationsSettings() {
const { organization, viewer } = useOrganizationContext()
const [tab, setTab] = useQueryState(
organizationIntegrationsTabParam.key,
organizationIntegrationsTabParam.parser
)
const accounts = useOrganizationAccounts(
viewer.isAdmin && tab === 'people' ? organization.id : undefined
)
const accounts = useOrganizationAccounts(viewer.isAdmin ? organization.id : undefined)
const update = useUpdateOrganizationAccounts()
const group = accounts.data?.credentialGroup
const updateConfigurations = () => {
if (!group || update.isPending) return
update.mutate(
{
organizationId: organization.id,
groupId: group.id,
update: { options: getOrganizationAccountUpdateOptions(group) },
},
{
onSuccess: () => toast.success('Provider configurations updated'),
onError: (error) => toast.error(error.message),
}
)
}
if (!viewer.isAdmin) return null

return (
Expand All @@ -35,6 +53,11 @@ export function OrganizationIntegrationsSettings() {
{ value: 'people', label: 'People' },
]}
/>
{tab === 'providers' && !accounts.error && group && group.options.length > 0 && (
<Chip disabled={update.isPending} onClick={updateConfigurations}>
Update configurations
</Chip>
)}
</div>
{tab === 'providers' && <OrganizationIntegrationsSetup />}
{tab === 'people' && (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,10 @@ import {
} from '@sim/emcn'
import { Plus } from '@sim/emcn/icons'
import { getErrorMessage } from '@sim/utils/errors'
import type {
OrganizationAccountsSettings,
UpdateOrganizationAccountsBody,
} from '@/lib/api/contracts/organization-accounts'
import type { OrganizationAccountsSettings } from '@/lib/api/contracts/organization-accounts'
import { getManagedMcpConnectorIcon } from '@/lib/credential-groups/managed-mcp-connector-icons'
import { MANAGED_MCP_CONNECTORS } from '@/lib/credential-groups/managed-mcp-connectors'
import { getOrganizationAccountUpdateOptions } from '@/lib/credential-groups/organization-account-options'
import {
type CredentialGroupProvider,
getCredentialGroupProviderService,
Expand Down Expand Up @@ -60,19 +58,7 @@ export function OrganizationAccountProviders({
const addMcp = useAddOrganizationAccountMcpProvider()
const removeMcp = useRemoveOrganizationAccountMcpProvider()
const pending = update.isPending || addMcp.isPending || removeMcp.isPending
const options: NonNullable<UpdateOrganizationAccountsBody['options']> = group.options.map(
(option) => {
const common = { id: option.id, label: option.label, required: option.required }
return option.provider === 'slack'
? {
...common,
provider: 'slack',
slackBotCredentialId: option.slackBotCredentialId,
requiredScopes: option.requiredScopes,
}
: { ...common, provider: option.provider }
}
)
const options = getOrganizationAccountUpdateOptions(group)
const updateConfigurations = () => {
if (pending) return
update.mutate(
Expand Down
21 changes: 21 additions & 0 deletions apps/sim/lib/credential-groups/organization-account-options.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import type {
OrganizationAccountsSettings,
UpdateOrganizationAccountsBody,
} from '@/lib/api/contracts/organization-accounts'

/** Preserves option identities and custom Slack scopes while the server refreshes managed OAuth policies. */
export function getOrganizationAccountUpdateOptions(
group: NonNullable<OrganizationAccountsSettings['credentialGroup']>
): NonNullable<UpdateOrganizationAccountsBody['options']> {
Comment thread
icecrasher321 marked this conversation as resolved.
return group.options.map((option) => {
const common = { id: option.id, label: option.label, required: option.required }
return option.provider === 'slack'
? {
...common,
provider: 'slack',
slackBotCredentialId: option.slackBotCredentialId,
requiredScopes: option.requiredScopes,
}
: { ...common, provider: option.provider }
})
}
4 changes: 3 additions & 1 deletion apps/sim/lib/credential-groups/service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -306,7 +306,9 @@ export async function ensureWorkspaceAccountsGroup(
) {
throw new OrchestrationError(
'validation',
`Update ${preparedOption.label} in Connected accounts before connecting this source`
scope.kind === 'organization'
? `Update ${preparedOption.label} using Update configurations in organization settings before connecting this source`
: `Update ${preparedOption.label} in Connected accounts before connecting this source`
)
}
return existing
Expand Down
Loading