mirror of
https://github.com/langgenius/dify.git
synced 2026-05-25 11:27:19 +08:00
Compare commits
3 Commits
copilot/qu
...
laipz8200/
| Author | SHA1 | Date | |
|---|---|---|---|
| 4fa49b27f8 | |||
| 19b334e5ca | |||
| dce25a3909 |
@ -246,7 +246,11 @@ class AdvancedChatAppRunner(WorkflowBasedAppRunner):
|
||||
for layer in self._graph_engine_layers:
|
||||
workflow_entry.graph_engine.layer(layer)
|
||||
|
||||
generator = workflow_entry.run()
|
||||
generator = self._iter_workflow_events(
|
||||
workflow_entry,
|
||||
workflow_entry.run(),
|
||||
stream=self.application_generate_entity.stream,
|
||||
)
|
||||
|
||||
for event in generator:
|
||||
self._handle_event(workflow_entry, event)
|
||||
|
||||
@ -169,7 +169,11 @@ class WorkflowAppRunner(WorkflowBasedAppRunner):
|
||||
for layer in self._graph_engine_layers:
|
||||
workflow_entry.graph_engine.layer(layer)
|
||||
|
||||
generator = workflow_entry.run()
|
||||
generator = self._iter_workflow_events(
|
||||
workflow_entry,
|
||||
workflow_entry.run(),
|
||||
stream=self.application_generate_entity.stream,
|
||||
)
|
||||
|
||||
for event in generator:
|
||||
self._handle_event(workflow_entry, event)
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import logging
|
||||
import time
|
||||
from collections.abc import Mapping, Sequence
|
||||
from collections.abc import Iterable, Mapping, Sequence
|
||||
from typing import Any, cast
|
||||
|
||||
from pydantic import ValidationError
|
||||
@ -51,6 +51,7 @@ from core.workflow.workflow_entry import WorkflowEntry
|
||||
from core.workflow.workflow_run_outputs import project_node_outputs_for_workflow_run
|
||||
from graphon.entities.graph_config import NodeConfigDictAdapter
|
||||
from graphon.entities.pause_reason import HumanInputRequired
|
||||
from graphon.filters import GraphEventFilterContext, ResponseStreamFilter, filter_graph_events
|
||||
from graphon.graph import Graph
|
||||
from graphon.graph_engine.layers import GraphEngineLayer
|
||||
from graphon.graph_events import (
|
||||
@ -381,6 +382,21 @@ class WorkflowBasedAppRunner:
|
||||
|
||||
return graph, variable_pool
|
||||
|
||||
@staticmethod
|
||||
def _iter_workflow_events(
|
||||
workflow_entry: WorkflowEntry,
|
||||
events: Iterable[GraphEngineEvent],
|
||||
*,
|
||||
stream: bool,
|
||||
) -> Iterable[GraphEngineEvent]:
|
||||
_ = stream
|
||||
|
||||
return filter_graph_events(
|
||||
events,
|
||||
context=GraphEventFilterContext.from_engine(workflow_entry.graph_engine),
|
||||
filters=[ResponseStreamFilter()],
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def _build_agent_strategy_info(event: NodeRunStartedEvent) -> AgentStrategyInfo | None:
|
||||
raw_agent_strategy = event.extras.get("agent_strategy")
|
||||
|
||||
@ -97,6 +97,7 @@ dify-trace-mlflow = { workspace = true }
|
||||
dify-trace-opik = { workspace = true }
|
||||
dify-trace-tencent = { workspace = true }
|
||||
dify-trace-weave = { workspace = true }
|
||||
graphon = { git = "https://github.com/langgenius/graphon.git", rev = "a24cf1f227c4b91494779be40c754cb4107c60ed" }
|
||||
|
||||
[tool.uv]
|
||||
default-groups = ["storage", "tools", "vdb-all", "trace-all"]
|
||||
|
||||
@ -100,6 +100,7 @@ class TestAdvancedChatAppRunnerConversationVariables:
|
||||
mock_app_generate_entity.single_iteration_run = None
|
||||
mock_app_generate_entity.single_loop_run = None
|
||||
mock_app_generate_entity.trace_manager = None
|
||||
mock_app_generate_entity.stream = False
|
||||
|
||||
# Create runner
|
||||
runner = AdvancedChatAppRunner(
|
||||
@ -245,6 +246,7 @@ class TestAdvancedChatAppRunnerConversationVariables:
|
||||
mock_app_generate_entity.single_iteration_run = None
|
||||
mock_app_generate_entity.single_loop_run = None
|
||||
mock_app_generate_entity.trace_manager = None
|
||||
mock_app_generate_entity.stream = False
|
||||
|
||||
# Create runner
|
||||
runner = AdvancedChatAppRunner(
|
||||
@ -405,6 +407,7 @@ class TestAdvancedChatAppRunnerConversationVariables:
|
||||
mock_app_generate_entity.single_iteration_run = None
|
||||
mock_app_generate_entity.single_loop_run = None
|
||||
mock_app_generate_entity.trace_manager = None
|
||||
mock_app_generate_entity.stream = False
|
||||
|
||||
# Create runner
|
||||
runner = AdvancedChatAppRunner(
|
||||
|
||||
@ -64,6 +64,7 @@ def build_runner():
|
||||
gen.single_iteration_run = None
|
||||
gen.single_loop_run = None
|
||||
gen.trace_manager = None
|
||||
gen.stream = False
|
||||
|
||||
runner = AdvancedChatAppRunner(
|
||||
application_generate_entity=gen,
|
||||
|
||||
@ -234,6 +234,39 @@ class TestWorkflowBasedAppRunner:
|
||||
assert graph is not None
|
||||
assert variable_pool.get(["sys", "conversation_id"]).value == "conv-1"
|
||||
|
||||
@pytest.mark.parametrize("stream", [False, True])
|
||||
def test_iter_workflow_events_filters_response_stream(self, stream: bool):
|
||||
runner = WorkflowBasedAppRunner(queue_manager=SimpleNamespace(), app_id="app")
|
||||
graph_runtime_state = GraphRuntimeState(
|
||||
variable_pool=VariablePool.from_bootstrap(system_variables=default_system_variables()),
|
||||
start_at=0.0,
|
||||
)
|
||||
workflow_entry = SimpleNamespace(
|
||||
graph_engine=SimpleNamespace(
|
||||
graph=SimpleNamespace(nodes={}),
|
||||
graph_runtime_state=graph_runtime_state,
|
||||
)
|
||||
)
|
||||
|
||||
events = iter(
|
||||
[
|
||||
GraphRunStartedEvent(),
|
||||
NodeRunStreamChunkEvent(
|
||||
id="exec",
|
||||
node_id="llm",
|
||||
node_type=BuiltinNodeTypes.LLM,
|
||||
selector=["llm", "text"],
|
||||
chunk="raw",
|
||||
is_final=False,
|
||||
),
|
||||
GraphRunSucceededEvent(outputs={"answer": "done"}),
|
||||
]
|
||||
)
|
||||
|
||||
filtered_events = list(runner._iter_workflow_events(workflow_entry, events, stream=stream))
|
||||
|
||||
assert [type(event) for event in filtered_events] == [GraphRunStartedEvent, GraphRunSucceededEvent]
|
||||
|
||||
def test_handle_graph_run_events_and_pause_notifications(self, monkeypatch: pytest.MonkeyPatch):
|
||||
published: list[object] = []
|
||||
|
||||
|
||||
@ -53,6 +53,7 @@ def test_run_uses_single_node_execution_branch(
|
||||
app_generate_entity.trace_manager = None
|
||||
app_generate_entity.single_iteration_run = single_iteration_run
|
||||
app_generate_entity.single_loop_run = single_loop_run
|
||||
app_generate_entity.stream = False
|
||||
|
||||
workflow = MagicMock(spec=Workflow)
|
||||
workflow.tenant_id = "tenant"
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
from graphon.filters import GraphEventFilterContext, ResponseStreamFilter, filter_graph_events
|
||||
from graphon.graph_engine import GraphEngine, GraphEngineConfig
|
||||
from graphon.graph_engine.command_channels import InMemoryChannel
|
||||
from graphon.graph_events import (
|
||||
@ -31,7 +32,13 @@ def test_tool_in_chatflow():
|
||||
config=GraphEngineConfig(),
|
||||
)
|
||||
|
||||
events = list(engine.run())
|
||||
events = list(
|
||||
filter_graph_events(
|
||||
engine.run(),
|
||||
context=GraphEventFilterContext.from_engine(engine),
|
||||
filters=[ResponseStreamFilter()],
|
||||
)
|
||||
)
|
||||
|
||||
# Check for successful completion
|
||||
success_events = [e for e in events if isinstance(e, GraphRunSucceededEvent)]
|
||||
|
||||
8
api/uv.lock
generated
8
api/uv.lock
generated
@ -1628,7 +1628,7 @@ requires-dist = [
|
||||
{ name = "gmpy2", specifier = ">=2.3.0" },
|
||||
{ name = "google-api-python-client", specifier = ">=2.196.0" },
|
||||
{ name = "google-cloud-aiplatform", specifier = ">=1.151.0,<2.0.0" },
|
||||
{ name = "graphon", specifier = "~=0.4.0" },
|
||||
{ name = "graphon", git = "https://github.com/langgenius/graphon.git?rev=a24cf1f227c4b91494779be40c754cb4107c60ed" },
|
||||
{ name = "gunicorn", specifier = ">=26.0.0" },
|
||||
{ name = "httpx", extras = ["socks"], specifier = ">=0.28.1,<1.0.0" },
|
||||
{ name = "httpx-sse", specifier = "~=0.4.0" },
|
||||
@ -2985,7 +2985,7 @@ httpx = [
|
||||
[[package]]
|
||||
name = "graphon"
|
||||
version = "0.4.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
source = { git = "https://github.com/langgenius/graphon.git?rev=a24cf1f227c4b91494779be40c754cb4107c60ed#a24cf1f227c4b91494779be40c754cb4107c60ed" }
|
||||
dependencies = [
|
||||
{ name = "charset-normalizer" },
|
||||
{ name = "httpx" },
|
||||
@ -3005,10 +3005,6 @@ dependencies = [
|
||||
{ name = "unstructured", extra = ["docx", "epub", "md", "ppt", "pptx"] },
|
||||
{ name = "webvtt-py" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/76/24/eb1e7983404dcac84816b76ea450e1bb97023e55e00c699d609340bc361e/graphon-0.4.0.tar.gz", hash = "sha256:afb0c7a58f89e09cfa585296429b4d08cd0df80b9ac54d550f88e7d76ec48ee0", size = 261812, upload-time = "2026-05-13T11:48:39.198Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/b7/de/bad6b3fd1e4b4defc16e6ea106e55c44725a159f1d191a99877bce1c9931/graphon-0.4.0-py3-none-any.whl", hash = "sha256:b33f95886da823d5b1b53d663a4f5f8fa383c37740f3bd19297b8d140fcb804c", size = 372711, upload-time = "2026-05-13T11:48:37.712Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "graphql-core"
|
||||
|
||||
@ -34,16 +34,8 @@ if [[ -f "$EXCLUDES_FILE" ]]; then
|
||||
fi
|
||||
|
||||
tmp_output="$(mktemp)"
|
||||
pyrefly_command=(
|
||||
uv run --directory api --dev pyrefly check
|
||||
"${pyrefly_args[@]}"
|
||||
)
|
||||
if (( ${#target_paths[@]} > 0 )); then
|
||||
pyrefly_command+=("${target_paths[@]}")
|
||||
fi
|
||||
|
||||
set +e
|
||||
"${pyrefly_command[@]}" >"$tmp_output" 2>&1
|
||||
uv run --directory api --dev pyrefly check "${pyrefly_args[@]}" "${target_paths[@]}" >"$tmp_output" 2>&1
|
||||
pyrefly_status=$?
|
||||
set -e
|
||||
|
||||
|
||||
@ -8,14 +8,14 @@
|
||||
|
||||
Snapshot generated from `packages/contracts/generated/api/readiness.json` after running `pnpm -C packages/contracts gen-api-contract-from-openapi`.
|
||||
|
||||
Are we OpenAPI ready? **No.** Current generated API contracts are **16.6% ready**.
|
||||
Are we OpenAPI ready? **No.** Current generated API contracts are **16.7% ready**.
|
||||
|
||||
| Surface | Ready | Not ready | Total | Ready % |
|
||||
| --------- | ------: | --------: | ------: | --------: |
|
||||
| console | 95 | 475 | 570 | 16.7% |
|
||||
| console | 96 | 474 | 570 | 16.8% |
|
||||
| service | 16 | 72 | 88 | 18.2% |
|
||||
| web | 5 | 36 | 41 | 12.2% |
|
||||
| **total** | **116** | **583** | **699** | **16.6%** |
|
||||
| **total** | **117** | **582** | **699** | **16.7%** |
|
||||
|
||||
Readiness here means the generated contract operation is not marked with:
|
||||
|
||||
|
||||
@ -426,16 +426,10 @@ export const imports = {
|
||||
|
||||
/**
|
||||
* Get workflow online users
|
||||
*
|
||||
* Generated contract types may be inaccurate because backend OpenAPI annotations are incomplete. Do not migrate callers until the generated contract is accurate.
|
||||
*
|
||||
* @deprecated
|
||||
*/
|
||||
export const post3 = oc
|
||||
.route({
|
||||
deprecated: true,
|
||||
description:
|
||||
'Get workflow online users\n\nGenerated contract types may be inaccurate because backend OpenAPI annotations are incomplete. Do not migrate callers until the generated contract is accurate.',
|
||||
description: 'Get workflow online users',
|
||||
inputStructure: 'detailed',
|
||||
method: 'POST',
|
||||
operationId: 'postAppsWorkflowsOnlineUsers',
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
{
|
||||
"surfaces": {
|
||||
"console": {
|
||||
"notReady": 475,
|
||||
"notReady": 474,
|
||||
"total": 570
|
||||
},
|
||||
"service": {
|
||||
|
||||
@ -59,6 +59,7 @@ const defaultProviderContext = {
|
||||
|
||||
const defaultModalContext: ModalContextState = {
|
||||
setShowAccountSettingModal: noop,
|
||||
setShowApiBasedExtensionModal: noop,
|
||||
setShowModerationSettingModal: noop,
|
||||
setShowExternalDataToolModal: noop,
|
||||
setShowPricingModal: noop,
|
||||
|
||||
@ -55,6 +55,7 @@ const mockUseProviderContext = vi.fn<() => ProviderContextState>()
|
||||
|
||||
const buildModalContext = (): ModalContextState => ({
|
||||
setShowAccountSettingModal: mockSetShowAccountSettingModal,
|
||||
setShowApiBasedExtensionModal: vi.fn(),
|
||||
setShowModerationSettingModal: vi.fn(),
|
||||
setShowExternalDataToolModal: vi.fn(),
|
||||
setShowPricingModal: mockSetShowPricingModal,
|
||||
|
||||
@ -1,6 +1,8 @@
|
||||
import type { ApiBasedExtensionResponse } from '@dify/contracts/api/console/api-based-extension/types.gen'
|
||||
import { fireEvent, render, screen, waitFor } from '@testing-library/react'
|
||||
import { addApiBasedExtension, updateApiBasedExtension } from '@/service/common'
|
||||
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 { useApiBasedExtensions } from '@/service/use-common'
|
||||
import ApiBasedExtensionPage from '../index'
|
||||
|
||||
@ -8,16 +10,19 @@ vi.mock('@/service/use-common', () => ({
|
||||
useApiBasedExtensions: vi.fn(),
|
||||
}))
|
||||
|
||||
vi.mock('@/service/common', () => ({
|
||||
addApiBasedExtension: vi.fn(),
|
||||
updateApiBasedExtension: vi.fn(),
|
||||
vi.mock('@/context/modal-context', () => ({
|
||||
useModalContext: 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', () => {
|
||||
@ -123,17 +128,13 @@ describe('ApiBasedExtensionPage', () => {
|
||||
fireEvent.click(screen.getByText('common.apiBasedExtension.add'))
|
||||
|
||||
// Assert
|
||||
expect(screen.getByRole('dialog', { name: 'common.apiBasedExtension.modal.title' })).toBeInTheDocument()
|
||||
expect(mockSetShowApiBasedExtensionModal).toHaveBeenCalledWith(expect.objectContaining({
|
||||
payload: {},
|
||||
}))
|
||||
})
|
||||
|
||||
it('should call refetch when add modal saves successfully', async () => {
|
||||
it('should call refetch when onSaveCallback is executed from the modal', () => {
|
||||
// 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,
|
||||
@ -143,23 +144,25 @@ 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'))
|
||||
|
||||
// Assert
|
||||
await waitFor(() => {
|
||||
expect(mockRefetch).toHaveBeenCalled()
|
||||
})
|
||||
// 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()
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
it('should call refetch when an item is updated', async () => {
|
||||
it('should call refetch when an item is updated', () => {
|
||||
// Arrange
|
||||
const extension: ApiBasedExtensionResponse = { id: '1', name: 'Extension 1', api_endpoint: 'url1', api_key: 'long-api-key' }
|
||||
vi.mocked(updateApiBasedExtension).mockResolvedValue({ ...extension, name: 'Updated' })
|
||||
const mockData: ApiBasedExtensionResponse[] = [
|
||||
{ id: '1', name: 'Extension 1', api_endpoint: 'url1', api_key: 'key1' },
|
||||
]
|
||||
vi.mocked(useApiBasedExtensions).mockReturnValue({
|
||||
data: [extension],
|
||||
data: mockData,
|
||||
isPending: false,
|
||||
refetch: mockRefetch,
|
||||
} as unknown as ReturnType<typeof useApiBasedExtensions>)
|
||||
@ -168,12 +171,16 @@ describe('ApiBasedExtensionPage', () => {
|
||||
|
||||
// Act - Click edit on the rendered item
|
||||
fireEvent.click(screen.getByText('common.operation.edit'))
|
||||
fireEvent.click(screen.getByText('common.operation.save'))
|
||||
|
||||
// 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()
|
||||
}
|
||||
|
||||
// Assert
|
||||
await waitFor(() => {
|
||||
expect(mockRefetch).toHaveBeenCalled()
|
||||
})
|
||||
expect(mockRefetch).toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@ -1,10 +1,17 @@
|
||||
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(),
|
||||
}))
|
||||
@ -17,16 +24,19 @@ describe('Item Component', () => {
|
||||
api_key: 'test-api-key',
|
||||
}
|
||||
const mockOnUpdate = vi.fn()
|
||||
const mockOnEdit = vi.fn()
|
||||
const mockSetShowApiBasedExtensionModal = 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} onEdit={mockOnEdit} onUpdate={mockOnUpdate} />)
|
||||
render(<Item data={mockData} onUpdate={mockOnUpdate} />)
|
||||
|
||||
// Assert
|
||||
// Assert
|
||||
@ -44,7 +54,7 @@ describe('Item Component', () => {
|
||||
}
|
||||
|
||||
// Act
|
||||
render(<Item data={minimalData} onEdit={mockOnEdit} onUpdate={mockOnUpdate} />)
|
||||
render(<Item data={minimalData} onUpdate={mockOnUpdate} />)
|
||||
|
||||
// Assert
|
||||
// Assert
|
||||
@ -54,20 +64,41 @@ describe('Item Component', () => {
|
||||
})
|
||||
|
||||
describe('Modal Interactions', () => {
|
||||
it('should request editing with the current extension when clicking edit button', () => {
|
||||
it('should open edit modal with correct payload when clicking edit button', () => {
|
||||
// Act
|
||||
render(<Item data={mockData} onEdit={mockOnEdit} onUpdate={mockOnUpdate} />)
|
||||
render(<Item data={mockData} onUpdate={mockOnUpdate} />)
|
||||
fireEvent.click(screen.getByText('common.operation.edit'))
|
||||
|
||||
// Assert
|
||||
expect(mockOnEdit).toHaveBeenCalledWith(mockData)
|
||||
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)
|
||||
}
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
describe('Deletion', () => {
|
||||
it('should show delete confirmation dialog when clicking delete button', () => {
|
||||
// Act
|
||||
render(<Item data={mockData} onEdit={mockOnEdit} onUpdate={mockOnUpdate} />)
|
||||
render(<Item data={mockData} onUpdate={mockOnUpdate} />)
|
||||
fireEvent.click(screen.getByText('common.operation.delete'))
|
||||
|
||||
// Assert
|
||||
@ -78,7 +109,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} onEdit={mockOnEdit} onUpdate={mockOnUpdate} />)
|
||||
render(<Item data={mockData} onUpdate={mockOnUpdate} />)
|
||||
|
||||
// Act
|
||||
fireEvent.click(screen.getByText('common.operation.delete'))
|
||||
@ -98,7 +129,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} onEdit={mockOnEdit} onUpdate={mockOnUpdate} />)
|
||||
render(<Item data={mockData} onUpdate={mockOnUpdate} />)
|
||||
|
||||
// Act
|
||||
fireEvent.click(screen.getByText('common.operation.delete'))
|
||||
@ -116,7 +147,7 @@ describe('Item Component', () => {
|
||||
|
||||
it('should close delete confirmation when clicking cancel button', async () => {
|
||||
// Act
|
||||
render(<Item data={mockData} onEdit={mockOnEdit} onUpdate={mockOnUpdate} />)
|
||||
render(<Item data={mockData} onUpdate={mockOnUpdate} />)
|
||||
fireEvent.click(screen.getByText('common.operation.delete'))
|
||||
fireEvent.click(screen.getByText('common.operation.cancel'))
|
||||
|
||||
@ -128,7 +159,7 @@ describe('Item Component', () => {
|
||||
|
||||
it('should not call delete API when canceling deletion', () => {
|
||||
// Act
|
||||
render(<Item data={mockData} onEdit={mockOnEdit} onUpdate={mockOnUpdate} />)
|
||||
render(<Item data={mockData} onUpdate={mockOnUpdate} />)
|
||||
fireEvent.click(screen.getByText('common.operation.delete'))
|
||||
fireEvent.click(screen.getByText('common.operation.cancel'))
|
||||
|
||||
@ -157,7 +188,7 @@ describe('Item Component', () => {
|
||||
} as unknown as ReturnType<typeof reactI18next.useTranslation>)
|
||||
|
||||
// Act
|
||||
render(<Item data={mockData} onEdit={mockOnEdit} onUpdate={mockOnUpdate} />)
|
||||
render(<Item data={mockData} 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 { ComponentProps, ReactElement } from 'react'
|
||||
import type { 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 mockOnOpenChange = vi.fn()
|
||||
const mockOnCancel = vi.fn()
|
||||
const mockOnSave = vi.fn()
|
||||
const mockDocLink = vi.fn((path?: string) => `https://docs.dify.ai${path || ''}`)
|
||||
const mockExtension = (overrides: Partial<ApiBasedExtensionResponse> = {}): ApiBasedExtensionResponse => ({
|
||||
@ -46,19 +46,6 @@ 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()
|
||||
@ -68,10 +55,9 @@ describe('ApiBasedExtensionModal', () => {
|
||||
describe('Rendering', () => {
|
||||
it('should render correctly for adding a new extension', () => {
|
||||
// Act
|
||||
renderModal()
|
||||
render(<ApiBasedExtensionModal data={{}} onCancel={mockOnCancel} onSave={mockOnSave} />)
|
||||
|
||||
// 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()
|
||||
@ -83,7 +69,7 @@ describe('ApiBasedExtensionModal', () => {
|
||||
const data = mockExtension()
|
||||
|
||||
// Act
|
||||
renderModal({ extension: data })
|
||||
render(<ApiBasedExtensionModal data={data} onCancel={mockOnCancel} onSave={mockOnSave} />)
|
||||
|
||||
// Assert
|
||||
expect(screen.getByText('common.apiBasedExtension.modal.editTitle')).toBeInTheDocument()
|
||||
@ -91,14 +77,6 @@ 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', () => {
|
||||
@ -111,7 +89,7 @@ describe('ApiBasedExtensionModal', () => {
|
||||
api_key: 'secret-key',
|
||||
})
|
||||
vi.mocked(addApiBasedExtension).mockResolvedValue(newExtension)
|
||||
renderModal()
|
||||
render(<ApiBasedExtensionModal data={{}} onCancel={mockOnCancel} onSave={mockOnSave} />)
|
||||
|
||||
// Act
|
||||
fireEvent.change(screen.getByPlaceholderText('common.apiBasedExtension.modal.name.placeholder'), { target: { value: 'New Ext' } })
|
||||
@ -137,7 +115,7 @@ describe('ApiBasedExtensionModal', () => {
|
||||
// Arrange
|
||||
const data = mockExtension({ api_key: 'long-secret-key' })
|
||||
vi.mocked(updateApiBasedExtension).mockResolvedValue({ ...data, name: 'Updated' })
|
||||
renderModal({ extension: data })
|
||||
render(<ApiBasedExtensionModal data={data} onCancel={mockOnCancel} onSave={mockOnSave} />)
|
||||
|
||||
// Act
|
||||
fireEvent.change(screen.getByDisplayValue('Existing'), { target: { value: 'Updated' } })
|
||||
@ -147,11 +125,12 @@ describe('ApiBasedExtensionModal', () => {
|
||||
await waitFor(() => {
|
||||
expect(updateApiBasedExtension).toHaveBeenCalledWith({
|
||||
url: '/api-based-extension/1',
|
||||
body: {
|
||||
body: expect.objectContaining({
|
||||
id: '1',
|
||||
name: 'Updated',
|
||||
api_endpoint: 'url',
|
||||
api_key: '[__HIDDEN__]',
|
||||
},
|
||||
}),
|
||||
})
|
||||
expect(mockToast.success).toHaveBeenCalledWith('common.actionMsg.modifiedSuccessfully')
|
||||
expect(mockOnSave).toHaveBeenCalled()
|
||||
@ -162,7 +141,7 @@ describe('ApiBasedExtensionModal', () => {
|
||||
// Arrange
|
||||
const data = mockExtension({ api_key: 'old-key' })
|
||||
vi.mocked(updateApiBasedExtension).mockResolvedValue({ ...data, api_key: 'new-longer-key' })
|
||||
renderModal({ extension: data })
|
||||
render(<ApiBasedExtensionModal data={data} onCancel={mockOnCancel} onSave={mockOnSave} />)
|
||||
|
||||
// Act
|
||||
fireEvent.change(screen.getByDisplayValue('old-key'), { target: { value: 'new-longer-key' } })
|
||||
@ -172,11 +151,9 @@ describe('ApiBasedExtensionModal', () => {
|
||||
await waitFor(() => {
|
||||
expect(updateApiBasedExtension).toHaveBeenCalledWith({
|
||||
url: '/api-based-extension/1',
|
||||
body: {
|
||||
name: 'Existing',
|
||||
api_endpoint: 'url',
|
||||
body: expect.objectContaining({
|
||||
api_key: 'new-longer-key',
|
||||
},
|
||||
}),
|
||||
})
|
||||
})
|
||||
})
|
||||
@ -185,7 +162,7 @@ describe('ApiBasedExtensionModal', () => {
|
||||
describe('Validation', () => {
|
||||
it('should show error if api key is too short', async () => {
|
||||
// Arrange
|
||||
renderModal()
|
||||
render(<ApiBasedExtensionModal data={{}} onCancel={mockOnCancel} onSave={mockOnSave} />)
|
||||
|
||||
// Act
|
||||
fireEvent.change(screen.getByPlaceholderText('common.apiBasedExtension.modal.name.placeholder'), { target: { value: 'Ext' } })
|
||||
@ -203,7 +180,7 @@ describe('ApiBasedExtensionModal', () => {
|
||||
it('should work when onSave is not provided', async () => {
|
||||
// Arrange
|
||||
vi.mocked(addApiBasedExtension).mockResolvedValue(mockExtension({ id: 'new-id' }))
|
||||
renderModal({ onSave: undefined })
|
||||
render(<ApiBasedExtensionModal data={{}} onCancel={mockOnCancel} />)
|
||||
|
||||
// Act
|
||||
fireEvent.change(screen.getByPlaceholderText('common.apiBasedExtension.modal.name.placeholder'), { target: { value: 'New Ext' } })
|
||||
@ -217,56 +194,15 @@ describe('ApiBasedExtensionModal', () => {
|
||||
})
|
||||
})
|
||||
|
||||
it('should request closing when clicking cancel button', () => {
|
||||
it('should call onCancel when clicking cancel button', () => {
|
||||
// Arrange
|
||||
renderModal()
|
||||
render(<ApiBasedExtensionModal data={{}} onCancel={mockOnCancel} onSave={mockOnSave} />)
|
||||
|
||||
// Act
|
||||
fireEvent.click(screen.getByText('common.operation.cancel'))
|
||||
|
||||
// Assert
|
||||
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()
|
||||
expect(mockOnCancel).toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
|
||||
@ -294,7 +230,7 @@ describe('ApiBasedExtensionModal', () => {
|
||||
} as unknown as ReturnType<typeof reactI18next.useTranslation>)
|
||||
|
||||
// Act
|
||||
const { container } = renderModal({ onSave: undefined })
|
||||
const { container } = render(<ApiBasedExtensionModal data={{}} onCancel={mockOnCancel} />)
|
||||
|
||||
// Assert
|
||||
const inputs = container.querySelectorAll('input')
|
||||
|
||||
@ -1,10 +1,9 @@
|
||||
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, waitFor } from '@testing-library/react'
|
||||
import { fireEvent, render, screen } 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'
|
||||
|
||||
@ -16,15 +15,12 @@ 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[] = [
|
||||
@ -36,6 +32,7 @@ describe('ApiBasedExtensionSelector', () => {
|
||||
vi.clearAllMocks()
|
||||
vi.mocked(useModalContext).mockReturnValue({
|
||||
setShowAccountSettingModal: mockSetShowAccountSettingModal,
|
||||
setShowApiBasedExtensionModal: mockSetShowApiBasedExtensionModal,
|
||||
} as unknown as ModalContextState)
|
||||
vi.mocked(useApiBasedExtensions).mockReturnValue({
|
||||
data: mockData,
|
||||
@ -106,29 +103,26 @@ 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
|
||||
await waitFor(() => {
|
||||
expect(mockRefetch).toHaveBeenCalled()
|
||||
})
|
||||
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()
|
||||
}
|
||||
}
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@ -1,42 +1,24 @@
|
||||
import type { ApiBasedExtensionResponse } from '@dify/contracts/api/console/api-based-extension/types.gen'
|
||||
import { Button } from '@langgenius/dify-ui/button'
|
||||
import { useState } from 'react'
|
||||
import {
|
||||
RiAddLine,
|
||||
} from '@remixicon/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 = () => {
|
||||
setDialogState({
|
||||
extension: {},
|
||||
onSave: () => mutate(),
|
||||
setShowApiBasedExtensionModal({
|
||||
payload: {},
|
||||
onSaveCallback: () => 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>
|
||||
@ -51,7 +33,6 @@ const ApiBasedExtensionPage = () => {
|
||||
<Item
|
||||
key={item.id}
|
||||
data={item}
|
||||
onEdit={handleEditApiBasedExtension}
|
||||
onUpdate={() => mutate()}
|
||||
/>
|
||||
))
|
||||
@ -62,19 +43,9 @@ const ApiBasedExtensionPage = () => {
|
||||
className="w-full"
|
||||
onClick={handleOpenApiBasedExtensionModal}
|
||||
>
|
||||
<span className="mr-1 i-ri-add-line h-4 w-4" aria-hidden="true" />
|
||||
<RiAddLine className="mr-1 h-4 w-4" />
|
||||
{t('apiBasedExtension.add', { ns: 'common' })}
|
||||
</Button>
|
||||
{
|
||||
dialogState && (
|
||||
<ApiBasedExtensionModal
|
||||
open
|
||||
extension={dialogState.extension}
|
||||
onOpenChange={handleApiBasedExtensionModalOpenChange}
|
||||
onSave={handleSaveApiBasedExtension}
|
||||
/>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
import type { ApiBasedExtensionResponse } from '@dify/contracts/api/console/api-based-extension/types.gen'
|
||||
import type { FC } from 'react'
|
||||
import {
|
||||
AlertDialog,
|
||||
AlertDialogActions,
|
||||
@ -8,25 +9,32 @@ 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 = ({
|
||||
const Item: FC<ItemProps> = ({
|
||||
data,
|
||||
onEdit,
|
||||
onUpdate,
|
||||
}: ItemProps) => {
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
const { setShowApiBasedExtensionModal } = useModalContext()
|
||||
const [showDeleteConfirm, setShowDeleteConfirm] = useState(false)
|
||||
|
||||
const handleOpenApiBasedExtensionModal = () => {
|
||||
onEdit(data)
|
||||
setShowApiBasedExtensionModal({
|
||||
payload: data,
|
||||
onSaveCallback: () => onUpdate(),
|
||||
})
|
||||
}
|
||||
const handleDeleteApiBasedExtension = async () => {
|
||||
await deleteApiBasedExtension(`/api-based-extension/${data.id}`)
|
||||
@ -36,27 +44,28 @@ const Item = ({
|
||||
}
|
||||
|
||||
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 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="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="mb-0.5 text-[13px] font-medium text-text-secondary">{data.name}</div>
|
||||
<div className="truncate text-xs text-text-tertiary">{data.api_endpoint}</div>
|
||||
<div className="text-xs text-text-tertiary">{data.api_endpoint}</div>
|
||||
</div>
|
||||
<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">
|
||||
<div className="hidden items-center group-hover:flex">
|
||||
<Button
|
||||
className="mr-1"
|
||||
onClick={handleOpenApiBasedExtensionModal}
|
||||
>
|
||||
<span className="mr-1 i-ri-edit-line h-4 w-4" aria-hidden="true" />
|
||||
<RiEditLine className="mr-1 h-4 w-4" />
|
||||
{t('operation.edit', { ns: 'common' })}
|
||||
</Button>
|
||||
<Button
|
||||
onClick={() => setShowDeleteConfirm(true)}
|
||||
>
|
||||
<span className="mr-1 i-ri-delete-bin-line h-4 w-4" aria-hidden="true" />
|
||||
<RiDeleteBinLine className="mr-1 h-4 w-4" />
|
||||
{t('operation.delete', { ns: 'common' })}
|
||||
</Button>
|
||||
</div>
|
||||
<AlertDialog open={showDeleteConfirm} onOpenChange={open => !open && setShowDeleteConfirm(false)}>
|
||||
<AlertDialogContent backdropProps={{ forceRender: true }}>
|
||||
<AlertDialogContent>
|
||||
<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,57 +2,51 @@ 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, DialogCloseButton, DialogContent, DialogTitle } from '@langgenius/dify-ui/dialog'
|
||||
import { Dialog, DialogContent } 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 = {
|
||||
open: boolean
|
||||
extension: Partial<ApiBasedExtensionResponse>
|
||||
onOpenChange: (open: boolean) => void
|
||||
data: Partial<ApiBasedExtensionResponse>
|
||||
onCancel: () => void
|
||||
onSave?: (newData: ApiBasedExtensionResponse) => void
|
||||
}
|
||||
const ApiBasedExtensionModal = ({ open, extension, onOpenChange, onSave }: ApiBasedExtensionModalProps) => {
|
||||
const ApiBasedExtensionModal: FC<ApiBasedExtensionModalProps> = ({ data, onCancel, onSave }) => {
|
||||
const { t } = useTranslation()
|
||||
const docLink = useDocLink()
|
||||
const [localData, setLocalData] = useState(extension)
|
||||
const [localeData, setLocaleData] = useState(data)
|
||||
const [loading, setLoading] = useState(false)
|
||||
const handleDataChange = (field: ApiBasedExtensionField, value: string) => {
|
||||
setLocalData({ ...localData, [field]: value })
|
||||
const handleDataChange = (type: string, value: string) => {
|
||||
setLocaleData({ ...localeData, [type]: value })
|
||||
}
|
||||
const handleSave = async () => {
|
||||
setLoading(true)
|
||||
if (localData.api_key && localData.api_key.length < 5) {
|
||||
if (localeData && localeData.api_key && localeData.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 (!extension.id) {
|
||||
if (!data.id) {
|
||||
res = await addApiBasedExtension({
|
||||
url: '/api-based-extension',
|
||||
body: payload,
|
||||
body: localeData as ApiBasedExtensionPayload,
|
||||
})
|
||||
}
|
||||
else {
|
||||
res = await updateApiBasedExtension({
|
||||
url: `/api-based-extension/${extension.id}`,
|
||||
url: `/api-based-extension/${data.id}`,
|
||||
body: {
|
||||
...payload,
|
||||
api_key: extension.api_key === localData.api_key ? '[__HIDDEN__]' : payload.api_key,
|
||||
},
|
||||
...localeData,
|
||||
api_key: data.api_key === localeData.api_key ? '[__HIDDEN__]' : localeData.api_key,
|
||||
} as ApiBasedExtensionPayload,
|
||||
})
|
||||
toast.success(t('actionMsg.modifiedSuccessfully', { ns: 'common' }))
|
||||
}
|
||||
@ -63,47 +57,44 @@ const ApiBasedExtensionModal = ({ open, extension, onOpenChange, onSave }: ApiBa
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={onOpenChange} disablePointerDismissal>
|
||||
<DialogContent
|
||||
backdropProps={{ forceRender: true }}
|
||||
className="w-160 border-none p-8 pb-6 text-left"
|
||||
>
|
||||
<DialogCloseButton />
|
||||
<Dialog open>
|
||||
<DialogContent className="w-[640px]! max-w-none! border-none p-8! pb-6! text-left align-middle">
|
||||
|
||||
<DialogTitle className="mb-2 pr-8 text-xl font-semibold text-text-primary">
|
||||
{extension.name
|
||||
<div className="mb-2 text-xl font-semibold text-text-primary">
|
||||
{data.name
|
||||
? t('apiBasedExtension.modal.editTitle', { ns: 'common' })
|
||||
: t('apiBasedExtension.modal.title', { ns: 'common' })}
|
||||
</DialogTitle>
|
||||
</div>
|
||||
<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={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' }) || ''} />
|
||||
<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' }) || ''} />
|
||||
</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="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" />
|
||||
<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" />
|
||||
{t('apiBasedExtension.link', { ns: 'common' })}
|
||||
</a>
|
||||
</div>
|
||||
<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' }) || ''} />
|
||||
<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' }) || ''} />
|
||||
</div>
|
||||
<div className="py-2">
|
||||
<div className="text-sm leading-9 font-medium text-text-primary">
|
||||
{t('apiBasedExtension.modal.apiKey.title', { 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 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>
|
||||
</div>
|
||||
<div className="mt-6 flex items-center justify-end gap-2">
|
||||
<Button onClick={() => onOpenChange(false)}>
|
||||
<div className="mt-6 flex items-center justify-end">
|
||||
<Button onClick={onCancel} className="mr-2">
|
||||
{t('operation.cancel', { ns: 'common' })}
|
||||
</Button>
|
||||
<Button variant="primary" disabled={!localData.name || !localData.api_endpoint || !localData.api_key || loading} onClick={handleSave}>
|
||||
<Button variant="primary" disabled={!localeData.name || !localeData.api_endpoint || !localeData.api_key || loading} onClick={handleSave}>
|
||||
{t('operation.save', { ns: 'common' })}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
@ -1,25 +1,32 @@
|
||||
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 = ({
|
||||
const ApiBasedExtensionSelector: FC<ApiBasedExtensionSelectorProps> = ({
|
||||
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) => {
|
||||
@ -29,115 +36,91 @@ const ApiBasedExtensionSelector = ({
|
||||
|
||||
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>
|
||||
<span className={`i-ri-arrow-down-s-line h-4 w-4 text-text-secondary ${!open && 'opacity-60'}`} aria-hidden="true" />
|
||||
<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 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 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"
|
||||
>
|
||||
<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>
|
||||
<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"
|
||||
<div
|
||||
className="flex cursor-pointer items-center text-xs text-text-accent"
|
||||
onClick={() => {
|
||||
setOpen(false)
|
||||
setAddModalOpen(true)
|
||||
setShowAccountSettingModal({ payload: ACCOUNT_SETTING_TAB.API_BASED_EXTENSION })
|
||||
}}
|
||||
>
|
||||
<span className="mr-2 i-ri-add-line h-4 w-4" aria-hidden="true" />
|
||||
{t('operation.add', { ns: 'common' })}
|
||||
</button>
|
||||
{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>
|
||||
))
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
{
|
||||
addModalOpen && (
|
||||
<ApiBasedExtensionModal
|
||||
open
|
||||
extension={{}}
|
||||
onOpenChange={handleAddModalOpenChange}
|
||||
onSave={handleSaveApiBasedExtension}
|
||||
/>
|
||||
)
|
||||
}
|
||||
</>
|
||||
<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>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@ -26,6 +26,7 @@ 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,5 +1,6 @@
|
||||
'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'
|
||||
@ -35,6 +36,9 @@ 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,
|
||||
})
|
||||
@ -86,6 +90,7 @@ 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)
|
||||
@ -201,6 +206,12 @@ 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)
|
||||
@ -236,6 +247,7 @@ export const ModalContextProvider = ({
|
||||
return (
|
||||
<ModalContext.Provider value={{
|
||||
setShowAccountSettingModal,
|
||||
setShowApiBasedExtensionModal,
|
||||
setShowModerationSettingModal,
|
||||
setShowExternalDataToolModal,
|
||||
setShowPricingModal: handleShowPricingModal,
|
||||
@ -261,6 +273,15 @@ export const ModalContextProvider = ({
|
||||
)
|
||||
}
|
||||
|
||||
{
|
||||
!!showApiBasedExtensionModal && (
|
||||
<ApiBasedExtensionModal
|
||||
data={showApiBasedExtensionModal.payload}
|
||||
onCancel={() => setShowApiBasedExtensionModal(null)}
|
||||
onSave={handleSaveApiBasedExtension}
|
||||
/>
|
||||
)
|
||||
}
|
||||
{
|
||||
!!showModerationSettingModal && (
|
||||
<ModerationSettingModal
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
'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'
|
||||
@ -47,6 +48,7 @@ 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
|
||||
@ -66,6 +68,7 @@ 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