mirror of
https://github.com/langgenius/dify.git
synced 2026-05-26 04:00:39 -04:00
fix: improve API extension dialog controls (#36323)
This commit is contained in:
@@ -59,7 +59,6 @@ const defaultProviderContext = {
|
||||
|
||||
const defaultModalContext: ModalContextState = {
|
||||
setShowAccountSettingModal: noop,
|
||||
setShowApiBasedExtensionModal: noop,
|
||||
setShowModerationSettingModal: noop,
|
||||
setShowExternalDataToolModal: noop,
|
||||
setShowPricingModal: noop,
|
||||
|
||||
@@ -55,7 +55,6 @@ const mockUseProviderContext = vi.fn<() => ProviderContextState>()
|
||||
|
||||
const buildModalContext = (): ModalContextState => ({
|
||||
setShowAccountSettingModal: mockSetShowAccountSettingModal,
|
||||
setShowApiBasedExtensionModal: vi.fn(),
|
||||
setShowModerationSettingModal: vi.fn(),
|
||||
setShowExternalDataToolModal: vi.fn(),
|
||||
setShowPricingModal: mockSetShowPricingModal,
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
import type { ApiBasedExtensionResponse } from '@dify/contracts/api/console/api-based-extension/types.gen'
|
||||
import type { SetStateAction } from 'react'
|
||||
import type { ModalContextState, ModalState } from '@/context/modal-context'
|
||||
import { fireEvent, render, screen } from '@testing-library/react'
|
||||
import { useModalContext } from '@/context/modal-context'
|
||||
import { fireEvent, render, screen, waitFor } from '@testing-library/react'
|
||||
import { addApiBasedExtension, updateApiBasedExtension } from '@/service/common'
|
||||
import { useApiBasedExtensions } from '@/service/use-common'
|
||||
import ApiBasedExtensionPage from '../index'
|
||||
|
||||
@@ -10,19 +8,16 @@ vi.mock('@/service/use-common', () => ({
|
||||
useApiBasedExtensions: vi.fn(),
|
||||
}))
|
||||
|
||||
vi.mock('@/context/modal-context', () => ({
|
||||
useModalContext: vi.fn(),
|
||||
vi.mock('@/service/common', () => ({
|
||||
addApiBasedExtension: vi.fn(),
|
||||
updateApiBasedExtension: vi.fn(),
|
||||
}))
|
||||
|
||||
describe('ApiBasedExtensionPage', () => {
|
||||
const mockRefetch = vi.fn<() => void>()
|
||||
const mockSetShowApiBasedExtensionModal = vi.fn<(value: SetStateAction<ModalState<Partial<ApiBasedExtensionResponse>> | null>) => void>()
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
vi.mocked(useModalContext).mockReturnValue({
|
||||
setShowApiBasedExtensionModal: mockSetShowApiBasedExtensionModal,
|
||||
} as unknown as ModalContextState)
|
||||
})
|
||||
|
||||
describe('Rendering', () => {
|
||||
@@ -128,13 +123,17 @@ describe('ApiBasedExtensionPage', () => {
|
||||
fireEvent.click(screen.getByText('common.apiBasedExtension.add'))
|
||||
|
||||
// Assert
|
||||
expect(mockSetShowApiBasedExtensionModal).toHaveBeenCalledWith(expect.objectContaining({
|
||||
payload: {},
|
||||
}))
|
||||
expect(screen.getByRole('dialog', { name: 'common.apiBasedExtension.modal.title' })).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should call refetch when onSaveCallback is executed from the modal', () => {
|
||||
it('should call refetch when add modal saves successfully', async () => {
|
||||
// Arrange
|
||||
vi.mocked(addApiBasedExtension).mockResolvedValue({
|
||||
id: 'new-id',
|
||||
name: 'New Ext',
|
||||
api_endpoint: 'https://api.test',
|
||||
api_key: 'secret-key',
|
||||
})
|
||||
vi.mocked(useApiBasedExtensions).mockReturnValue({
|
||||
data: [],
|
||||
isPending: false,
|
||||
@@ -144,25 +143,23 @@ describe('ApiBasedExtensionPage', () => {
|
||||
// Act
|
||||
render(<ApiBasedExtensionPage />)
|
||||
fireEvent.click(screen.getByText('common.apiBasedExtension.add'))
|
||||
fireEvent.change(screen.getByPlaceholderText('common.apiBasedExtension.modal.name.placeholder'), { target: { value: 'New Ext' } })
|
||||
fireEvent.change(screen.getByPlaceholderText('common.apiBasedExtension.modal.apiEndpoint.placeholder'), { target: { value: 'https://api.test' } })
|
||||
fireEvent.change(screen.getByPlaceholderText('common.apiBasedExtension.modal.apiKey.placeholder'), { target: { value: 'secret-key' } })
|
||||
fireEvent.click(screen.getByText('common.operation.save'))
|
||||
|
||||
// Trigger callback manually from the mock call
|
||||
const callArgs = mockSetShowApiBasedExtensionModal.mock.calls[0]![0]
|
||||
if (typeof callArgs === 'object' && callArgs !== null && 'onSaveCallback' in callArgs) {
|
||||
if (callArgs.onSaveCallback) {
|
||||
callArgs.onSaveCallback()
|
||||
// Assert
|
||||
expect(mockRefetch).toHaveBeenCalled()
|
||||
}
|
||||
}
|
||||
// Assert
|
||||
await waitFor(() => {
|
||||
expect(mockRefetch).toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
|
||||
it('should call refetch when an item is updated', () => {
|
||||
it('should call refetch when an item is updated', async () => {
|
||||
// Arrange
|
||||
const mockData: ApiBasedExtensionResponse[] = [
|
||||
{ id: '1', name: 'Extension 1', api_endpoint: 'url1', api_key: 'key1' },
|
||||
]
|
||||
const extension: ApiBasedExtensionResponse = { id: '1', name: 'Extension 1', api_endpoint: 'url1', api_key: 'long-api-key' }
|
||||
vi.mocked(updateApiBasedExtension).mockResolvedValue({ ...extension, name: 'Updated' })
|
||||
vi.mocked(useApiBasedExtensions).mockReturnValue({
|
||||
data: mockData,
|
||||
data: [extension],
|
||||
isPending: false,
|
||||
refetch: mockRefetch,
|
||||
} as unknown as ReturnType<typeof useApiBasedExtensions>)
|
||||
@@ -171,16 +168,12 @@ describe('ApiBasedExtensionPage', () => {
|
||||
|
||||
// Act - Click edit on the rendered item
|
||||
fireEvent.click(screen.getByText('common.operation.edit'))
|
||||
|
||||
// Retrieve the onSaveCallback from the modal call and execute it
|
||||
const callArgs = mockSetShowApiBasedExtensionModal.mock.calls[0]![0]
|
||||
if (typeof callArgs === 'object' && callArgs !== null && 'onSaveCallback' in callArgs) {
|
||||
if (callArgs.onSaveCallback)
|
||||
callArgs.onSaveCallback()
|
||||
}
|
||||
fireEvent.click(screen.getByText('common.operation.save'))
|
||||
|
||||
// Assert
|
||||
expect(mockRefetch).toHaveBeenCalled()
|
||||
await waitFor(() => {
|
||||
expect(mockRefetch).toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,17 +1,10 @@
|
||||
import type { ApiBasedExtensionResponse } from '@dify/contracts/api/console/api-based-extension/types.gen'
|
||||
import type { TFunction } from 'i18next'
|
||||
import type { ModalContextState } from '@/context/modal-context'
|
||||
import { fireEvent, render, screen, waitFor, within } from '@testing-library/react'
|
||||
import * as reactI18next from 'react-i18next'
|
||||
import { useModalContext } from '@/context/modal-context'
|
||||
import { deleteApiBasedExtension } from '@/service/common'
|
||||
import Item from '../item'
|
||||
|
||||
// Mock dependencies
|
||||
vi.mock('@/context/modal-context', () => ({
|
||||
useModalContext: vi.fn(),
|
||||
}))
|
||||
|
||||
vi.mock('@/service/common', () => ({
|
||||
deleteApiBasedExtension: vi.fn(),
|
||||
}))
|
||||
@@ -24,19 +17,16 @@ describe('Item Component', () => {
|
||||
api_key: 'test-api-key',
|
||||
}
|
||||
const mockOnUpdate = vi.fn()
|
||||
const mockSetShowApiBasedExtensionModal = vi.fn()
|
||||
const mockOnEdit = vi.fn()
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
vi.mocked(useModalContext).mockReturnValue({
|
||||
setShowApiBasedExtensionModal: mockSetShowApiBasedExtensionModal,
|
||||
} as unknown as ModalContextState)
|
||||
})
|
||||
|
||||
describe('Rendering', () => {
|
||||
it('should render extension data correctly', () => {
|
||||
// Act
|
||||
render(<Item data={mockData} onUpdate={mockOnUpdate} />)
|
||||
render(<Item data={mockData} onEdit={mockOnEdit} onUpdate={mockOnUpdate} />)
|
||||
|
||||
// Assert
|
||||
// Assert
|
||||
@@ -54,7 +44,7 @@ describe('Item Component', () => {
|
||||
}
|
||||
|
||||
// Act
|
||||
render(<Item data={minimalData} onUpdate={mockOnUpdate} />)
|
||||
render(<Item data={minimalData} onEdit={mockOnEdit} onUpdate={mockOnUpdate} />)
|
||||
|
||||
// Assert
|
||||
// Assert
|
||||
@@ -64,41 +54,20 @@ describe('Item Component', () => {
|
||||
})
|
||||
|
||||
describe('Modal Interactions', () => {
|
||||
it('should open edit modal with correct payload when clicking edit button', () => {
|
||||
it('should request editing with the current extension when clicking edit button', () => {
|
||||
// Act
|
||||
render(<Item data={mockData} onUpdate={mockOnUpdate} />)
|
||||
render(<Item data={mockData} onEdit={mockOnEdit} onUpdate={mockOnUpdate} />)
|
||||
fireEvent.click(screen.getByText('common.operation.edit'))
|
||||
|
||||
// Assert
|
||||
expect(mockSetShowApiBasedExtensionModal).toHaveBeenCalledWith(expect.objectContaining({
|
||||
payload: mockData,
|
||||
}))
|
||||
const lastCall = mockSetShowApiBasedExtensionModal.mock.calls[0]![0]
|
||||
if (typeof lastCall === 'object' && lastCall !== null && 'onSaveCallback' in lastCall)
|
||||
expect(lastCall.onSaveCallback).toBeInstanceOf(Function)
|
||||
})
|
||||
|
||||
it('should execute onUpdate callback when edit modal save callback is invoked', () => {
|
||||
// Act
|
||||
render(<Item data={mockData} onUpdate={mockOnUpdate} />)
|
||||
fireEvent.click(screen.getByText('common.operation.edit'))
|
||||
|
||||
// Assert
|
||||
const modalCallArg = mockSetShowApiBasedExtensionModal.mock.calls[0]![0]
|
||||
if (typeof modalCallArg === 'object' && modalCallArg !== null && 'onSaveCallback' in modalCallArg) {
|
||||
const onSaveCallback = modalCallArg.onSaveCallback
|
||||
if (onSaveCallback) {
|
||||
onSaveCallback()
|
||||
expect(mockOnUpdate).toHaveBeenCalledTimes(1)
|
||||
}
|
||||
}
|
||||
expect(mockOnEdit).toHaveBeenCalledWith(mockData)
|
||||
})
|
||||
})
|
||||
|
||||
describe('Deletion', () => {
|
||||
it('should show delete confirmation dialog when clicking delete button', () => {
|
||||
// Act
|
||||
render(<Item data={mockData} onUpdate={mockOnUpdate} />)
|
||||
render(<Item data={mockData} onEdit={mockOnEdit} onUpdate={mockOnUpdate} />)
|
||||
fireEvent.click(screen.getByText('common.operation.delete'))
|
||||
|
||||
// Assert
|
||||
@@ -109,7 +78,7 @@ describe('Item Component', () => {
|
||||
it('should call delete API and triggers onUpdate when confirming deletion', async () => {
|
||||
// Arrange
|
||||
vi.mocked(deleteApiBasedExtension).mockResolvedValue({ result: 'success' })
|
||||
render(<Item data={mockData} onUpdate={mockOnUpdate} />)
|
||||
render(<Item data={mockData} onEdit={mockOnEdit} onUpdate={mockOnUpdate} />)
|
||||
|
||||
// Act
|
||||
fireEvent.click(screen.getByText('common.operation.delete'))
|
||||
@@ -129,7 +98,7 @@ describe('Item Component', () => {
|
||||
it('should hide delete confirmation dialog after successful deletion', async () => {
|
||||
// Arrange
|
||||
vi.mocked(deleteApiBasedExtension).mockResolvedValue({ result: 'success' })
|
||||
render(<Item data={mockData} onUpdate={mockOnUpdate} />)
|
||||
render(<Item data={mockData} onEdit={mockOnEdit} onUpdate={mockOnUpdate} />)
|
||||
|
||||
// Act
|
||||
fireEvent.click(screen.getByText('common.operation.delete'))
|
||||
@@ -147,7 +116,7 @@ describe('Item Component', () => {
|
||||
|
||||
it('should close delete confirmation when clicking cancel button', async () => {
|
||||
// Act
|
||||
render(<Item data={mockData} onUpdate={mockOnUpdate} />)
|
||||
render(<Item data={mockData} onEdit={mockOnEdit} onUpdate={mockOnUpdate} />)
|
||||
fireEvent.click(screen.getByText('common.operation.delete'))
|
||||
fireEvent.click(screen.getByText('common.operation.cancel'))
|
||||
|
||||
@@ -159,7 +128,7 @@ describe('Item Component', () => {
|
||||
|
||||
it('should not call delete API when canceling deletion', () => {
|
||||
// Act
|
||||
render(<Item data={mockData} onUpdate={mockOnUpdate} />)
|
||||
render(<Item data={mockData} onEdit={mockOnEdit} onUpdate={mockOnUpdate} />)
|
||||
fireEvent.click(screen.getByText('common.operation.delete'))
|
||||
fireEvent.click(screen.getByText('common.operation.cancel'))
|
||||
|
||||
@@ -188,7 +157,7 @@ describe('Item Component', () => {
|
||||
} as unknown as ReturnType<typeof reactI18next.useTranslation>)
|
||||
|
||||
// Act
|
||||
render(<Item data={mockData} onUpdate={mockOnUpdate} />)
|
||||
render(<Item data={mockData} onEdit={mockOnEdit} onUpdate={mockOnUpdate} />)
|
||||
const allButtons = screen.getAllByRole('button')
|
||||
const editBtn = screen.getByText('operation.edit')
|
||||
const deleteBtn = allButtons.find(btn => btn !== editBtn)
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { ApiBasedExtensionResponse } from '@dify/contracts/api/console/api-based-extension/types.gen'
|
||||
import type { TFunction } from 'i18next'
|
||||
import type { ReactElement } from 'react'
|
||||
import type { ComponentProps, ReactElement } from 'react'
|
||||
import { fireEvent, render as RTLRender, screen, waitFor } from '@testing-library/react'
|
||||
import * as reactI18next from 'react-i18next'
|
||||
import { useDocLink } from '@/context/i18n'
|
||||
@@ -34,7 +34,7 @@ vi.mock('@langgenius/dify-ui/toast', () => ({
|
||||
}))
|
||||
|
||||
describe('ApiBasedExtensionModal', () => {
|
||||
const mockOnCancel = vi.fn()
|
||||
const mockOnOpenChange = vi.fn()
|
||||
const mockOnSave = vi.fn()
|
||||
const mockDocLink = vi.fn((path?: string) => `https://docs.dify.ai${path || ''}`)
|
||||
const mockExtension = (overrides: Partial<ApiBasedExtensionResponse> = {}): ApiBasedExtensionResponse => ({
|
||||
@@ -46,6 +46,19 @@ describe('ApiBasedExtensionModal', () => {
|
||||
})
|
||||
|
||||
const render = (ui: ReactElement) => RTLRender(ui)
|
||||
const renderModal = (props: Partial<ComponentProps<typeof ApiBasedExtensionModal>> = {}) => render(
|
||||
<ApiBasedExtensionModal
|
||||
open
|
||||
extension={{}}
|
||||
onOpenChange={mockOnOpenChange}
|
||||
onSave={mockOnSave}
|
||||
{...props}
|
||||
/>,
|
||||
)
|
||||
const expectCloseRequested = () => {
|
||||
const calls = mockOnOpenChange.mock.calls
|
||||
expect(calls[calls.length - 1]?.[0]).toBe(false)
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
@@ -55,9 +68,10 @@ describe('ApiBasedExtensionModal', () => {
|
||||
describe('Rendering', () => {
|
||||
it('should render correctly for adding a new extension', () => {
|
||||
// Act
|
||||
render(<ApiBasedExtensionModal data={{}} onCancel={mockOnCancel} onSave={mockOnSave} />)
|
||||
renderModal()
|
||||
|
||||
// Assert
|
||||
expect(screen.getByRole('dialog', { name: 'common.apiBasedExtension.modal.title' })).toBeInTheDocument()
|
||||
expect(screen.getByText('common.apiBasedExtension.modal.title')).toBeInTheDocument()
|
||||
expect(screen.getByPlaceholderText('common.apiBasedExtension.modal.name.placeholder')).toBeInTheDocument()
|
||||
expect(screen.getByPlaceholderText('common.apiBasedExtension.modal.apiEndpoint.placeholder')).toBeInTheDocument()
|
||||
@@ -69,7 +83,7 @@ describe('ApiBasedExtensionModal', () => {
|
||||
const data = mockExtension()
|
||||
|
||||
// Act
|
||||
render(<ApiBasedExtensionModal data={data} onCancel={mockOnCancel} onSave={mockOnSave} />)
|
||||
renderModal({ extension: data })
|
||||
|
||||
// Assert
|
||||
expect(screen.getByText('common.apiBasedExtension.modal.editTitle')).toBeInTheDocument()
|
||||
@@ -77,6 +91,14 @@ describe('ApiBasedExtensionModal', () => {
|
||||
expect(screen.getByDisplayValue('url')).toBeInTheDocument()
|
||||
expect(screen.getByDisplayValue('key')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should not render dialog content when closed', () => {
|
||||
// Act
|
||||
renderModal({ open: false })
|
||||
|
||||
// Assert
|
||||
expect(screen.queryByRole('dialog')).not.toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
describe('Form Submissions', () => {
|
||||
@@ -89,7 +111,7 @@ describe('ApiBasedExtensionModal', () => {
|
||||
api_key: 'secret-key',
|
||||
})
|
||||
vi.mocked(addApiBasedExtension).mockResolvedValue(newExtension)
|
||||
render(<ApiBasedExtensionModal data={{}} onCancel={mockOnCancel} onSave={mockOnSave} />)
|
||||
renderModal()
|
||||
|
||||
// Act
|
||||
fireEvent.change(screen.getByPlaceholderText('common.apiBasedExtension.modal.name.placeholder'), { target: { value: 'New Ext' } })
|
||||
@@ -115,7 +137,7 @@ describe('ApiBasedExtensionModal', () => {
|
||||
// Arrange
|
||||
const data = mockExtension({ api_key: 'long-secret-key' })
|
||||
vi.mocked(updateApiBasedExtension).mockResolvedValue({ ...data, name: 'Updated' })
|
||||
render(<ApiBasedExtensionModal data={data} onCancel={mockOnCancel} onSave={mockOnSave} />)
|
||||
renderModal({ extension: data })
|
||||
|
||||
// Act
|
||||
fireEvent.change(screen.getByDisplayValue('Existing'), { target: { value: 'Updated' } })
|
||||
@@ -125,12 +147,11 @@ describe('ApiBasedExtensionModal', () => {
|
||||
await waitFor(() => {
|
||||
expect(updateApiBasedExtension).toHaveBeenCalledWith({
|
||||
url: '/api-based-extension/1',
|
||||
body: expect.objectContaining({
|
||||
id: '1',
|
||||
body: {
|
||||
name: 'Updated',
|
||||
api_endpoint: 'url',
|
||||
api_key: '[__HIDDEN__]',
|
||||
}),
|
||||
},
|
||||
})
|
||||
expect(mockToast.success).toHaveBeenCalledWith('common.actionMsg.modifiedSuccessfully')
|
||||
expect(mockOnSave).toHaveBeenCalled()
|
||||
@@ -141,7 +162,7 @@ describe('ApiBasedExtensionModal', () => {
|
||||
// Arrange
|
||||
const data = mockExtension({ api_key: 'old-key' })
|
||||
vi.mocked(updateApiBasedExtension).mockResolvedValue({ ...data, api_key: 'new-longer-key' })
|
||||
render(<ApiBasedExtensionModal data={data} onCancel={mockOnCancel} onSave={mockOnSave} />)
|
||||
renderModal({ extension: data })
|
||||
|
||||
// Act
|
||||
fireEvent.change(screen.getByDisplayValue('old-key'), { target: { value: 'new-longer-key' } })
|
||||
@@ -151,9 +172,11 @@ describe('ApiBasedExtensionModal', () => {
|
||||
await waitFor(() => {
|
||||
expect(updateApiBasedExtension).toHaveBeenCalledWith({
|
||||
url: '/api-based-extension/1',
|
||||
body: expect.objectContaining({
|
||||
body: {
|
||||
name: 'Existing',
|
||||
api_endpoint: 'url',
|
||||
api_key: 'new-longer-key',
|
||||
}),
|
||||
},
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -162,7 +185,7 @@ describe('ApiBasedExtensionModal', () => {
|
||||
describe('Validation', () => {
|
||||
it('should show error if api key is too short', async () => {
|
||||
// Arrange
|
||||
render(<ApiBasedExtensionModal data={{}} onCancel={mockOnCancel} onSave={mockOnSave} />)
|
||||
renderModal()
|
||||
|
||||
// Act
|
||||
fireEvent.change(screen.getByPlaceholderText('common.apiBasedExtension.modal.name.placeholder'), { target: { value: 'Ext' } })
|
||||
@@ -180,7 +203,7 @@ describe('ApiBasedExtensionModal', () => {
|
||||
it('should work when onSave is not provided', async () => {
|
||||
// Arrange
|
||||
vi.mocked(addApiBasedExtension).mockResolvedValue(mockExtension({ id: 'new-id' }))
|
||||
render(<ApiBasedExtensionModal data={{}} onCancel={mockOnCancel} />)
|
||||
renderModal({ onSave: undefined })
|
||||
|
||||
// Act
|
||||
fireEvent.change(screen.getByPlaceholderText('common.apiBasedExtension.modal.name.placeholder'), { target: { value: 'New Ext' } })
|
||||
@@ -194,15 +217,56 @@ describe('ApiBasedExtensionModal', () => {
|
||||
})
|
||||
})
|
||||
|
||||
it('should call onCancel when clicking cancel button', () => {
|
||||
it('should request closing when clicking cancel button', () => {
|
||||
// Arrange
|
||||
render(<ApiBasedExtensionModal data={{}} onCancel={mockOnCancel} onSave={mockOnSave} />)
|
||||
renderModal()
|
||||
|
||||
// Act
|
||||
fireEvent.click(screen.getByText('common.operation.cancel'))
|
||||
|
||||
// Assert
|
||||
expect(mockOnCancel).toHaveBeenCalled()
|
||||
expectCloseRequested()
|
||||
})
|
||||
|
||||
it('should request closing when clicking close button', async () => {
|
||||
// Arrange
|
||||
renderModal()
|
||||
|
||||
// Act
|
||||
fireEvent.click(screen.getByRole('button', { name: 'Close' }))
|
||||
|
||||
// Assert
|
||||
await waitFor(() => {
|
||||
expectCloseRequested()
|
||||
})
|
||||
})
|
||||
|
||||
it('should request closing when pressing Escape', async () => {
|
||||
// Arrange
|
||||
renderModal()
|
||||
|
||||
// Act
|
||||
fireEvent.keyDown(document, { key: 'Escape' })
|
||||
|
||||
// Assert
|
||||
await waitFor(() => {
|
||||
expectCloseRequested()
|
||||
})
|
||||
})
|
||||
|
||||
it('should keep open when clicking outside the dialog', () => {
|
||||
// Arrange
|
||||
renderModal()
|
||||
|
||||
// Act
|
||||
const backdrop = document.querySelector('.bg-background-overlay')
|
||||
expect(backdrop).toBeInTheDocument()
|
||||
fireEvent.pointerDown(backdrop!)
|
||||
fireEvent.pointerUp(backdrop!)
|
||||
fireEvent.click(backdrop!)
|
||||
|
||||
// Assert
|
||||
expect(mockOnOpenChange).not.toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
|
||||
@@ -230,7 +294,7 @@ describe('ApiBasedExtensionModal', () => {
|
||||
} as unknown as ReturnType<typeof reactI18next.useTranslation>)
|
||||
|
||||
// Act
|
||||
const { container } = render(<ApiBasedExtensionModal data={{}} onCancel={mockOnCancel} />)
|
||||
const { container } = renderModal({ onSave: undefined })
|
||||
|
||||
// Assert
|
||||
const inputs = container.querySelectorAll('input')
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import type { ApiBasedExtensionResponse } from '@dify/contracts/api/console/api-based-extension/types.gen'
|
||||
import type { UseQueryResult } from '@tanstack/react-query'
|
||||
import type { ModalContextState } from '@/context/modal-context'
|
||||
import { fireEvent, render, screen } from '@testing-library/react'
|
||||
import { fireEvent, render, screen, waitFor } from '@testing-library/react'
|
||||
import { ACCOUNT_SETTING_TAB } from '@/app/components/header/account-setting/constants'
|
||||
import { useModalContext } from '@/context/modal-context'
|
||||
import { addApiBasedExtension } from '@/service/common'
|
||||
import { useApiBasedExtensions } from '@/service/use-common'
|
||||
import ApiBasedExtensionSelector from '../selector'
|
||||
|
||||
@@ -15,12 +16,15 @@ vi.mock('@/service/use-common', () => ({
|
||||
useApiBasedExtensions: vi.fn(),
|
||||
}))
|
||||
|
||||
vi.mock('@/service/common', () => ({
|
||||
addApiBasedExtension: vi.fn(),
|
||||
}))
|
||||
|
||||
vi.mock('@langgenius/dify-ui/popover', async () => await import('@/__mocks__/base-ui-popover'))
|
||||
|
||||
describe('ApiBasedExtensionSelector', () => {
|
||||
const mockOnChange = vi.fn()
|
||||
const mockSetShowAccountSettingModal = vi.fn()
|
||||
const mockSetShowApiBasedExtensionModal = vi.fn()
|
||||
const mockRefetch = vi.fn()
|
||||
|
||||
const mockData: ApiBasedExtensionResponse[] = [
|
||||
@@ -32,7 +36,6 @@ describe('ApiBasedExtensionSelector', () => {
|
||||
vi.clearAllMocks()
|
||||
vi.mocked(useModalContext).mockReturnValue({
|
||||
setShowAccountSettingModal: mockSetShowAccountSettingModal,
|
||||
setShowApiBasedExtensionModal: mockSetShowApiBasedExtensionModal,
|
||||
} as unknown as ModalContextState)
|
||||
vi.mocked(useApiBasedExtensions).mockReturnValue({
|
||||
data: mockData,
|
||||
@@ -103,26 +106,29 @@ describe('ApiBasedExtensionSelector', () => {
|
||||
})
|
||||
|
||||
it('should open add modal when clicking add button and refetches on save', async () => {
|
||||
// Arrange
|
||||
vi.mocked(addApiBasedExtension).mockResolvedValue({
|
||||
id: 'new-id',
|
||||
name: 'New Ext',
|
||||
api_endpoint: 'https://api.test',
|
||||
api_key: 'secret-key',
|
||||
})
|
||||
|
||||
// Act
|
||||
render(<ApiBasedExtensionSelector value="" onChange={mockOnChange} />)
|
||||
fireEvent.click(screen.getByText('common.apiBasedExtension.selector.placeholder'))
|
||||
|
||||
const addButton = await screen.findByText('common.operation.add')
|
||||
fireEvent.click(addButton)
|
||||
fireEvent.change(screen.getByPlaceholderText('common.apiBasedExtension.modal.name.placeholder'), { target: { value: 'New Ext' } })
|
||||
fireEvent.change(screen.getByPlaceholderText('common.apiBasedExtension.modal.apiEndpoint.placeholder'), { target: { value: 'https://api.test' } })
|
||||
fireEvent.change(screen.getByPlaceholderText('common.apiBasedExtension.modal.apiKey.placeholder'), { target: { value: 'secret-key' } })
|
||||
fireEvent.click(screen.getByText('common.operation.save'))
|
||||
|
||||
// Assert
|
||||
expect(mockSetShowApiBasedExtensionModal).toHaveBeenCalledWith(expect.objectContaining({
|
||||
payload: {},
|
||||
}))
|
||||
|
||||
// Trigger callback
|
||||
const lastCall = mockSetShowApiBasedExtensionModal.mock.calls[0]![0]
|
||||
if (typeof lastCall === 'object' && lastCall !== null && 'onSaveCallback' in lastCall) {
|
||||
if (lastCall.onSaveCallback) {
|
||||
lastCall.onSaveCallback()
|
||||
expect(mockRefetch).toHaveBeenCalled()
|
||||
}
|
||||
}
|
||||
await waitFor(() => {
|
||||
expect(mockRefetch).toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,24 +1,42 @@
|
||||
import type { ApiBasedExtensionResponse } from '@dify/contracts/api/console/api-based-extension/types.gen'
|
||||
import { Button } from '@langgenius/dify-ui/button'
|
||||
import {
|
||||
RiAddLine,
|
||||
} from '@remixicon/react'
|
||||
import { useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useModalContext } from '@/context/modal-context'
|
||||
import { useApiBasedExtensions } from '@/service/use-common'
|
||||
import Empty from './empty'
|
||||
import Item from './item'
|
||||
import ApiBasedExtensionModal from './modal'
|
||||
|
||||
type ApiBasedExtensionDialogState = {
|
||||
extension: Partial<ApiBasedExtensionResponse>
|
||||
onSave: () => void
|
||||
} | null
|
||||
|
||||
const ApiBasedExtensionPage = () => {
|
||||
const { t } = useTranslation()
|
||||
const { setShowApiBasedExtensionModal } = useModalContext()
|
||||
const { data, refetch: mutate, isPending: isLoading } = useApiBasedExtensions()
|
||||
const [dialogState, setDialogState] = useState<ApiBasedExtensionDialogState>(null)
|
||||
|
||||
const handleOpenApiBasedExtensionModal = () => {
|
||||
setShowApiBasedExtensionModal({
|
||||
payload: {},
|
||||
onSaveCallback: () => mutate(),
|
||||
setDialogState({
|
||||
extension: {},
|
||||
onSave: () => mutate(),
|
||||
})
|
||||
}
|
||||
const handleEditApiBasedExtension = (extension: ApiBasedExtensionResponse) => {
|
||||
setDialogState({
|
||||
extension,
|
||||
onSave: () => mutate(),
|
||||
})
|
||||
}
|
||||
const handleSaveApiBasedExtension = () => {
|
||||
dialogState?.onSave()
|
||||
setDialogState(null)
|
||||
}
|
||||
const handleApiBasedExtensionModalOpenChange = (open: boolean) => {
|
||||
if (!open)
|
||||
setDialogState(null)
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
@@ -33,6 +51,7 @@ const ApiBasedExtensionPage = () => {
|
||||
<Item
|
||||
key={item.id}
|
||||
data={item}
|
||||
onEdit={handleEditApiBasedExtension}
|
||||
onUpdate={() => mutate()}
|
||||
/>
|
||||
))
|
||||
@@ -43,9 +62,19 @@ const ApiBasedExtensionPage = () => {
|
||||
className="w-full"
|
||||
onClick={handleOpenApiBasedExtensionModal}
|
||||
>
|
||||
<RiAddLine className="mr-1 h-4 w-4" />
|
||||
<span className="mr-1 i-ri-add-line h-4 w-4" aria-hidden="true" />
|
||||
{t('apiBasedExtension.add', { ns: 'common' })}
|
||||
</Button>
|
||||
{
|
||||
dialogState && (
|
||||
<ApiBasedExtensionModal
|
||||
open
|
||||
extension={dialogState.extension}
|
||||
onOpenChange={handleApiBasedExtensionModalOpenChange}
|
||||
onSave={handleSaveApiBasedExtension}
|
||||
/>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import type { ApiBasedExtensionResponse } from '@dify/contracts/api/console/api-based-extension/types.gen'
|
||||
import type { FC } from 'react'
|
||||
import {
|
||||
AlertDialog,
|
||||
AlertDialogActions,
|
||||
@@ -9,32 +8,25 @@ import {
|
||||
AlertDialogTitle,
|
||||
} from '@langgenius/dify-ui/alert-dialog'
|
||||
import { Button } from '@langgenius/dify-ui/button'
|
||||
import {
|
||||
RiDeleteBinLine,
|
||||
RiEditLine,
|
||||
} from '@remixicon/react'
|
||||
import { useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useModalContext } from '@/context/modal-context'
|
||||
import { deleteApiBasedExtension } from '@/service/common'
|
||||
|
||||
type ItemProps = {
|
||||
data: ApiBasedExtensionResponse
|
||||
onEdit: (extension: ApiBasedExtensionResponse) => void
|
||||
onUpdate: () => void
|
||||
}
|
||||
const Item: FC<ItemProps> = ({
|
||||
const Item = ({
|
||||
data,
|
||||
onEdit,
|
||||
onUpdate,
|
||||
}) => {
|
||||
}: ItemProps) => {
|
||||
const { t } = useTranslation()
|
||||
const { setShowApiBasedExtensionModal } = useModalContext()
|
||||
const [showDeleteConfirm, setShowDeleteConfirm] = useState(false)
|
||||
|
||||
const handleOpenApiBasedExtensionModal = () => {
|
||||
setShowApiBasedExtensionModal({
|
||||
payload: data,
|
||||
onSaveCallback: () => onUpdate(),
|
||||
})
|
||||
onEdit(data)
|
||||
}
|
||||
const handleDeleteApiBasedExtension = async () => {
|
||||
await deleteApiBasedExtension(`/api-based-extension/${data.id}`)
|
||||
@@ -44,28 +36,27 @@ const Item: FC<ItemProps> = ({
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="group mb-2 flex items-center rounded-xl border-[0.5px] border-transparent bg-components-input-bg-normal px-4 py-2 hover:border-components-input-border-active hover:shadow-xs">
|
||||
<div className="grow">
|
||||
<div className="group mb-2 flex items-center rounded-xl border-[0.5px] border-transparent bg-components-input-bg-normal px-4 py-2 focus-within:border-components-input-border-active focus-within:shadow-xs hover:border-components-input-border-active hover:shadow-xs">
|
||||
<div className="min-w-0 grow">
|
||||
<div className="mb-0.5 text-[13px] font-medium text-text-secondary">{data.name}</div>
|
||||
<div className="text-xs text-text-tertiary">{data.api_endpoint}</div>
|
||||
<div className="truncate text-xs text-text-tertiary">{data.api_endpoint}</div>
|
||||
</div>
|
||||
<div className="hidden items-center group-hover:flex">
|
||||
<div className="pointer-events-none flex shrink-0 items-center gap-1 opacity-0 transition-opacity group-focus-within:pointer-events-auto group-focus-within:opacity-100 group-hover:pointer-events-auto group-hover:opacity-100">
|
||||
<Button
|
||||
className="mr-1"
|
||||
onClick={handleOpenApiBasedExtensionModal}
|
||||
>
|
||||
<RiEditLine className="mr-1 h-4 w-4" />
|
||||
<span className="mr-1 i-ri-edit-line h-4 w-4" aria-hidden="true" />
|
||||
{t('operation.edit', { ns: 'common' })}
|
||||
</Button>
|
||||
<Button
|
||||
onClick={() => setShowDeleteConfirm(true)}
|
||||
>
|
||||
<RiDeleteBinLine className="mr-1 h-4 w-4" />
|
||||
<span className="mr-1 i-ri-delete-bin-line h-4 w-4" aria-hidden="true" />
|
||||
{t('operation.delete', { ns: 'common' })}
|
||||
</Button>
|
||||
</div>
|
||||
<AlertDialog open={showDeleteConfirm} onOpenChange={open => !open && setShowDeleteConfirm(false)}>
|
||||
<AlertDialogContent>
|
||||
<AlertDialogContent backdropProps={{ forceRender: true }}>
|
||||
<div className="flex flex-col gap-2 px-6 pt-6 pb-4">
|
||||
<AlertDialogTitle className="w-full truncate title-2xl-semi-bold text-text-primary">
|
||||
{`${t('operation.delete', { ns: 'common' })} \u201C${data.name}\u201D?`}
|
||||
|
||||
@@ -2,51 +2,57 @@ import type {
|
||||
ApiBasedExtensionPayload,
|
||||
ApiBasedExtensionResponse,
|
||||
} from '@dify/contracts/api/console/api-based-extension/types.gen'
|
||||
import type { FC } from 'react'
|
||||
import { Button } from '@langgenius/dify-ui/button'
|
||||
import { Dialog, DialogContent } from '@langgenius/dify-ui/dialog'
|
||||
import { Dialog, DialogCloseButton, DialogContent, DialogTitle } from '@langgenius/dify-ui/dialog'
|
||||
import { toast } from '@langgenius/dify-ui/toast'
|
||||
import { useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { BookOpen01 } from '@/app/components/base/icons/src/vender/line/education'
|
||||
import { useDocLink } from '@/context/i18n'
|
||||
import { addApiBasedExtension, updateApiBasedExtension } from '@/service/common'
|
||||
|
||||
type ApiBasedExtensionField = 'name' | 'api_endpoint' | 'api_key'
|
||||
|
||||
type ApiBasedExtensionModalProps = {
|
||||
data: Partial<ApiBasedExtensionResponse>
|
||||
onCancel: () => void
|
||||
open: boolean
|
||||
extension: Partial<ApiBasedExtensionResponse>
|
||||
onOpenChange: (open: boolean) => void
|
||||
onSave?: (newData: ApiBasedExtensionResponse) => void
|
||||
}
|
||||
const ApiBasedExtensionModal: FC<ApiBasedExtensionModalProps> = ({ data, onCancel, onSave }) => {
|
||||
const ApiBasedExtensionModal = ({ open, extension, onOpenChange, onSave }: ApiBasedExtensionModalProps) => {
|
||||
const { t } = useTranslation()
|
||||
const docLink = useDocLink()
|
||||
const [localeData, setLocaleData] = useState(data)
|
||||
const [localData, setLocalData] = useState(extension)
|
||||
const [loading, setLoading] = useState(false)
|
||||
const handleDataChange = (type: string, value: string) => {
|
||||
setLocaleData({ ...localeData, [type]: value })
|
||||
const handleDataChange = (field: ApiBasedExtensionField, value: string) => {
|
||||
setLocalData({ ...localData, [field]: value })
|
||||
}
|
||||
const handleSave = async () => {
|
||||
setLoading(true)
|
||||
if (localeData && localeData.api_key && localeData.api_key?.length < 5) {
|
||||
if (localData.api_key && localData.api_key.length < 5) {
|
||||
toast.error(t('apiBasedExtension.modal.apiKey.lengthError', { ns: 'common' }))
|
||||
setLoading(false)
|
||||
return
|
||||
}
|
||||
try {
|
||||
const payload: ApiBasedExtensionPayload = {
|
||||
name: localData.name || '',
|
||||
api_endpoint: localData.api_endpoint || '',
|
||||
api_key: localData.api_key || '',
|
||||
}
|
||||
let res = {} as ApiBasedExtensionResponse
|
||||
if (!data.id) {
|
||||
if (!extension.id) {
|
||||
res = await addApiBasedExtension({
|
||||
url: '/api-based-extension',
|
||||
body: localeData as ApiBasedExtensionPayload,
|
||||
body: payload,
|
||||
})
|
||||
}
|
||||
else {
|
||||
res = await updateApiBasedExtension({
|
||||
url: `/api-based-extension/${data.id}`,
|
||||
url: `/api-based-extension/${extension.id}`,
|
||||
body: {
|
||||
...localeData,
|
||||
api_key: data.api_key === localeData.api_key ? '[__HIDDEN__]' : localeData.api_key,
|
||||
} as ApiBasedExtensionPayload,
|
||||
...payload,
|
||||
api_key: extension.api_key === localData.api_key ? '[__HIDDEN__]' : payload.api_key,
|
||||
},
|
||||
})
|
||||
toast.success(t('actionMsg.modifiedSuccessfully', { ns: 'common' }))
|
||||
}
|
||||
@@ -57,44 +63,47 @@ const ApiBasedExtensionModal: FC<ApiBasedExtensionModalProps> = ({ data, onCance
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
return (
|
||||
<Dialog open>
|
||||
<DialogContent className="w-[640px]! max-w-none! border-none p-8! pb-6! text-left align-middle">
|
||||
|
||||
<div className="mb-2 text-xl font-semibold text-text-primary">
|
||||
{data.name
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={onOpenChange} disablePointerDismissal>
|
||||
<DialogContent
|
||||
backdropProps={{ forceRender: true }}
|
||||
className="w-160 border-none p-8 pb-6 text-left"
|
||||
>
|
||||
<DialogCloseButton />
|
||||
|
||||
<DialogTitle className="mb-2 pr-8 text-xl font-semibold text-text-primary">
|
||||
{extension.name
|
||||
? t('apiBasedExtension.modal.editTitle', { ns: 'common' })
|
||||
: t('apiBasedExtension.modal.title', { ns: 'common' })}
|
||||
</div>
|
||||
</DialogTitle>
|
||||
<div className="py-2">
|
||||
<div className="text-sm leading-9 font-medium text-text-primary">
|
||||
{t('apiBasedExtension.modal.name.title', { ns: 'common' })}
|
||||
</div>
|
||||
<input value={localeData.name || ''} onChange={e => handleDataChange('name', e.target.value)} className="block h-9 w-full appearance-none rounded-lg bg-components-input-bg-normal px-3 text-sm text-text-primary outline-hidden" placeholder={t('apiBasedExtension.modal.name.placeholder', { ns: 'common' }) || ''} />
|
||||
<input value={localData.name || ''} onChange={e => handleDataChange('name', e.target.value)} className="block h-9 w-full appearance-none rounded-lg bg-components-input-bg-normal px-3 text-sm text-text-primary outline-hidden" placeholder={t('apiBasedExtension.modal.name.placeholder', { ns: 'common' }) || ''} />
|
||||
</div>
|
||||
<div className="py-2">
|
||||
<div className="flex h-9 items-center justify-between text-sm font-medium text-text-primary">
|
||||
{t('apiBasedExtension.modal.apiEndpoint.title', { ns: 'common' })}
|
||||
<a href={docLink('/use-dify/workspace/api-extension/api-extension')} target="_blank" rel="noopener noreferrer" className="group flex items-center text-xs font-normal text-text-accent">
|
||||
<BookOpen01 className="mr-1 h-3 w-3" />
|
||||
<a href={docLink('/use-dify/workspace/api-extension/api-extension')} target="_blank" rel="noopener noreferrer" className="flex items-center text-xs font-normal text-text-accent">
|
||||
<span className="mr-1 i-custom-vender-line-education-book-open-01 h-3 w-3" aria-hidden="true" />
|
||||
{t('apiBasedExtension.link', { ns: 'common' })}
|
||||
</a>
|
||||
</div>
|
||||
<input value={localeData.api_endpoint || ''} onChange={e => handleDataChange('api_endpoint', e.target.value)} className="block h-9 w-full appearance-none rounded-lg bg-components-input-bg-normal px-3 text-sm text-text-primary outline-hidden" placeholder={t('apiBasedExtension.modal.apiEndpoint.placeholder', { ns: 'common' }) || ''} />
|
||||
<input value={localData.api_endpoint || ''} onChange={e => handleDataChange('api_endpoint', e.target.value)} className="block h-9 w-full appearance-none rounded-lg bg-components-input-bg-normal px-3 text-sm text-text-primary outline-hidden" placeholder={t('apiBasedExtension.modal.apiEndpoint.placeholder', { ns: 'common' }) || ''} />
|
||||
</div>
|
||||
<div className="py-2">
|
||||
<div className="text-sm leading-9 font-medium text-text-primary">
|
||||
{t('apiBasedExtension.modal.apiKey.title', { ns: 'common' })}
|
||||
</div>
|
||||
<div className="flex items-center">
|
||||
<input value={localeData.api_key || ''} onChange={e => handleDataChange('api_key', e.target.value)} className="mr-2 block h-9 grow appearance-none rounded-lg bg-components-input-bg-normal px-3 text-sm text-text-primary outline-hidden" placeholder={t('apiBasedExtension.modal.apiKey.placeholder', { ns: 'common' }) || ''} />
|
||||
</div>
|
||||
<input value={localData.api_key || ''} onChange={e => handleDataChange('api_key', e.target.value)} className="block h-9 w-full appearance-none rounded-lg bg-components-input-bg-normal px-3 text-sm text-text-primary outline-hidden" placeholder={t('apiBasedExtension.modal.apiKey.placeholder', { ns: 'common' }) || ''} />
|
||||
</div>
|
||||
<div className="mt-6 flex items-center justify-end">
|
||||
<Button onClick={onCancel} className="mr-2">
|
||||
<div className="mt-6 flex items-center justify-end gap-2">
|
||||
<Button onClick={() => onOpenChange(false)}>
|
||||
{t('operation.cancel', { ns: 'common' })}
|
||||
</Button>
|
||||
<Button variant="primary" disabled={!localeData.name || !localeData.api_endpoint || !localeData.api_key || loading} onClick={handleSave}>
|
||||
<Button variant="primary" disabled={!localData.name || !localData.api_endpoint || !localData.api_key || loading} onClick={handleSave}>
|
||||
{t('operation.save', { ns: 'common' })}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
@@ -1,32 +1,25 @@
|
||||
import type { FC } from 'react'
|
||||
import { Popover, PopoverContent, PopoverTrigger } from '@langgenius/dify-ui/popover'
|
||||
import {
|
||||
RiAddLine,
|
||||
RiArrowDownSLine,
|
||||
} from '@remixicon/react'
|
||||
import { useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import {
|
||||
ArrowUpRight,
|
||||
} from '@/app/components/base/icons/src/vender/line/arrows'
|
||||
import { ACCOUNT_SETTING_TAB } from '@/app/components/header/account-setting/constants'
|
||||
import { useModalContext } from '@/context/modal-context'
|
||||
import { useApiBasedExtensions } from '@/service/use-common'
|
||||
import ApiBasedExtensionModal from './modal'
|
||||
|
||||
type ApiBasedExtensionSelectorProps = {
|
||||
value: string
|
||||
onChange: (value: string) => void
|
||||
}
|
||||
|
||||
const ApiBasedExtensionSelector: FC<ApiBasedExtensionSelectorProps> = ({
|
||||
const ApiBasedExtensionSelector = ({
|
||||
value,
|
||||
onChange,
|
||||
}) => {
|
||||
}: ApiBasedExtensionSelectorProps) => {
|
||||
const { t } = useTranslation()
|
||||
const [open, setOpen] = useState(false)
|
||||
const [addModalOpen, setAddModalOpen] = useState(false)
|
||||
const {
|
||||
setShowAccountSettingModal,
|
||||
setShowApiBasedExtensionModal,
|
||||
} = useModalContext()
|
||||
const { data, refetch: mutate } = useApiBasedExtensions()
|
||||
const handleSelect = (id: string) => {
|
||||
@@ -36,91 +29,115 @@ const ApiBasedExtensionSelector: FC<ApiBasedExtensionSelectorProps> = ({
|
||||
|
||||
const currentItem = data?.find(item => item.id === value)
|
||||
|
||||
const handleSaveApiBasedExtension = () => {
|
||||
mutate()
|
||||
setAddModalOpen(false)
|
||||
}
|
||||
const handleAddModalOpenChange = (nextOpen: boolean) => {
|
||||
if (!nextOpen)
|
||||
setAddModalOpen(false)
|
||||
}
|
||||
|
||||
return (
|
||||
<Popover
|
||||
open={open}
|
||||
onOpenChange={setOpen}
|
||||
>
|
||||
<PopoverTrigger
|
||||
render={(
|
||||
<button type="button" className="block w-full border-0 bg-transparent p-0 text-left">
|
||||
{
|
||||
currentItem
|
||||
? (
|
||||
<div className="flex h-9 cursor-pointer items-center justify-between rounded-lg bg-components-input-bg-normal pr-2.5 pl-3">
|
||||
<div className="text-sm text-text-primary">{currentItem.name}</div>
|
||||
<div className="flex items-center">
|
||||
<div className="mr-1.5 w-[270px] truncate text-right text-xs text-text-quaternary">
|
||||
{currentItem.api_endpoint}
|
||||
</div>
|
||||
<RiArrowDownSLine className={`h-4 w-4 text-text-secondary ${!open && 'opacity-60'}`} />
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
: (
|
||||
<div className="flex h-9 cursor-pointer items-center justify-between rounded-lg bg-components-input-bg-normal pr-2.5 pl-3 text-sm text-text-quaternary">
|
||||
{t('apiBasedExtension.selector.placeholder', { ns: 'common' })}
|
||||
<RiArrowDownSLine className={`h-4 w-4 text-text-secondary ${!open && 'opacity-60'}`} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
</button>
|
||||
)}
|
||||
/>
|
||||
<PopoverContent
|
||||
placement="bottom-start"
|
||||
sideOffset={4}
|
||||
className="w-[calc(100%-32px)] max-w-[576px]"
|
||||
popupClassName="border-0 bg-transparent p-0 shadow-none backdrop-blur-none"
|
||||
<>
|
||||
<Popover
|
||||
open={open}
|
||||
onOpenChange={setOpen}
|
||||
>
|
||||
<div className="z-10 w-full rounded-lg border-[0.5px] border-components-panel-border bg-components-panel-bg shadow-lg">
|
||||
<div className="p-1">
|
||||
<div className="flex items-center justify-between px-3 pt-2 pb-1">
|
||||
<div className="text-xs font-medium text-text-tertiary">
|
||||
{t('apiBasedExtension.selector.title', { ns: 'common' })}
|
||||
<PopoverTrigger
|
||||
render={(
|
||||
<button type="button" className="block w-full border-0 bg-transparent p-0 text-left">
|
||||
{
|
||||
currentItem
|
||||
? (
|
||||
<div className="flex h-9 cursor-pointer items-center justify-between rounded-lg bg-components-input-bg-normal pr-2.5 pl-3">
|
||||
<div className="text-sm text-text-primary">{currentItem.name}</div>
|
||||
<div className="flex items-center">
|
||||
<div className="mr-1.5 w-[270px] truncate text-right text-xs text-text-quaternary">
|
||||
{currentItem.api_endpoint}
|
||||
</div>
|
||||
<span className={`i-ri-arrow-down-s-line h-4 w-4 text-text-secondary ${!open && 'opacity-60'}`} aria-hidden="true" />
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
: (
|
||||
<div className="flex h-9 cursor-pointer items-center justify-between rounded-lg bg-components-input-bg-normal pr-2.5 pl-3 text-sm text-text-quaternary">
|
||||
{t('apiBasedExtension.selector.placeholder', { ns: 'common' })}
|
||||
<span className={`i-ri-arrow-down-s-line h-4 w-4 text-text-secondary ${!open && 'opacity-60'}`} aria-hidden="true" />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
</button>
|
||||
)}
|
||||
/>
|
||||
<PopoverContent
|
||||
placement="bottom-start"
|
||||
sideOffset={4}
|
||||
className="w-[calc(100%-32px)] max-w-[576px]"
|
||||
popupClassName="border-0 bg-transparent p-0 shadow-none backdrop-blur-none"
|
||||
>
|
||||
<div className="z-10 w-full rounded-lg border-[0.5px] border-components-panel-border bg-components-panel-bg shadow-lg">
|
||||
<div className="p-1">
|
||||
<div className="flex items-center justify-between px-3 pt-2 pb-1">
|
||||
<div className="text-xs font-medium text-text-tertiary">
|
||||
{t('apiBasedExtension.selector.title', { ns: 'common' })}
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
className="flex cursor-pointer items-center border-none bg-transparent p-0 text-xs text-text-accent"
|
||||
onClick={() => {
|
||||
setOpen(false)
|
||||
setShowAccountSettingModal({ payload: ACCOUNT_SETTING_TAB.API_BASED_EXTENSION })
|
||||
}}
|
||||
>
|
||||
{t('apiBasedExtension.selector.manage', { ns: 'common' })}
|
||||
<span className="ml-0.5 i-custom-vender-line-arrows-arrow-up-right h-3 w-3" aria-hidden="true" />
|
||||
</button>
|
||||
</div>
|
||||
<div
|
||||
className="flex cursor-pointer items-center text-xs text-text-accent"
|
||||
<div className="max-h-[250px] overflow-y-auto">
|
||||
{
|
||||
data?.map(item => (
|
||||
<button
|
||||
type="button"
|
||||
key={item.id}
|
||||
className="w-full cursor-pointer rounded-md border-none bg-transparent px-3 py-1.5 text-left hover:bg-state-base-hover"
|
||||
onClick={() => handleSelect(item.id!)}
|
||||
>
|
||||
<div className="text-sm text-text-primary">{item.name}</div>
|
||||
<div className="text-xs text-text-tertiary">{item.api_endpoint}</div>
|
||||
</button>
|
||||
))
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
<div className="h-px bg-divider-regular" />
|
||||
<div className="p-1">
|
||||
<button
|
||||
type="button"
|
||||
className="flex h-8 w-full cursor-pointer items-center border-none bg-transparent px-3 text-left text-sm text-text-accent"
|
||||
onClick={() => {
|
||||
setOpen(false)
|
||||
setShowAccountSettingModal({ payload: ACCOUNT_SETTING_TAB.API_BASED_EXTENSION })
|
||||
setAddModalOpen(true)
|
||||
}}
|
||||
>
|
||||
{t('apiBasedExtension.selector.manage', { ns: 'common' })}
|
||||
<ArrowUpRight className="ml-0.5 h-3 w-3" />
|
||||
</div>
|
||||
</div>
|
||||
<div className="max-h-[250px] overflow-y-auto">
|
||||
{
|
||||
data?.map(item => (
|
||||
<div
|
||||
key={item.id}
|
||||
className="w-full cursor-pointer rounded-md px-3 py-1.5 text-left hover:stroke-state-base-hover"
|
||||
onClick={() => handleSelect(item.id)}
|
||||
>
|
||||
<div className="text-sm text-text-primary">{item.name}</div>
|
||||
<div className="text-xs text-text-tertiary">{item.api_endpoint}</div>
|
||||
</div>
|
||||
))
|
||||
}
|
||||
<span className="mr-2 i-ri-add-line h-4 w-4" aria-hidden="true" />
|
||||
{t('operation.add', { ns: 'common' })}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="h-px bg-divider-regular" />
|
||||
<div className="p-1">
|
||||
<div
|
||||
className="flex h-8 cursor-pointer items-center px-3 text-sm text-text-accent"
|
||||
onClick={() => {
|
||||
setOpen(false)
|
||||
setShowApiBasedExtensionModal({ payload: {}, onSaveCallback: () => mutate() })
|
||||
}}
|
||||
>
|
||||
<RiAddLine className="mr-2 h-4 w-4" />
|
||||
{t('operation.add', { ns: 'common' })}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
{
|
||||
addModalOpen && (
|
||||
<ApiBasedExtensionModal
|
||||
open
|
||||
extension={{}}
|
||||
onOpenChange={handleAddModalOpenChange}
|
||||
onSave={handleSaveApiBasedExtension}
|
||||
/>
|
||||
)
|
||||
}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -26,7 +26,6 @@ const mockSetShowAccountSettingModal = vi.fn()
|
||||
vi.mock('@/context/modal-context', () => ({
|
||||
useModalContext: (): ModalContextState => ({
|
||||
setShowAccountSettingModal: mockSetShowAccountSettingModal,
|
||||
setShowApiBasedExtensionModal: vi.fn(),
|
||||
setShowModerationSettingModal: vi.fn(),
|
||||
setShowExternalDataToolModal: vi.fn(),
|
||||
setShowPricingModal: mockSetShowPricingModal,
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
'use client'
|
||||
|
||||
import type { ApiBasedExtensionResponse } from '@dify/contracts/api/console/api-based-extension/types.gen'
|
||||
import type { ReactNode, SetStateAction } from 'react'
|
||||
import type { ModalState, ModelModalType } from './modal-context'
|
||||
import type { OpeningStatement } from '@/app/components/base/features/types'
|
||||
@@ -36,9 +35,6 @@ import {
|
||||
const AccountSetting = dynamic(() => import('@/app/components/header/account-setting'), {
|
||||
ssr: false,
|
||||
})
|
||||
const ApiBasedExtensionModal = dynamic(() => import('@/app/components/header/account-setting/api-based-extension-page/modal'), {
|
||||
ssr: false,
|
||||
})
|
||||
const ModerationSettingModal = dynamic(() => import('@/app/components/base/features/new-feature-panel/moderation/moderation-setting-modal'), {
|
||||
ssr: false,
|
||||
})
|
||||
@@ -90,7 +86,6 @@ export const ModalContextProvider = ({
|
||||
? urlAccountModalState.payload
|
||||
: DEFAULT_ACCOUNT_SETTING_TAB)
|
||||
: null
|
||||
const [showApiBasedExtensionModal, setShowApiBasedExtensionModal] = useState<ModalState<Partial<ApiBasedExtensionResponse>> | null>(null)
|
||||
const [showModerationSettingModal, setShowModerationSettingModal] = useState<ModalState<ModerationConfig> | null>(null)
|
||||
const [showExternalDataToolModal, setShowExternalDataToolModal] = useState<ModalState<ExternalDataTool> | null>(null)
|
||||
const [showModelModal, setShowModelModal] = useState<ModalState<ModelModalType> | null>(null)
|
||||
@@ -206,12 +201,6 @@ export const ModalContextProvider = ({
|
||||
showOpeningModal.onCancelCallback()
|
||||
}, [showOpeningModal])
|
||||
|
||||
const handleSaveApiBasedExtension = (newApiBasedExtension: ApiBasedExtensionResponse) => {
|
||||
if (showApiBasedExtensionModal?.onSaveCallback)
|
||||
showApiBasedExtensionModal.onSaveCallback(newApiBasedExtension)
|
||||
setShowApiBasedExtensionModal(null)
|
||||
}
|
||||
|
||||
const handleSaveModeration = (newModerationConfig: ModerationConfig) => {
|
||||
if (showModerationSettingModal?.onSaveCallback)
|
||||
showModerationSettingModal.onSaveCallback(newModerationConfig)
|
||||
@@ -247,7 +236,6 @@ export const ModalContextProvider = ({
|
||||
return (
|
||||
<ModalContext.Provider value={{
|
||||
setShowAccountSettingModal,
|
||||
setShowApiBasedExtensionModal,
|
||||
setShowModerationSettingModal,
|
||||
setShowExternalDataToolModal,
|
||||
setShowPricingModal: handleShowPricingModal,
|
||||
@@ -273,15 +261,6 @@ export const ModalContextProvider = ({
|
||||
)
|
||||
}
|
||||
|
||||
{
|
||||
!!showApiBasedExtensionModal && (
|
||||
<ApiBasedExtensionModal
|
||||
data={showApiBasedExtensionModal.payload}
|
||||
onCancel={() => setShowApiBasedExtensionModal(null)}
|
||||
onSave={handleSaveApiBasedExtension}
|
||||
/>
|
||||
)
|
||||
}
|
||||
{
|
||||
!!showModerationSettingModal && (
|
||||
<ModerationSettingModal
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
'use client'
|
||||
|
||||
import type { ApiBasedExtensionResponse } from '@dify/contracts/api/console/api-based-extension/types.gen'
|
||||
import type { Dispatch, SetStateAction } from 'react'
|
||||
import type { TriggerEventsLimitModalPayload } from './hooks/use-trigger-events-limit-modal'
|
||||
import type { OpeningStatement } from '@/app/components/base/features/types'
|
||||
@@ -48,7 +47,6 @@ export type ModelModalType = {
|
||||
|
||||
export type ModalContextState = {
|
||||
setShowAccountSettingModal: Dispatch<SetStateAction<ModalState<AccountSettingTab> | null>>
|
||||
setShowApiBasedExtensionModal: Dispatch<SetStateAction<ModalState<Partial<ApiBasedExtensionResponse>> | null>>
|
||||
setShowModerationSettingModal: Dispatch<SetStateAction<ModalState<ModerationConfig> | null>>
|
||||
setShowExternalDataToolModal: Dispatch<SetStateAction<ModalState<ExternalDataTool> | null>>
|
||||
setShowPricingModal: () => void
|
||||
@@ -68,7 +66,6 @@ export type ModalContextState = {
|
||||
|
||||
export const ModalContext = createContext<ModalContextState>({
|
||||
setShowAccountSettingModal: noop,
|
||||
setShowApiBasedExtensionModal: noop,
|
||||
setShowModerationSettingModal: noop,
|
||||
setShowExternalDataToolModal: noop,
|
||||
setShowPricingModal: noop,
|
||||
|
||||
Reference in New Issue
Block a user