{
})
})
- it('should call mutateExternalKnowledgeApis in onSaveCallback', async () => {
+ it('should invalidate the generated external API query after creation', async () => {
render(
)
const createButton = screen.getByText('dataset.createExternalAPI').closest('button')!
fireEvent.click(createButton)
const callArgs = mockSetShowExternalKnowledgeAPIModal.mock.calls[0]![0]
- callArgs.onSaveCallback()
+ await callArgs.onSaveCallback()
- expect(mockMutateExternalKnowledgeApis).toHaveBeenCalled()
+ expect(mockInvalidateQueries).toHaveBeenCalledWith({
+ queryKey: externalKnowledgeApiQueryKey,
+ })
})
- it('should call mutateExternalKnowledgeApis in onCancelCallback', async () => {
+ it('should not refresh the query when creation is canceled', async () => {
render(
)
const createButton = screen.getByText('dataset.createExternalAPI').closest('button')!
fireEvent.click(createButton)
const callArgs = mockSetShowExternalKnowledgeAPIModal.mock.calls[0]![0]
- callArgs.onCancelCallback()
- expect(mockMutateExternalKnowledgeApis).toHaveBeenCalled()
+ expect(callArgs.onCancelCallback).toBeUndefined()
+ expect(mockInvalidateQueries).not.toHaveBeenCalled()
})
})
diff --git a/web/app/components/datasets/external-api/external-api-panel/index.tsx b/web/app/components/datasets/external-api/external-api-panel/index.tsx
index a5eca0b4c90..fee862d8488 100644
--- a/web/app/components/datasets/external-api/external-api-panel/index.tsx
+++ b/web/app/components/datasets/external-api/external-api-panel/index.tsx
@@ -1,13 +1,14 @@
import { Button } from '@langgenius/dify-ui/button'
import { cn } from '@langgenius/dify-ui/cn'
import { RiAddLine, RiBookOpenLine, RiCloseLine } from '@remixicon/react'
+import { useQuery, useQueryClient } from '@tanstack/react-query'
import * as React from 'react'
import { useTranslation } from 'react-i18next'
import ActionButton from '@/app/components/base/action-button'
import Loading from '@/app/components/base/loading'
-import { useExternalKnowledgeApi } from '@/context/external-knowledge-api-context'
import { useDocLink } from '@/context/i18n'
import { useModalContext } from '@/context/modal-context'
+import { consoleQuery } from '@/service/client'
import ExternalKnowledgeAPICard from '../external-knowledge-api-card'
type ExternalAPIPanelProps = {
@@ -22,8 +23,11 @@ const ExternalAPIPanel: React.FC
= ({
const { t } = useTranslation()
const docLink = useDocLink()
const { setShowExternalKnowledgeAPIModal } = useModalContext()
- const { externalKnowledgeApiList, mutateExternalKnowledgeApis, isLoading } =
- useExternalKnowledgeApi()
+ const queryClient = useQueryClient()
+ const externalKnowledgeApiQueryOptions =
+ consoleQuery.datasets.externalKnowledgeApi.get.queryOptions({ input: {} })
+ const { data, isLoading } = useQuery(externalKnowledgeApiQueryOptions)
+ const externalKnowledgeApiList = data?.data ?? []
const handleOpenExternalAPIModal = () => {
if (!canManageExternalKnowledgeApi) return
@@ -32,10 +36,9 @@ const ExternalAPIPanel: React.FC = ({
payload: { name: '', settings: { endpoint: '', api_key: '' } },
datasetBindings: [],
onSaveCallback: () => {
- mutateExternalKnowledgeApis()
- },
- onCancelCallback: () => {
- mutateExternalKnowledgeApis()
+ void queryClient.invalidateQueries({
+ queryKey: externalKnowledgeApiQueryOptions.queryKey,
+ })
},
isEditMode: false,
})
diff --git a/web/app/components/datasets/external-api/external-knowledge-api-card/__tests__/index.spec.tsx b/web/app/components/datasets/external-api/external-knowledge-api-card/__tests__/index.spec.tsx
index e441016cf5e..5a3bc766f24 100644
--- a/web/app/components/datasets/external-api/external-knowledge-api-card/__tests__/index.spec.tsx
+++ b/web/app/components/datasets/external-api/external-knowledge-api-card/__tests__/index.spec.tsx
@@ -1,8 +1,14 @@
+import type { ExternalKnowledgeApiResponse } from '@dify/contracts/api/console/datasets/types.gen'
import type { ExternalAPIItem } from '@/models/datasets'
import { fireEvent, render, screen, waitFor } from '@testing-library/react'
import { beforeEach, describe, expect, it, vi } from 'vitest'
// Import mocked services
-import { checkUsageExternalAPI, deleteExternalAPI, fetchExternalAPI } from '@/service/datasets'
+import {
+ checkUsageExternalAPI,
+ deleteExternalAPI,
+ fetchExternalAPI,
+ updateExternalAPI,
+} from '@/service/datasets'
import ExternalKnowledgeAPICard from '../index'
// Mock API services
@@ -15,7 +21,8 @@ vi.mock('@/service/datasets', () => ({
// Mock contexts
const mockSetShowExternalKnowledgeAPIModal = vi.fn()
-const mockMutateExternalKnowledgeApis = vi.fn()
+const mockInvalidateQueries = vi.fn()
+const externalKnowledgeApiQueryKey = ['console', 'datasets', 'externalKnowledgeApi', 'get']
vi.mock('@/context/modal-context', () => ({
useModalContext: () => ({
@@ -23,14 +30,30 @@ vi.mock('@/context/modal-context', () => ({
}),
}))
-vi.mock('@/context/external-knowledge-api-context', () => ({
- useExternalKnowledgeApi: () => ({
- mutateExternalKnowledgeApis: mockMutateExternalKnowledgeApis,
- }),
+vi.mock('@tanstack/react-query', async (importOriginal) => {
+ const original = await importOriginal()
+ return {
+ ...original,
+ useQueryClient: () => ({ invalidateQueries: mockInvalidateQueries }),
+ }
+})
+
+vi.mock('@/service/client', () => ({
+ consoleQuery: {
+ datasets: {
+ externalKnowledgeApi: {
+ get: {
+ queryOptions: () => ({
+ queryKey: ['console', 'datasets', 'externalKnowledgeApi', 'get'],
+ }),
+ },
+ },
+ },
+ },
}))
describe('ExternalKnowledgeAPICard', () => {
- const mockApi: ExternalAPIItem = {
+ const mockApi: ExternalKnowledgeApiResponse = {
id: 'api-123',
tenant_id: 'tenant-1',
name: 'Test External API',
@@ -146,7 +169,7 @@ describe('ExternalKnowledgeAPICard', () => {
consoleSpy.mockRestore()
})
- it('should call mutate on save callback', async () => {
+ it('should invalidate the generated list query after editing', async () => {
const mockResponse: ExternalAPIItem = {
id: 'api-123',
tenant_id: 'tenant-1',
@@ -171,14 +194,31 @@ describe('ExternalKnowledgeAPICard', () => {
expect(mockSetShowExternalKnowledgeAPIModal).toHaveBeenCalled()
})
- // Simulate save callback
const modalCall = mockSetShowExternalKnowledgeAPIModal.mock.calls[0]![0]
- modalCall.onSaveCallback()
+ await modalCall.onEditCallback({
+ name: 'Updated External API',
+ settings: {
+ endpoint: 'https://updated.example.com/knowledge',
+ api_key: 'updated-secret-key',
+ },
+ })
- expect(mockMutateExternalKnowledgeApis).toHaveBeenCalled()
+ expect(updateExternalAPI).toHaveBeenCalledWith({
+ apiTemplateId: 'api-123',
+ body: expect.objectContaining({
+ name: 'Updated External API',
+ settings: {
+ endpoint: 'https://updated.example.com/knowledge',
+ api_key: 'updated-secret-key',
+ },
+ }),
+ })
+ expect(mockInvalidateQueries).toHaveBeenCalledWith({
+ queryKey: externalKnowledgeApiQueryKey,
+ })
})
- it('should call mutate on cancel callback', async () => {
+ it('should not refresh the list query when editing is canceled', async () => {
const mockResponse: ExternalAPIItem = {
id: 'api-123',
tenant_id: 'tenant-1',
@@ -203,11 +243,10 @@ describe('ExternalKnowledgeAPICard', () => {
expect(mockSetShowExternalKnowledgeAPIModal).toHaveBeenCalled()
})
- // Simulate cancel callback
const modalCall = mockSetShowExternalKnowledgeAPIModal.mock.calls[0]![0]
- modalCall.onCancelCallback()
- expect(mockMutateExternalKnowledgeApis).toHaveBeenCalled()
+ expect(modalCall.onCancelCallback).toBeUndefined()
+ expect(mockInvalidateQueries).not.toHaveBeenCalled()
})
})
@@ -262,7 +301,9 @@ describe('ExternalKnowledgeAPICard', () => {
await waitFor(() => {
expect(deleteExternalAPI).toHaveBeenCalledWith({ apiTemplateId: 'api-123' })
- expect(mockMutateExternalKnowledgeApis).toHaveBeenCalled()
+ expect(mockInvalidateQueries).toHaveBeenCalledWith({
+ queryKey: externalKnowledgeApiQueryKey,
+ })
})
})
@@ -335,7 +376,7 @@ describe('ExternalKnowledgeAPICard', () => {
describe('Edge Cases', () => {
it('should handle API with empty endpoint', () => {
- const apiWithEmptyEndpoint: ExternalAPIItem = {
+ const apiWithEmptyEndpoint: ExternalKnowledgeApiResponse = {
...mockApi,
settings: { endpoint: '', api_key: 'key' },
}
diff --git a/web/app/components/datasets/external-api/external-knowledge-api-card/index.tsx b/web/app/components/datasets/external-api/external-knowledge-api-card/index.tsx
index 71fd8b7a910..54b100e9361 100644
--- a/web/app/components/datasets/external-api/external-knowledge-api-card/index.tsx
+++ b/web/app/components/datasets/external-api/external-knowledge-api-card/index.tsx
@@ -1,5 +1,5 @@
+import type { ExternalKnowledgeApiResponse } from '@dify/contracts/api/console/datasets/types.gen'
import type { CreateExternalAPIReq } from '../declarations'
-import type { ExternalAPIItem } from '@/models/datasets'
import {
AlertDialog,
AlertDialogActions,
@@ -10,13 +10,14 @@ import {
AlertDialogTitle,
} from '@langgenius/dify-ui/alert-dialog'
import { RiDeleteBinLine, RiEditLine } from '@remixicon/react'
+import { useQueryClient } from '@tanstack/react-query'
import * as React from 'react'
import { useState } from 'react'
import { useTranslation } from 'react-i18next'
import ActionButton from '@/app/components/base/action-button'
import { ApiConnectionMod } from '@/app/components/base/icons/src/vender/solid/development'
-import { useExternalKnowledgeApi } from '@/context/external-knowledge-api-context'
import { useModalContext } from '@/context/modal-context'
+import { consoleQuery } from '@/service/client'
import {
checkUsageExternalAPI,
deleteExternalAPI,
@@ -25,7 +26,7 @@ import {
} from '@/service/datasets'
type ExternalKnowledgeAPICardProps = {
- api: ExternalAPIItem
+ api: ExternalKnowledgeApiResponse
canManageExternalKnowledgeApi: boolean
}
@@ -37,7 +38,12 @@ const ExternalKnowledgeAPICard: React.FC = ({
const [showConfirm, setShowConfirm] = useState(false)
const [isHovered, setIsHovered] = useState(false)
const [usageCount, setUsageCount] = useState(0)
- const { mutateExternalKnowledgeApis } = useExternalKnowledgeApi()
+ const queryClient = useQueryClient()
+ const externalKnowledgeApiQueryKey = consoleQuery.datasets.externalKnowledgeApi.get.queryOptions({
+ input: {},
+ }).queryKey
+ const endpoint =
+ api.settings && typeof api.settings.endpoint === 'string' ? api.settings.endpoint : ''
const { t } = useTranslation()
@@ -56,12 +62,6 @@ const ExternalKnowledgeAPICard: React.FC = ({
setShowExternalKnowledgeAPIModal({
payload: formValue,
- onSaveCallback: () => {
- mutateExternalKnowledgeApis()
- },
- onCancelCallback: () => {
- mutateExternalKnowledgeApis()
- },
isEditMode: true,
datasetBindings: response.dataset_bindings,
onEditCallback: async (updatedData: CreateExternalAPIReq) => {
@@ -78,7 +78,7 @@ const ExternalKnowledgeAPICard: React.FC = ({
},
},
})
- mutateExternalKnowledgeApis()
+ await queryClient.invalidateQueries({ queryKey: externalKnowledgeApiQueryKey })
} catch (error) {
console.error('Error updating external knowledge API:', error)
}
@@ -109,7 +109,7 @@ const ExternalKnowledgeAPICard: React.FC = ({
const response = await deleteExternalAPI({ apiTemplateId: api.id })
if (response && response.result === 'success') {
setShowConfirm(false)
- mutateExternalKnowledgeApis()
+ await queryClient.invalidateQueries({ queryKey: externalKnowledgeApiQueryKey })
} else {
console.error('Failed to delete external API')
}
@@ -128,9 +128,7 @@ const ExternalKnowledgeAPICard: React.FC = ({
{api.name}
-
diff --git a/web/app/components/datasets/external-knowledge-base/connector/__tests__/index.spec.tsx b/web/app/components/datasets/external-knowledge-base/connector/__tests__/index.spec.tsx
index fca2f5aca38..0dd4d7d57bb 100644
--- a/web/app/components/datasets/external-knowledge-base/connector/__tests__/index.spec.tsx
+++ b/web/app/components/datasets/external-knowledge-base/connector/__tests__/index.spec.tsx
@@ -79,12 +79,27 @@ const createDefaultMockApiList = (): ExternalAPIItem[] => [
let mockExternalKnowledgeApiList: ExternalAPIItem[] = createDefaultMockApiList()
-vi.mock('@/context/external-knowledge-api-context', () => ({
- useExternalKnowledgeApi: () => ({
- externalKnowledgeApiList: mockExternalKnowledgeApiList,
- mutateExternalKnowledgeApis: vi.fn(),
- isLoading: false,
- }),
+vi.mock('@tanstack/react-query', async (importOriginal) => {
+ const original = await importOriginal
()
+ return {
+ ...original,
+ useQuery: () => ({ data: { data: mockExternalKnowledgeApiList } }),
+ useQueryClient: () => ({ invalidateQueries: vi.fn() }),
+ }
+})
+
+vi.mock('@/service/client', () => ({
+ consoleQuery: {
+ datasets: {
+ externalKnowledgeApi: {
+ get: {
+ queryOptions: () => ({
+ queryKey: ['console', 'datasets', 'externalKnowledgeApi', 'get'],
+ }),
+ },
+ },
+ },
+ },
}))
// Suppress console.error helper
diff --git a/web/app/components/datasets/external-knowledge-base/create/ExternalApiSelect.tsx b/web/app/components/datasets/external-knowledge-base/create/ExternalApiSelect.tsx
index 2dc8ca62b4b..1bd52132e57 100644
--- a/web/app/components/datasets/external-knowledge-base/create/ExternalApiSelect.tsx
+++ b/web/app/components/datasets/external-knowledge-base/create/ExternalApiSelect.tsx
@@ -1,11 +1,12 @@
import { RiAddLine, RiArrowDownSLine } from '@remixicon/react'
+import { useQueryClient } from '@tanstack/react-query'
import * as React from 'react'
import { useEffect, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { ApiConnectionMod } from '@/app/components/base/icons/src/vender/solid/development'
-import { useExternalKnowledgeApi } from '@/context/external-knowledge-api-context'
import { useModalContext } from '@/context/modal-context'
import { useRouter } from '@/next/navigation'
+import { consoleQuery } from '@/service/client'
type ApiItem = {
value: string
@@ -26,7 +27,10 @@ const ExternalApiSelect: React.FC = ({ items, value, onS
items.find((item) => item.value === value) || null,
)
const { setShowExternalKnowledgeAPIModal } = useModalContext()
- const { mutateExternalKnowledgeApis } = useExternalKnowledgeApi()
+ const queryClient = useQueryClient()
+ const externalKnowledgeApiQueryKey = consoleQuery.datasets.externalKnowledgeApi.get.queryOptions({
+ input: {},
+ }).queryKey
const router = useRouter()
useEffect(() => {
@@ -38,12 +42,9 @@ const ExternalApiSelect: React.FC = ({ items, value, onS
setShowExternalKnowledgeAPIModal({
payload: { name: '', settings: { endpoint: '', api_key: '' } },
onSaveCallback: async () => {
- mutateExternalKnowledgeApis()
+ await queryClient.invalidateQueries({ queryKey: externalKnowledgeApiQueryKey })
router.refresh()
},
- onCancelCallback: () => {
- mutateExternalKnowledgeApis()
- },
isEditMode: false,
})
}
diff --git a/web/app/components/datasets/external-knowledge-base/create/ExternalApiSelection.tsx b/web/app/components/datasets/external-knowledge-base/create/ExternalApiSelection.tsx
index 6cee272f841..e5d1ba46d6e 100644
--- a/web/app/components/datasets/external-knowledge-base/create/ExternalApiSelection.tsx
+++ b/web/app/components/datasets/external-knowledge-base/create/ExternalApiSelection.tsx
@@ -2,13 +2,14 @@
import { Button } from '@langgenius/dify-ui/button'
import { RiAddLine } from '@remixicon/react'
+import { useQuery, useQueryClient } from '@tanstack/react-query'
import * as React from 'react'
import { useEffect, useState } from 'react'
import { useTranslation } from 'react-i18next'
import Input from '@/app/components/base/input'
-import { useExternalKnowledgeApi } from '@/context/external-knowledge-api-context'
import { useModalContext } from '@/context/modal-context'
import { useRouter } from '@/next/navigation'
+import { consoleQuery } from '@/service/client'
import ExternalApiSelect from './ExternalApiSelect'
type ExternalApiSelectionProps = {
@@ -24,15 +25,18 @@ const ExternalApiSelection: React.FC = ({
}) => {
const { t } = useTranslation()
const router = useRouter()
- const { externalKnowledgeApiList } = useExternalKnowledgeApi()
+ const queryClient = useQueryClient()
+ const externalKnowledgeApiQueryOptions =
+ consoleQuery.datasets.externalKnowledgeApi.get.queryOptions({ input: {} })
+ const { data } = useQuery(externalKnowledgeApiQueryOptions)
+ const externalKnowledgeApiList = data?.data ?? []
const [selectedApiId, setSelectedApiId] = useState(external_knowledge_api_id)
const { setShowExternalKnowledgeAPIModal } = useModalContext()
- const { mutateExternalKnowledgeApis } = useExternalKnowledgeApi()
const apiItems = externalKnowledgeApiList.map((api) => ({
value: api.id,
name: api.name,
- url: api.settings.endpoint,
+ url: api.settings && typeof api.settings.endpoint === 'string' ? api.settings.endpoint : '',
}))
useEffect(() => {
@@ -48,12 +52,11 @@ const ExternalApiSelection: React.FC = ({
setShowExternalKnowledgeAPIModal({
payload: { name: '', settings: { endpoint: '', api_key: '' } },
onSaveCallback: async () => {
- mutateExternalKnowledgeApis()
+ await queryClient.invalidateQueries({
+ queryKey: externalKnowledgeApiQueryOptions.queryKey,
+ })
router.refresh()
},
- onCancelCallback: () => {
- mutateExternalKnowledgeApis()
- },
isEditMode: false,
})
}
diff --git a/web/app/components/datasets/external-knowledge-base/create/__tests__/ExternalApiSelect.spec.tsx b/web/app/components/datasets/external-knowledge-base/create/__tests__/ExternalApiSelect.spec.tsx
index 7af75fbcdd2..5dfa11f5b79 100644
--- a/web/app/components/datasets/external-knowledge-base/create/__tests__/ExternalApiSelect.spec.tsx
+++ b/web/app/components/datasets/external-knowledge-base/create/__tests__/ExternalApiSelect.spec.tsx
@@ -9,7 +9,7 @@ const mocks = vi.hoisted(() => ({
push: vi.fn(),
refresh: vi.fn(),
setShowExternalKnowledgeAPIModal: vi.fn(),
- mutateExternalKnowledgeApis: vi.fn(),
+ invalidateQueries: vi.fn(),
}))
vi.mock('@/next/navigation', () => ({
@@ -22,10 +22,28 @@ vi.mock('@/context/modal-context', () => ({
}),
}))
-vi.mock('@/context/external-knowledge-api-context', () => ({
- useExternalKnowledgeApi: () => ({
- mutateExternalKnowledgeApis: mocks.mutateExternalKnowledgeApis,
- }),
+const externalKnowledgeApiQueryKey = ['console', 'datasets', 'externalKnowledgeApi', 'get']
+
+vi.mock('@tanstack/react-query', async (importOriginal) => {
+ const original = await importOriginal()
+ return {
+ ...original,
+ useQueryClient: () => ({ invalidateQueries: mocks.invalidateQueries }),
+ }
+})
+
+vi.mock('@/service/client', () => ({
+ consoleQuery: {
+ datasets: {
+ externalKnowledgeApi: {
+ get: {
+ queryOptions: () => ({
+ queryKey: ['console', 'datasets', 'externalKnowledgeApi', 'get'],
+ }),
+ },
+ },
+ },
+ },
}))
vi.mock('@/app/components/base/icons/src/vender/solid/development', () => ({
@@ -87,11 +105,19 @@ describe('ExternalApiSelect', () => {
expect(screen.getByText('dataset.createNewExternalAPI')).toBeInTheDocument()
})
- it('should call setShowExternalKnowledgeAPIModal when add new clicked', () => {
+ it('should invalidate the generated query after creating an external API', async () => {
render()
fireEvent.click(screen.getByText('dataset.selectExternalKnowledgeAPI.placeholder'))
fireEvent.click(screen.getByText('dataset.createNewExternalAPI'))
expect(mocks.setShowExternalKnowledgeAPIModal).toHaveBeenCalledOnce()
+
+ const modalConfig = mocks.setShowExternalKnowledgeAPIModal.mock.calls[0]![0]
+ await modalConfig.onSaveCallback()
+
+ expect(mocks.invalidateQueries).toHaveBeenCalledWith({
+ queryKey: externalKnowledgeApiQueryKey,
+ })
+ expect(mocks.refresh).toHaveBeenCalledOnce()
})
it('should show item URLs in dropdown', () => {
diff --git a/web/app/components/datasets/external-knowledge-base/create/__tests__/ExternalApiSelection.spec.tsx b/web/app/components/datasets/external-knowledge-base/create/__tests__/ExternalApiSelection.spec.tsx
index cfd79fe8bc9..5b3240501dd 100644
--- a/web/app/components/datasets/external-knowledge-base/create/__tests__/ExternalApiSelection.spec.tsx
+++ b/web/app/components/datasets/external-knowledge-base/create/__tests__/ExternalApiSelection.spec.tsx
@@ -6,6 +6,7 @@ const mocks = vi.hoisted(() => ({
push: vi.fn(),
refresh: vi.fn(),
setShowExternalKnowledgeAPIModal: vi.fn(),
+ invalidateQueries: vi.fn(),
externalKnowledgeApiList: [] as Array<{
id: string
name: string
@@ -23,10 +24,29 @@ vi.mock('@/context/modal-context', () => ({
}),
}))
-vi.mock('@/context/external-knowledge-api-context', () => ({
- useExternalKnowledgeApi: () => ({
- externalKnowledgeApiList: mocks.externalKnowledgeApiList,
- }),
+const externalKnowledgeApiQueryKey = ['console', 'datasets', 'externalKnowledgeApi', 'get']
+
+vi.mock('@tanstack/react-query', async (importOriginal) => {
+ const original = await importOriginal()
+ return {
+ ...original,
+ useQuery: () => ({ data: { data: mocks.externalKnowledgeApiList } }),
+ useQueryClient: () => ({ invalidateQueries: mocks.invalidateQueries }),
+ }
+})
+
+vi.mock('@/service/client', () => ({
+ consoleQuery: {
+ datasets: {
+ externalKnowledgeApi: {
+ get: {
+ queryOptions: () => ({
+ queryKey: ['console', 'datasets', 'externalKnowledgeApi', 'get'],
+ }),
+ },
+ },
+ },
+ },
}))
vi.mock('../ExternalApiSelect', () => ({
@@ -108,4 +128,19 @@ describe('ExternalApiSelection', () => {
expect(mocks.setShowExternalKnowledgeAPIModal).toHaveBeenCalledOnce()
})
+
+ it('invalidates the generated query after creating an external API', async () => {
+ const user = userEvent.setup()
+ mocks.externalKnowledgeApiList = []
+ render()
+
+ await user.click(screen.getByRole('button', { name: 'dataset.noExternalKnowledge' }))
+ const modalConfig = mocks.setShowExternalKnowledgeAPIModal.mock.calls[0]![0]
+ await modalConfig.onSaveCallback()
+
+ expect(mocks.invalidateQueries).toHaveBeenCalledWith({
+ queryKey: externalKnowledgeApiQueryKey,
+ })
+ expect(mocks.refresh).toHaveBeenCalledOnce()
+ })
})
diff --git a/web/app/components/datasets/external-knowledge-base/create/__tests__/index.spec.tsx b/web/app/components/datasets/external-knowledge-base/create/__tests__/index.spec.tsx
index 0a1237b6fc0..521d9d3dcd5 100644
--- a/web/app/components/datasets/external-knowledge-base/create/__tests__/index.spec.tsx
+++ b/web/app/components/datasets/external-knowledge-base/create/__tests__/index.spec.tsx
@@ -21,7 +21,7 @@ vi.mock('@/context/i18n', () => ({
`https://docs.dify.ai/en${path?.startsWith('/use-dify/') ? `/cloud${path}` : path || ''}`,
}))
-// Mock external context providers (these are external dependencies)
+// Mock the shared modal owner.
const mockSetShowExternalKnowledgeAPIModal = vi.fn()
vi.mock('@/context/modal-context', () => ({
useModalContext: () => ({
@@ -59,15 +59,31 @@ const createDefaultMockApiList = (): ExternalAPIItem[] => [
}),
]
-const mockMutateExternalKnowledgeApis = vi.fn()
+const mockInvalidateQueries = vi.fn()
+const externalKnowledgeApiQueryKey = ['console', 'datasets', 'externalKnowledgeApi', 'get']
let mockExternalKnowledgeApiList: ExternalAPIItem[] = createDefaultMockApiList()
-vi.mock('@/context/external-knowledge-api-context', () => ({
- useExternalKnowledgeApi: () => ({
- externalKnowledgeApiList: mockExternalKnowledgeApiList,
- mutateExternalKnowledgeApis: mockMutateExternalKnowledgeApis,
- isLoading: false,
- }),
+vi.mock('@tanstack/react-query', async (importOriginal) => {
+ const original = await importOriginal()
+ return {
+ ...original,
+ useQuery: () => ({ data: { data: mockExternalKnowledgeApiList } }),
+ useQueryClient: () => ({ invalidateQueries: mockInvalidateQueries }),
+ }
+})
+
+vi.mock('@/service/client', () => ({
+ consoleQuery: {
+ datasets: {
+ externalKnowledgeApi: {
+ get: {
+ queryOptions: () => ({
+ queryKey: ['console', 'datasets', 'externalKnowledgeApi', 'get'],
+ }),
+ },
+ },
+ },
+ },
}))
// Helper to render component with default props
@@ -523,7 +539,7 @@ describe('ExternalKnowledgeBaseCreate', () => {
)
})
- it('should call mutate and router.refresh on modal save callback', async () => {
+ it('should invalidate the generated query and refresh after modal save', async () => {
const user = userEvent.setup()
// Set empty API list
mockExternalKnowledgeApiList = []
@@ -536,11 +552,13 @@ describe('ExternalKnowledgeBaseCreate', () => {
const modalCall = mockSetShowExternalKnowledgeAPIModal.mock.calls[0]![0]
await modalCall.onSaveCallback()
- expect(mockMutateExternalKnowledgeApis).toHaveBeenCalled()
+ expect(mockInvalidateQueries).toHaveBeenCalledWith({
+ queryKey: externalKnowledgeApiQueryKey,
+ })
expect(mockRefresh).toHaveBeenCalled()
})
- it('should call mutate on modal cancel callback', async () => {
+ it('should not invalidate the generated query when the modal is canceled', async () => {
const user = userEvent.setup()
// Set empty API list
mockExternalKnowledgeApiList = []
@@ -551,9 +569,8 @@ describe('ExternalKnowledgeBaseCreate', () => {
// Get the callback and invoke it
const modalCall = mockSetShowExternalKnowledgeAPIModal.mock.calls[0]![0]
- modalCall.onCancelCallback()
-
- expect(mockMutateExternalKnowledgeApis).toHaveBeenCalled()
+ expect(modalCall.onCancelCallback).toBeUndefined()
+ expect(mockInvalidateQueries).not.toHaveBeenCalled()
})
it('should display API URL in dropdown', async () => {
@@ -600,7 +617,7 @@ describe('ExternalKnowledgeBaseCreate', () => {
)
})
- it('should call mutate and refresh on save callback from ExternalApiSelect dropdown', async () => {
+ it('should invalidate and refresh after saving from the API dropdown', async () => {
const user = userEvent.setup()
renderComponent()
@@ -614,11 +631,13 @@ describe('ExternalKnowledgeBaseCreate', () => {
const modalCall = mockSetShowExternalKnowledgeAPIModal.mock.calls[0]![0]
await modalCall.onSaveCallback()
- expect(mockMutateExternalKnowledgeApis).toHaveBeenCalled()
+ expect(mockInvalidateQueries).toHaveBeenCalledWith({
+ queryKey: externalKnowledgeApiQueryKey,
+ })
expect(mockRefresh).toHaveBeenCalled()
})
- it('should call mutate on cancel callback from ExternalApiSelect dropdown', async () => {
+ it('should not invalidate after canceling from the API dropdown', async () => {
const user = userEvent.setup()
renderComponent()
@@ -630,9 +649,8 @@ describe('ExternalKnowledgeBaseCreate', () => {
// Get the callback from the modal call and invoke it
const modalCall = mockSetShowExternalKnowledgeAPIModal.mock.calls[0]![0]
- modalCall.onCancelCallback()
-
- expect(mockMutateExternalKnowledgeApis).toHaveBeenCalled()
+ expect(modalCall.onCancelCallback).toBeUndefined()
+ expect(mockInvalidateQueries).not.toHaveBeenCalled()
})
it('should close dropdown after selecting an API', async () => {
diff --git a/web/app/components/datasets/list/__tests__/index.spec.tsx b/web/app/components/datasets/list/__tests__/index.spec.tsx
index b787406e41a..eeaedba150d 100644
--- a/web/app/components/datasets/list/__tests__/index.spec.tsx
+++ b/web/app/components/datasets/list/__tests__/index.spec.tsx
@@ -126,15 +126,6 @@ const renderList = (
const render = (ui: ReactElement) => renderList(ui)
const renderWithNuqs = renderList
-// Mock external api panel context
-const mockSetShowExternalApiPanel = vi.fn()
-vi.mock('@/context/external-api-panel-context', () => ({
- useExternalApiPanel: () => ({
- showExternalApiPanel: false,
- setShowExternalApiPanel: mockSetShowExternalApiPanel,
- }),
-}))
-
// Mock useDocumentTitle hook
vi.mock('@/hooks/use-document-title', () => ({
default: vi.fn(),
@@ -336,6 +327,24 @@ describe('List', () => {
expect(onUrlUpdate.mock.calls.at(-1)?.[0].searchParams.get('view')).toBe('new')
})
+ it('should reset each view panel when its owning list unmounts', async () => {
+ const user = userEvent.setup()
+ mockConsoleState.knowledgeFsEnabled = true
+ renderWithNuqs(
)
+
+ await user.click(screen.getByRole('button', { name: 'dataset.externalAPIPanelTitle' }))
+ expect(screen.getByTestId('external-api-panel')).toBeInTheDocument()
+
+ await user.click(screen.getByRole('button', { name: 'dataset.newKnowledge.new' }))
+ expect(screen.queryByTestId('external-api-panel')).not.toBeInTheDocument()
+
+ await user.click(screen.getByRole('button', { name: 'dataset.externalAPIPanelTitle' }))
+ expect(screen.getByTestId('external-api-panel')).toBeInTheDocument()
+
+ await user.click(screen.getByRole('button', { name: 'dataset.newKnowledge.legacy' }))
+ expect(screen.queryByTestId('external-api-panel')).not.toBeInTheDocument()
+ })
+
it('should restore the New Knowledge view from the URL', () => {
mockConsoleState.knowledgeFsEnabled = true
@@ -435,6 +444,7 @@ describe('List', () => {
render(
)
expect(screen.queryByText(/externalAPIPanelTitle/)).not.toBeInTheDocument()
+ expect(screen.queryByTestId('external-api-panel')).not.toBeInTheDocument()
})
})
@@ -483,7 +493,7 @@ describe('List', () => {
const button = screen.getByText(/externalAPIPanelTitle/)
fireEvent.click(button)
- expect(mockSetShowExternalApiPanel).toHaveBeenCalledWith(true)
+ expect(screen.getByTestId('external-api-panel')).toBeInTheDocument()
})
it('should update search input value', () => {
@@ -654,69 +664,15 @@ describe('List', () => {
}
})
- it('should show ExternalAPIPanel when showExternalApiPanel is true', async () => {
- // Re-mock to show external API panel
- vi.doMock('@/context/external-api-panel-context', () => ({
- useExternalApiPanel: () => ({
- showExternalApiPanel: true,
- setShowExternalApiPanel: mockSetShowExternalApiPanel,
- }),
- }))
+ it('should close ExternalAPIPanel when onClose is called', () => {
+ render(
)
- vi.resetModules()
- const { default: ListComponent } = await import('../index')
-
- render()
-
- expect(screen.getByTestId('external-api-panel')).toBeInTheDocument()
- expect(screen.getByTestId('external-api-panel')).toHaveAttribute(
- 'data-can-manage-external-knowledge-api',
- 'true',
- )
- })
-
- it('should not show ExternalAPIPanel without dataset.external.connect even when panel state is open', async () => {
- mockConsoleState = {
- isCurrentWorkspaceEditor: true,
- isCurrentWorkspaceManager: true,
- isCurrentWorkspaceOwner: true,
- knowledgeFsEnabled: false,
- workspacePermissionKeys: ['dataset.create_and_management'],
- }
- vi.doMock('@/context/external-api-panel-context', () => ({
- useExternalApiPanel: () => ({
- showExternalApiPanel: true,
- setShowExternalApiPanel: mockSetShowExternalApiPanel,
- }),
- }))
-
- vi.resetModules()
- const { default: ListComponent } = await import('../index')
-
- render()
+ fireEvent.click(screen.getByText(/externalAPIPanelTitle/))
+ fireEvent.click(screen.getByText('Close Panel'))
expect(screen.queryByTestId('external-api-panel')).not.toBeInTheDocument()
})
- it('should close ExternalAPIPanel when onClose is called', async () => {
- vi.doMock('@/context/external-api-panel-context', () => ({
- useExternalApiPanel: () => ({
- showExternalApiPanel: true,
- setShowExternalApiPanel: mockSetShowExternalApiPanel,
- }),
- }))
-
- vi.resetModules()
- const { default: ListComponent } = await import('../index')
-
- render()
-
- const closeButton = screen.getByText('Close Panel')
- fireEvent.click(closeButton)
-
- expect(mockSetShowExternalApiPanel).toHaveBeenCalledWith(false)
- })
-
it('should show TagManagementModal when tag management is opened', () => {
render(
)
fireEvent.click(screen.getByText('Manage Tags'))
diff --git a/web/app/components/datasets/list/index.tsx b/web/app/components/datasets/list/index.tsx
index ed907c3b304..f4dc7cb2a8e 100644
--- a/web/app/components/datasets/list/index.tsx
+++ b/web/app/components/datasets/list/index.tsx
@@ -18,7 +18,6 @@ import {
getStepByStepTourGuides,
STEP_BY_STEP_TOUR_TARGETS,
} from '@/app/components/step-by-step-tour/target-registry'
-import { useExternalApiPanel } from '@/context/external-api-panel-context'
import { workspacePermissionKeysAtom } from '@/context/permission-state'
import { isCurrentWorkspaceOwnerAtom } from '@/context/workspace-state'
import { NewKnowledgeList } from '@/features/new-rag/new-knowledge-list'
@@ -50,7 +49,7 @@ function LegacyList({
const { push } = useRouter()
const isCurrentWorkspaceOwner = useAtomValue(isCurrentWorkspaceOwnerAtom)
const [showTagManagementModal, setShowTagManagementModal] = useState(false)
- const { showExternalApiPanel, setShowExternalApiPanel } = useExternalApiPanel()
+ const [showExternalApiPanel, setShowExternalApiPanel] = useState(false)
const [includeAll, { toggle: toggleIncludeAll }] = useBoolean(false)
const invalidDatasetList = useInvalidDatasetList()
useDocumentTitle(t(($) => $.knowledge, { ns: 'dataset' }))
diff --git a/web/context/external-api-panel-context.tsx b/web/context/external-api-panel-context.tsx
deleted file mode 100644
index 5c6f0ad738b..00000000000
--- a/web/context/external-api-panel-context.tsx
+++ /dev/null
@@ -1,29 +0,0 @@
-'use client'
-
-import * as React from 'react'
-import { createContext, use, useState } from 'react'
-
-type ExternalApiPanelContextType = {
- showExternalApiPanel: boolean
- setShowExternalApiPanel: (show: boolean) => void
-}
-
-const ExternalApiPanelContext = createContext(undefined)
-
-export const ExternalApiPanelProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
- const [showExternalApiPanel, setShowExternalApiPanel] = useState(false)
-
- return (
-
- {children}
-
- )
-}
-
-export const useExternalApiPanel = () => {
- const context = use(ExternalApiPanelContext)
- if (context === undefined)
- throw new Error('useExternalApiPanel must be used within an ExternalApiPanelProvider')
-
- return context
-}
diff --git a/web/context/external-knowledge-api-context.tsx b/web/context/external-knowledge-api-context.tsx
deleted file mode 100644
index 0e3f40809c9..00000000000
--- a/web/context/external-knowledge-api-context.tsx
+++ /dev/null
@@ -1,57 +0,0 @@
-'use client'
-
-import type { FC, ReactNode } from 'react'
-import type { ExternalAPIItem, ExternalAPIListResponse } from '@/models/datasets'
-import { createContext, use, useCallback, useMemo } from 'react'
-import { useExternalKnowledgeApiList } from '@/service/knowledge/use-dataset'
-
-type ExternalKnowledgeApiContextType = {
- externalKnowledgeApiList: ExternalAPIItem[]
- mutateExternalKnowledgeApis: () => Promise
- isLoading: boolean
-}
-
-const ExternalKnowledgeApiContext = createContext(
- undefined,
-)
-
-type ExternalKnowledgeApiProviderProps = {
- children: ReactNode
- enabled?: boolean
-}
-
-export const ExternalKnowledgeApiProvider: FC = ({
- children,
- enabled = true,
-}) => {
- const { data, refetch, isLoading } = useExternalKnowledgeApiList({ enabled })
-
- const mutateExternalKnowledgeApis = useCallback(() => {
- if (!enabled) return Promise.resolve(undefined)
-
- return refetch().then((res) => res.data)
- }, [enabled, refetch])
-
- const contextValue = useMemo(
- () => ({
- externalKnowledgeApiList: data?.data || [],
- mutateExternalKnowledgeApis,
- isLoading,
- }),
- [data, mutateExternalKnowledgeApis, isLoading],
- )
-
- return (
-
- {children}
-
- )
-}
-
-export const useExternalKnowledgeApi = () => {
- const context = use(ExternalKnowledgeApiContext)
- if (context === undefined)
- throw new Error('useExternalKnowledgeApi must be used within a ExternalKnowledgeApiProvider')
-
- return context
-}
diff --git a/web/features/new-rag/__tests__/new-knowledge-list.spec.tsx b/web/features/new-rag/__tests__/new-knowledge-list.spec.tsx
index fe0acf093cd..1b98fbf24d6 100644
--- a/web/features/new-rag/__tests__/new-knowledge-list.spec.tsx
+++ b/web/features/new-rag/__tests__/new-knowledge-list.spec.tsx
@@ -16,10 +16,6 @@ type ListKnowledgeSpacesInfiniteOptions = {
}
}
-const externalApiPanelMock = vi.hoisted(() => ({
- open: false,
- setOpen: vi.fn(),
-}))
const toastInfoMock = vi.hoisted(() => vi.fn())
vi.mock('@langgenius/dify-ui/toast', () => ({
@@ -46,13 +42,6 @@ const permissionStateMock = vi.hoisted(() => ({
workspacePermissionKeysAtom: Symbol('workspacePermissionKeysAtom'),
}))
-vi.mock('@/context/external-api-panel-context', () => ({
- useExternalApiPanel: () => ({
- showExternalApiPanel: externalApiPanelMock.open,
- setShowExternalApiPanel: externalApiPanelMock.setOpen,
- }),
-}))
-
vi.mock('@/service/knowledge/use-dataset', () => ({
useDatasetApiBaseUrl: () => ({ data: { api_base_url: 'https://api.example.com' } }),
}))
@@ -62,7 +51,14 @@ vi.mock('@/app/components/datasets/extra-info/service-api', () => ({
}))
vi.mock('@/app/components/datasets/external-api/external-api-panel', () => ({
- default: () => external API panel
,
+ default: ({ onClose }: { onClose: () => void }) => (
+
+ external API panel
+
+
+ ),
}))
vi.mock('@tanstack/react-query', async (importOriginal) => {
@@ -109,7 +105,6 @@ const setResolvedPage = (items: KnowledgeSpaceList['items'] = []) => {
describe('NewKnowledgeList', () => {
beforeEach(() => {
vi.clearAllMocks()
- externalApiPanelMock.open = false
queryMock.data = undefined
queryMock.error = null
queryMock.hasNextPage = false
@@ -219,7 +214,9 @@ describe('NewKnowledgeList', () => {
renderWithNuqs()
await user.click(screen.getByRole('button', { name: 'dataset.externalAPIPanelTitle' }))
- expect(externalApiPanelMock.setOpen).toHaveBeenCalledWith(true)
+ expect(screen.getByText('external API panel')).toBeInTheDocument()
+ await user.click(screen.getByRole('button', { name: 'close external API panel' }))
+ expect(screen.queryByText('external API panel')).not.toBeInTheDocument()
expect(screen.getByRole('button', { name: 'dataset.serviceApi.title' })).toBeInTheDocument()
const tags = screen.getByRole('button', { name: 'dataset.newKnowledge.tags' })
@@ -296,6 +293,10 @@ describe('NewKnowledgeList', () => {
expect(
screen.queryByRole('link', { name: /common\.operation\.create/ }),
).not.toBeInTheDocument()
+ expect(
+ screen.queryByRole('button', { name: 'dataset.externalAPIPanelTitle' }),
+ ).not.toBeInTheDocument()
+ expect(screen.queryByText('external API panel')).not.toBeInTheDocument()
expect(screen.getByText('dataset.newKnowledge.readOnlyEmpty')).toBeInTheDocument()
})
diff --git a/web/features/new-rag/new-knowledge-list.tsx b/web/features/new-rag/new-knowledge-list.tsx
index c63f44dc059..eadea7beafb 100644
--- a/web/features/new-rag/new-knowledge-list.tsx
+++ b/web/features/new-rag/new-knowledge-list.tsx
@@ -9,7 +9,6 @@ import { useTranslation } from 'react-i18next'
import { SearchInput } from '@/app/components/base/search-input'
import ExternalAPIPanel from '@/app/components/datasets/external-api/external-api-panel'
import ServiceApi from '@/app/components/datasets/extra-info/service-api'
-import { useExternalApiPanel } from '@/context/external-api-panel-context'
import { workspacePermissionKeysAtom } from '@/context/permission-state'
import Link from '@/next/link'
import { consoleQuery } from '@/service/client'
@@ -60,7 +59,7 @@ export function NewKnowledgeList({
const { t } = useTranslation('dataset')
const { t: tCommon } = useTranslation('common')
const { data: apiBaseInfo } = useDatasetApiBaseUrl()
- const { showExternalApiPanel, setShowExternalApiPanel } = useExternalApiPanel()
+ const [showExternalApiPanel, setShowExternalApiPanel] = useState(false)
const workspacePermissionKeys = useAtomValue(workspacePermissionKeysAtom)
const canCreate = hasPermission(workspacePermissionKeys, 'dataset.create_and_management')
const canConnect = hasPermission(workspacePermissionKeys, 'dataset.external.connect')
diff --git a/web/models/datasets.ts b/web/models/datasets.ts
index d6fe1f6c571..5d4503e932a 100644
--- a/web/models/datasets.ts
+++ b/web/models/datasets.ts
@@ -223,14 +223,6 @@ export type DataSetListResponse = {
total: number
}
-export type ExternalAPIListResponse = {
- data: ExternalAPIItem[]
- has_more: boolean
- limit: number
- page: number
- total: number
-}
-
export type QA = {
question: string
answer: string
diff --git a/web/service/knowledge/use-dataset.ts b/web/service/knowledge/use-dataset.ts
index f2919db02fd..d4f42db4859 100644
--- a/web/service/knowledge/use-dataset.ts
+++ b/web/service/knowledge/use-dataset.ts
@@ -6,7 +6,6 @@ import type {
DatasetListRequest,
DataSetListResponse,
ErrorDocsResponse,
- ExternalAPIListResponse,
FetchDatasetsParams,
HitTestingRecordsResponse,
IndexingStatusBatchRequest,
@@ -189,14 +188,6 @@ export const useInvalidateDatasetApiKeys = () => {
}
}
-export const useExternalKnowledgeApiList = (options?: { enabled?: boolean }) => {
- return useQuery({
- queryKey: [NAME_SPACE, 'external-knowledge-api'],
- queryFn: () => get('/datasets/external-knowledge-api'),
- enabled: options?.enabled ?? true,
- })
-}
-
export const useDatasetTestingRecords = (
datasetId?: string,
params?: { page: number; limit: number },