fix: improve API extension dialog controls (#36323)

This commit is contained in:
yyh
2026-05-18 16:03:08 +08:00
committed by GitHub
parent 730a0bef9e
commit 06ea0f7ac2
13 changed files with 339 additions and 288 deletions

View File

@@ -59,7 +59,6 @@ const defaultProviderContext = {
const defaultModalContext: ModalContextState = {
setShowAccountSettingModal: noop,
setShowApiBasedExtensionModal: noop,
setShowModerationSettingModal: noop,
setShowExternalDataToolModal: noop,
setShowPricingModal: noop,

View File

@@ -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,

View File

@@ -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()
})
})
})
})

View File

@@ -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)

View File

@@ -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')

View File

@@ -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()
})
})
})
})

View File

@@ -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>
)
}

View File

@@ -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?`}

View File

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

View File

@@ -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}
/>
)
}
</>
)
}

View File

@@ -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,

View File

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

View File

@@ -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,