diff --git a/api/core/tools/tool_manager.py b/api/core/tools/tool_manager.py index 87cf6d7085..0a7811bb53 100644 --- a/api/core/tools/tool_manager.py +++ b/api/core/tools/tool_manager.py @@ -1078,6 +1078,13 @@ class ToolManager: if parameter.form == ToolParameter.ToolParameterForm.FORM: if variable_pool: config = tool_configurations.get(parameter.name, {}) + + selector_value = cls._extract_runtime_selector_value(parameter, config) + if selector_value is not None: + # Selector parameters carry structured dictionaries, not scalar ToolInput values. + runtime_parameters[parameter.name] = selector_value + continue + if not (config and isinstance(config, dict) and config.get("value") is not None): continue tool_input = ToolNodeData.ToolInput.model_validate(tool_configurations.get(parameter.name, {})) @@ -1105,5 +1112,39 @@ class ToolManager: runtime_parameters[parameter.name] = value return runtime_parameters + @classmethod + def _extract_runtime_selector_value(cls, parameter: ToolParameter, config: Any) -> dict[str, Any] | None: + if parameter.type not in { + ToolParameter.ToolParameterType.MODEL_SELECTOR, + ToolParameter.ToolParameterType.APP_SELECTOR, + }: + return None + if not isinstance(config, dict): + return None + + input_value = config.get("value") + if isinstance(input_value, dict) and cls._is_selector_value(parameter, input_value): + return cast("dict[str, Any]", parameter.init_frontend_parameter(input_value)) + + if cls._is_selector_value(parameter, config): + selector_value = dict(config) + selector_value.pop("type", None) + selector_value.pop("value", None) + return cast("dict[str, Any]", parameter.init_frontend_parameter(selector_value)) + + return None + + @classmethod + def _is_selector_value(cls, parameter: ToolParameter, value: Mapping[str, Any]) -> bool: + if parameter.type == ToolParameter.ToolParameterType.MODEL_SELECTOR: + return ( + isinstance(value.get("provider"), str) + and isinstance(value.get("model"), str) + and isinstance(value.get("model_type"), str) + ) + if parameter.type == ToolParameter.ToolParameterType.APP_SELECTOR: + return isinstance(value.get("app_id"), str) + return False + ToolManager.load_hardcoded_providers_cache() diff --git a/api/core/workflow/human_input_adapter.py b/api/core/workflow/human_input_adapter.py index 4b765e6aea..731ae2b858 100644 --- a/api/core/workflow/human_input_adapter.py +++ b/api/core/workflow/human_input_adapter.py @@ -272,6 +272,14 @@ def _adapt_tool_node_data_for_graph(node_data: Mapping[str, Any]) -> dict[str, A normalized_tool_configurations[name] = value continue + selector_value = _extract_selector_configuration(value) + if selector_value is not None: + # Model/app selectors are dictionaries even when they come through the legacy tool configuration path. + # Move them to tool_parameters so graph validation does not flatten them as primitive constants. + found_legacy_tool_inputs = True + normalized_tool_parameters.setdefault(name, {"type": "constant", "value": selector_value}) + continue + input_type = value.get("type") input_value = value.get("value") if input_type not in {"mixed", "variable", "constant"}: @@ -310,6 +318,28 @@ def _flatten_legacy_tool_configuration_value(*, input_type: Any, input_value: An return None +def _extract_selector_configuration(value: Mapping[str, Any]) -> dict[str, Any] | None: + input_value = value.get("value") + if isinstance(input_value, Mapping) and _is_selector_configuration(input_value): + return dict(input_value) + + if _is_selector_configuration(value): + selector_value = dict(value) + selector_value.pop("type", None) + selector_value.pop("value", None) + return selector_value + + return None + + +def _is_selector_configuration(value: Mapping[str, Any]) -> bool: + return ( + isinstance(value.get("provider"), str) + and isinstance(value.get("model"), str) + and isinstance(value.get("model_type"), str) + ) or isinstance(value.get("app_id"), str) + + def _normalize_email_recipients(recipients: Mapping[str, Any]) -> dict[str, Any]: normalized = dict(recipients) diff --git a/api/core/workflow/node_runtime.py b/api/core/workflow/node_runtime.py index b8725853c4..c1d3a856fb 100644 --- a/api/core/workflow/node_runtime.py +++ b/api/core/workflow/node_runtime.py @@ -501,11 +501,15 @@ class DifyToolNodeRuntime(ToolNodeRuntimeProtocol): @staticmethod def _build_tool_runtime_spec(node_data: ToolNodeData) -> _WorkflowToolRuntimeSpec: + tool_configurations = dict(node_data.tool_configurations) + tool_configurations.update( + {name: tool_input.model_dump(mode="python") for name, tool_input in node_data.tool_parameters.items()} + ) return _WorkflowToolRuntimeSpec( provider_type=CoreToolProviderType(node_data.provider_type.value), provider_id=node_data.provider_id, tool_name=node_data.tool_name, - tool_configurations=dict(node_data.tool_configurations), + tool_configurations=tool_configurations, credential_id=node_data.credential_id, ) diff --git a/api/tests/unit_tests/core/tools/test_tool_manager.py b/api/tests/unit_tests/core/tools/test_tool_manager.py index 9ebaa0417b..c9b3dfb186 100644 --- a/api/tests/unit_tests/core/tools/test_tool_manager.py +++ b/api/tests/unit_tests/core/tools/test_tool_manager.py @@ -925,3 +925,78 @@ def test_convert_tool_parameters_type_constant_branch(): ) assert constant == {"text": "fixed"} + + +def test_convert_tool_parameters_type_model_selector_from_legacy_top_level_config(): + model_param = ToolParameter.get_simple_instance( + name="vision_llm_model", + llm_description="vision model", + typ=ToolParameter.ToolParameterType.MODEL_SELECTOR, + required=True, + ) + model_param.form = ToolParameter.ToolParameterForm.FORM + variable_pool = Mock() + + runtime_parameters = ToolManager._convert_tool_parameters_type( + parameters=[model_param], + variable_pool=variable_pool, + tool_configurations={ + "vision_llm_model": { + "type": "constant", + "value": "", + "provider": "langgenius/tongyi/tongyi", + "model": "qwen3-vl-plus", + "model_type": "llm", + "mode": "chat", + } + }, + typ="workflow", + ) + + assert runtime_parameters == { + "vision_llm_model": { + "provider": "langgenius/tongyi/tongyi", + "model": "qwen3-vl-plus", + "model_type": "llm", + "mode": "chat", + } + } + + +def test_convert_tool_parameters_type_model_selector_from_constant_value_config(): + model_param = ToolParameter.get_simple_instance( + name="tts_model", + llm_description="tts model", + typ=ToolParameter.ToolParameterType.MODEL_SELECTOR, + required=True, + ) + model_param.form = ToolParameter.ToolParameterForm.FORM + variable_pool = Mock() + + runtime_parameters = ToolManager._convert_tool_parameters_type( + parameters=[model_param], + variable_pool=variable_pool, + tool_configurations={ + "tts_model": { + "type": "constant", + "value": { + "provider": "langgenius/tongyi/tongyi", + "model": "qwen3-tts-flash", + "model_type": "tts", + "language": "Chinese", + "voice": "Cherry", + }, + } + }, + typ="workflow", + ) + + assert runtime_parameters == { + "tts_model": { + "provider": "langgenius/tongyi/tongyi", + "model": "qwen3-tts-flash", + "model_type": "tts", + "language": "Chinese", + "voice": "Cherry", + } + } diff --git a/api/tests/unit_tests/core/workflow/test_human_input_adapter.py b/api/tests/unit_tests/core/workflow/test_human_input_adapter.py index 8b5fceeb37..51049f8792 100644 --- a/api/tests/unit_tests/core/workflow/test_human_input_adapter.py +++ b/api/tests/unit_tests/core/workflow/test_human_input_adapter.py @@ -166,6 +166,71 @@ def test_adapt_node_data_for_graph_migrates_legacy_tool_configurations() -> None } +def test_adapt_node_data_for_graph_preserves_model_selector_top_level_configurations() -> None: + normalized = adapt_node_data_for_graph( + { + "type": BuiltinNodeTypes.TOOL, + "tool_configurations": { + "vision_llm_model": { + "type": "constant", + "value": "", + "provider": "langgenius/tongyi/tongyi", + "model": "qwen3-vl-plus", + "model_type": "llm", + "mode": "chat", + }, + }, + } + ) + + assert normalized["tool_configurations"] == {} + assert normalized["tool_parameters"] == { + "vision_llm_model": { + "type": "constant", + "value": { + "provider": "langgenius/tongyi/tongyi", + "model": "qwen3-vl-plus", + "model_type": "llm", + "mode": "chat", + }, + } + } + + +def test_adapt_node_data_for_graph_flattens_constant_model_selector_value() -> None: + normalized = adapt_node_data_for_graph( + { + "type": BuiltinNodeTypes.TOOL, + "tool_configurations": { + "tts_model": { + "type": "constant", + "value": { + "provider": "langgenius/tongyi/tongyi", + "model": "qwen3-tts-flash", + "model_type": "tts", + "language": "Chinese", + "voice": "Cherry", + }, + }, + }, + } + ) + + assert normalized["tool_configurations"] == {} + assert normalized["tool_parameters"] == { + "tts_model": { + "type": "constant", + "value": { + "provider": "langgenius/tongyi/tongyi", + "model": "qwen3-tts-flash", + "model_type": "tts", + "language": "Chinese", + "voice": "Cherry", + }, + } + } + + def test_adapt_node_config_for_graph_rewrites_nested_node_data() -> None: normalized = adapt_node_config_for_graph( { diff --git a/api/tests/unit_tests/core/workflow/test_node_runtime.py b/api/tests/unit_tests/core/workflow/test_node_runtime.py index 5a43369a1a..0d13151f42 100644 --- a/api/tests/unit_tests/core/workflow/test_node_runtime.py +++ b/api/tests/unit_tests/core/workflow/test_node_runtime.py @@ -22,6 +22,7 @@ from core.workflow.node_runtime import ( DifyPromptMessageSerializer, DifyRetrieverAttachmentLoader, DifyToolFileManager, + DifyToolNodeRuntime, apply_dify_debug_email_recipient, build_dify_llm_file_saver, resolve_dify_run_context, @@ -30,6 +31,7 @@ from graphon.file import FileTransferMethod, FileType from graphon.model_runtime.entities.common_entities import I18nObject from graphon.model_runtime.entities.model_entities import AIModelEntity, FetchFrom, ModelType from graphon.nodes.human_input.entities import HumanInputNodeData +from graphon.nodes.tool.entities import ToolNodeData, ToolProviderType from tests.workflow_test_utils import build_test_run_context @@ -334,6 +336,41 @@ def test_dify_human_input_runtime_builds_debug_repository(monkeypatch: pytest.Mo ) +def test_dify_tool_runtime_spec_prefers_tool_parameters_for_runtime_form_values() -> None: + node_data = ToolNodeData( + provider_id="video-mixcut-agent", + provider_type=ToolProviderType.PLUGIN, + provider_name="sawyer-shi/video-mixcut-agent", + tool_name="mixcut", + tool_label="MixCut", + tool_configurations={"count": 2}, + tool_parameters={ + "vision_llm_model": { + "type": "constant", + "value": { + "provider": "langgenius/tongyi/tongyi", + "model": "qwen3-vl-plus", + "model_type": "llm", + }, + } + }, + ) + + spec = DifyToolNodeRuntime._build_tool_runtime_spec(node_data) + + assert spec.tool_configurations == { + "count": 2, + "vision_llm_model": { + "type": "constant", + "value": { + "provider": "langgenius/tongyi/tongyi", + "model": "qwen3-vl-plus", + "model_type": "llm", + }, + }, + } + + def test_dify_human_input_runtime_create_form_filters_debugger_delivery_methods() -> None: repository = MagicMock() repository.create_form.return_value = sentinel.form diff --git a/eslint-suppressions.json b/eslint-suppressions.json index f52fb977f0..cd37f0ed89 100644 --- a/eslint-suppressions.json +++ b/eslint-suppressions.json @@ -438,11 +438,6 @@ "count": 1 } }, - "web/app/components/app/configuration/dataset-config/select-dataset/index.tsx": { - "no-restricted-imports": { - "count": 1 - } - }, "web/app/components/app/configuration/dataset-config/settings-modal/index.tsx": { "react/set-state-in-effect": { "count": 2 @@ -570,30 +565,6 @@ "count": 2 } }, - "web/app/components/app/overview/customize/index.tsx": { - "no-restricted-imports": { - "count": 1 - } - }, - "web/app/components/app/overview/embedded/index.tsx": { - "no-restricted-imports": { - "count": 1 - }, - "react/set-state-in-effect": { - "count": 1 - } - }, - "web/app/components/app/overview/settings/index.tsx": { - "no-restricted-imports": { - "count": 2 - }, - "react/set-state-in-effect": { - "count": 3 - }, - "regexp/no-unused-capturing-group": { - "count": 1 - } - }, "web/app/components/app/overview/trigger-card.tsx": { "ts/no-explicit-any": { "count": 1 @@ -637,17 +608,6 @@ "count": 1 } }, - "web/app/components/apps/app-card.tsx": { - "no-restricted-imports": { - "count": 1 - }, - "react/set-state-in-effect": { - "count": 1 - }, - "ts/no-explicit-any": { - "count": 2 - } - }, "web/app/components/apps/new-app-card.tsx": { "react-hooks-extra/no-direct-set-state-in-use-effect": { "count": 1 @@ -3591,11 +3551,6 @@ "count": 1 } }, - "web/app/components/workflow/dsl-export-confirm-modal.tsx": { - "no-restricted-imports": { - "count": 1 - } - }, "web/app/components/workflow/header/run-mode.tsx": { "no-console": { "count": 1 @@ -4283,11 +4238,6 @@ "count": 1 } }, - "web/app/components/workflow/nodes/knowledge-retrieval/components/metadata/condition-list/condition-operator.tsx": { - "no-restricted-imports": { - "count": 1 - } - }, "web/app/components/workflow/nodes/knowledge-retrieval/components/metadata/condition-list/condition-value-method.tsx": { "no-restricted-imports": { "count": 1 diff --git a/web/app/account/(commonLayout)/account-page/index.tsx b/web/app/account/(commonLayout)/account-page/index.tsx index 75d4e5afa8..2a4ae86f84 100644 --- a/web/app/account/(commonLayout)/account-page/index.tsx +++ b/web/app/account/(commonLayout)/account-page/index.tsx @@ -223,116 +223,107 @@ export default function AccountPage() { )} {!IS_CE_EDITION && } - { - editNameModalVisible && ( - !open && setEditNameModalVisible(false)}> - -
{t('account.editName', { ns: 'common' })}
-
{t('account.name', { ns: 'common' })}
- setEditName(e.target.value)} - /> -
- - -
-
-
- ) - } - { - editPasswordModalVisible && ( - !open && (setEditPasswordModalVisible(false), resetPasswordForm())}> - -
{userProfile.is_password_set ? t('account.resetPassword', { ns: 'common' }) : t('account.setPassword', { ns: 'common' })}
- {userProfile.is_password_set && ( - <> -
{t('account.currentPassword', { ns: 'common' })}
-
- setCurrentPassword(e.target.value)} - /> - -
- -
-
- - )} -
- {userProfile.is_password_set ? t('account.newPassword', { ns: 'common' }) : t('account.password', { ns: 'common' })} -
+ !open && setEditNameModalVisible(false)}> + +
{t('account.editName', { ns: 'common' })}
+
{t('account.name', { ns: 'common' })}
+ setEditName(e.target.value)} + /> +
+ + +
+
+
+ !open && (setEditPasswordModalVisible(false), resetPasswordForm())}> + +
{userProfile.is_password_set ? t('account.resetPassword', { ns: 'common' }) : t('account.setPassword', { ns: 'common' })}
+ {userProfile.is_password_set && ( + <> +
{t('account.currentPassword', { ns: 'common' })}
setPassword(e.target.value)} + type={showCurrentPassword ? 'text' : 'password'} + value={currentPassword} + onChange={e => setCurrentPassword(e.target.value)} />
-
{t('account.confirmPassword', { ns: 'common' })}
-
- setConfirmPassword(e.target.value)} - /> -
- -
-
-
- - -
-
-
- ) - } + + )} +
+ {userProfile.is_password_set ? t('account.newPassword', { ns: 'common' }) : t('account.password', { ns: 'common' })} +
+
+ setPassword(e.target.value)} + /> +
+ +
+
+
{t('account.confirmPassword', { ns: 'common' })}
+
+ setConfirmPassword(e.target.value)} + /> +
+ +
+
+
+ + +
+
+
{ showDeleteAccountModal && ( ) } - {showUpdateEmail && ( - setShowUpdateEmail(false)} - email={userProfile.email} - /> - )} + setShowUpdateEmail(false)} + email={userProfile.email} + /> ) } diff --git a/web/app/components/app-sidebar/app-info/__tests__/app-info-modals.spec.tsx b/web/app/components/app-sidebar/app-info/__tests__/app-info-modals.spec.tsx index 2fdd35cc43..218d4b94e6 100644 --- a/web/app/components/app-sidebar/app-info/__tests__/app-info-modals.spec.tsx +++ b/web/app/components/app-sidebar/app-info/__tests__/app-info-modals.spec.tsx @@ -46,6 +46,12 @@ vi.mock('@/app/components/workflow/update-dsl-modal', () => ({ })) vi.mock('@/app/components/workflow/dsl-export-confirm-modal', () => ({ + DSLExportConfirmContent: ({ onConfirm, onClose }: { onConfirm: (include?: boolean) => void, onClose: () => void }) => ( +
+ + +
+ ), default: ({ onConfirm, onClose }: { onConfirm: (include?: boolean) => void, onClose: () => void }) => (
diff --git a/web/app/components/app-sidebar/app-info/__tests__/app-operations.spec.tsx b/web/app/components/app-sidebar/app-info/__tests__/app-operations.spec.tsx index ff6aed2c71..5daf0c7100 100644 --- a/web/app/components/app-sidebar/app-info/__tests__/app-operations.spec.tsx +++ b/web/app/components/app-sidebar/app-info/__tests__/app-operations.spec.tsx @@ -228,6 +228,21 @@ describe('AppOperations', () => { }) describe('Visible operations click', () => { + it('should keep focus ring inside visible operation buttons', () => { + const cleanup = setupDomMeasurements(500, 60, [80]) + const editOp = createOperation('edit', 'Edit') + + render() + + const visibleButton = screen.getAllByText('Edit') + .map(label => label.closest('button')) + .find(button => button?.tabIndex !== -1) + + expect(visibleButton).toHaveClass('focus-visible:ring-inset') + + cleanup() + }) + it('should call onClick when a visible operation is clicked', async () => { const cleanup = setupDomMeasurements(500, 60, [80, 80]) const user = userEvent.setup() diff --git a/web/app/components/app-sidebar/app-info/app-info-modals.tsx b/web/app/components/app-sidebar/app-info/app-info-modals.tsx index 9535725cd3..e1ed1d62ef 100644 --- a/web/app/components/app-sidebar/app-info/app-info-modals.tsx +++ b/web/app/components/app-sidebar/app-info/app-info-modals.tsx @@ -16,13 +16,13 @@ import * as React from 'react' import { useCallback, useState } from 'react' import { useTranslation } from 'react-i18next' import Input from '@/app/components/base/input' +import { DSLExportConfirmContent } from '@/app/components/workflow/dsl-export-confirm-modal' import dynamic from '@/next/dynamic' const SwitchAppModal = dynamic(() => import('@/app/components/app/switch-app-modal'), { ssr: false }) const CreateAppModal = dynamic(() => import('@/app/components/explore/create-app-modal'), { ssr: false }) const DuplicateAppModal = dynamic(() => import('@/app/components/app/duplicate-modal'), { ssr: false }) const UpdateDSLModal = dynamic(() => import('@/app/components/workflow/update-dsl-modal'), { ssr: false }) -const DSLExportConfirmModal = dynamic(() => import('@/app/components/workflow/dsl-export-confirm-modal'), { ssr: false }) type AppInfoModalsProps = { appDetail: App & Partial @@ -54,7 +54,14 @@ const AppInfoModals = ({ const { t } = useTranslation() const [confirmDeleteInput, setConfirmDeleteInput] = useState('') const [isConfirmingExport, setIsConfirmingExport] = useState(false) + const [isSecretExporting, setIsSecretExporting] = useState(false) const isDeleteConfirmDisabled = confirmDeleteInput !== appDetail.name + const exportDialogMode = secretEnvList.length > 0 + ? 'secret' + : activeModal === 'exportWarning' + ? 'warning' + : null + const isExportDialogOpen = exportDialogMode !== null const handleDeleteDialogClose = () => { setConfirmDeleteInput('') @@ -74,6 +81,22 @@ const AppInfoModals = ({ } }, [handleConfirmExport, isConfirmingExport]) + const handleExportDialogClose = useCallback(() => { + if (exportDialogMode === 'secret') { + setSecretEnvList([]) + return + } + + closeModal() + }, [closeModal, exportDialogMode, setSecretEnvList]) + + const handleExportDialogOpenChange = useCallback((open: boolean) => { + if (open || isConfirmingExport || isSecretExporting) + return + + handleExportDialogClose() + }, [handleExportDialogClose, isConfirmingExport, isSecretExporting]) + return ( <> {activeModal === 'switch' && ( @@ -163,38 +186,42 @@ const AppInfoModals = ({ onBackup={exportCheck} /> )} - !open && closeModal()}> - -
- - {t('sidebar.exportWarning', { ns: 'workflow' })} - - - {t('sidebar.exportWarningDesc', { ns: 'workflow' })} - -
- - {t('operation.cancel', { ns: 'common' })} - - {isConfirmingExport - ? t('operation.exporting', { ns: 'common' }) - : t('operation.confirm', { ns: 'common' })} - - -
+ + {exportDialogMode === 'secret' + ? ( + setSecretEnvList([])} + onExportingChange={setIsSecretExporting} + /> + ) + : exportDialogMode === 'warning' && ( + +
+ + {t('sidebar.exportWarning', { ns: 'workflow' })} + + + {t('sidebar.exportWarningDesc', { ns: 'workflow' })} + +
+ + {t('operation.cancel', { ns: 'common' })} + + {isConfirmingExport + ? t('operation.exporting', { ns: 'common' }) + : t('operation.confirm', { ns: 'common' })} + + +
+ )}
- {secretEnvList.length > 0 && ( - setSecretEnvList([])} - /> - )} ) } diff --git a/web/app/components/app-sidebar/app-info/app-operations.tsx b/web/app/components/app-sidebar/app-info/app-operations.tsx index cc6afd739c..2e3270a222 100644 --- a/web/app/components/app-sidebar/app-info/app-operations.tsx +++ b/web/app/components/app-sidebar/app-info/app-operations.tsx @@ -133,7 +133,7 @@ const AppOperations = ({ data-targetid={operation.id} size="small" variant="secondary" - className="gap-px" + className="gap-px focus-visible:ring-inset" tabIndex={-1} > {cloneElement(operation.icon, { className: 'h-3.5 w-3.5 text-components-button-secondary-text' })} @@ -146,7 +146,7 @@ const AppOperations = ({ id="more-measure" size="small" variant="secondary" - className="gap-px" + className="gap-px focus-visible:ring-inset" tabIndex={-1} > @@ -162,7 +162,7 @@ const AppOperations = ({ data-targetid={operation.id} size="small" variant="secondary" - className="gap-px" + className="gap-px focus-visible:ring-inset" onClick={operation.onClick} > {cloneElement(operation.icon, { className: 'h-3.5 w-3.5 text-components-button-secondary-text' })} @@ -178,7 +178,7 @@ const AppOperations = ({ + ))} + {isFetchingNextPage && } +
+ + )} + {!isLoading && ( +
+
+ {selected.length > 0 && `${selected.length} ${t('feature.dataSet.selected', { ns: 'appDebug' })}`} +
+
+ + +
- - )} - {!isLoading && ( -
-
- {selected.length > 0 && `${selected.length} ${t('feature.dataSet.selected', { ns: 'appDebug' })}`} -
-
- - -
-
- )} - + )} + + ) } export default React.memo(SelectDataSet) diff --git a/web/app/components/app/configuration/debug/chat-user-input.tsx b/web/app/components/app/configuration/debug/chat-user-input.tsx index 2eff7ac3ca..5d6b6cdf89 100644 --- a/web/app/components/app/configuration/debug/chat-user-input.tsx +++ b/web/app/components/app/configuration/debug/chat-user-input.tsx @@ -113,7 +113,7 @@ const ChatUserInput = ({ {String(inputs[key] || t('placeholder.select', { ns: 'common' }))} - + {(options || []).map(option => ( {option} 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 c2a438b5e9..bfcc13c23c 100644 --- a/web/app/components/app/configuration/prompt-value-panel/index.tsx +++ b/web/app/components/app/configuration/prompt-value-panel/index.tsx @@ -167,7 +167,7 @@ const PromptValuePanel: FC = ({ {String(inputs[key] || t('placeholder.select', { ns: 'common' }))} - + {(options || []).map(option => ( {option} diff --git a/web/app/components/app/overview/customize/__tests__/index.spec.tsx b/web/app/components/app/overview/customize/__tests__/index.spec.tsx index 0e065fcdfe..1f703afcd8 100644 --- a/web/app/components/app/overview/customize/__tests__/index.spec.tsx +++ b/web/app/components/app/overview/customize/__tests__/index.spec.tsx @@ -8,13 +8,6 @@ vi.mock('@/context/i18n', () => ({ useDocLink: () => mockDocLink, })) -// Mock window.open -const mockWindowOpen = vi.fn() -Object.defineProperty(window, 'open', { - value: mockWindowOpen, - writable: true, -}) - describe('CustomizeModal', () => { const defaultProps = { isShow: true, @@ -287,7 +280,7 @@ describe('CustomizeModal', () => { // User interactions tests - verify user actions trigger expected behaviors describe('User Interactions', () => { - it('should call window.open with doc link when way 2 button is clicked', async () => { + it('should render the API docs link for way 2', async () => { // Arrange const props = { ...defaultProps } @@ -298,16 +291,10 @@ describe('CustomizeModal', () => { expect(screen.getByText('appOverview.overview.appInfo.customize.way2.operation')).toBeInTheDocument() }) - const way2Button = screen.getByText('appOverview.overview.appInfo.customize.way2.operation').closest('button') - expect(way2Button).toBeInTheDocument() - fireEvent.click(way2Button!) - - // Assert - expect(mockWindowOpen).toHaveBeenCalledTimes(1) - expect(mockWindowOpen).toHaveBeenCalledWith( - expect.stringContaining('/use-dify/publish/developing-with-apis'), - '_blank', - ) + const way2Link = screen.getByRole('link', { name: /way2\.operation/i }) + expect(way2Link).toHaveAttribute('href', expect.stringContaining('/use-dify/publish/developing-with-apis')) + expect(way2Link).toHaveAttribute('target', '_blank') + expect(way2Link).toHaveAttribute('rel', 'noopener noreferrer') }) it('should call onClose when modal close button is clicked', async () => { diff --git a/web/app/components/app/overview/customize/index.tsx b/web/app/components/app/overview/customize/index.tsx index e4b8aabf69..46527c30f9 100644 --- a/web/app/components/app/overview/customize/index.tsx +++ b/web/app/components/app/overview/customize/index.tsx @@ -1,10 +1,9 @@ 'use client' import type { FC } from 'react' -import { ArrowTopRightOnSquareIcon } from '@heroicons/react/24/outline' import { Button } from '@langgenius/dify-ui/button' +import { Dialog, DialogCloseButton, DialogContent, DialogDescription, DialogTitle } from '@langgenius/dify-ui/dialog' import * as React from 'react' import { useTranslation } from 'react-i18next' -import Modal from '@/app/components/base/modal' import Tag from '@/app/components/base/tag' import { useDocLink } from '@/context/i18n' import { AppModeEnum } from '@/types/app' @@ -25,7 +24,7 @@ const StepNum: FC<{ children: React.ReactNode }> = ({ children }) => ( const GithubIcon = ({ className }: { className: string }) => { return ( - + ) @@ -43,90 +42,85 @@ const CustomizeModal: FC = ({ const { t } = useTranslation() const docLink = useDocLink() const isChatApp = mode === AppModeEnum.CHAT || mode === AppModeEnum.ADVANCED_CHAT + const apiDocLink = docLink('/use-dify/publish/developing-with-apis') return ( - - - - -
- 3 -
-
{t(`${prefixCustomize}.way1.step3`, { ns: 'appOverview' })}
-
{t(`${prefixCustomize}.way1.step3Tip`, { ns: 'appOverview' })}
-
-              NEXT_PUBLIC_APP_ID=
-              {`'${appId}'`}
-              {' '}
-              
- NEXT_PUBLIC_APP_KEY= - {'\'\''} - {' '} -
- NEXT_PUBLIC_API_URL= - {`'${api_base_url}'`} -
+
+ 3 +
+
{t(`${prefixCustomize}.way1.step3`, { ns: 'appOverview' })}
+
{t(`${prefixCustomize}.way1.step3Tip`, { ns: 'appOverview' })}
+
+                NEXT_PUBLIC_APP_ID=
+                {`'${appId}'`}
+                {' '}
+                
+ NEXT_PUBLIC_APP_KEY= + {'\'\''} + {' '} +
+ NEXT_PUBLIC_API_URL= + {`'${api_base_url}'`} +
+
-
-
-
- - {t(`${prefixCustomize}.way`, { ns: 'appOverview' })} - {' '} - 2 - -

{t(`${prefixCustomize}.way2.name`, { ns: 'appOverview' })}

- -
- + +
+ + {t(`${prefixCustomize}.way`, { ns: 'appOverview' })} + {' '} + 2 + +

{t(`${prefixCustomize}.way2.name`, { ns: 'appOverview' })}

+ +
+ + ) } diff --git a/web/app/components/app/overview/embedded/index.tsx b/web/app/components/app/overview/embedded/index.tsx index e810c7f1eb..12203178f1 100644 --- a/web/app/components/app/overview/embedded/index.tsx +++ b/web/app/components/app/overview/embedded/index.tsx @@ -1,17 +1,13 @@ import type { SiteInfo } from '@/models/share' import { cn } from '@langgenius/dify-ui/cn' +import { Dialog, DialogCloseButton, DialogContent, DialogTitle } from '@langgenius/dify-ui/dialog' import { Tooltip, TooltipContent, TooltipTrigger } from '@langgenius/dify-ui/tooltip' -import { - RiClipboardFill, - RiClipboardLine, -} from '@remixicon/react' import copy from 'copy-to-clipboard' import * as React from 'react' -import { useEffect, useState } from 'react' +import { useState } from 'react' 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 { IS_CE_EDITION } from '@/config' import { useAppContext } from '@/context/app-context' import { basePath } from '@/utils/var' @@ -86,16 +82,18 @@ const prefixEmbedded = 'overview.appInfo.embedded' type Option = keyof typeof OPTION_MAP -type OptionStatus = { - iframe: boolean - scripts: boolean - chromePlugin: boolean +const OPTIONS: Option[] = ['iframe', 'scripts', 'chromePlugin'] + +const optionIconClassName: Record = { + iframe: style.iframeIcon!, + scripts: style.scriptsIcon!, + chromePlugin: style.chromePluginIcon!, } const Embedded = ({ siteInfo, isShow, onClose, appBaseUrl, accessToken, className }: Props) => { const { t } = useTranslation() const [option, setOption] = useState