diff --git a/apps/sim/app/o/[organizationId]/settings/integrations/sources/[connectorId]/source-detail.test.tsx b/apps/sim/app/o/[organizationId]/settings/integrations/sources/[connectorId]/source-detail.test.tsx
index 0dbbe4341de..d277c6a025d 100644
--- a/apps/sim/app/o/[organizationId]/settings/integrations/sources/[connectorId]/source-detail.test.tsx
+++ b/apps/sim/app/o/[organizationId]/settings/integrations/sources/[connectorId]/source-detail.test.tsx
@@ -13,6 +13,7 @@ const mocks = vi.hoisted(() => ({
detail: vi.fn(),
integrations: vi.fn(),
push: vi.fn(),
+ replace: vi.fn(),
documents: vi.fn(),
actions: vi.fn(),
recovery: vi.fn(),
@@ -23,7 +24,7 @@ const mocks = vi.hoisted(() => ({
save: vi.fn(),
}))
vi.mock('next/navigation', () => ({
- useRouter: () => ({ push: mocks.push }),
+ useRouter: () => ({ push: mocks.push, replace: mocks.replace }),
usePathname: () => '/o/org-one/settings/integrations/sources/source-one',
}))
vi.mock('@/app/o/[organizationId]/providers/organization-provider', () => ({
@@ -185,6 +186,17 @@ describe('organization source detail navigation', () => {
expect(button, `Missing ${text}`).toBeTruthy()
await act(async () => button!.click())
}
+ it.each(['documents', 'settings', 'history'])(
+ 'replaces the removed connection with Sources from the %s view',
+ async (view) => {
+ await render(`?view=${view}`)
+ const options: ConnectorActionsOptions = mocks.actions.mock.lastCall![0]
+ act(() => options.onRemoved?.())
+ expect(mocks.replace).toHaveBeenCalledWith('/o/org-one/settings/integrations')
+ expect(mocks.push).not.toHaveBeenCalled()
+ }
+ )
+
it('opens documents by default and uses the exact canonical search index', async () => {
await render()
expect(mocks.detail).toHaveBeenLastCalledWith('index-one', 'source-one')
diff --git a/apps/sim/app/o/[organizationId]/settings/integrations/sources/[connectorId]/source-detail.tsx b/apps/sim/app/o/[organizationId]/settings/integrations/sources/[connectorId]/source-detail.tsx
index 939dfd52563..df92230fa62 100644
--- a/apps/sim/app/o/[organizationId]/settings/integrations/sources/[connectorId]/source-detail.tsx
+++ b/apps/sim/app/o/[organizationId]/settings/integrations/sources/[connectorId]/source-detail.tsx
@@ -197,6 +197,8 @@ function SourceDetailContent({
const description =
[title === meta?.name ? undefined : meta?.name, status].filter(Boolean).join(' · ') || undefined
const onBack = () => router.push(backHref)
+ const onRemoved = () =>
+ router.replace(organizationRoutes(organization.id).settingsSection('integrations'))
const onViewChange = (value: string) => {
const next = sourceViewParam.parser.parse(value)
if (next) void setView(next)
@@ -254,6 +256,7 @@ function SourceDetailContent({
queryError={integrationFeedback}
backText={backText}
onBack={onBack}
+ onRemoved={onRemoved}
onViewChange={onViewChange}
/>
)
@@ -264,7 +267,7 @@ function SourceDetailContent({
title={title}
description={description}
docsLink={meta?.searchDocsUrl}
- onRemoved={onBack}
+ onRemoved={onRemoved}
>
{integrationFeedback}
@@ -367,6 +370,7 @@ interface SourceSettingsEditorProps {
queryError?: ReactNode
backText: string
onBack: () => void
+ onRemoved: () => void
onViewChange: (view: string) => void
}
@@ -400,6 +404,7 @@ function SourceSettingsForm({
queryError,
backText,
onBack,
+ onRemoved,
onViewChange,
onSaved,
onDiscard,
@@ -420,7 +425,7 @@ function SourceSettingsForm({
description={description}
docsLink={form.docsUrl}
lifecycleDisabled={form.dirty || form.saving}
- onRemoved={onBack}
+ onRemoved={onRemoved}
actions={saveDiscardActions({
dirty: form.dirty,
saving: form.saving,
diff --git a/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/components/connectors-section/connectors-section.test.tsx b/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/components/connectors-section/connectors-section.test.tsx
index b8559b55027..c8574915cbe 100644
--- a/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/components/connectors-section/connectors-section.test.tsx
+++ b/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/components/connectors-section/connectors-section.test.tsx
@@ -36,6 +36,7 @@ const {
isFetching: false,
},
lifecycle: {
+ removeOptions: { onSuccess: undefined as (() => void) | undefined },
sync: { mutate: vi.fn(), reset: vi.fn(), error: null as Error | null, isPending: false },
update: { mutate: vi.fn(), reset: vi.fn(), error: null as Error | null, isPending: false },
remove: { mutate: vi.fn(), reset: vi.fn(), error: null as Error | null, isPending: false },
@@ -235,7 +236,10 @@ vi.mock('@/hooks/queries/kb/connectors', () => ({
isPlaceholderData: lifecycle.detail.isPlaceholderData,
refetch: lifecycle.detail.refetch,
})),
- useDeleteConnector: () => lifecycle.remove,
+ useDeleteConnector: (options: { onSuccess: () => void }) => {
+ lifecycle.removeOptions = options
+ return lifecycle.remove
+ },
useTriggerSync: () => lifecycle.sync,
useUpdateConnector: () => lifecycle.update,
}))
@@ -943,15 +947,12 @@ describe('shared connector lifecycle actions', () => {
expect(dialog.textContent).not.toContain('remain unless')
}
act(() => findButton(dialog, 'Remove').click())
- expect(lifecycle.remove.mutate).toHaveBeenCalledWith(
- {
- knowledgeBaseId: 'knowledge-1',
- connectorId: 'connector-1',
- deleteDocuments: accessMode !== 'workspace',
- },
- expect.any(Object)
- )
- act(() => lifecycle.remove.mutate.mock.calls[0][1].onSuccess())
+ expect(lifecycle.remove.mutate).toHaveBeenCalledWith({
+ knowledgeBaseId: 'knowledge-1',
+ connectorId: 'connector-1',
+ deleteDocuments: accessMode !== 'workspace',
+ })
+ act(() => lifecycle.removeOptions.onSuccess?.())
expect(onRemoved).toHaveBeenCalledOnce()
expect(container.querySelector('[role="dialog"]')).toBeNull()
}
diff --git a/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/components/connectors-section/use-connector-actions.ts b/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/components/connectors-section/use-connector-actions.ts
index 9fbbcda5690..c413b5733b4 100644
--- a/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/components/connectors-section/use-connector-actions.ts
+++ b/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/components/connectors-section/use-connector-actions.ts
@@ -31,9 +31,15 @@ export function useConnectorActions({
}: ConnectorActionsOptions) {
const sync = useTriggerSync()
const update = useUpdateConnector()
- const remove = useDeleteConnector()
const [confirmRemove, setConfirmRemove] = useState(false)
const [deleteDocuments, setDeleteDocuments] = useState(false)
+ const remove = useDeleteConnector({
+ onSuccess: () => {
+ setConfirmRemove(false)
+ setDeleteDocuments(false)
+ onRemoved?.()
+ },
+ })
const requiresDocumentDeletion = connector.accessMode !== 'workspace'
const state = getConnectorSyncState(connector)
const actionsDisabled = disabled || sync.isPending || update.isPending || remove.isPending
@@ -117,20 +123,11 @@ export function useConnectorActions({
error: remove.error,
onConfirm: () => {
if (!canEdit || actionsDisabled) return
- remove.mutate(
- {
- knowledgeBaseId,
- connectorId: connector.id,
- deleteDocuments: requiresDocumentDeletion || deleteDocuments,
- },
- {
- onSuccess: () => {
- setConfirmRemove(false)
- setDeleteDocuments(false)
- onRemoved?.()
- },
- }
- )
+ remove.mutate({
+ knowledgeBaseId,
+ connectorId: connector.id,
+ deleteDocuments: requiresDocumentDeletion || deleteDocuments,
+ })
},
},
}
diff --git a/apps/sim/hooks/queries/kb/connectors-cache.test.tsx b/apps/sim/hooks/queries/kb/connectors-cache.test.tsx
index 89ee868601c..72af149fd60 100644
--- a/apps/sim/hooks/queries/kb/connectors-cache.test.tsx
+++ b/apps/sim/hooks/queries/kb/connectors-cache.test.tsx
@@ -388,6 +388,46 @@ describe('connector Search result cache reconciliation', () => {
})
describe('Search source list reconciliation', () => {
+ it('runs removal navigation before refetches and retains it after the caller unmounts', async () => {
+ const client = createQueryClient()
+ const request = Promise.withResolvers