diff --git a/api/events/event_handlers/delete_tool_parameters_cache_when_sync_draft_workflow.py b/api/events/event_handlers/delete_tool_parameters_cache_when_sync_draft_workflow.py index ba9758175f..f1196445ed 100644 --- a/api/events/event_handlers/delete_tool_parameters_cache_when_sync_draft_workflow.py +++ b/api/events/event_handlers/delete_tool_parameters_cache_when_sync_draft_workflow.py @@ -3,6 +3,7 @@ import logging from core.tools.entities.tool_entities import ToolProviderType from core.tools.tool_manager import ToolManager from core.tools.utils.configuration import ToolParameterConfigurationManager +from core.workflow.human_input_adapter import adapt_node_config_for_graph from events.app_event import app_draft_workflow_was_synced from graphon.nodes import BuiltinNodeTypes from graphon.nodes.tool.entities import ToolEntity @@ -19,7 +20,8 @@ def handle(sender, **kwargs): for node_data in synced_draft_workflow.graph_dict.get("nodes", []): if node_data.get("data", {}).get("type") == BuiltinNodeTypes.TOOL: try: - tool_entity = ToolEntity.model_validate(node_data["data"]) + adapted_node_data = adapt_node_config_for_graph(node_data) + tool_entity = ToolEntity.model_validate(adapted_node_data["data"]) provider_type = ToolProviderType(tool_entity.provider_type.value) tool_runtime = ToolManager.get_tool_runtime( provider_type=provider_type, diff --git a/api/models/workflow.py b/api/models/workflow.py index cb1723440b..7936c06a5a 100644 --- a/api/models/workflow.py +++ b/api/models/workflow.py @@ -1568,12 +1568,14 @@ class WorkflowDraftVariable(Base): ), ) - # Relationship to WorkflowDraftVariableFile + # WorkflowDraftVariableFile uses TypeBase while WorkflowDraftVariable uses Base, so the relationship + # must resolve the class object lazily instead of relying on string lookup across registries. variable_file: Mapped[Optional["WorkflowDraftVariableFile"]] = orm.relationship( + lambda: WorkflowDraftVariableFile, foreign_keys=[file_id], lazy="raise", uselist=False, - primaryjoin="WorkflowDraftVariableFile.id == WorkflowDraftVariable.file_id", + primaryjoin=lambda: orm.foreign(WorkflowDraftVariable.file_id) == WorkflowDraftVariableFile.id, ) # Cache for deserialized value @@ -1892,7 +1894,7 @@ class WorkflowDraftVariable(Base): return self.last_edited_at is not None -class WorkflowDraftVariableFile(Base): +class WorkflowDraftVariableFile(TypeBase): """Stores metadata about files associated with large workflow draft variables. This model acts as an intermediary between WorkflowDraftVariable and UploadFile, @@ -1906,18 +1908,7 @@ class WorkflowDraftVariableFile(Base): __tablename__ = "workflow_draft_variable_files" # Primary key - id: Mapped[str] = mapped_column( - StringUUID, - primary_key=True, - default=lambda: str(uuidv7()), - ) - - created_at: Mapped[datetime] = mapped_column( - DateTime, - nullable=False, - default=naive_utc_now, - server_default=func.current_timestamp(), - ) + id: Mapped[str] = mapped_column(StringUUID, primary_key=True, default_factory=lambda: str(uuidv7()), init=False) tenant_id: Mapped[str] = mapped_column( StringUUID, @@ -1969,15 +1960,23 @@ class WorkflowDraftVariableFile(Base): nullable=False, ) - # Relationship to UploadFile + # Rows are created with `upload_file_id`; callers should load this relationship explicitly when needed. upload_file: Mapped["UploadFile"] = orm.relationship( UploadFile, foreign_keys=[upload_file_id], lazy="raise", + init=False, uselist=False, primaryjoin=lambda: orm.foreign(WorkflowDraftVariableFile.upload_file_id) == UploadFile.id, ) + created_at: Mapped[datetime] = mapped_column( + DateTime, + nullable=False, + default_factory=naive_utc_now, + server_default=func.current_timestamp(), + ) + def is_system_variable_editable(name: str) -> bool: return name in _EDITABLE_SYSTEM_VARIABLE diff --git a/api/services/workflow_draft_variable_service.py b/api/services/workflow_draft_variable_service.py index 96f936ff9b..a55448e352 100644 --- a/api/services/workflow_draft_variable_service.py +++ b/api/services/workflow_draft_variable_service.py @@ -1083,10 +1083,9 @@ class DraftVariableSaver: mimetype=content_type, user=self._user, ) - + assert self._user.current_tenant_id # Create WorkflowDraftVariableFile record variable_file = WorkflowDraftVariableFile( - id=uuidv7(), upload_file_id=upload_file.id, size=original_size, length=original_length, @@ -1095,6 +1094,7 @@ class DraftVariableSaver: tenant_id=self._user.current_tenant_id, user_id=self._user.id, ) + variable_file.id = str(uuidv7()) engine = bind = self._session.get_bind() assert isinstance(engine, Engine) with sessionmaker(bind=engine, expire_on_commit=False).begin() as session: diff --git a/api/tests/unit_tests/controllers/console/app/workflow_draft_variables_test.py b/api/tests/unit_tests/controllers/console/app/workflow_draft_variables_test.py index 22b80b748e..62fa82e339 100644 --- a/api/tests/unit_tests/controllers/console/app/workflow_draft_variables_test.py +++ b/api/tests/unit_tests/controllers/console/app/workflow_draft_variables_test.py @@ -1,7 +1,7 @@ import uuid from collections import OrderedDict from typing import Any, NamedTuple -from unittest.mock import MagicMock, patch +from unittest.mock import patch import pytest from flask_restx import marshal @@ -29,15 +29,18 @@ class TestWorkflowDraftVariableFields: def test_serialize_full_content(self): """Test that _serialize_full_content uses pre-loaded relationships.""" # Create mock objects with relationships pre-loaded - mock_variable_file = MagicMock(spec=WorkflowDraftVariableFile) - mock_variable_file.size = 100000 - mock_variable_file.length = 50 - mock_variable_file.value_type = SegmentType.OBJECT - mock_variable_file.upload_file_id = "test-upload-file-id" - - mock_variable = MagicMock(spec=WorkflowDraftVariable) - mock_variable.file_id = "test-file-id" - mock_variable.variable_file = mock_variable_file + mock_variable = WorkflowDraftVariable( + file_id="test-file-id", + variable_file=WorkflowDraftVariableFile( + size=100000, + length=50, + value_type=SegmentType.OBJECT, + upload_file_id="test-upload-file-id", + tenant_id=str(uuid.uuid4()), + app_id=str(uuid.uuid4()), + user_id=str(uuid.uuid4()), + ), + ) # Mock the file helpers with patch("controllers.console.app.workflow_draft_variable.file_helpers", autospec=True) as mock_file_helpers: @@ -84,7 +87,7 @@ class TestWorkflowDraftVariableFields: expected_without_value: OrderedDict[str, Any] = OrderedDict( { - "id": str(conv_var.id), + "id": conv_var.id, "type": conv_var.get_variable_type().value, "name": "conv_var", "description": "", @@ -117,7 +120,7 @@ class TestWorkflowDraftVariableFields: expected_without_value = OrderedDict( { - "id": str(sys_var.id), + "id": sys_var.id, "type": sys_var.get_variable_type().value, "name": "sys_var", "description": "", @@ -149,7 +152,7 @@ class TestWorkflowDraftVariableFields: expected_without_value: OrderedDict[str, Any] = OrderedDict( { - "id": str(node_var.id), + "id": node_var.id, "type": node_var.get_variable_type().value, "name": "node_var", "description": "", @@ -180,19 +183,22 @@ class TestWorkflowDraftVariableFields: node_var.id = str(uuid.uuid4()) node_var.last_edited_at = naive_utc_now() variable_file = WorkflowDraftVariableFile( - id=str(uuidv7()), upload_file_id=str(uuid.uuid4()), size=1024, length=10, value_type=SegmentType.ARRAY_STRING, + tenant_id=str(uuidv7()), + app_id=str(uuidv7()), + user_id=str(uuidv7()), ) + variable_file.id = str(uuidv7()) node_var.variable_file = variable_file node_var.file_id = variable_file.id expected_without_value: OrderedDict[str, Any] = OrderedDict( { - "id": str(node_var.id), - "type": node_var.get_variable_type().value, + "id": node_var.id, + "type": node_var.get_variable_type(), "name": "node_var", "description": "", "selector": ["test_node", "node_var"], @@ -235,7 +241,7 @@ class TestWorkflowDraftVariableList: node_var.id = str(uuid.uuid4()) node_var_dict = OrderedDict( { - "id": str(node_var.id), + "id": node_var.id, "type": node_var.get_variable_type().value, "name": "test_var", "description": "", diff --git a/api/tests/unit_tests/services/workflow/test_draft_var_loader_simple.py b/api/tests/unit_tests/services/workflow/test_draft_var_loader_simple.py index 497c26a9b3..fb5cf7bc6e 100644 --- a/api/tests/unit_tests/services/workflow/test_draft_var_loader_simple.py +++ b/api/tests/unit_tests/services/workflow/test_draft_var_loader_simple.py @@ -33,42 +33,6 @@ class TestDraftVarLoaderSimple: fallback_variables=[], ) - def test_load_offloaded_variable_string_type_unit(self, draft_var_loader): - """Test _load_offloaded_variable with string type - isolated unit test.""" - # Create mock objects - upload_file = Mock(spec=UploadFile) - upload_file.key = "storage/key/test.txt" - - variable_file = Mock(spec=WorkflowDraftVariableFile) - variable_file.value_type = SegmentType.STRING - variable_file.upload_file = upload_file - - draft_var = Mock(spec=WorkflowDraftVariable) - draft_var.id = "draft-var-id" - draft_var.node_id = "test-node-id" - draft_var.name = "test_variable" - draft_var.description = "test description" - draft_var.get_selector.return_value = ["test-node-id", "test_variable"] - draft_var.variable_file = variable_file - - test_content = "This is the full string content" - - with patch("services.workflow_draft_variable_service.storage") as mock_storage: - mock_storage.load.return_value = test_content.encode() - - # Execute the method - selector_tuple, variable = draft_var_loader._load_offloaded_variable(draft_var) - - # Verify results - assert selector_tuple == ("test-node-id", "test_variable") - assert variable.id == "draft-var-id" - assert variable.name == "test_variable" - assert variable.description == "test description" - assert variable.value == test_content - - # Verify storage was called correctly - mock_storage.load.assert_called_once_with("storage/key/test.txt") - def test_load_offloaded_variable_object_type_unit(self, draft_var_loader): """Test _load_offloaded_variable with object type - isolated unit test.""" # Create mock objects @@ -139,47 +103,6 @@ class TestDraftVarLoaderSimple: result = draft_var_loader._selector_to_tuple(selector) assert result == ("node_id", "var_name") - def test_load_offloaded_variable_number_type_unit(self, draft_var_loader): - """Test _load_offloaded_variable with number type - isolated unit test.""" - # Create mock objects - upload_file = Mock(spec=UploadFile) - upload_file.key = "storage/key/test_number.json" - - variable_file = Mock(spec=WorkflowDraftVariableFile) - variable_file.value_type = SegmentType.NUMBER - variable_file.upload_file = upload_file - - draft_var = Mock(spec=WorkflowDraftVariable) - draft_var.id = "draft-var-id" - draft_var.node_id = "test-node-id" - draft_var.name = "test_number" - draft_var.description = "test number description" - draft_var.get_selector.return_value = ["test-node-id", "test_number"] - draft_var.variable_file = variable_file - - test_number = 123.45 - test_json_content = json.dumps(test_number) - - with patch("services.workflow_draft_variable_service.storage") as mock_storage: - mock_storage.load.return_value = test_json_content.encode() - from graphon.variables.segments import FloatSegment - - mock_segment = FloatSegment(value=test_number) - draft_var.build_segment_from_serialized_value.return_value = mock_segment - - # Execute the method - selector_tuple, variable = draft_var_loader._load_offloaded_variable(draft_var) - - # Verify results - assert selector_tuple == ("test-node-id", "test_number") - assert variable.id == "draft-var-id" - assert variable.name == "test_number" - assert variable.description == "test number description" - - # Verify method calls - mock_storage.load.assert_called_once_with("storage/key/test_number.json") - draft_var.build_segment_from_serialized_value.assert_called_once_with(SegmentType.NUMBER, test_number) - def test_load_offloaded_variable_array_type_unit(self, draft_var_loader): """Test _load_offloaded_variable with array type - isolated unit test.""" # Create mock objects @@ -229,12 +152,13 @@ class TestDraftVarLoaderSimple: variable_file.value_type = SegmentType.FILE variable_file.upload_file = upload_file - draft_var = WorkflowDraftVariable() - draft_var.id = "draft-var-id" - draft_var.app_id = "app-1" - draft_var.node_id = "test-node-id" - draft_var.name = "test_file" - draft_var.description = "test file description" + draft_var = WorkflowDraftVariable( + id="draft-var-id", + app_id="app-1", + node_id="test-node-id", + name="test_file", + description="test file description", + ) draft_var._set_selector(["test-node-id", "test_file"]) draft_var.variable_file = variable_file diff --git a/api/tests/unit_tests/services/workflow/test_workflow_draft_variable_service.py b/api/tests/unit_tests/services/workflow/test_workflow_draft_variable_service.py index b14d767568..663eec6a06 100644 --- a/api/tests/unit_tests/services/workflow/test_workflow_draft_variable_service.py +++ b/api/tests/unit_tests/services/workflow/test_workflow_draft_variable_service.py @@ -200,7 +200,7 @@ class TestDraftVariableSaver: user=mock_user, ) - def test_draft_saver_with_small_variables(self, draft_saver, mock_session): + def test_draft_saver_with_small_variables(self, draft_saver: DraftVariableSaver, mock_session): with patch( "services.workflow_draft_variable_service.DraftVariableSaver._try_offload_large_variable", autospec=True ) as _mock_try_offload: @@ -212,18 +212,21 @@ class TestDraftVariableSaver: assert draft_var.file_id is None _mock_try_offload.return_value = None - def test_draft_saver_with_large_variables(self, draft_saver, mock_session): + def test_draft_saver_with_large_variables(self, draft_saver: DraftVariableSaver, mock_session): with patch( "services.workflow_draft_variable_service.DraftVariableSaver._try_offload_large_variable", autospec=True ) as _mock_try_offload: mock_segment = StringSegment(value="small value") mock_draft_var_file = WorkflowDraftVariableFile( - id=str(uuidv7()), + tenant_id=str(uuidv7()), + app_id=str(uuidv7()), + user_id=str(uuidv7()), size=1024, length=10, value_type=SegmentType.ARRAY_STRING, - upload_file_id=str(uuid.uuid4()), + upload_file_id=str(uuidv7()), ) + mock_draft_var_file.id = str(uuidv7()) _mock_try_offload.return_value = mock_segment, mock_draft_var_file draft_var = draft_saver._create_draft_variable(name="small_var", value=mock_segment, visible=True) diff --git a/eslint-suppressions.json b/eslint-suppressions.json index e82305ea0c..963fa31759 100644 --- a/eslint-suppressions.json +++ b/eslint-suppressions.json @@ -119,11 +119,6 @@ "count": 3 } }, - "web/app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/tracing/config-popup.tsx": { - "no-restricted-imports": { - "count": 1 - } - }, "web/app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/tracing/provider-panel.tsx": { "ts/no-explicit-any": { "count": 1 @@ -207,11 +202,6 @@ "count": 1 } }, - "web/app/components/app-sidebar/dataset-info/dropdown.tsx": { - "ts/no-explicit-any": { - "count": 1 - } - }, "web/app/components/app-sidebar/index.tsx": { "ts/no-explicit-any": { "count": 1 @@ -526,11 +516,6 @@ "count": 2 } }, - "web/app/components/app/configuration/debug/debug-with-multiple-model/model-parameter-trigger.tsx": { - "no-restricted-imports": { - "count": 1 - } - }, "web/app/components/app/configuration/debug/debug-with-multiple-model/text-generation-item.tsx": { "ts/no-explicit-any": { "count": 8 @@ -559,11 +544,6 @@ "count": 1 } }, - "web/app/components/app/configuration/prompt-value-panel/index.tsx": { - "no-restricted-imports": { - "count": 1 - } - }, "web/app/components/app/configuration/prompt-value-panel/utils.ts": { "ts/no-explicit-any": { "count": 1 @@ -620,9 +600,6 @@ } }, "web/app/components/app/log/list.tsx": { - "no-restricted-imports": { - "count": 1 - }, "react/set-state-in-effect": { "count": 6 }, @@ -645,7 +622,7 @@ }, "web/app/components/app/overview/embedded/index.tsx": { "no-restricted-imports": { - "count": 2 + "count": 1 }, "react/set-state-in-effect": { "count": 1 @@ -791,11 +768,6 @@ "count": 3 } }, - "web/app/components/base/audio-btn/index.tsx": { - "no-restricted-imports": { - "count": 1 - } - }, "web/app/components/base/audio-gallery/AudioPlayer.tsx": { "ts/no-explicit-any": { "count": 2 @@ -871,9 +843,6 @@ } }, "web/app/components/base/chat/chat-with-history/header/index.tsx": { - "no-restricted-imports": { - "count": 1 - }, "ts/no-explicit-any": { "count": 2 } @@ -991,11 +960,6 @@ "count": 7 } }, - "web/app/components/base/chat/embedded-chatbot/header/index.tsx": { - "no-restricted-imports": { - "count": 1 - } - }, "web/app/components/base/chat/embedded-chatbot/hooks.tsx": { "react-hooks-extra/no-direct-set-state-in-use-effect": { "count": 3 @@ -1037,11 +1001,6 @@ "count": 1 } }, - "web/app/components/base/copy-feedback/index.tsx": { - "no-restricted-imports": { - "count": 1 - } - }, "web/app/components/base/date-and-time-picker/hooks.ts": { "react/no-unnecessary-use-prefix": { "count": 2 @@ -1093,11 +1052,6 @@ "count": 1 } }, - "web/app/components/base/features/new-feature-panel/annotation-reply/annotation-ctrl-button.tsx": { - "no-restricted-imports": { - "count": 1 - } - }, "web/app/components/base/features/new-feature-panel/annotation-reply/config-param-modal.tsx": { "no-restricted-imports": { "count": 1 @@ -1123,15 +1077,7 @@ "count": 2 } }, - "web/app/components/base/features/new-feature-panel/feature-bar.tsx": { - "no-restricted-imports": { - "count": 1 - } - }, "web/app/components/base/features/new-feature-panel/feature-card.tsx": { - "no-restricted-imports": { - "count": 1 - }, "ts/no-explicit-any": { "count": 5 } @@ -1168,9 +1114,6 @@ } }, "web/app/components/base/file-uploader/file-list-in-log.tsx": { - "no-restricted-imports": { - "count": 1 - }, "react/no-missing-key": { "count": 1 } @@ -1193,11 +1136,6 @@ "count": 2 } }, - "web/app/components/base/file-uploader/pdf-preview.tsx": { - "no-restricted-imports": { - "count": 1 - } - }, "web/app/components/base/file-uploader/store.tsx": { "react-refresh/only-export-components": { "count": 4 @@ -1215,7 +1153,7 @@ }, "web/app/components/base/form/components/base/base-field.tsx": { "no-restricted-imports": { - "count": 2 + "count": 1 }, "ts/no-explicit-any": { "count": 3 @@ -1593,15 +1531,7 @@ "count": 1 } }, - "web/app/components/base/image-uploader/image-list.tsx": { - "no-restricted-imports": { - "count": 1 - } - }, "web/app/components/base/image-uploader/image-preview.tsx": { - "no-restricted-imports": { - "count": 1 - }, "ts/no-explicit-any": { "count": 1 } @@ -1764,9 +1694,6 @@ } }, "web/app/components/base/new-audio-button/index.tsx": { - "no-restricted-imports": { - "count": 1 - }, "ts/no-explicit-any": { "count": 1 } @@ -1949,11 +1876,6 @@ "count": 1 } }, - "web/app/components/base/qrcode/index.tsx": { - "no-restricted-imports": { - "count": 1 - } - }, "web/app/components/base/radio-card/index.stories.tsx": { "ts/no-explicit-any": { "count": 1 @@ -1982,25 +1904,6 @@ "count": 1 } }, - "web/app/components/base/select/index.stories.tsx": { - "no-console": { - "count": 4 - }, - "ts/no-explicit-any": { - "count": 1 - } - }, - "web/app/components/base/select/index.tsx": { - "react/set-state-in-effect": { - "count": 2 - }, - "style/multiline-ternary": { - "count": 2 - }, - "ts/no-explicit-any": { - "count": 1 - } - }, "web/app/components/base/sort/index.tsx": { "ts/no-explicit-any": { "count": 2 @@ -2262,11 +2165,6 @@ "count": 1 } }, - "web/app/components/datasets/create/step-two/components/general-chunking-options.tsx": { - "no-restricted-imports": { - "count": 1 - } - }, "web/app/components/datasets/create/step-two/components/index.ts": { "no-barrel-files/no-barrel-files": { "count": 5 @@ -2374,11 +2272,6 @@ "count": 1 } }, - "web/app/components/datasets/documents/components/document-list/components/document-table-row.tsx": { - "no-restricted-imports": { - "count": 1 - } - }, "web/app/components/datasets/documents/components/document-list/components/index.ts": { "no-barrel-files/no-barrel-files": { "count": 2 @@ -2553,11 +2446,6 @@ "count": 1 } }, - "web/app/components/datasets/documents/detail/metadata/components/doc-type-selector.tsx": { - "no-restricted-imports": { - "count": 1 - } - }, "web/app/components/datasets/documents/detail/metadata/components/metadata-field-list.tsx": { "ts/no-non-null-asserted-optional-chain": { "count": 1 @@ -2619,11 +2507,6 @@ "count": 1 } }, - "web/app/components/datasets/hit-testing/components/query-input/textarea.tsx": { - "no-restricted-imports": { - "count": 1 - } - }, "web/app/components/datasets/hit-testing/components/result-item-external.tsx": { "no-restricted-imports": { "count": 1 @@ -2639,21 +2522,11 @@ "count": 1 } }, - "web/app/components/datasets/list/dataset-card/components/dataset-card-footer.tsx": { - "no-restricted-imports": { - "count": 1 - } - }, "web/app/components/datasets/list/dataset-card/hooks/use-dataset-card-state.ts": { "react/set-state-in-effect": { "count": 1 } }, - "web/app/components/datasets/metadata/edit-metadata-batch/edited-beacon.tsx": { - "no-restricted-imports": { - "count": 1 - } - }, "web/app/components/datasets/metadata/edit-metadata-batch/input-combined.tsx": { "ts/no-explicit-any": { "count": 2 @@ -2722,11 +2595,6 @@ "count": 1 } }, - "web/app/components/datasets/settings/index-method/keyword-number.tsx": { - "no-restricted-imports": { - "count": 1 - } - }, "web/app/components/datasets/settings/summary-index-setting.tsx": { "no-restricted-imports": { "count": 1 @@ -2790,11 +2658,6 @@ "count": 1 } }, - "web/app/components/explore/try-app/app/chat.tsx": { - "no-restricted-imports": { - "count": 1 - } - }, "web/app/components/explore/try-app/index.tsx": { "no-restricted-imports": { "count": 1 @@ -3043,15 +2906,7 @@ "count": 2 } }, - "web/app/components/header/account-setting/model-provider-page/model-selector/feature-icon.tsx": { - "no-restricted-imports": { - "count": 1 - } - }, "web/app/components/header/account-setting/model-provider-page/provider-added-card/cooldown-timer.tsx": { - "no-restricted-imports": { - "count": 1 - }, "react/set-state-in-effect": { "count": 2 } @@ -3067,9 +2922,6 @@ } }, "web/app/components/header/account-setting/model-provider-page/provider-added-card/model-load-balancing-configs.tsx": { - "no-restricted-imports": { - "count": 1 - }, "ts/no-explicit-any": { "count": 5 } @@ -3085,11 +2937,6 @@ "count": 3 } }, - "web/app/components/header/account-setting/model-provider-page/provider-added-card/priority-use-tip.tsx": { - "no-restricted-imports": { - "count": 1 - } - }, "web/app/components/header/account-setting/model-provider-page/utils.ts": { "no-barrel-files/no-barrel-files": { "count": 2 @@ -3182,33 +3029,12 @@ "count": 1 } }, - "web/app/components/plugins/plugin-auth/authorize/add-oauth-button.tsx": { - "ts/no-explicit-any": { - "count": 2 - } - }, - "web/app/components/plugins/plugin-auth/authorize/index.tsx": { - "no-restricted-imports": { - "count": 1 - } - }, - "web/app/components/plugins/plugin-auth/authorize/oauth-client-settings.tsx": { - "no-restricted-imports": { - "count": 1 - }, - "ts/no-explicit-any": { - "count": 2 - } - }, "web/app/components/plugins/plugin-auth/authorized-in-node.tsx": { "ts/no-explicit-any": { "count": 1 } }, "web/app/components/plugins/plugin-auth/authorized/item.tsx": { - "no-restricted-imports": { - "count": 1 - }, "ts/no-explicit-any": { "count": 1 } @@ -3303,9 +3129,6 @@ } }, "web/app/components/plugins/plugin-detail-panel/multiple-tool-selector/index.tsx": { - "no-restricted-imports": { - "count": 1 - }, "ts/no-explicit-any": { "count": 1 } @@ -3320,11 +3143,6 @@ "count": 2 } }, - "web/app/components/plugins/plugin-detail-panel/subscription-list/create/common-modal.tsx": { - "no-restricted-imports": { - "count": 1 - } - }, "web/app/components/plugins/plugin-detail-panel/subscription-list/create/hooks/use-common-modal-state.ts": { "erasable-syntax-only/enums": { "count": 1 @@ -3338,9 +3156,6 @@ "web/app/components/plugins/plugin-detail-panel/subscription-list/create/index.tsx": { "no-barrel-files/no-barrel-files": { "count": 3 - }, - "no-restricted-imports": { - "count": 1 } }, "web/app/components/plugins/plugin-detail-panel/subscription-list/create/oauth-client.tsx": { @@ -3376,11 +3191,6 @@ "count": 2 } }, - "web/app/components/plugins/plugin-detail-panel/subscription-list/list-view.tsx": { - "no-restricted-imports": { - "count": 1 - } - }, "web/app/components/plugins/plugin-detail-panel/subscription-list/log-viewer.tsx": { "erasable-syntax-only/enums": { "count": 1 @@ -3389,11 +3199,6 @@ "count": 2 } }, - "web/app/components/plugins/plugin-detail-panel/subscription-list/selector-view.tsx": { - "no-restricted-imports": { - "count": 1 - } - }, "web/app/components/plugins/plugin-detail-panel/subscription-list/subscription-card.tsx": { "no-restricted-imports": { "count": 1 @@ -3416,7 +3221,7 @@ }, "web/app/components/plugins/plugin-detail-panel/tool-selector/components/tool-item.tsx": { "no-restricted-imports": { - "count": 2 + "count": 1 } }, "web/app/components/plugins/plugin-detail-panel/tool-selector/hooks/index.ts": { @@ -3457,11 +3262,6 @@ "count": 2 } }, - "web/app/components/plugins/plugin-page/index.tsx": { - "no-restricted-imports": { - "count": 1 - } - }, "web/app/components/plugins/plugin-page/install-plugin-dropdown.tsx": { "react/set-state-in-effect": { "count": 2 @@ -4103,11 +3903,6 @@ "count": 1 } }, - "web/app/components/workflow/nodes/_base/components/config-vision.tsx": { - "no-restricted-imports": { - "count": 1 - } - }, "web/app/components/workflow/nodes/_base/components/editor/code-editor/editor-support-vars.tsx": { "react/set-state-in-effect": { "count": 1 @@ -4156,9 +3951,6 @@ } }, "web/app/components/workflow/nodes/_base/components/input-support-select-var.tsx": { - "no-restricted-imports": { - "count": 1 - }, "ts/no-explicit-any": { "count": 1 } @@ -4222,9 +4014,6 @@ } }, "web/app/components/workflow/nodes/_base/components/setting-item.tsx": { - "no-restricted-imports": { - "count": 1 - }, "ts/no-explicit-any": { "count": 1 } @@ -4239,11 +4028,6 @@ "count": 8 } }, - "web/app/components/workflow/nodes/_base/components/variable/object-child-tree-panel/picker/field.tsx": { - "no-restricted-imports": { - "count": 1 - } - }, "web/app/components/workflow/nodes/_base/components/variable/output-var-list.tsx": { "ts/no-non-null-asserted-optional-chain": { "count": 1 @@ -4557,11 +4341,6 @@ "count": 1 } }, - "web/app/components/workflow/nodes/human-input/components/delivery-method/index.tsx": { - "no-restricted-imports": { - "count": 1 - } - }, "web/app/components/workflow/nodes/human-input/components/delivery-method/method-item.tsx": { "no-restricted-imports": { "count": 1 @@ -4677,11 +4456,6 @@ "count": 1 } }, - "web/app/components/workflow/nodes/knowledge-base/components/index-method.tsx": { - "no-restricted-imports": { - "count": 1 - } - }, "web/app/components/workflow/nodes/knowledge-base/components/retrieval-setting/hooks.tsx": { "ts/no-explicit-any": { "count": 4 @@ -4819,11 +4593,6 @@ "count": 2 } }, - "web/app/components/workflow/nodes/llm/components/json-schema-config-modal/json-schema-generator/prompt-editor.tsx": { - "no-restricted-imports": { - "count": 1 - } - }, "web/app/components/workflow/nodes/llm/components/json-schema-config-modal/visual-editor/context.tsx": { "react-refresh/only-export-components": { "count": 2 @@ -5157,11 +4926,6 @@ "count": 7 } }, - "web/app/components/workflow/nodes/trigger-schedule/components/monthly-days-selector.tsx": { - "no-restricted-imports": { - "count": 1 - } - }, "web/app/components/workflow/nodes/trigger-schedule/default.ts": { "regexp/no-unused-capturing-group": { "count": 2 @@ -5299,11 +5063,6 @@ "count": 12 } }, - "web/app/components/workflow/panel/debug-and-preview/index.tsx": { - "no-restricted-imports": { - "count": 1 - } - }, "web/app/components/workflow/panel/env-panel/variable-modal.tsx": { "no-restricted-imports": { "count": 1 @@ -5528,9 +5287,6 @@ } }, "web/app/components/workflow/variable-inspect/group.tsx": { - "no-restricted-imports": { - "count": 1 - }, "ts/no-explicit-any": { "count": 2 } @@ -5554,17 +5310,11 @@ } }, "web/app/components/workflow/variable-inspect/right.tsx": { - "no-restricted-imports": { - "count": 1 - }, "ts/no-explicit-any": { "count": 3 } }, "web/app/components/workflow/variable-inspect/trigger.tsx": { - "no-restricted-imports": { - "count": 1 - }, "ts/no-explicit-any": { "count": 1 } @@ -5658,11 +5408,6 @@ "count": 1 } }, - "web/app/signin/_header.tsx": { - "no-restricted-imports": { - "count": 1 - } - }, "web/app/signin/components/mail-and-password-auth.tsx": { "ts/no-explicit-any": { "count": 1 diff --git a/packages/dify-ui/src/select/__tests__/index.spec.tsx b/packages/dify-ui/src/select/__tests__/index.spec.tsx index f2f3221eda..ac8be5917e 100644 --- a/packages/dify-ui/src/select/__tests__/index.spec.tsx +++ b/packages/dify-ui/src/select/__tests__/index.spec.tsx @@ -170,6 +170,12 @@ describe('Select wrappers', () => { expect(screen.getByRole('combobox', { name: 'city select' }).element().querySelector('.i-ri-arrow-down-s-line')).toBeInTheDocument() }) + + it('should include open state feedback classes', async () => { + const screen = await renderOpenSelect() + + expect(screen.getByRole('combobox', { name: 'city select' }).element().className).toContain('data-open:bg-state-base-hover-alt') + }) }) describe('SelectContent', () => { diff --git a/packages/dify-ui/src/select/index.tsx b/packages/dify-ui/src/select/index.tsx index 017093c584..2f2f91d9c6 100644 --- a/packages/dify-ui/src/select/index.tsx +++ b/packages/dify-ui/src/select/index.tsx @@ -21,7 +21,7 @@ export const SelectGroup = BaseSelect.Group const selectTriggerVariants = cva( [ 'group flex w-full items-center border-0 bg-components-input-bg-normal text-left text-components-input-text-filled outline-hidden', - 'hover:bg-state-base-hover-alt focus-visible:bg-state-base-hover-alt', + 'hover:bg-state-base-hover-alt focus-visible:bg-state-base-hover-alt data-open:bg-state-base-hover-alt', 'data-placeholder:text-components-input-text-placeholder', 'data-readonly:cursor-default data-readonly:bg-transparent data-readonly:hover:bg-transparent', 'data-disabled:cursor-not-allowed data-disabled:bg-components-input-bg-disabled data-disabled:text-components-input-text-filled-disabled data-disabled:hover:bg-components-input-bg-disabled', diff --git a/web/app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/tracing/config-popup.tsx b/web/app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/tracing/config-popup.tsx index 471ab86e12..2b0f978906 100644 --- a/web/app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/tracing/config-popup.tsx +++ b/web/app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/tracing/config-popup.tsx @@ -3,12 +3,12 @@ import type { FC, JSX } from 'react' import type { AliyunConfig, ArizeConfig, DatabricksConfig, LangFuseConfig, LangSmithConfig, MLflowConfig, OpikConfig, PhoenixConfig, TencentConfig, WeaveConfig } from './type' import { cn } from '@langgenius/dify-ui/cn' import { Switch } from '@langgenius/dify-ui/switch' +import { Tooltip, TooltipContent, TooltipTrigger } from '@langgenius/dify-ui/tooltip' import { useBoolean } from 'ahooks' import * as React from 'react' import { useCallback, useState } from 'react' import { useTranslation } from 'react-i18next' import Divider from '@/app/components/base/divider' -import Tooltip from '@/app/components/base/tooltip' import Indicator from '@/app/components/header/indicator' import ProviderConfigModal from './provider-config-modal' import ProviderPanel from './provider-panel' @@ -338,10 +338,13 @@ const ConfigPopup: FC = ({ <> {providerAllNotConfigured ? ( - - {switchContent} + + + + {t(`${I18N_PREFIX}.disabledTip`, { ns: 'app' })} + ) : switchContent} diff --git a/web/app/components/app-sidebar/dataset-info/__tests__/dropdown-callbacks.spec.tsx b/web/app/components/app-sidebar/dataset-info/__tests__/dropdown-callbacks.spec.tsx index ceb8302ee6..05a06f2f77 100644 --- a/web/app/components/app-sidebar/dataset-info/__tests__/dropdown-callbacks.spec.tsx +++ b/web/app/components/app-sidebar/dataset-info/__tests__/dropdown-callbacks.spec.tsx @@ -113,7 +113,9 @@ vi.mock('@/service/datasets', () => ({ })) vi.mock('@langgenius/dify-ui/toast', () => ({ - toast: (...args: unknown[]) => mockToast(...args), + toast: { + error: (...args: unknown[]) => mockToast(...args), + }, })) vi.mock('@/app/components/datasets/rename-modal', () => ({ @@ -220,7 +222,7 @@ describe('Dropdown callback coverage', () => { await user.click(screen.getByText('datasetPipeline.operations.exportPipeline')) await waitFor(() => { - expect(mockToast).toHaveBeenCalledWith('app.exportFailed', { type: 'error' }) + expect(mockToast).toHaveBeenCalledWith('app.exportFailed') }) }) @@ -257,7 +259,7 @@ describe('Dropdown callback coverage', () => { await user.click(screen.getByText('common.operation.delete')) await waitFor(() => { - expect(mockToast).toHaveBeenCalledWith('check failed', { type: 'error' }) + expect(mockToast).toHaveBeenCalledWith('check failed') }) expect(screen.queryByText('dataset.deleteDatasetConfirmTitle')).not.toBeInTheDocument() }) diff --git a/web/app/components/app-sidebar/dataset-info/dropdown.tsx b/web/app/components/app-sidebar/dataset-info/dropdown.tsx index 8f3a25738a..fa5a40f8a4 100644 --- a/web/app/components/app-sidebar/dataset-info/dropdown.tsx +++ b/web/app/components/app-sidebar/dataset-info/dropdown.tsx @@ -34,6 +34,25 @@ type DropDownProps = { expand: boolean } +type JsonErrorResponse = { + json: () => Promise<{ message?: string }> +} + +const isJsonErrorResponse = (error: unknown): error is JsonErrorResponse => { + return typeof error === 'object' + && error !== null + && 'json' in error + && typeof error.json === 'function' +} + +const getErrorMessage = async (error: unknown) => { + if (!isJsonErrorResponse(error)) + return 'Unknown error' + + const res = await error.json() + return res?.message || 'Unknown error' +} + const DropDown = ({ expand, }: DropDownProps) => { @@ -78,7 +97,7 @@ const DropDown = ({ downloadBlob({ data: file, fileName: `${name}.pipeline` }) } catch { - toast(t('exportFailed', { ns: 'app' }), { type: 'error' }) + toast.error(t('exportFailed', { ns: 'app' })) } }, [dataset, exportPipelineConfig, t]) @@ -89,9 +108,8 @@ const DropDown = ({ setConfirmMessage(isUsedByApp ? t('datasetUsedByApp', { ns: 'dataset' })! : t('deleteDatasetConfirmContent', { ns: 'dataset' })!) setShowConfirmDelete(true) } - catch (e: any) { - const res = await e.json() - toast(res?.message || 'Unknown error', { type: 'error' }) + catch (e: unknown) { + toast.error(await getErrorMessage(e)) } }, [dataset.id, t]) @@ -112,10 +130,15 @@ const DropDown = ({ open={open} onOpenChange={setOpen} > - }> - - - + + )} + > + ({ - default: ({ children, popupContent }: { children: ReactNode, popupContent: string }) => ( -
{children}
- ), -})) - const createModelAndParameter = (overrides: Partial = {}): ModelAndParameter => ({ id: 'model-1', model: 'gpt-3.5-turbo', @@ -385,14 +380,15 @@ describe('ModelParameterTrigger', () => { expect(screen.getByText('common.modelProvider.selectModel')).toBeInTheDocument() }) - it('should render configured model id and incompatible tooltip when model is missing from the provider list', () => { + it('should render configured model id and incompatible tooltip when model is missing from the provider list', async () => { renderComponent() expect(screen.getByText('gpt-3.5-turbo')).toBeInTheDocument() - expect(screen.getByTestId('tooltip')).toHaveAttribute('data-content', 'common.modelProvider.selector.incompatibleTip') + await userEvent.hover(screen.getByLabelText('common.modelProvider.selector.incompatibleTip')) + expect(await screen.findByText('common.modelProvider.selector.incompatibleTip')).toBeInTheDocument() }) - it('should render configure required tooltip for no-configure status', () => { + it('should render configure required tooltip for no-configure status', async () => { const { unmount } = renderComponent() const triggerContent = capturedModalProps?.renderTrigger({ open: false, @@ -403,10 +399,11 @@ describe('ModelParameterTrigger', () => { unmount() render(<>{triggerContent}) - expect(screen.getByTestId('tooltip')).toHaveAttribute('data-content', 'common.modelProvider.selector.configureRequired') + await userEvent.hover(screen.getByLabelText('common.modelProvider.selector.configureRequired')) + expect(await screen.findByText('common.modelProvider.selector.configureRequired')).toBeInTheDocument() }) - it('should render disabled tooltip for disabled status', () => { + it('should render disabled tooltip for disabled status', async () => { const { unmount } = renderComponent() const triggerContent = capturedModalProps?.renderTrigger({ open: false, @@ -417,7 +414,8 @@ describe('ModelParameterTrigger', () => { unmount() render(<>{triggerContent}) - expect(screen.getByTestId('tooltip')).toHaveAttribute('data-content', 'common.modelProvider.selector.disabled') + await userEvent.hover(screen.getByLabelText('common.modelProvider.selector.disabled')) + expect(await screen.findByText('common.modelProvider.selector.disabled')).toBeInTheDocument() }) it('should apply expanded and warning styles when the trigger is open for a non-active status', () => { diff --git a/web/app/components/app/configuration/debug/debug-with-multiple-model/model-parameter-trigger.tsx b/web/app/components/app/configuration/debug/debug-with-multiple-model/model-parameter-trigger.tsx index a870538edc..bf889e20ea 100644 --- a/web/app/components/app/configuration/debug/debug-with-multiple-model/model-parameter-trigger.tsx +++ b/web/app/components/app/configuration/debug/debug-with-multiple-model/model-parameter-trigger.tsx @@ -1,9 +1,9 @@ import type { FC } from 'react' import type { ModelAndParameter } from '../types' import type { FormValue } from '@/app/components/header/account-setting/model-provider-page/declarations' +import { Tooltip, TooltipContent, TooltipTrigger } from '@langgenius/dify-ui/tooltip' import { memo } from 'react' import { useTranslation } from 'react-i18next' -import Tooltip from '@/app/components/base/tooltip' import { DERIVED_MODEL_STATUS_BADGE_I18N, DERIVED_MODEL_STATUS_TOOLTIP_I18N, @@ -132,8 +132,18 @@ const ModelParameterTrigger: FC = ({ { !isEmpty && !isActive && statusLabelKey && ( - - + + + )} + /> + + {t((statusTooltipKey || statusLabelKey) as 'modelProvider.selector.incompatible', { ns: 'common' })} + ) } diff --git a/web/app/components/app/configuration/prompt-value-panel/index.tsx b/web/app/components/app/configuration/prompt-value-panel/index.tsx index c3ba69bf34..c2a438b5e9 100644 --- a/web/app/components/app/configuration/prompt-value-panel/index.tsx +++ b/web/app/components/app/configuration/prompt-value-panel/index.tsx @@ -5,6 +5,7 @@ import type { VisionFile, VisionSettings } from '@/types/app' import { Button } from '@langgenius/dify-ui/button' import { cn } from '@langgenius/dify-ui/cn' import { Select, SelectContent, SelectItem, SelectItemIndicator, SelectItemText, SelectTrigger } from '@langgenius/dify-ui/select' +import { Tooltip, TooltipContent, TooltipTrigger } from '@langgenius/dify-ui/tooltip' import { RiArrowDownSLine, RiArrowRightSLine, @@ -19,7 +20,6 @@ import FeatureBar from '@/app/components/base/features/new-feature-panel/feature import TextGenerationImageUploader from '@/app/components/base/image-uploader/text-generation-image-uploader' import Input from '@/app/components/base/input' import Textarea from '@/app/components/base/textarea' -import Tooltip from '@/app/components/base/tooltip' import BoolInput from '@/app/components/workflow/nodes/_base/components/before-run-form/bool-input' import ConfigContext from '@/context/debug-configuration' import { AppModeEnum, ModelModeType } from '@/types/app' @@ -224,16 +224,23 @@ const PromptValuePanel: FC = ({
{canNotRun && ( - - + + onSend?.()} + className="w-[96px]" + > + )} {!canNotRun && ( diff --git a/web/app/components/app/log/list.tsx b/web/app/components/app/log/list.tsx index da96d41804..1633d53ccc 100644 --- a/web/app/components/app/log/list.tsx +++ b/web/app/components/app/log/list.tsx @@ -10,6 +10,7 @@ import { } from '@heroicons/react/24/outline' import { cn } from '@langgenius/dify-ui/cn' import { toast } from '@langgenius/dify-ui/toast' +import { Tooltip, TooltipContent, TooltipTrigger } from '@langgenius/dify-ui/tooltip' import { RiCloseLine, RiEditFill } from '@remixicon/react' import dayjs from 'dayjs' import timezone from 'dayjs/plugin/timezone' @@ -30,7 +31,6 @@ import CopyIcon from '@/app/components/base/copy-icon' import Drawer from '@/app/components/base/drawer' import Loading from '@/app/components/base/loading' import MessageLogModal from '@/app/components/base/message-log-modal' -import Tooltip from '@/app/components/base/tooltip' import { WorkflowContextProvider } from '@/app/components/workflow/context' import { useAppContext } from '@/context/app-context' import useBreakpoints, { MediaType } from '@/hooks/use-breakpoints' @@ -409,10 +409,15 @@ function DetailPanel({ detail, onFeedback }: IDetailPanel) {
{isChatMode ? t('detail.conversationId', { ns: 'appLog' }) : t('detail.time', { ns: 'appLog' })}
{isChatMode && (
- -
{detail.id}
+ + {detail.id}
+ )} + /> + + {detail.id} +
@@ -769,18 +774,20 @@ const ConversationList: FC = ({ logs, appDetail, onRefresh }) // Annotated data needs to be highlighted const renderTdValue = (value: string | number | null, isEmptyStyle: boolean, isHighlight = false, annotation?: LogAnnotation) => { return ( - + + {value || '-'} + + )} + /> + {`${t('detail.annotationTip', { ns: 'appLog', user: annotation?.account?.name })} ${formatTime(annotation?.created_at || dayjs().unix(), 'MM-DD hh:mm A')}`} - )} - popupClassName={(isHighlight && !isChatMode) ? '' : 'hidden!'} - > -
- {value || '-'} -
+
) } diff --git a/web/app/components/app/overview/embedded/index.tsx b/web/app/components/app/overview/embedded/index.tsx index 029566587e..e810c7f1eb 100644 --- a/web/app/components/app/overview/embedded/index.tsx +++ b/web/app/components/app/overview/embedded/index.tsx @@ -1,5 +1,6 @@ import type { SiteInfo } from '@/models/share' import { cn } from '@langgenius/dify-ui/cn' +import { Tooltip, TooltipContent, TooltipTrigger } from '@langgenius/dify-ui/tooltip' import { RiClipboardFill, RiClipboardLine, @@ -11,7 +12,6 @@ import { useTranslation } from 'react-i18next' import ActionButton from '@/app/components/base/action-button' import { useThemeContext } from '@/app/components/base/chat/embedded-chatbot/theme/theme-context' import Modal from '@/app/components/base/modal' -import Tooltip from '@/app/components/base/tooltip' import { IS_CE_EDITION } from '@/config' import { useAppContext } from '@/context/app-context' import { basePath } from '@/utils/var' @@ -174,21 +174,24 @@ const Embedded = ({ siteInfo, isShow, onClose, appBaseUrl, accessToken, classNam
{t(`${prefixEmbedded}.${option}`, { ns: 'appOverview' })}
- + +
+ {isCopied[option] && } + {!isCopied[option] && } +
+ + )} + /> + + {(isCopied[option] ? t(`${prefixEmbedded}.copied`, { ns: 'appOverview' }) - : t(`${prefixEmbedded}.copy`, { ns: 'appOverview' })) || '' - } - > - -
- {isCopied[option] && } - {!isCopied[option] && } -
-
+ : t(`${prefixEmbedded}.copy`, { ns: 'appOverview' })) || ''} +
diff --git a/web/app/components/base/audio-btn/index.tsx b/web/app/components/base/audio-btn/index.tsx index 47fefe19e5..3ca453213d 100644 --- a/web/app/components/base/audio-btn/index.tsx +++ b/web/app/components/base/audio-btn/index.tsx @@ -1,9 +1,9 @@ 'use client' +import { Tooltip, TooltipContent, TooltipTrigger } from '@langgenius/dify-ui/tooltip' import { t } from 'i18next' import { useState } from 'react' import { AudioPlayerManager } from '@/app/components/base/audio-btn/audio.player.manager' import Loading from '@/app/components/base/loading' -import Tooltip from '@/app/components/base/tooltip' import { useParams, usePathname } from '@/next/navigation' import s from './style.module.css' @@ -82,27 +82,34 @@ const AudioBtn = ({ return (
- - + + + + + )} + /> + + {tooltipContent} +
) diff --git a/web/app/components/base/chat/chat-with-history/header/index.tsx b/web/app/components/base/chat/chat-with-history/header/index.tsx index 95b6146afa..75cfab4228 100644 --- a/web/app/components/base/chat/chat-with-history/header/index.tsx +++ b/web/app/components/base/chat/chat-with-history/header/index.tsx @@ -9,6 +9,7 @@ import { AlertDialogTitle, } from '@langgenius/dify-ui/alert-dialog' import { cn } from '@langgenius/dify-ui/cn' +import { Tooltip, TooltipContent, TooltipTrigger } from '@langgenius/dify-ui/tooltip' import { RiEditBoxLine, RiLayoutRight2Line, @@ -20,7 +21,6 @@ import ActionButton, { ActionButtonState } from '@/app/components/base/action-bu import AppIcon from '@/app/components/base/app-icon' import ViewFormDropdown from '@/app/components/base/chat/chat-with-history/inputs-form/view-form-dropdown' import RenameModal from '@/app/components/base/chat/chat-with-history/sidebar/rename-modal' -import Tooltip from '@/app/components/base/tooltip' import { useChatWithHistoryContext, } from '../context' @@ -117,31 +117,41 @@ const Header = () => {
{isSidebarCollapsed && ( - -
- - - -
+ + + + + + + )} + /> + + {t('chat.newChatTip', { ns: 'share' })} + )}
{currentConversationId && ( - - - - + + + + + )} + /> + + {t('chat.resetChat', { ns: 'share' })} + )} {currentConversationId && inputsForms.length > 0 && ( diff --git a/web/app/components/base/chat/embedded-chatbot/header/index.tsx b/web/app/components/base/chat/embedded-chatbot/header/index.tsx index 598e3068de..6d0cc9bc06 100644 --- a/web/app/components/base/chat/embedded-chatbot/header/index.tsx +++ b/web/app/components/base/chat/embedded-chatbot/header/index.tsx @@ -1,6 +1,7 @@ import type { FC } from 'react' import type { Theme } from '../theme/theme-context' import { cn } from '@langgenius/dify-ui/cn' +import { Tooltip, TooltipContent, TooltipTrigger } from '@langgenius/dify-ui/tooltip' import { useSuspenseQuery } from '@tanstack/react-query' import * as React from 'react' import { useCallback, useEffect, useState } from 'react' @@ -9,7 +10,6 @@ import ActionButton from '@/app/components/base/action-button' import ViewFormDropdown from '@/app/components/base/chat/embedded-chatbot/inputs-form/view-form-dropdown' import Divider from '@/app/components/base/divider' import DifyLogo from '@/app/components/base/logo/dify-logo' -import Tooltip from '@/app/components/base/tooltip' import { systemFeaturesQueryOptions } from '@/service/system-features' import { isClient } from '@/utils/client' import { @@ -111,26 +111,36 @@ const Header: FC = ({ )} { showToggleExpandButton && ( - - - { - expanded - ?
- :
- } - + + + { + expanded + ?
+ :
+ } + + )} + /> + + {expanded ? t('chat.collapse', { ns: 'share' }) : t('chat.expand', { ns: 'share' })} + ) } {currentConversationId && allowResetChat && ( - - -
- + + +
+ + )} + /> + + {t('chat.resetChat', { ns: 'share' })} + )} {currentConversationId && inputsForms.length > 0 && !allInputsHidden && ( @@ -158,26 +168,36 @@ const Header: FC = ({
{ showToggleExpandButton && ( - - - { - expanded - ?
- :
- } - + + + { + expanded + ?
+ :
+ } + + )} + /> + + {expanded ? t('chat.collapse', { ns: 'share' }) : t('chat.expand', { ns: 'share' })} + ) } {currentConversationId && allowResetChat && ( - - -
- + + +
+ + )} + /> + + {t('chat.resetChat', { ns: 'share' })} + )} {currentConversationId && inputsForms.length > 0 && !allInputsHidden && ( diff --git a/web/app/components/base/copy-feedback/index.tsx b/web/app/components/base/copy-feedback/index.tsx index 431b697a6a..860b88b245 100644 --- a/web/app/components/base/copy-feedback/index.tsx +++ b/web/app/components/base/copy-feedback/index.tsx @@ -1,4 +1,5 @@ 'use client' +import { Tooltip, TooltipContent, TooltipTrigger } from '@langgenius/dify-ui/tooltip' import { RiClipboardFill, RiClipboardLine, @@ -6,7 +7,6 @@ import { import { useCallback } from 'react' import { useTranslation } from 'react-i18next' import ActionButton from '@/app/components/base/action-button' -import Tooltip from '@/app/components/base/tooltip' import { useClipboard } from '@/hooks/use-clipboard' import copyStyle from './style.module.css' @@ -35,15 +35,20 @@ const CopyFeedback = ({ content }: Props) => { }, [copy, content]) return ( - - -
- {copied && } - {!copied && } -
-
+ + +
+ {copied && } + {!copied && } +
+ + )} + /> + + {safeText} +
) } @@ -65,18 +70,23 @@ export const CopyFeedbackNew = ({ content, className }: Pick -
-
-
-
+ + +
+
+
+ )} + /> + + {safeText} +
) } diff --git a/web/app/components/base/features/new-feature-panel/annotation-reply/annotation-ctrl-button.tsx b/web/app/components/base/features/new-feature-panel/annotation-reply/annotation-ctrl-button.tsx index 3f3e68b32e..090f4d459f 100644 --- a/web/app/components/base/features/new-feature-panel/annotation-reply/annotation-ctrl-button.tsx +++ b/web/app/components/base/features/new-feature-panel/annotation-reply/annotation-ctrl-button.tsx @@ -1,11 +1,11 @@ 'use client' import type { FC } from 'react' import { toast } from '@langgenius/dify-ui/toast' +import { Tooltip, TooltipContent, TooltipTrigger } from '@langgenius/dify-ui/tooltip' import { RiEditLine, RiFileEditLine } from '@remixicon/react' import * as React from 'react' import { useTranslation } from 'react-i18next' import ActionButton from '@/app/components/base/action-button' -import Tooltip from '@/app/components/base/tooltip' import { useModalContext } from '@/context/modal-context' import { useProviderContext } from '@/context/provider-context' import { addAnnotation } from '@/service/annotation' @@ -40,17 +40,31 @@ const AnnotationCtrlButton: FC = ({ cached, query, answer, appId, message return ( <> {cached && ( - - - - + + + + + )} + /> + + {t('feature.annotation.edit', { ns: 'appDebug' })} + )} {!cached && answer && ( - - - - + + + + + )} + /> + + {t('feature.annotation.add', { ns: 'appDebug' })} + )} diff --git a/web/app/components/base/features/new-feature-panel/feature-bar.tsx b/web/app/components/base/features/new-feature-panel/feature-bar.tsx index 9b442ca52f..bf9d1c6cf0 100644 --- a/web/app/components/base/features/new-feature-panel/feature-bar.tsx +++ b/web/app/components/base/features/new-feature-panel/feature-bar.tsx @@ -1,5 +1,6 @@ import { Button } from '@langgenius/dify-ui/button' import { cn } from '@langgenius/dify-ui/cn' +import { Tooltip, TooltipContent, TooltipTrigger } from '@langgenius/dify-ui/tooltip' import { RiApps2AddLine, RiArrowRightLine, RiSparklingFill } from '@remixicon/react' import * as React from 'react' import { useMemo, useState } from 'react' @@ -7,7 +8,6 @@ import { useTranslation } from 'react-i18next' import { useFeatures } from '@/app/components/base/features/hooks' import VoiceSettings from '@/app/components/base/features/new-feature-panel/text-to-speech/voice-settings' import { Citations, ContentModeration, FolderUpload, LoveMessage, MessageFast, Microphone01, TextToAudio, VirtualAssistant } from '@/app/components/base/icons/src/vender/features' -import Tooltip from '@/app/components/base/tooltip' type Props = { isChatMode?: boolean @@ -51,86 +51,131 @@ const FeatureBar = ({
{!!features.moreLikeThis?.enabled && ( - -
- -
+ + + +
+ )} + /> + + {t('feature.moreLikeThis.title', { ns: 'appDebug' })} + )} {!!features.opening?.enabled && ( - -
- -
+ + + +
+ )} + /> + + {t('feature.conversationOpener.title', { ns: 'appDebug' })} +
)} {!!features.moderation?.enabled && ( - -
- -
+ + + +
+ )} + /> + + {t('feature.moderation.title', { ns: 'appDebug' })} +
)} {!!features.speech2text?.enabled && ( - -
- -
+ + + +
+ )} + /> + + {t('feature.speechToText.title', { ns: 'appDebug' })} + )} {!!features.text2speech?.enabled && ( - -
- -
+ + + +
+ )} + /> + + {t('feature.textToSpeech.title', { ns: 'appDebug' })} +
)} {showFileUpload && !!features.file?.enabled && ( - -
- -
+ + + +
+ )} + /> + + {t('feature.fileUpload.title', { ns: 'appDebug' })} + )} {!!features.suggested?.enabled && ( - -
- -
+ + + +
+ )} + /> + + {t('feature.suggestedQuestionsAfterAnswer.title', { ns: 'appDebug' })} +
)} {isChatMode && !!features.citation?.enabled && ( - -
- -
+ + + +
+ )} + /> + + {t('feature.citation.title', { ns: 'appDebug' })} + )} {isChatMode && !!features.annotationReply?.enabled && ( - -
- -
+ + + +
+ )} + /> + + {t('feature.annotation.title', { ns: 'appDebug' })} +
)}
diff --git a/web/app/components/base/features/new-feature-panel/feature-card.tsx b/web/app/components/base/features/new-feature-panel/feature-card.tsx index 58725a31a1..0c25a514fe 100644 --- a/web/app/components/base/features/new-feature-panel/feature-card.tsx +++ b/web/app/components/base/features/new-feature-panel/feature-card.tsx @@ -1,9 +1,9 @@ import { Switch } from '@langgenius/dify-ui/switch' +import { Tooltip, TooltipContent, TooltipTrigger } from '@langgenius/dify-ui/tooltip' import { RiQuestionLine, } from '@remixicon/react' import * as React from 'react' -import Tooltip from '@/app/components/base/tooltip' type Props = { icon: any @@ -41,10 +41,15 @@ const FeatureCard = ({
{title} {tooltip && ( - -
+ +
+ )} + /> + + {tooltip} +
)}
diff --git a/web/app/components/base/file-uploader/file-list-in-log.tsx b/web/app/components/base/file-uploader/file-list-in-log.tsx index 14dd171b59..9edd1ffed4 100644 --- a/web/app/components/base/file-uploader/file-list-in-log.tsx +++ b/web/app/components/base/file-uploader/file-list-in-log.tsx @@ -1,10 +1,10 @@ import type { FileEntity } from './types' import { cn } from '@langgenius/dify-ui/cn' +import { Tooltip, TooltipContent, TooltipTrigger } from '@langgenius/dify-ui/tooltip' import { RiArrowRightSLine } from '@remixicon/react' import * as React from 'react' import { useMemo, useState } from 'react' import { useTranslation } from 'react-i18next' -import Tooltip from '@/app/components/base/tooltip' import { SupportUploadFileTypes } from '@/app/components/workflow/types' import FileImageRender from './file-image-render' import FileTypeIcon from './file-type-icon' @@ -49,27 +49,37 @@ const FileListInLog = ({ fileList, isExpanded = false, noBorder = false, noPaddi return ( <> {isImageFile && ( - -
- -
+ + + +
+ )} + /> + + {name} +
)} {!isImageFile && ( - -
- -
+ + + +
+ )} + /> + + {name} + )} diff --git a/web/app/components/base/file-uploader/pdf-preview.tsx b/web/app/components/base/file-uploader/pdf-preview.tsx index bfd115401b..c129f9cfb3 100644 --- a/web/app/components/base/file-uploader/pdf-preview.tsx +++ b/web/app/components/base/file-uploader/pdf-preview.tsx @@ -1,4 +1,5 @@ import type { FC } from 'react' +import { Tooltip, TooltipContent, TooltipTrigger } from '@langgenius/dify-ui/tooltip' import { RiCloseLine, RiZoomInLine, RiZoomOutLine } from '@remixicon/react' import { noop } from 'es-toolkit/function' import { t } from 'i18next' @@ -7,7 +8,6 @@ import { useState } from 'react' import { createPortal } from 'react-dom' import { useHotkeys } from 'react-hotkeys-hook' import Loading from '@/app/components/base/loading' -import Tooltip from '@/app/components/base/tooltip' import useBreakpoints, { MediaType } from '@/hooks/use-breakpoints' import { PdfHighlighter, PdfLoader } from './pdf-highlighter-adapter' @@ -76,29 +76,50 @@ const PdfPreview: FC = ({ }}
- -
- -
+ + + +
+ )} + /> + + {t('operation.zoomOut', { ns: 'common' })} +
- -
- -
+ + + + + )} + /> + + {t('operation.zoomIn', { ns: 'common' })} + - -
- -
+ + + + + )} + /> + + {t('operation.cancel', { ns: 'common' })} + , document.body, diff --git a/web/app/components/base/form/components/base/__tests__/base-field.spec.tsx b/web/app/components/base/form/components/base/__tests__/base-field.spec.tsx index 54d7accad4..a4d1323e6a 100644 --- a/web/app/components/base/form/components/base/__tests__/base-field.spec.tsx +++ b/web/app/components/base/form/components/base/__tests__/base-field.spec.tsx @@ -2,6 +2,7 @@ import type { AnyFieldApi } from '@tanstack/react-form' import type { FormSchema } from '@/app/components/base/form/types' import { useForm } from '@tanstack/react-form' import { act, fireEvent, render, screen } from '@testing-library/react' +import userEvent from '@testing-library/user-event' import { FormItemValidateStatusEnum, FormTypeEnum } from '@/app/components/base/form/types' import BaseField from '../base-field' @@ -117,6 +118,25 @@ describe('BaseField', () => { expect(screen.queryByText('Beta')).not.toBeInTheDocument() }) + it('should not render current select value when it is filtered out by show_on conditions', () => { + renderBaseField({ + formSchema: { + type: FormTypeEnum.select, + name: 'mode', + label: 'Mode', + required: false, + options: [ + { label: 'Alpha', value: 'alpha' }, + { label: 'Beta', value: 'beta', show_on: [{ variable: 'enabled', value: 'yes' }] }, + ], + }, + defaultValues: { mode: 'beta', enabled: 'no' }, + }) + + expect(screen.getByRole('combobox', { name: 'Mode' })).not.toHaveTextContent('beta') + expect(screen.getByRole('combobox', { name: 'Mode' })).toHaveTextContent('common.placeholder.input') + }) + it('should render dynamic select loading state', () => { mockDynamicOptions.mockReturnValue({ data: undefined, @@ -238,6 +258,7 @@ describe('BaseField', () => { }) it('should render dynamic options and allow selecting one', async () => { + const user = userEvent.setup() mockDynamicOptions.mockReturnValue({ data: { options: [ @@ -258,13 +279,42 @@ describe('BaseField', () => { defaultValues: { plugin_option: '' }, }) - await act(async () => { - fireEvent.click(screen.getByText('common.placeholder.input')) + await user.click(screen.getByRole('combobox', { name: 'Plugin option' })) + await user.click(screen.getByRole('option', { name: 'Option A' })) + expect(screen.getByRole('combobox', { name: 'Plugin option' })).toHaveTextContent('Option A') + }) + + it('should preserve multiple dynamic select values', async () => { + const user = userEvent.setup() + mockDynamicOptions.mockReturnValue({ + data: { + options: [ + { label: { en_US: 'Option A', zh_Hans: '选项A' }, value: 'a' }, + { label: { en_US: 'Option B', zh_Hans: '选项B' }, value: 'b' }, + ], + }, + isLoading: false, + error: null, }) - await act(async () => { - fireEvent.click(screen.getByText('Option A')) + + renderBaseField({ + formSchema: { + type: FormTypeEnum.dynamicSelect, + name: 'plugin_options', + label: 'Plugin options', + required: false, + multiple: true, + }, + defaultValues: { plugin_options: ['a'] }, + showCurrentValue: true, }) - expect(screen.getByText('Option A')).toBeInTheDocument() + + expect(screen.getByRole('combobox', { name: 'Plugin options' })).toHaveTextContent('common.dynamicSelect.selected') + + await user.click(screen.getByRole('combobox', { name: 'Plugin options' })) + await user.click(screen.getByRole('option', { name: 'Option B' })) + + expect(screen.getByTestId('field-value')).toHaveTextContent('a,b') }) it('should update boolean field when users choose false', async () => { diff --git a/web/app/components/base/form/components/base/base-field.tsx b/web/app/components/base/form/components/base/base-field.tsx index b1e17bdefc..a256bccd8b 100644 --- a/web/app/components/base/form/components/base/base-field.tsx +++ b/web/app/components/base/form/components/base/base-field.tsx @@ -1,6 +1,15 @@ import type { AnyFieldApi } from '@tanstack/react-form' import type { FieldState, FormSchema, TypeWithI18N } from '@/app/components/base/form/types' import { cn } from '@langgenius/dify-ui/cn' +import { + Select, + SelectContent, + SelectItem, + SelectItemIndicator, + SelectItemText, + SelectTrigger, + SelectValue, +} from '@langgenius/dify-ui/select' import { useStore } from '@tanstack/react-form' import { isValidElement, @@ -14,7 +23,6 @@ import { FormItemValidateStatusEnum, FormTypeEnum } from '@/app/components/base/ import Input from '@/app/components/base/input' import Radio from '@/app/components/base/radio' import RadioE from '@/app/components/base/radio/ui' -import PureSelect from '@/app/components/base/select/pure' import Tooltip from '@/app/components/base/tooltip' import { useRenderI18nObject } from '@/hooks/use-i18n' import { useTriggerPluginDynamicOptions } from '@/service/use-triggers' @@ -43,6 +51,19 @@ const getTranslatedContent = ({ content, render }: { return '' } +type SelectOption = { + label: string + value: string +} + +const getSingleSelectValue = (value: unknown, options: SelectOption[]) => { + return options.find(option => option.value === value)?.value ?? null +} + +const getSingleSelectLabel = (value: unknown, options: SelectOption[], placeholder: string | undefined) => { + return options.find(option => option.value === value)?.label ?? placeholder +} + const VALIDATE_STATUS_STYLE_MAP: Record = { [FormItemValidateStatusEnum.Error]: { componentClassName: 'border-components-input-border-destructive focus:border-components-input-border-destructive', @@ -121,7 +142,7 @@ const BaseField = ({ if (!results[1]) results[1] = t('placeholder.input', { ns: 'common' }) return results - }, [label, placeholder, tooltip, description, help, renderI18nObject]) + }, [label, placeholder, tooltip, description, help, renderI18nObject, t]) const watchedVariables = useMemo(() => { const variables = new Set() @@ -184,6 +205,13 @@ const BaseField = ({ field.handleChange(value) onChange?.(field.name, value) }, [field, onChange]) + const dynamicPlaceholder = isDynamicOptionsLoading + ? t('dynamicSelect.loading', { ns: 'common' }) + : translatedPlaceholder + const dynamicNoticeTitle = dynamicOptionsError + ? t('dynamicSelect.error', { ns: 'common' }) + : (!dynamicOptions.length ? t('dynamicSelect.noData', { ns: 'common' }) : null) + const dynamicNoticeClassName = dynamicOptionsError ? 'text-text-destructive-secondary' : undefined return ( <> @@ -223,19 +251,58 @@ const BaseField = ({ ) } { - formItemType === FormTypeEnum.select && !multiple && ( - handleChange(v)} - disabled={disabled} - placeholder={translatedPlaceholder} - options={memorizedOptions} - triggerPopupSameWidth - popupProps={{ - className: 'max-h-[320px] overflow-y-auto', - }} - /> - ) + formItemType === FormTypeEnum.select && (multiple + ? ( + + ) + : ( + + )) } { formItemType === FormTypeEnum.checkbox /* && multiple */ && ( @@ -249,24 +316,76 @@ const BaseField = ({ ) } { - formItemType === FormTypeEnum.dynamicSelect && ( - - ) + formItemType === FormTypeEnum.dynamicSelect && (multiple + ? ( + + ) + : ( + + )) } { formItemType === FormTypeEnum.radio && ( diff --git a/web/app/components/base/form/components/field/__tests__/custom-select.spec.tsx b/web/app/components/base/form/components/field/__tests__/custom-select.spec.tsx deleted file mode 100644 index 5470df58a3..0000000000 --- a/web/app/components/base/form/components/field/__tests__/custom-select.spec.tsx +++ /dev/null @@ -1,49 +0,0 @@ -import { fireEvent, render, screen } from '@testing-library/react' -import CustomSelectField from '../custom-select' - -const mockField = { - name: 'custom-select-field', - state: { - value: 'small', - }, - handleChange: vi.fn(), -} - -vi.mock('../../..', () => ({ - useFieldContext: () => mockField, -})) - -describe('CustomSelectField', () => { - beforeEach(() => { - vi.clearAllMocks() - mockField.state.value = 'small' - }) - - it('should render select placeholder or selected value', () => { - render( - , - ) - expect(screen.getByText('Small')).toBeInTheDocument() - }) - - it('should update value when users select another option', () => { - render( - , - ) - fireEvent.click(screen.getByText('Small')) - fireEvent.click(screen.getByText('Large')) - expect(mockField.handleChange).toHaveBeenCalledWith('large') - }) -}) diff --git a/web/app/components/base/form/components/field/__tests__/select.spec.tsx b/web/app/components/base/form/components/field/__tests__/select.spec.tsx index 0bf6b4e022..45cc87d157 100644 --- a/web/app/components/base/form/components/field/__tests__/select.spec.tsx +++ b/web/app/components/base/form/components/field/__tests__/select.spec.tsx @@ -1,4 +1,5 @@ -import { fireEvent, render, screen } from '@testing-library/react' +import { render, screen } from '@testing-library/react' +import userEvent from '@testing-library/user-event' import SelectField from '../select' const mockField = { @@ -29,10 +30,27 @@ describe('SelectField', () => { ]} />, ) - expect(screen.getByText('Alpha')).toBeInTheDocument() + expect(screen.getByRole('combobox', { name: 'Mode' })).toHaveTextContent('Alpha') }) - it('should update value when users select another option', () => { + it('should render the option label when selected value is an empty string', () => { + mockField.state.value = '' + + render( + , + ) + + expect(screen.getByRole('combobox', { name: 'Mode' })).toHaveTextContent('No default selected') + }) + + it('should update value when users select another option', async () => { + const user = userEvent.setup() render( { ]} />, ) - fireEvent.click(screen.getByText('Alpha')) - fireEvent.click(screen.getByText('Beta')) + await user.click(screen.getByRole('combobox', { name: 'Mode' })) + await user.click(screen.getByRole('option', { name: 'Beta' })) expect(mockField.handleChange).toHaveBeenCalledWith('beta') }) }) diff --git a/web/app/components/base/form/components/field/custom-select.tsx b/web/app/components/base/form/components/field/custom-select.tsx deleted file mode 100644 index 5808d4004d..0000000000 --- a/web/app/components/base/form/components/field/custom-select.tsx +++ /dev/null @@ -1,41 +0,0 @@ -import type { CustomSelectProps, Option } from '../../../select/custom' -import type { LabelProps } from '../label' -import { cn } from '@langgenius/dify-ui/cn' -import { useFieldContext } from '../..' -import CustomSelect from '../../../select/custom' -import Label from '../label' - -type CustomSelectFieldProps = { - label: string - labelOptions?: Omit - options: T[] - className?: string -} & Omit, 'options' | 'value' | 'onChange'> - -const CustomSelectField = ({ - label, - labelOptions, - options, - className, - ...selectProps -}: CustomSelectFieldProps) => { - const field = useFieldContext() - - return ( -
-
- ) -} - -export default CustomSelectField diff --git a/web/app/components/base/form/components/field/input-type-select/__tests__/index.spec.tsx b/web/app/components/base/form/components/field/input-type-select/__tests__/index.spec.tsx index bb7ae80a34..f666315ddf 100644 --- a/web/app/components/base/form/components/field/input-type-select/__tests__/index.spec.tsx +++ b/web/app/components/base/form/components/field/input-type-select/__tests__/index.spec.tsx @@ -1,4 +1,5 @@ -import { fireEvent, render, screen } from '@testing-library/react' +import { render, screen } from '@testing-library/react' +import userEvent from '@testing-library/user-event' import InputTypeSelectField from '../index' const mockField = { @@ -20,17 +21,20 @@ describe('InputTypeSelectField', () => { }) it('should render label and selected option', () => { - render() + const { container } = render() expect(screen.getByText('Input type')).toBeInTheDocument() expect(screen.getByText('appDebug.variableConfig.text-input')).toBeInTheDocument() + expect(container.querySelector('[role="combobox"] span > div')).not.toBeInTheDocument() + expect(container.querySelector('[role="combobox"] > span > span')).toHaveClass('flex', 'min-w-0', 'items-center', 'gap-x-0.5') }) - it('should update value when users choose another input type', () => { + it('should update value when users choose another input type', async () => { + const user = userEvent.setup() render() - fireEvent.click(screen.getByText('appDebug.variableConfig.text-input')) - fireEvent.click(screen.getByText('appDebug.variableConfig.number')) + await user.click(screen.getByRole('combobox', { name: 'Input type' })) + await user.click(screen.getByRole('option', { name: /appDebug.variableConfig.number/ })) expect(mockField.handleChange).toHaveBeenCalledWith('number') }) diff --git a/web/app/components/base/form/components/field/input-type-select/__tests__/trigger.spec.tsx b/web/app/components/base/form/components/field/input-type-select/__tests__/trigger.spec.tsx index 0957ac41c1..a7a1f2a294 100644 --- a/web/app/components/base/form/components/field/input-type-select/__tests__/trigger.spec.tsx +++ b/web/app/components/base/form/components/field/input-type-select/__tests__/trigger.spec.tsx @@ -5,7 +5,7 @@ const MockIcon = () => describe('InputTypeSelect Trigger', () => { it('should show placeholder text when no option is selected', () => { - render() + render() expect(screen.getByText('common.placeholder.select')).toBeInTheDocument() }) @@ -18,11 +18,25 @@ describe('InputTypeSelect Trigger', () => { Icon: MockIcon, type: 'string', }} - open={false} />, ) expect(screen.getByText('Text Input')).toBeInTheDocument() expect(screen.getByText('string')).toBeInTheDocument() }) + + it('should keep selected option parts in one inline flex row', () => { + render( + , + ) + + expect(screen.getByText('Text Input').parentElement).toHaveClass('flex', 'min-w-0', 'items-center', 'gap-x-0.5') + }) }) diff --git a/web/app/components/base/form/components/field/input-type-select/index.tsx b/web/app/components/base/form/components/field/input-type-select/index.tsx index 5e150240f6..37f9a510d4 100644 --- a/web/app/components/base/form/components/field/input-type-select/index.tsx +++ b/web/app/components/base/form/components/field/input-type-select/index.tsx @@ -1,10 +1,13 @@ -import type { CustomSelectProps } from '../../../../select/custom' import type { LabelProps } from '../../label' import type { FileTypeSelectOption, InputType } from './types' import { cn } from '@langgenius/dify-ui/cn' -import { useCallback } from 'react' +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, +} from '@langgenius/dify-ui/select' import { useFieldContext } from '../../..' -import CustomSelect from '../../../../select/custom' import Label from '../../label' import { useInputTypeOptions } from './hooks' import Option from './option' @@ -15,24 +18,19 @@ type InputTypeSelectFieldProps = { labelOptions?: Omit supportFile: boolean className?: string -} & Omit, 'options' | 'value' | 'onChange' | 'CustomTrigger' | 'CustomOption'> + disabled?: boolean +} const InputTypeSelectField = ({ label, labelOptions, supportFile, className, - ...customSelectProps + disabled, }: InputTypeSelectFieldProps) => { const field = useFieldContext() const inputTypeOptions = useInputTypeOptions(supportFile) - - const renderTrigger = useCallback((option: FileTypeSelectOption | undefined, open: boolean) => { - return - }, []) - const renderOption = useCallback((option: FileTypeSelectOption) => { - return