From 0536549f735d25ced0e15b9f755bceef8002bd70 Mon Sep 17 00:00:00 2001 From: kenwoodjw Date: Wed, 29 Apr 2026 10:27:02 +0800 Subject: [PATCH 1/4] fix: flaky WordExtractor close test in CI (#35652) Signed-off-by: kenwoodjw --- .../core/rag/extractor/test_word_extractor.py | 24 ++++++++----------- 1 file changed, 10 insertions(+), 14 deletions(-) diff --git a/api/tests/unit_tests/core/rag/extractor/test_word_extractor.py b/api/tests/unit_tests/core/rag/extractor/test_word_extractor.py index 0220fb6d4a..b9f2449cfb 100644 --- a/api/tests/unit_tests/core/rag/extractor/test_word_extractor.py +++ b/api/tests/unit_tests/core/rag/extractor/test_word_extractor.py @@ -1,14 +1,12 @@ """Primarily used for testing merged cell scenarios""" -import gc import io import os import tempfile -import warnings from collections import UserDict from pathlib import Path from types import SimpleNamespace -from unittest.mock import AsyncMock, MagicMock +from unittest.mock import MagicMock import pytest from docx import Document @@ -377,23 +375,21 @@ def test_close_is_idempotent(): extractor.temp_file.close.assert_called_once() -def test_close_handles_async_close_mock(): +async def _async_close() -> None: + return None + + +def test_close_closes_awaitable_close_result(): extractor = object.__new__(WordExtractor) extractor._closed = False extractor.temp_file = MagicMock() - extractor.temp_file.close = AsyncMock() + close_result = _async_close() + extractor.temp_file.close = MagicMock(return_value=close_result) - with warnings.catch_warnings(record=True) as caught: - warnings.simplefilter("always") - extractor.close() - gc.collect() + extractor.close() + assert close_result.cr_frame is None extractor.temp_file.close.assert_called_once() - assert not [ - warning - for warning in caught - if issubclass(warning.category, RuntimeWarning) and "AsyncMockMixin._execute_mock_call" in str(warning.message) - ] def test_extract_images_handles_invalid_external_cases(monkeypatch): From 16d408d908911fc3ee37b92370727102272cf885 Mon Sep 17 00:00:00 2001 From: hyl64 <78853927+hyl64@users.noreply.github.com> Date: Wed, 29 Apr 2026 10:41:15 +0800 Subject: [PATCH 2/4] fix: refresh MCP tool metadata after updates and align App DSL test stubs (#35354) Co-authored-by: Stephen Zhou --- .../services/test_app_dsl_service.py | 91 +++++++------------ .../mcp/detail/__tests__/content.spec.tsx | 5 + .../components/tools/mcp/detail/content.tsx | 5 +- 3 files changed, 41 insertions(+), 60 deletions(-) diff --git a/api/tests/test_containers_integration_tests/services/test_app_dsl_service.py b/api/tests/test_containers_integration_tests/services/test_app_dsl_service.py index 77ce28b999..1835650c42 100644 --- a/api/tests/test_containers_integration_tests/services/test_app_dsl_service.py +++ b/api/tests/test_containers_integration_tests/services/test_app_dsl_service.py @@ -3,6 +3,7 @@ from __future__ import annotations import base64 import json from types import SimpleNamespace +from typing import Any, cast from unittest.mock import MagicMock, patch from uuid import uuid4 @@ -17,7 +18,7 @@ from core.trigger.constants import ( ) from extensions.ext_redis import redis_client from graphon.enums import BuiltinNodeTypes -from models import Account, AppMode +from models import Account, App, AppMode from models.model import AppModelConfig, IconType from services import app_dsl_service from services.account_service import AccountService, TenantService @@ -67,6 +68,22 @@ def _pending_yaml_content(version: str = "99.0.0") -> bytes: return (f'version: "{version}"\nkind: app\napp:\n name: Loop Test\n mode: workflow\n').encode() +def _app_stub(**overrides: Any) -> App: + defaults = { + "id": str(uuid4()), + "tenant_id": _DEFAULT_TENANT_ID, + "mode": AppMode.WORKFLOW.value, + "name": "n", + "description": "d", + "icon_type": IconType.EMOJI, + "icon": "i", + "icon_background": "#fff", + "use_icon_as_answer_icon": False, + "app_model_config": None, + } + return cast(App, SimpleNamespace(**(defaults | overrides))) + + class TestAppDslService: """Integration tests for AppDslService using testcontainers.""" @@ -585,7 +602,7 @@ class TestAppDslService: def test_check_dependencies_returns_empty_when_no_redis_data(self, db_session_with_containers): service = AppDslService(db_session_with_containers) - app_model = SimpleNamespace(id=str(uuid4()), tenant_id=_DEFAULT_TENANT_ID) + app_model = _app_stub() result = service.check_dependencies(app_model=app_model) assert result.leaked_dependencies == [] @@ -614,7 +631,7 @@ class TestAppDslService: ) service = AppDslService(db_session_with_containers) - result = service.check_dependencies(app_model=SimpleNamespace(id=app_id, tenant_id=_DEFAULT_TENANT_ID)) + result = service.check_dependencies(app_model=_app_stub(id=app_id)) assert len(result.leaked_dependencies) == 1 def test_check_dependencies_with_real_app(self, db_session_with_containers, mock_external_service_dependencies): @@ -656,9 +673,7 @@ class TestAppDslService: lambda _m: SimpleNamespace(kind="conv"), ) - app = SimpleNamespace( - id=str(uuid4()), - tenant_id=_DEFAULT_TENANT_ID, + app = _app_stub( mode=AppMode.WORKFLOW.value, name="old", description="old-desc", @@ -667,7 +682,6 @@ class TestAppDslService: icon_background="#111111", updated_by=None, updated_at=None, - app_model_config=None, ) service = AppDslService(db_session_with_containers) updated = service._create_or_update_app( @@ -745,15 +759,7 @@ class TestAppDslService: service = AppDslService(db_session_with_containers) with pytest.raises(ValueError, match="Missing workflow data"): service._create_or_update_app( - app=SimpleNamespace( - id=str(uuid4()), - tenant_id=_DEFAULT_TENANT_ID, - mode=AppMode.WORKFLOW.value, - name="n", - description="d", - icon_background="#fff", - app_model_config=None, - ), + app=_app_stub(mode=AppMode.WORKFLOW.value), data={"app": {"mode": AppMode.WORKFLOW.value}}, account=_account_mock(), ) @@ -762,15 +768,7 @@ class TestAppDslService: service = AppDslService(db_session_with_containers) with pytest.raises(ValueError, match="Missing model_config"): service._create_or_update_app( - app=SimpleNamespace( - id=str(uuid4()), - tenant_id=_DEFAULT_TENANT_ID, - mode=AppMode.CHAT.value, - name="n", - description="d", - icon_background="#fff", - app_model_config=None, - ), + app=_app_stub(mode=AppMode.CHAT.value), data={"app": {"mode": AppMode.CHAT.value}}, account=_account_mock(), ) @@ -799,15 +797,7 @@ class TestAppDslService: service = AppDslService(db_session_with_containers) with pytest.raises(ValueError, match="Invalid app mode"): service._create_or_update_app( - app=SimpleNamespace( - id=str(uuid4()), - tenant_id=_DEFAULT_TENANT_ID, - mode=AppMode.RAG_PIPELINE.value, - name="n", - description="d", - icon_background="#fff", - app_model_config=None, - ), + app=_app_stub(mode=AppMode.RAG_PIPELINE.value), data={"app": {"mode": AppMode.RAG_PIPELINE.value}}, account=_account_mock(), ) @@ -828,29 +818,16 @@ class TestAppDslService: lambda *_args, **_kwargs: model_calls.append(True), ) - workflow_app = SimpleNamespace( + workflow_app = _app_stub( mode=AppMode.WORKFLOW.value, - tenant_id=_DEFAULT_TENANT_ID, - name="n", - icon="i", icon_type="emoji", - icon_background="#fff", - description="d", - use_icon_as_answer_icon=False, - app_model_config=None, ) AppDslService.export_dsl(workflow_app) assert workflow_calls == [True] - chat_app = SimpleNamespace( + chat_app = _app_stub( mode=AppMode.CHAT.value, - tenant_id=_DEFAULT_TENANT_ID, - name="n", - icon="i", icon_type="emoji", - icon_background="#fff", - description="d", - use_icon_as_answer_icon=False, app_model_config=SimpleNamespace(to_dict=lambda: {"agent_mode": {"tools": []}}), ) AppDslService.export_dsl(chat_app) @@ -863,16 +840,14 @@ class TestAppDslService: lambda **_kwargs: None, ) - emoji_app = SimpleNamespace( + emoji_app = _app_stub( mode=AppMode.WORKFLOW.value, - tenant_id=_DEFAULT_TENANT_ID, name="Emoji App", icon="🎨", icon_type=IconType.EMOJI, icon_background="#FF5733", description="App with emoji icon", use_icon_as_answer_icon=True, - app_model_config=None, ) yaml_output = AppDslService.export_dsl(emoji_app) data = yaml.safe_load(yaml_output) @@ -880,16 +855,14 @@ class TestAppDslService: assert data["app"]["icon_type"] == "emoji" assert data["app"]["icon_background"] == "#FF5733" - image_app = SimpleNamespace( + image_app = _app_stub( mode=AppMode.WORKFLOW.value, - tenant_id=_DEFAULT_TENANT_ID, name="Image App", icon="https://example.com/icon.png", icon_type=IconType.IMAGE, icon_background="#FFEAD5", description="App with image icon", use_icon_as_answer_icon=False, - app_model_config=None, ) yaml_output = AppDslService.export_dsl(image_app) data = yaml.safe_load(yaml_output) @@ -1106,7 +1079,7 @@ class TestAppDslService: export_data: dict = {} AppDslService._append_workflow_export_data( export_data=export_data, - app_model=SimpleNamespace(tenant_id=_DEFAULT_TENANT_ID), + app_model=_app_stub(), include_secret=False, workflow_id=None, ) @@ -1132,7 +1105,7 @@ class TestAppDslService: with pytest.raises(ValueError, match="Missing draft workflow configuration"): AppDslService._append_workflow_export_data( export_data={}, - app_model=SimpleNamespace(tenant_id=_DEFAULT_TENANT_ID), + app_model=_app_stub(), include_secret=False, workflow_id=None, ) @@ -1160,7 +1133,7 @@ class TestAppDslService: monkeypatch.setattr(app_dsl_service, "jsonable_encoder", lambda x: x) app_model_config = SimpleNamespace(to_dict=lambda: {"agent_mode": {"tools": [{"credential_id": "secret"}]}}) - app_model = SimpleNamespace(tenant_id=_DEFAULT_TENANT_ID, app_model_config=app_model_config) + app_model = _app_stub(app_model_config=app_model_config) export_data: dict = {} AppDslService._append_model_config_export_data(export_data, app_model) @@ -1169,7 +1142,7 @@ class TestAppDslService: def test_append_model_config_export_data_requires_app_config(self): with pytest.raises(ValueError, match="Missing app configuration"): - AppDslService._append_model_config_export_data({}, SimpleNamespace(app_model_config=None)) + AppDslService._append_model_config_export_data({}, _app_stub(app_model_config=None)) # ── Dependency Extraction ───────────────────────────────────────── diff --git a/web/app/components/tools/mcp/detail/__tests__/content.spec.tsx b/web/app/components/tools/mcp/detail/__tests__/content.spec.tsx index f7bf8181ed..a7d5225348 100644 --- a/web/app/components/tools/mcp/detail/__tests__/content.spec.tsx +++ b/web/app/components/tools/mcp/detail/__tests__/content.spec.tsx @@ -12,6 +12,7 @@ const mockAuthorizeMcp = vi.fn().mockResolvedValue({ result: 'success' }) const mockUpdateMCP = vi.fn().mockResolvedValue({ result: 'success' }) const mockDeleteMCP = vi.fn().mockResolvedValue({ result: 'success' }) const mockInvalidateMCPTools = vi.fn() +const mockInvalidateAllMCPTools = vi.fn() const mockOpenOAuthPopup = vi.fn() // Mutable mock state @@ -33,6 +34,7 @@ vi.mock('@/service/use-tools', () => ({ isFetching: mockIsFetching, }), useInvalidateMCPTools: () => mockInvalidateMCPTools, + useInvalidateAllMCPTools: () => mockInvalidateAllMCPTools, useUpdateMCPTools: () => ({ mutateAsync: mockUpdateTools, isPending: mockIsUpdating, @@ -180,6 +182,7 @@ describe('MCPDetailContent', () => { mockUpdateMCP.mockClear() mockDeleteMCP.mockClear() mockInvalidateMCPTools.mockClear() + mockInvalidateAllMCPTools.mockClear() mockOpenOAuthPopup.mockClear() // Reset mock return values @@ -513,6 +516,7 @@ describe('MCPDetailContent', () => { await waitFor(() => { expect(mockUpdateTools).toHaveBeenCalledWith('mcp-1') expect(mockInvalidateMCPTools).toHaveBeenCalledWith('mcp-1') + expect(mockInvalidateAllMCPTools).toHaveBeenCalled() expect(onUpdate).toHaveBeenCalled() }) }) @@ -530,6 +534,7 @@ describe('MCPDetailContent', () => { await waitFor(() => { expect(mockUpdateTools).toHaveBeenCalledWith('mcp-1') + expect(mockInvalidateAllMCPTools).toHaveBeenCalled() }) }) }) diff --git a/web/app/components/tools/mcp/detail/content.tsx b/web/app/components/tools/mcp/detail/content.tsx index 35c8a35a6f..c785516eee 100644 --- a/web/app/components/tools/mcp/detail/content.tsx +++ b/web/app/components/tools/mcp/detail/content.tsx @@ -26,6 +26,7 @@ import { openOAuthPopup } from '@/hooks/use-oauth' import { useAuthorizeMCP, useDeleteMCP, + useInvalidateAllMCPTools, useInvalidateMCPTools, useMCPTools, useUpdateMCP, @@ -61,6 +62,7 @@ const MCPDetailContent: FC = ({ const { data, isFetching: isGettingTools } = useMCPTools(detail.is_team_authorization ? detail.id : '') const invalidateMCPTools = useInvalidateMCPTools() + const invalidateAllMCPTools = useInvalidateAllMCPTools() const { mutateAsync: updateTools, isPending: isUpdating } = useUpdateMCPTools() const { mutateAsync: authorizeMcp, isPending: isAuthorizing } = useAuthorizeMCP() const toolList = data?.tools || [] @@ -76,8 +78,9 @@ const MCPDetailContent: FC = ({ return await updateTools(detail.id) invalidateMCPTools(detail.id) + invalidateAllMCPTools() onUpdate() - }, [detail, hideUpdateConfirm, invalidateMCPTools, onUpdate, updateTools]) + }, [detail, hideUpdateConfirm, invalidateAllMCPTools, invalidateMCPTools, onUpdate, updateTools]) const { mutateAsync: updateMCP } = useUpdateMCP({}) const { mutateAsync: deleteMCP } = useDeleteMCP({}) From d23cefe005b1e4507cb517fbd32f021c60225df9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=9D=9E=E6=B3=95=E6=93=8D=E4=BD=9C?= Date: Wed, 29 Apr 2026 11:00:06 +0800 Subject: [PATCH 3/4] fix: improve workflow as tool overlays (#35661) Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com> --- eslint-suppressions.json | 15 - .../__tests__/configure-button.spec.tsx | 38 +- .../workflow-tool/__tests__/index.spec.tsx | 8 +- .../__tests__/method-selector.spec.tsx | 23 +- .../confirm-modal/__tests__/index.spec.tsx | 11 +- .../workflow-tool/confirm-modal/index.tsx | 47 +- .../__tests__/use-configure-button.spec.ts | 1 - .../hooks/use-configure-button.ts | 1 - .../components/tools/workflow-tool/index.tsx | 490 +++++++++++------- .../tools/workflow-tool/method-selector.tsx | 70 +-- 10 files changed, 404 insertions(+), 300 deletions(-) diff --git a/eslint-suppressions.json b/eslint-suppressions.json index e1c8bda126..c724609e88 100644 --- a/eslint-suppressions.json +++ b/eslint-suppressions.json @@ -3820,21 +3820,6 @@ "count": 4 } }, - "web/app/components/tools/workflow-tool/confirm-modal/index.tsx": { - "no-restricted-imports": { - "count": 1 - } - }, - "web/app/components/tools/workflow-tool/index.tsx": { - "no-restricted-imports": { - "count": 1 - } - }, - "web/app/components/tools/workflow-tool/method-selector.tsx": { - "no-restricted-imports": { - "count": 1 - } - }, "web/app/components/workflow-app/components/workflow-children.tsx": { "ts/no-explicit-any": { "count": 3 diff --git a/web/app/components/tools/workflow-tool/__tests__/configure-button.spec.tsx b/web/app/components/tools/workflow-tool/__tests__/configure-button.spec.tsx index 646a095622..7060e29f95 100644 --- a/web/app/components/tools/workflow-tool/__tests__/configure-button.spec.tsx +++ b/web/app/components/tools/workflow-tool/__tests__/configure-button.spec.tsx @@ -9,6 +9,8 @@ import WorkflowToolConfigureButton from '../configure-button' import WorkflowToolAsModal from '../index' import MethodSelector from '../method-selector' +vi.mock('@langgenius/dify-ui/popover', () => import('@/__mocks__/base-ui-popover')) + // Mock Next.js navigation const mockPush = vi.fn() vi.mock('@/next/navigation', () => ({ @@ -83,12 +85,11 @@ vi.mock('@/app/components/base/drawer-plus', () => ({ }, })) -// Mock EmojiPicker - simplified for testing -vi.mock('@/app/components/base/emoji-picker', () => ({ - default: ({ onSelect, onClose }: { onSelect: (icon: string, background: string) => void, onClose: () => void }) => ( +// Mock EmojiPickerInner - simplified for testing +vi.mock('@/app/components/base/emoji-picker/Inner', () => ({ + default: ({ onSelect }: { onSelect: (icon: string, background: string) => void }) => (
-
), })) @@ -978,6 +979,7 @@ describe('WorkflowToolAsModal', () => { // Select emoji await user.click(screen.getByTestId('select-emoji')) + await user.click(screen.getByRole('button', { name: 'app.iconPicker.ok' })) // Assert const updatedIcon = screen.getByTestId('app-icon') @@ -1002,7 +1004,7 @@ describe('WorkflowToolAsModal', () => { expect(screen.getByTestId('emoji-picker'))!.toBeInTheDocument() - await user.click(screen.getByTestId('close-emoji-picker')) + await user.click(screen.getByRole('button', { name: 'app.iconPicker.cancel' })) // Assert // Assert @@ -1501,7 +1503,7 @@ describe('MethodSelector', () => { // Assert // Assert - expect(screen.getByTestId('portal-trigger'))!.toBeInTheDocument() + expect(screen.getByTestId('popover-trigger'))!.toBeInTheDocument() }) it('should display parameter method text when value is llm', () => { @@ -1562,11 +1564,11 @@ describe('MethodSelector', () => { // Act render() - await user.click(screen.getByTestId('portal-trigger')) + await user.click(screen.getByTestId('popover-trigger')) // Assert // Assert - expect(screen.getByTestId('portal-content'))!.toBeInTheDocument() + expect(screen.getByTestId('popover-content'))!.toBeInTheDocument() }) it('should call onChange with llm when parameter option clicked', async () => { @@ -1580,7 +1582,7 @@ describe('MethodSelector', () => { // Act render() - await user.click(screen.getByTestId('portal-trigger')) + await user.click(screen.getByTestId('popover-trigger')) const paramOption = screen.getAllByText('tools.createTool.toolInput.methodParameter')[0] await user.click(paramOption!) @@ -1600,7 +1602,7 @@ describe('MethodSelector', () => { // Act render() - await user.click(screen.getByTestId('portal-trigger')) + await user.click(screen.getByTestId('popover-trigger')) const settingOption = screen.getByText('tools.createTool.toolInput.methodSetting') await user.click(settingOption) @@ -1621,12 +1623,12 @@ describe('MethodSelector', () => { render() // First click - open - await user.click(screen.getByTestId('portal-trigger')) - expect(screen.getByTestId('portal-content'))!.toBeInTheDocument() + await user.click(screen.getByTestId('popover-trigger')) + expect(screen.getByTestId('popover-content'))!.toBeInTheDocument() // Second click - close - await user.click(screen.getByTestId('portal-trigger')) - expect(screen.queryByTestId('portal-content')).not.toBeInTheDocument() + await user.click(screen.getByTestId('popover-trigger')) + expect(screen.queryByTestId('popover-content')).not.toBeInTheDocument() }) }) @@ -1642,10 +1644,10 @@ describe('MethodSelector', () => { // Act render() - await user.click(screen.getByTestId('portal-trigger')) + await user.click(screen.getByTestId('popover-trigger')) // Assert - the first option (llm) should have a check icon container - const content = screen.getByTestId('portal-content') + const content = screen.getByTestId('popover-content') expect(content)!.toBeInTheDocument() }) @@ -1659,10 +1661,10 @@ describe('MethodSelector', () => { // Act render() - await user.click(screen.getByTestId('portal-trigger')) + await user.click(screen.getByTestId('popover-trigger')) // Assert - const content = screen.getByTestId('portal-content') + const content = screen.getByTestId('popover-content') expect(content)!.toBeInTheDocument() }) }) diff --git a/web/app/components/tools/workflow-tool/__tests__/index.spec.tsx b/web/app/components/tools/workflow-tool/__tests__/index.spec.tsx index 2ec289fcf6..9f5532f1f7 100644 --- a/web/app/components/tools/workflow-tool/__tests__/index.spec.tsx +++ b/web/app/components/tools/workflow-tool/__tests__/index.spec.tsx @@ -18,11 +18,10 @@ vi.mock('@/app/components/base/drawer-plus', () => ({ ), })) -vi.mock('@/app/components/base/emoji-picker', () => ({ - default: ({ onSelect, onClose }: { onSelect: (icon: string, background: string) => void, onClose: () => void }) => ( +vi.mock('@/app/components/base/emoji-picker/Inner', () => ({ + default: ({ onSelect }: { onSelect: (icon: string, background: string) => void }) => (
-
), })) @@ -129,6 +128,7 @@ describe('WorkflowToolAsModal', () => { await user.click(screen.getByTestId('append-label')) await user.click(screen.getByTestId('app-icon')) await user.click(screen.getByTestId('select-emoji')) + await user.click(screen.getByRole('button', { name: 'app.iconPicker.ok' })) await user.click(screen.getByRole('button', { name: 'common.operation.save' })) expect(onCreate).toHaveBeenCalledWith(expect.objectContaining({ @@ -195,6 +195,6 @@ describe('WorkflowToolAsModal', () => { />, ) - expect(screen.getAllByText('tools.createTool.toolOutput.reservedParameterDuplicateTip').length).toBeGreaterThan(0) + expect(screen.getAllByTestId('reserved-output-warning').length).toBeGreaterThan(0) }) }) diff --git a/web/app/components/tools/workflow-tool/__tests__/method-selector.spec.tsx b/web/app/components/tools/workflow-tool/__tests__/method-selector.spec.tsx index d1126bf762..19b796f2db 100644 --- a/web/app/components/tools/workflow-tool/__tests__/method-selector.spec.tsx +++ b/web/app/components/tools/workflow-tool/__tests__/method-selector.spec.tsx @@ -4,6 +4,8 @@ import userEvent from '@testing-library/user-event' import { beforeEach, describe, expect, it, vi } from 'vitest' import MethodSelector from '../method-selector' +vi.mock('@langgenius/dify-ui/popover', () => import('@/__mocks__/base-ui-popover')) + // Test utilities const defaultProps: ComponentProps = { value: 'llm', @@ -139,6 +141,24 @@ describe('MethodSelector', () => { expect(onChange).toHaveBeenCalledWith('form') }) + it('should close dropdown after an option is clicked', async () => { + const user = userEvent.setup() + renderComponent({ value: 'llm' }) + + const trigger = screen.getByText('tools.createTool.toolInput.methodParameter') + await user.click(trigger) + + await waitFor(() => { + expect(screen.getByText('tools.createTool.toolInput.methodSettingTip'))!.toBeInTheDocument() + }) + + await user.click(screen.getByText('tools.createTool.toolInput.methodSettingTip')) + + await waitFor(() => { + expect(screen.queryByText('tools.createTool.toolInput.methodSettingTip')).not.toBeInTheDocument() + }) + }) + it('should toggle dropdown open state', async () => { const user = userEvent.setup() renderComponent() @@ -235,10 +255,9 @@ describe('MethodSelector', () => { await user.click(trigger) await waitFor(() => { + expect(screen.getByTestId('popover-content')).toBeInTheDocument() const dropdown = document.querySelector('.w-\\[320px\\]') expect(dropdown)!.toBeInTheDocument() - expect(dropdown)!.toHaveClass('rounded-lg') - expect(dropdown)!.toHaveClass('shadow-lg') }) }) diff --git a/web/app/components/tools/workflow-tool/confirm-modal/__tests__/index.spec.tsx b/web/app/components/tools/workflow-tool/confirm-modal/__tests__/index.spec.tsx index c5bce8b663..6535564a32 100644 --- a/web/app/components/tools/workflow-tool/confirm-modal/__tests__/index.spec.tsx +++ b/web/app/components/tools/workflow-tool/confirm-modal/__tests__/index.spec.tsx @@ -93,13 +93,12 @@ describe('ConfirmModal', () => { // Arrange & Act renderComponent() - // Assert - Check for the dialog panel with modal content - // The real modal structure has nested divs, we need to find the one with our classes - const dialogContent = document.querySelector('.relative.rounded-2xl') + // Assert + const dialogContent = screen.getByRole('dialog') expect(dialogContent).toBeInTheDocument() - expect(dialogContent).toHaveClass('w-[600px]') - expect(dialogContent).toHaveClass('max-w-[600px]') - expect(dialogContent).toHaveClass('p-8') + expect(dialogContent).toHaveClass('w-[600px]!') + expect(dialogContent).toHaveClass('max-w-[600px]!') + expect(dialogContent).toHaveClass('p-8!') }) }) diff --git a/web/app/components/tools/workflow-tool/confirm-modal/index.tsx b/web/app/components/tools/workflow-tool/confirm-modal/index.tsx index ba45387731..4f17862c1a 100644 --- a/web/app/components/tools/workflow-tool/confirm-modal/index.tsx +++ b/web/app/components/tools/workflow-tool/confirm-modal/index.tsx @@ -2,11 +2,9 @@ import { Button } from '@langgenius/dify-ui/button' import { cn } from '@langgenius/dify-ui/cn' -import { RiCloseLine } from '@remixicon/react' -import { noop } from 'es-toolkit/function' +import { Dialog, DialogContent, DialogTitle } from '@langgenius/dify-ui/dialog' import { useTranslation } from 'react-i18next' import { AlertTriangle } from '@/app/components/base/icons/src/vender/solid/alertsAndFeedback' -import Modal from '@/app/components/base/modal' type ConfirmModalProps = { show: boolean @@ -18,28 +16,29 @@ const ConfirmModal = ({ show, onConfirm, onClose }: ConfirmModalProps) => { const { t } = useTranslation() return ( - -
- -
-
- -
-
{t('createTool.confirmTitle', { ns: 'tools' })}
-
- {t('createTool.confirmTip', { ns: 'tools' })} -
-
-
- - + + +
+
-
- +
+ +
+ {t('createTool.confirmTitle', { ns: 'tools' })} +
+ {t('createTool.confirmTip', { ns: 'tools' })} +
+
+
+ + +
+
+ + ) } diff --git a/web/app/components/tools/workflow-tool/hooks/__tests__/use-configure-button.spec.ts b/web/app/components/tools/workflow-tool/hooks/__tests__/use-configure-button.spec.ts index efbf16d590..8bc3db95da 100644 --- a/web/app/components/tools/workflow-tool/hooks/__tests__/use-configure-button.spec.ts +++ b/web/app/components/tools/workflow-tool/hooks/__tests__/use-configure-button.spec.ts @@ -437,7 +437,6 @@ describe('useConfigureButton', () => { expect(onRefreshData).toHaveBeenCalled() expect(mockInvalidateAllWorkflowTools).toHaveBeenCalled() expect(mockInvalidateWorkflowToolDetailByAppID).toHaveBeenCalledWith('app-123') - expect(mockToastNotify).toHaveBeenCalledWith({ type: 'success', message: expect.any(String) }) expect(result.current.showModal).toBe(false) }) diff --git a/web/app/components/tools/workflow-tool/hooks/use-configure-button.ts b/web/app/components/tools/workflow-tool/hooks/use-configure-button.ts index 9f0a43635c..33965aa5ee 100644 --- a/web/app/components/tools/workflow-tool/hooks/use-configure-button.ts +++ b/web/app/components/tools/workflow-tool/hooks/use-configure-button.ts @@ -206,7 +206,6 @@ export function useConfigureButton(options: UseConfigureButtonOptions) { onRefreshData?.() invalidateAllWorkflowTools() invalidateDetail(workflowAppId) - toast.success(t('api.actionSuccess', { ns: 'common' })) setShowModal(false) } catch (e) { diff --git a/web/app/components/tools/workflow-tool/index.tsx b/web/app/components/tools/workflow-tool/index.tsx index 353e85beba..6f8258f185 100644 --- a/web/app/components/tools/workflow-tool/index.tsx +++ b/web/app/components/tools/workflow-tool/index.tsx @@ -3,18 +3,18 @@ import type { FC } from 'react' import type { Emoji, WorkflowToolProviderOutputParameter, WorkflowToolProviderOutputSchema, WorkflowToolProviderParameter, WorkflowToolProviderRequest } from '../types' import { Button } from '@langgenius/dify-ui/button' import { cn } from '@langgenius/dify-ui/cn' +import { Dialog, DialogContent, DialogTitle } from '@langgenius/dify-ui/dialog' import { toast } from '@langgenius/dify-ui/toast' -import { RiErrorWarningLine } from '@remixicon/react' +import { Tooltip, TooltipContent, TooltipTrigger } from '@langgenius/dify-ui/tooltip' import { produce } from 'immer' import * as React from 'react' import { useMemo, useState } from 'react' import { useTranslation } from 'react-i18next' import AppIcon from '@/app/components/base/app-icon' -import Drawer from '@/app/components/base/drawer-plus' -import EmojiPicker from '@/app/components/base/emoji-picker' +import Divider from '@/app/components/base/divider' +import EmojiPickerInner from '@/app/components/base/emoji-picker/Inner' import Input from '@/app/components/base/input' import Textarea from '@/app/components/base/textarea' -import Tooltip from '@/app/components/base/tooltip' import LabelSelector from '@/app/components/tools/labels/selector' import ConfirmModal from '@/app/components/tools/workflow-tool/confirm-modal' import MethodSelector from '@/app/components/tools/workflow-tool/method-selector' @@ -53,6 +53,111 @@ type Props = { workflow_tool_id: string }>) => void } + +type WorkflowToolDrawerProps = { + title: string + onHide: () => void + children: React.ReactNode +} + +const InfoTooltip = ({ children }: { children: React.ReactNode }) => { + return ( + + + )} + /> + +
+ {children} +
+
+
+ ) +} + +const WorkflowToolDrawer = ({ title, onHide, children }: WorkflowToolDrawerProps) => { + return ( + + +
+
+
+ + {title} + + +
+
+
+ {children} +
+
+
+
+ ) +} + +type WorkflowToolEmojiPickerProps = { + onSelect: (icon: string, background: string) => void + onClose: () => void +} + +const WorkflowToolEmojiPicker = ({ onSelect, onClose }: WorkflowToolEmojiPickerProps) => { + const { t } = useTranslation() + const [selectedEmoji, setSelectedEmoji] = useState('') + const [selectedBackground, setSelectedBackground] = useState() + + return ( + + + + {t('iconPicker.emoji', { ns: 'app' })} + + { + setSelectedEmoji(emoji) + setSelectedBackground(background) + }} + /> + +
+ + +
+
+
+ ) +} + // Add and Edit const WorkflowToolAsModal: FC = ({ isAdd, @@ -138,210 +243,201 @@ const WorkflowToolAsModal: FC = ({ return ( <> - -
- {/* name & icon */} -
-
- {t('createTool.name', { ns: 'tools' })} - {' '} - * -
-
- { setShowEmojiPicker(true) }} className="cursor-pointer" iconType="emoji" icon={emoji.content} background={emoji.background} /> - setLabel(e.target.value)} - /> -
+ > +
+
+ {/* name & icon */} +
+
+ {t('createTool.name', { ns: 'tools' })} + {' '} + *
- {/* name for tool call */} -
-
- {t('createTool.nameForToolCall', { ns: 'tools' })} - {' '} - * - - {t('createTool.nameForToolCallPlaceHolder', { ns: 'tools' })} -
- )} - /> -
+
+ { setShowEmojiPicker(true) }} className="cursor-pointer" iconType="emoji" icon={emoji.content} background={emoji.background} /> setName(e.target.value)} - /> - {!isWorkflowToolNameValid(name) && ( -
{t('createTool.nameForToolCallTip', { ns: 'tools' })}
- )} -
- {/* description */} -
-
{t('createTool.description', { ns: 'tools' })}
-