mirror of
https://github.com/langgenius/dify.git
synced 2026-05-09 04:36:31 +08:00
Merge branch 'main' into 4-27-app-deploy
This commit is contained in:
commit
0a028faae6
@ -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()
|
||||
|
||||
@ -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)
|
||||
|
||||
|
||||
@ -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,
|
||||
)
|
||||
|
||||
|
||||
@ -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",
|
||||
}
|
||||
}
|
||||
|
||||
@ -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(
|
||||
{
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -223,116 +223,107 @@ export default function AccountPage() {
|
||||
)}
|
||||
{!IS_CE_EDITION && <Button className="mt-2 text-components-button-destructive-secondary-text" onClick={() => setShowDeleteAccountModal(true)}>{t('account.delete', { ns: 'common' })}</Button>}
|
||||
</div>
|
||||
{
|
||||
editNameModalVisible && (
|
||||
<Dialog open={editNameModalVisible} onOpenChange={open => !open && setEditNameModalVisible(false)}>
|
||||
<DialogContent className="w-[420px]! p-6!">
|
||||
<div className="mb-6 title-2xl-semi-bold text-text-primary">{t('account.editName', { ns: 'common' })}</div>
|
||||
<div className={titleClassName}>{t('account.name', { ns: 'common' })}</div>
|
||||
<Input
|
||||
className="mt-2"
|
||||
value={editName}
|
||||
onChange={e => setEditName(e.target.value)}
|
||||
/>
|
||||
<div className="mt-10 flex justify-end">
|
||||
<Button className="mr-2" onClick={() => setEditNameModalVisible(false)}>{t('operation.cancel', { ns: 'common' })}</Button>
|
||||
<Button
|
||||
disabled={editing || !editName}
|
||||
variant="primary"
|
||||
onClick={handleSaveName}
|
||||
>
|
||||
{t('operation.save', { ns: 'common' })}
|
||||
</Button>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
)
|
||||
}
|
||||
{
|
||||
editPasswordModalVisible && (
|
||||
<Dialog open={editPasswordModalVisible} onOpenChange={open => !open && (setEditPasswordModalVisible(false), resetPasswordForm())}>
|
||||
<DialogContent className="w-[420px]! p-6!">
|
||||
<div className="mb-6 title-2xl-semi-bold text-text-primary">{userProfile.is_password_set ? t('account.resetPassword', { ns: 'common' }) : t('account.setPassword', { ns: 'common' })}</div>
|
||||
{userProfile.is_password_set && (
|
||||
<>
|
||||
<div className={titleClassName}>{t('account.currentPassword', { ns: 'common' })}</div>
|
||||
<div className="relative mt-2">
|
||||
<Input
|
||||
type={showCurrentPassword ? 'text' : 'password'}
|
||||
value={currentPassword}
|
||||
onChange={e => setCurrentPassword(e.target.value)}
|
||||
/>
|
||||
|
||||
<div className="absolute inset-y-0 right-0 flex items-center">
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
onClick={() => setShowCurrentPassword(!showCurrentPassword)}
|
||||
>
|
||||
{showCurrentPassword ? '👀' : '😝'}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
<div className="mt-8 system-sm-semibold text-text-secondary">
|
||||
{userProfile.is_password_set ? t('account.newPassword', { ns: 'common' }) : t('account.password', { ns: 'common' })}
|
||||
</div>
|
||||
<Dialog open={editNameModalVisible} onOpenChange={open => !open && setEditNameModalVisible(false)}>
|
||||
<DialogContent className="w-105 p-6">
|
||||
<div className="mb-6 title-2xl-semi-bold text-text-primary">{t('account.editName', { ns: 'common' })}</div>
|
||||
<div className={titleClassName}>{t('account.name', { ns: 'common' })}</div>
|
||||
<Input
|
||||
className="mt-2"
|
||||
value={editName}
|
||||
onChange={e => setEditName(e.target.value)}
|
||||
/>
|
||||
<div className="mt-10 flex justify-end">
|
||||
<Button className="mr-2" onClick={() => setEditNameModalVisible(false)}>{t('operation.cancel', { ns: 'common' })}</Button>
|
||||
<Button
|
||||
disabled={editing || !editName}
|
||||
variant="primary"
|
||||
onClick={handleSaveName}
|
||||
>
|
||||
{t('operation.save', { ns: 'common' })}
|
||||
</Button>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
<Dialog open={editPasswordModalVisible} onOpenChange={open => !open && (setEditPasswordModalVisible(false), resetPasswordForm())}>
|
||||
<DialogContent className="w-[420px]! p-6!">
|
||||
<div className="mb-6 title-2xl-semi-bold text-text-primary">{userProfile.is_password_set ? t('account.resetPassword', { ns: 'common' }) : t('account.setPassword', { ns: 'common' })}</div>
|
||||
{userProfile.is_password_set && (
|
||||
<>
|
||||
<div className={titleClassName}>{t('account.currentPassword', { ns: 'common' })}</div>
|
||||
<div className="relative mt-2">
|
||||
<Input
|
||||
type={showPassword ? 'text' : 'password'}
|
||||
value={password}
|
||||
onChange={e => setPassword(e.target.value)}
|
||||
type={showCurrentPassword ? 'text' : 'password'}
|
||||
value={currentPassword}
|
||||
onChange={e => setCurrentPassword(e.target.value)}
|
||||
/>
|
||||
<div className="absolute inset-y-0 right-0 flex items-center">
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
onClick={() => setShowPassword(!showPassword)}
|
||||
onClick={() => setShowCurrentPassword(!showCurrentPassword)}
|
||||
>
|
||||
{showPassword ? '👀' : '😝'}
|
||||
{showCurrentPassword ? '👀' : '😝'}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-8 system-sm-semibold text-text-secondary">{t('account.confirmPassword', { ns: 'common' })}</div>
|
||||
<div className="relative mt-2">
|
||||
<Input
|
||||
type={showConfirmPassword ? 'text' : 'password'}
|
||||
value={confirmPassword}
|
||||
onChange={e => setConfirmPassword(e.target.value)}
|
||||
/>
|
||||
<div className="absolute inset-y-0 right-0 flex items-center">
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
onClick={() => setShowConfirmPassword(!showConfirmPassword)}
|
||||
>
|
||||
{showConfirmPassword ? '👀' : '😝'}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-10 flex justify-end">
|
||||
<Button
|
||||
className="mr-2"
|
||||
onClick={() => {
|
||||
setEditPasswordModalVisible(false)
|
||||
resetPasswordForm()
|
||||
}}
|
||||
>
|
||||
{t('operation.cancel', { ns: 'common' })}
|
||||
</Button>
|
||||
<Button
|
||||
disabled={editing}
|
||||
variant="primary"
|
||||
onClick={handleSavePassword}
|
||||
>
|
||||
{userProfile.is_password_set ? t('operation.reset', { ns: 'common' }) : t('operation.save', { ns: 'common' })}
|
||||
</Button>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
)
|
||||
}
|
||||
</>
|
||||
)}
|
||||
<div className="mt-8 system-sm-semibold text-text-secondary">
|
||||
{userProfile.is_password_set ? t('account.newPassword', { ns: 'common' }) : t('account.password', { ns: 'common' })}
|
||||
</div>
|
||||
<div className="relative mt-2">
|
||||
<Input
|
||||
type={showPassword ? 'text' : 'password'}
|
||||
value={password}
|
||||
onChange={e => setPassword(e.target.value)}
|
||||
/>
|
||||
<div className="absolute inset-y-0 right-0 flex items-center">
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
onClick={() => setShowPassword(!showPassword)}
|
||||
>
|
||||
{showPassword ? '👀' : '😝'}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-8 system-sm-semibold text-text-secondary">{t('account.confirmPassword', { ns: 'common' })}</div>
|
||||
<div className="relative mt-2">
|
||||
<Input
|
||||
type={showConfirmPassword ? 'text' : 'password'}
|
||||
value={confirmPassword}
|
||||
onChange={e => setConfirmPassword(e.target.value)}
|
||||
/>
|
||||
<div className="absolute inset-y-0 right-0 flex items-center">
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
onClick={() => setShowConfirmPassword(!showConfirmPassword)}
|
||||
>
|
||||
{showConfirmPassword ? '👀' : '😝'}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-10 flex justify-end">
|
||||
<Button
|
||||
className="mr-2"
|
||||
onClick={() => {
|
||||
setEditPasswordModalVisible(false)
|
||||
resetPasswordForm()
|
||||
}}
|
||||
>
|
||||
{t('operation.cancel', { ns: 'common' })}
|
||||
</Button>
|
||||
<Button
|
||||
disabled={editing}
|
||||
variant="primary"
|
||||
onClick={handleSavePassword}
|
||||
>
|
||||
{userProfile.is_password_set ? t('operation.reset', { ns: 'common' }) : t('operation.save', { ns: 'common' })}
|
||||
</Button>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
{
|
||||
showDeleteAccountModal && (
|
||||
<DeleteAccount
|
||||
@ -341,13 +332,11 @@ export default function AccountPage() {
|
||||
/>
|
||||
)
|
||||
}
|
||||
{showUpdateEmail && (
|
||||
<EmailChangeModal
|
||||
show={showUpdateEmail}
|
||||
onClose={() => setShowUpdateEmail(false)}
|
||||
email={userProfile.email}
|
||||
/>
|
||||
)}
|
||||
<EmailChangeModal
|
||||
show={showUpdateEmail}
|
||||
onClose={() => setShowUpdateEmail(false)}
|
||||
email={userProfile.email}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
@ -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 }) => (
|
||||
<div data-testid="dsl-export-confirm-modal">
|
||||
<button type="button" onClick={() => onConfirm(true)}>Export Include</button>
|
||||
<button type="button" onClick={onClose}>Close Export</button>
|
||||
</div>
|
||||
),
|
||||
default: ({ onConfirm, onClose }: { onConfirm: (include?: boolean) => void, onClose: () => void }) => (
|
||||
<div data-testid="dsl-export-confirm-modal">
|
||||
<button type="button" onClick={() => onConfirm(true)}>Export Include</button>
|
||||
|
||||
@ -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(<AppOperations gap={4} operations={[editOp]} />)
|
||||
|
||||
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()
|
||||
|
||||
@ -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<AppSSO>
|
||||
@ -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}
|
||||
/>
|
||||
)}
|
||||
<AlertDialog open={activeModal === 'exportWarning'} onOpenChange={open => !open && closeModal()}>
|
||||
<AlertDialogContent>
|
||||
<div className="flex flex-col gap-2 px-6 pt-6 pb-4">
|
||||
<AlertDialogTitle className="w-full truncate title-2xl-semi-bold text-text-primary">
|
||||
{t('sidebar.exportWarning', { ns: 'workflow' })}
|
||||
</AlertDialogTitle>
|
||||
<AlertDialogDescription className="w-full system-md-regular wrap-break-word whitespace-pre-wrap text-text-tertiary">
|
||||
{t('sidebar.exportWarningDesc', { ns: 'workflow' })}
|
||||
</AlertDialogDescription>
|
||||
</div>
|
||||
<AlertDialogActions>
|
||||
<AlertDialogCancelButton>{t('operation.cancel', { ns: 'common' })}</AlertDialogCancelButton>
|
||||
<AlertDialogConfirmButton
|
||||
tone="default"
|
||||
loading={isConfirmingExport}
|
||||
disabled={isConfirmingExport}
|
||||
onClick={handleExportWarningConfirm}
|
||||
>
|
||||
{isConfirmingExport
|
||||
? t('operation.exporting', { ns: 'common' })
|
||||
: t('operation.confirm', { ns: 'common' })}
|
||||
</AlertDialogConfirmButton>
|
||||
</AlertDialogActions>
|
||||
</AlertDialogContent>
|
||||
<AlertDialog open={isExportDialogOpen} onOpenChange={handleExportDialogOpenChange}>
|
||||
{exportDialogMode === 'secret'
|
||||
? (
|
||||
<DSLExportConfirmContent
|
||||
envList={secretEnvList}
|
||||
onConfirm={onExport}
|
||||
onClose={() => setSecretEnvList([])}
|
||||
onExportingChange={setIsSecretExporting}
|
||||
/>
|
||||
)
|
||||
: exportDialogMode === 'warning' && (
|
||||
<AlertDialogContent>
|
||||
<div className="flex flex-col gap-2 px-6 pt-6 pb-4">
|
||||
<AlertDialogTitle className="w-full truncate title-2xl-semi-bold text-text-primary">
|
||||
{t('sidebar.exportWarning', { ns: 'workflow' })}
|
||||
</AlertDialogTitle>
|
||||
<AlertDialogDescription className="w-full system-md-regular wrap-break-word whitespace-pre-wrap text-text-tertiary">
|
||||
{t('sidebar.exportWarningDesc', { ns: 'workflow' })}
|
||||
</AlertDialogDescription>
|
||||
</div>
|
||||
<AlertDialogActions>
|
||||
<AlertDialogCancelButton>{t('operation.cancel', { ns: 'common' })}</AlertDialogCancelButton>
|
||||
<AlertDialogConfirmButton
|
||||
tone="default"
|
||||
loading={isConfirmingExport}
|
||||
disabled={isConfirmingExport}
|
||||
onClick={handleExportWarningConfirm}
|
||||
>
|
||||
{isConfirmingExport
|
||||
? t('operation.exporting', { ns: 'common' })
|
||||
: t('operation.confirm', { ns: 'common' })}
|
||||
</AlertDialogConfirmButton>
|
||||
</AlertDialogActions>
|
||||
</AlertDialogContent>
|
||||
)}
|
||||
</AlertDialog>
|
||||
{secretEnvList.length > 0 && (
|
||||
<DSLExportConfirmModal
|
||||
envList={secretEnvList}
|
||||
onConfirm={onExport}
|
||||
onClose={() => setSecretEnvList([])}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
@ -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}
|
||||
>
|
||||
<RiMoreLine className="h-3.5 w-3.5 text-components-button-secondary-text" />
|
||||
@ -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 = ({
|
||||
<Button
|
||||
size="small"
|
||||
variant="secondary"
|
||||
className="gap-px"
|
||||
className="gap-px focus-visible:ring-inset"
|
||||
/>
|
||||
)}
|
||||
>
|
||||
|
||||
@ -121,7 +121,7 @@ type BaseItemProps = {
|
||||
}
|
||||
function BaseItem({ icon, onRemove, children }: BaseItemProps) {
|
||||
return (
|
||||
<div className="group flex flex-row items-center gap-x-1 rounded-full border-[0.5px] bg-components-badge-white-to-dark p-1 pr-1.5 shadow-xs">
|
||||
<div className="group flex flex-row items-center gap-x-1 rounded-full border-[0.5px] border-components-panel-border-subtle bg-components-badge-white-to-dark p-1 pr-1.5 shadow-xs">
|
||||
<div className="h-5 w-5 overflow-hidden rounded-full bg-components-icon-bg-blue-solid">
|
||||
<div className="bg-access-app-icon-mask-bg flex h-full w-full items-center justify-center">
|
||||
{icon}
|
||||
|
||||
@ -74,7 +74,7 @@ const AgentSetting: FC<Props> = ({
|
||||
</div>
|
||||
{/* Body */}
|
||||
<div
|
||||
className="grow overflow-y-auto border-b p-6 pt-5 pb-[68px]"
|
||||
className="grow overflow-y-auto border-b border-divider-regular p-6 pt-5 pb-[68px]"
|
||||
style={{
|
||||
borderBottom: 'rgba(0, 0, 0, 0.05)',
|
||||
}}
|
||||
|
||||
@ -174,14 +174,12 @@ const ConfigurationView: FC<ConfigurationViewModel> = ({
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
|
||||
{isShowSelectDataSet && (
|
||||
<SelectDataSet
|
||||
isShow={isShowSelectDataSet}
|
||||
onClose={onCloseSelectDataSet}
|
||||
selectedIds={selectedIds}
|
||||
onSelect={onSelectDataSets}
|
||||
/>
|
||||
)}
|
||||
<SelectDataSet
|
||||
isShow={isShowSelectDataSet}
|
||||
onClose={onCloseSelectDataSet}
|
||||
selectedIds={selectedIds}
|
||||
onSelect={onSelectDataSets}
|
||||
/>
|
||||
|
||||
{isShowHistoryModal && (
|
||||
<EditHistoryModal
|
||||
|
||||
@ -83,7 +83,6 @@ const VarPicker: FC<Props> = ({
|
||||
placement="bottom-end"
|
||||
sideOffset={8}
|
||||
popupClassName="border-none bg-transparent p-0 shadow-none backdrop-blur-none"
|
||||
positionerProps={{ style: { zIndex: 1000 } }}
|
||||
>
|
||||
{options.length > 0
|
||||
? (
|
||||
|
||||
@ -3,14 +3,14 @@ import type { FC } from 'react'
|
||||
import type { DataSet } from '@/models/datasets'
|
||||
import { Button } from '@langgenius/dify-ui/button'
|
||||
import { cn } from '@langgenius/dify-ui/cn'
|
||||
import { Dialog, DialogCloseButton, DialogContent, DialogTitle } from '@langgenius/dify-ui/dialog'
|
||||
import { useInfiniteScroll } from 'ahooks'
|
||||
import * as React from 'react'
|
||||
import { useMemo, useRef, useState } from 'react'
|
||||
import { useCallback, useMemo, useRef, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import AppIcon from '@/app/components/base/app-icon'
|
||||
import Badge from '@/app/components/base/badge'
|
||||
import Loading from '@/app/components/base/loading'
|
||||
import Modal from '@/app/components/base/modal'
|
||||
import { ModelFeatureEnum } from '@/app/components/header/account-setting/model-provider-page/declarations'
|
||||
import FeatureIcon from '@/app/components/header/account-setting/model-provider-page/model-selector/feature-icon'
|
||||
import { useKnowledge } from '@/hooks/use-knowledge'
|
||||
@ -79,100 +79,103 @@ const SelectDataSet: FC<ISelectDataSetProps> = ({
|
||||
onSelect(selected)
|
||||
}
|
||||
|
||||
const handleClose = useCallback(() => {
|
||||
setSelectedIdsInModal(selectedIds)
|
||||
onClose()
|
||||
}, [onClose, selectedIds])
|
||||
|
||||
const handleOpenChange = useCallback((open: boolean) => {
|
||||
if (!open)
|
||||
handleClose()
|
||||
}, [handleClose])
|
||||
|
||||
return (
|
||||
<Modal
|
||||
isShow={isShow}
|
||||
onClose={onClose}
|
||||
className="w-[400px]"
|
||||
title={t('feature.dataSet.selectTitle', { ns: 'appDebug' })}
|
||||
>
|
||||
{(isLoading && datasets.length === 0) && (
|
||||
<div className="flex h-[200px]">
|
||||
<Loading type="area" />
|
||||
</div>
|
||||
)}
|
||||
<Dialog open={isShow} onOpenChange={handleOpenChange}>
|
||||
<DialogContent className="w-100 overflow-hidden">
|
||||
<DialogTitle className="title-2xl-semi-bold text-text-primary">
|
||||
{t('feature.dataSet.selectTitle', { ns: 'appDebug' })}
|
||||
</DialogTitle>
|
||||
<DialogCloseButton aria-label={t('operation.close', { ns: 'common' })} />
|
||||
{(isLoading && datasets.length === 0) && (
|
||||
<div className="flex h-50">
|
||||
<Loading type="area" />
|
||||
</div>
|
||||
)}
|
||||
|
||||
{hasNoData && (
|
||||
<div
|
||||
className="mt-6 flex h-[128px] items-center justify-center space-x-1 rounded-lg border text-[13px]"
|
||||
style={{
|
||||
background: 'rgba(0, 0, 0, 0.02)',
|
||||
borderColor: 'rgba(0, 0, 0, 0.02',
|
||||
}}
|
||||
>
|
||||
<span className="text-text-tertiary">{t('feature.dataSet.noDataSet', { ns: 'appDebug' })}</span>
|
||||
<Link href="/datasets/create" className="font-normal text-text-accent">{t('feature.dataSet.toCreate', { ns: 'appDebug' })}</Link>
|
||||
</div>
|
||||
)}
|
||||
{hasNoData && (
|
||||
<div className="mt-6 flex h-32 items-center justify-center space-x-1 rounded-lg border border-divider-subtle bg-components-panel-on-panel-item-bg text-[13px]">
|
||||
<span className="text-text-tertiary">{t('feature.dataSet.noDataSet', { ns: 'appDebug' })}</span>
|
||||
<Link href="/datasets/create" className="font-normal text-text-accent">{t('feature.dataSet.toCreate', { ns: 'appDebug' })}</Link>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{datasets.length > 0 && (
|
||||
<>
|
||||
<div ref={listRef} className="mt-7 max-h-[286px] space-y-1 overflow-y-auto">
|
||||
{datasets.map(item => (
|
||||
<div
|
||||
key={item.id}
|
||||
className={cn(
|
||||
'flex h-10 cursor-pointer items-center rounded-lg border-[0.5px] border-components-panel-border-subtle bg-components-panel-on-panel-item-bg px-2 shadow-xs hover:border-components-panel-border hover:bg-components-panel-on-panel-item-bg-hover hover:shadow-sm',
|
||||
selectedIdsInModal.includes(item.id) && 'border-[1.5px] border-components-option-card-option-selected-border bg-state-accent-hover shadow-xs hover:border-components-option-card-option-selected-border hover:bg-state-accent-hover hover:shadow-xs',
|
||||
!item.embedding_available && 'hover:border-components-panel-border-subtle hover:bg-components-panel-on-panel-item-bg hover:shadow-xs',
|
||||
)}
|
||||
onClick={() => {
|
||||
if (!item.embedding_available)
|
||||
return
|
||||
toggleSelect(item)
|
||||
}}
|
||||
>
|
||||
<div className="mr-1 flex grow items-center overflow-hidden">
|
||||
<div className={cn('mr-2', !item.embedding_available && 'opacity-30')}>
|
||||
<AppIcon
|
||||
size="tiny"
|
||||
iconType={item.icon_info.icon_type}
|
||||
icon={item.icon_info.icon}
|
||||
background={item.icon_info.icon_type === 'image' ? undefined : item.icon_info.icon_background}
|
||||
imageUrl={item.icon_info.icon_type === 'image' ? item.icon_info.icon_url : undefined}
|
||||
/>
|
||||
</div>
|
||||
<div className={cn('max-w-[200px] truncate text-[13px] font-medium text-text-secondary', !item.embedding_available && 'max-w-[120px]! opacity-30')}>{item.name}</div>
|
||||
{!item.embedding_available && (
|
||||
<span className="ml-1 shrink-0 rounded-md border border-divider-deep px-1 text-xs leading-[18px] font-normal text-text-tertiary">{t('unavailable', { ns: 'dataset' })}</span>
|
||||
{datasets.length > 0 && (
|
||||
<>
|
||||
<div ref={listRef} className="mt-7 max-h-71.5 space-y-1 overflow-y-auto">
|
||||
{datasets.map(item => (
|
||||
<button
|
||||
key={item.id}
|
||||
type="button"
|
||||
disabled={!item.embedding_available}
|
||||
className={cn(
|
||||
'flex h-10 w-full cursor-pointer items-center rounded-lg border-[0.5px] border-components-panel-border-subtle bg-components-panel-on-panel-item-bg px-2 text-left shadow-xs outline-hidden hover:border-components-panel-border hover:bg-components-panel-on-panel-item-bg-hover hover:shadow-sm focus-visible:ring-2 focus-visible:ring-state-accent-solid',
|
||||
selectedIdsInModal.includes(item.id) && 'border-[1.5px] border-components-option-card-option-selected-border bg-state-accent-hover shadow-xs hover:border-components-option-card-option-selected-border hover:bg-state-accent-hover hover:shadow-xs',
|
||||
!item.embedding_available && 'cursor-default hover:border-components-panel-border-subtle hover:bg-components-panel-on-panel-item-bg hover:shadow-xs',
|
||||
)}
|
||||
</div>
|
||||
{item.is_multimodal && (
|
||||
<div className="mr-1 shrink-0">
|
||||
<FeatureIcon feature={ModelFeatureEnum.vision} />
|
||||
onClick={() => toggleSelect(item)}
|
||||
>
|
||||
<div className="mr-1 flex grow items-center overflow-hidden">
|
||||
<div className={cn('mr-2', !item.embedding_available && 'opacity-30')}>
|
||||
<AppIcon
|
||||
size="tiny"
|
||||
iconType={item.icon_info.icon_type}
|
||||
icon={item.icon_info.icon}
|
||||
background={item.icon_info.icon_type === 'image' ? undefined : item.icon_info.icon_background}
|
||||
imageUrl={item.icon_info.icon_type === 'image' ? item.icon_info.icon_url : undefined}
|
||||
/>
|
||||
</div>
|
||||
<div className={cn('max-w-50 truncate text-[13px] font-medium text-text-secondary', !item.embedding_available && 'max-w-30! opacity-30')}>{item.name}</div>
|
||||
{!item.embedding_available && (
|
||||
<span className="ml-1 shrink-0 rounded-md border border-divider-deep px-1 text-xs leading-[18px] font-normal text-text-tertiary">{t('unavailable', { ns: 'dataset' })}</span>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
{
|
||||
!!item.indexing_technique && (
|
||||
<Badge
|
||||
className="shrink-0"
|
||||
text={formatIndexingTechniqueAndMethod(item.indexing_technique, item.retrieval_model_dict?.search_method)}
|
||||
/>
|
||||
)
|
||||
}
|
||||
{
|
||||
item.provider === 'external' && (
|
||||
<Badge className="shrink-0" text={t('externalTag', { ns: 'dataset' })} />
|
||||
)
|
||||
}
|
||||
</div>
|
||||
))}
|
||||
{isFetchingNextPage && <Loading />}
|
||||
{item.is_multimodal && (
|
||||
<div className="mr-1 shrink-0">
|
||||
<FeatureIcon feature={ModelFeatureEnum.vision} />
|
||||
</div>
|
||||
)}
|
||||
{
|
||||
!!item.indexing_technique && (
|
||||
<Badge
|
||||
className="shrink-0"
|
||||
text={formatIndexingTechniqueAndMethod(item.indexing_technique, item.retrieval_model_dict?.search_method)}
|
||||
/>
|
||||
)
|
||||
}
|
||||
{
|
||||
item.provider === 'external' && (
|
||||
<Badge className="shrink-0" text={t('externalTag', { ns: 'dataset' })} />
|
||||
)
|
||||
}
|
||||
</button>
|
||||
))}
|
||||
{isFetchingNextPage && <Loading />}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
{!isLoading && (
|
||||
<div className="mt-8 flex items-center justify-between">
|
||||
<div className="text-sm font-medium text-text-secondary">
|
||||
{selected.length > 0 && `${selected.length} ${t('feature.dataSet.selected', { ns: 'appDebug' })}`}
|
||||
</div>
|
||||
<div className="flex space-x-2">
|
||||
<Button onClick={handleClose}>{t('operation.cancel', { ns: 'common' })}</Button>
|
||||
<Button variant="primary" onClick={handleSelect} disabled={hasNoData}>{t('operation.add', { ns: 'common' })}</Button>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
{!isLoading && (
|
||||
<div className="mt-8 flex items-center justify-between">
|
||||
<div className="text-sm font-medium text-text-secondary">
|
||||
{selected.length > 0 && `${selected.length} ${t('feature.dataSet.selected', { ns: 'appDebug' })}`}
|
||||
</div>
|
||||
<div className="flex space-x-2">
|
||||
<Button onClick={onClose}>{t('operation.cancel', { ns: 'common' })}</Button>
|
||||
<Button variant="primary" onClick={handleSelect} disabled={hasNoData}>{t('operation.add', { ns: 'common' })}</Button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</Modal>
|
||||
)}
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
)
|
||||
}
|
||||
export default React.memo(SelectDataSet)
|
||||
|
||||
@ -113,7 +113,7 @@ const ChatUserInput = ({
|
||||
<SelectTrigger className="w-full">
|
||||
{String(inputs[key] || t('placeholder.select', { ns: 'common' }))}
|
||||
</SelectTrigger>
|
||||
<SelectContent popupClassName="w-(--anchor-width)">
|
||||
<SelectContent>
|
||||
{(options || []).map(option => (
|
||||
<SelectItem key={option} value={option}>
|
||||
<SelectItemText>{option}</SelectItemText>
|
||||
|
||||
@ -167,7 +167,7 @@ const PromptValuePanel: FC<IPromptValuePanelProps> = ({
|
||||
<SelectTrigger className="w-full bg-gray-50">
|
||||
{String(inputs[key] || t('placeholder.select', { ns: 'common' }))}
|
||||
</SelectTrigger>
|
||||
<SelectContent popupClassName="w-(--anchor-width)">
|
||||
<SelectContent>
|
||||
{(options || []).map(option => (
|
||||
<SelectItem key={option} value={option}>
|
||||
<SelectItemText>{option}</SelectItemText>
|
||||
|
||||
@ -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 () => {
|
||||
|
||||
@ -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 (
|
||||
<svg width="18" height="18" viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg" className={className}>
|
||||
<svg aria-hidden="true" width="18" height="18" viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg" className={className}>
|
||||
<path d="M5.80078 13.7109C5.80078 13.6406 5.73047 13.5703 5.625 13.5703C5.51953 13.5703 5.44922 13.6406 5.44922 13.7109C5.44922 13.7812 5.51953 13.8516 5.625 13.8164C5.73047 13.8164 5.80078 13.7812 5.80078 13.7109ZM4.71094 13.5352C4.71094 13.6055 4.78125 13.7109 4.88672 13.7109C4.95703 13.7461 5.0625 13.7109 5.09766 13.6406C5.09766 13.5703 5.0625 13.5 4.95703 13.4648C4.85156 13.4297 4.74609 13.4648 4.71094 13.5352ZM6.29297 13.5C6.1875 13.5 6.11719 13.5703 6.11719 13.6758C6.11719 13.7461 6.22266 13.7812 6.32812 13.7461C6.43359 13.7109 6.50391 13.6758 6.46875 13.6055C6.46875 13.5352 6.36328 13.4648 6.29297 13.5ZM8.57812 0C3.72656 0 0 3.72656 0 8.57812C0 12.4805 2.42578 15.8203 5.94141 17.0156C6.39844 17.0859 6.53906 16.8047 6.53906 16.5938C6.53906 16.3477 6.53906 15.1523 6.53906 14.4141C6.53906 14.4141 4.07812 14.9414 3.55078 13.3594C3.55078 13.3594 3.16406 12.3398 2.60156 12.0938C2.60156 12.0938 1.79297 11.5312 2.63672 11.5312C2.63672 11.5312 3.51562 11.6016 4.00781 12.4453C4.78125 13.8164 6.04688 13.4297 6.57422 13.1836C6.64453 12.6211 6.85547 12.2344 7.13672 11.9883C5.16797 11.7773 3.16406 11.4961 3.16406 8.12109C3.16406 7.13672 3.44531 6.67969 4.00781 6.04688C3.90234 5.80078 3.62109 4.88672 4.11328 3.65625C4.81641 3.44531 6.53906 4.60547 6.53906 4.60547C7.24219 4.39453 7.98047 4.32422 8.71875 4.32422C9.49219 4.32422 10.2305 4.39453 10.9336 4.60547C10.9336 4.60547 12.6211 3.41016 13.3594 3.65625C13.8516 4.88672 13.5352 5.80078 13.4648 6.04688C14.0273 6.67969 14.3789 7.13672 14.3789 8.12109C14.3789 11.4961 12.3047 11.7773 10.3359 11.9883C10.6523 12.2695 10.9336 12.7969 10.9336 13.6406C10.9336 14.8008 10.8984 16.2773 10.8984 16.5586C10.8984 16.8047 11.0742 17.0859 11.5312 16.9805C15.0469 15.8203 17.4375 12.4805 17.4375 8.57812C17.4375 3.72656 13.4648 0 8.57812 0ZM3.41016 12.1289C3.33984 12.1641 3.375 12.2695 3.41016 12.3398C3.48047 12.375 3.55078 12.4102 3.62109 12.375C3.65625 12.3398 3.65625 12.2344 3.58594 12.1641C3.51562 12.1289 3.44531 12.0938 3.41016 12.1289ZM3.02344 11.8477C2.98828 11.918 3.02344 11.9531 3.09375 11.9883C3.16406 12.0234 3.23438 12.0234 3.26953 11.9531C3.26953 11.918 3.23438 11.8828 3.16406 11.8477C3.09375 11.8125 3.05859 11.8125 3.02344 11.8477ZM4.14844 13.1133C4.11328 13.1484 4.11328 13.2539 4.21875 13.3242C4.28906 13.3945 4.39453 13.4297 4.42969 13.3594C4.46484 13.3242 4.46484 13.2188 4.39453 13.1484C4.32422 13.0781 4.21875 13.043 4.14844 13.1133ZM3.76172 12.5859C3.69141 12.6211 3.69141 12.7266 3.76172 12.7969C3.83203 12.8672 3.90234 12.9023 3.97266 12.8672C4.00781 12.832 4.00781 12.7266 3.97266 12.6562C3.90234 12.5859 3.83203 12.5508 3.76172 12.5859Z" fill="#1F2A37" />
|
||||
</svg>
|
||||
)
|
||||
@ -43,90 +42,85 @@ const CustomizeModal: FC<IShareLinkProps> = ({
|
||||
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 (
|
||||
<Modal
|
||||
title={t(`${prefixCustomize}.title`, { ns: 'appOverview' })}
|
||||
description={t(`${prefixCustomize}.explanation`, { ns: 'appOverview' })}
|
||||
isShow={isShow}
|
||||
onClose={onClose}
|
||||
className="w-[640px] max-w-2xl!"
|
||||
closable={true}
|
||||
>
|
||||
<div className="mt-4 w-full rounded-lg border-[0.5px] border-components-panel-border px-6 py-5">
|
||||
<Tag bordered={true} hideBg={true} className="border-text-accent-secondary text-text-accent-secondary uppercase">
|
||||
{t(`${prefixCustomize}.way`, { ns: 'appOverview' })}
|
||||
{' '}
|
||||
1
|
||||
</Tag>
|
||||
<p className="my-2 system-sm-medium text-text-secondary">{t(`${prefixCustomize}.way1.name`, { ns: 'appOverview' })}</p>
|
||||
<div className="flex py-4">
|
||||
<StepNum>1</StepNum>
|
||||
<div className="flex flex-col">
|
||||
<div className="text-text-primary">{t(`${prefixCustomize}.way1.step1`, { ns: 'appOverview' })}</div>
|
||||
<div className="mt-1 mb-2 text-xs text-text-tertiary">{t(`${prefixCustomize}.way1.step1Tip`, { ns: 'appOverview' })}</div>
|
||||
<a href={`https://github.com/langgenius/${isChatApp ? 'webapp-conversation' : 'webapp-text-generator'}`} target="_blank" rel="noopener noreferrer">
|
||||
<Button>
|
||||
<Dialog open={isShow} onOpenChange={open => !open && onClose()}>
|
||||
<DialogContent className="max-h-[calc(100dvh-2rem)] w-[640px] overflow-visible">
|
||||
<DialogTitle className="title-2xl-semi-bold text-text-primary">
|
||||
{t(`${prefixCustomize}.title`, { ns: 'appOverview' })}
|
||||
</DialogTitle>
|
||||
<DialogDescription className="mt-2 body-md-regular text-text-secondary">
|
||||
{t(`${prefixCustomize}.explanation`, { ns: 'appOverview' })}
|
||||
</DialogDescription>
|
||||
<DialogCloseButton data-testid="modal-close-button" />
|
||||
<div className="mt-4 w-full rounded-lg border-[0.5px] border-components-panel-border px-6 py-5">
|
||||
<Tag bordered={true} hideBg={true} className="border-text-accent-secondary text-text-accent-secondary uppercase">
|
||||
{t(`${prefixCustomize}.way`, { ns: 'appOverview' })}
|
||||
{' '}
|
||||
1
|
||||
</Tag>
|
||||
<p className="my-2 system-sm-medium text-text-secondary">{t(`${prefixCustomize}.way1.name`, { ns: 'appOverview' })}</p>
|
||||
<div className="flex py-4">
|
||||
<StepNum>1</StepNum>
|
||||
<div className="flex flex-col">
|
||||
<div className="text-text-primary">{t(`${prefixCustomize}.way1.step1`, { ns: 'appOverview' })}</div>
|
||||
<div className="mt-1 mb-2 text-xs text-text-tertiary">{t(`${prefixCustomize}.way1.step1Tip`, { ns: 'appOverview' })}</div>
|
||||
<Button render={<a href={`https://github.com/langgenius/${isChatApp ? 'webapp-conversation' : 'webapp-text-generator'}`} target="_blank" rel="noopener noreferrer" />}>
|
||||
<GithubIcon className="mr-2 text-text-secondary" />
|
||||
{t(`${prefixCustomize}.way1.step1Operation`, { ns: 'appOverview' })}
|
||||
</Button>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex pt-4">
|
||||
<StepNum>2</StepNum>
|
||||
<div className="flex flex-col">
|
||||
<div className="text-text-primary">{t(`${prefixCustomize}.way1.step2`, { ns: 'appOverview' })}</div>
|
||||
<div className="mt-1 mb-2 text-xs text-text-tertiary">{t(`${prefixCustomize}.way1.step2Tip`, { ns: 'appOverview' })}</div>
|
||||
<a href="https://vercel.com/docs/concepts/deployments/git/vercel-for-github" target="_blank" rel="noopener noreferrer">
|
||||
<Button>
|
||||
<div className="flex pt-4">
|
||||
<StepNum>2</StepNum>
|
||||
<div className="flex flex-col">
|
||||
<div className="text-text-primary">{t(`${prefixCustomize}.way1.step2`, { ns: 'appOverview' })}</div>
|
||||
<div className="mt-1 mb-2 text-xs text-text-tertiary">{t(`${prefixCustomize}.way1.step2Tip`, { ns: 'appOverview' })}</div>
|
||||
<Button render={<a href="https://vercel.com/docs/concepts/deployments/git/vercel-for-github" target="_blank" rel="noopener noreferrer" />}>
|
||||
<div className="mr-1.5 border-t-0 border-r-[7px] border-b-12 border-l-[7px] border-solid border-text-primary border-t-transparent border-r-transparent border-l-transparent"></div>
|
||||
<span>{t(`${prefixCustomize}.way1.step2Operation`, { ns: 'appOverview' })}</span>
|
||||
</Button>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex py-4">
|
||||
<StepNum>3</StepNum>
|
||||
<div className="flex w-full flex-col overflow-hidden">
|
||||
<div className="text-text-primary">{t(`${prefixCustomize}.way1.step3`, { ns: 'appOverview' })}</div>
|
||||
<div className="mt-1 mb-2 text-xs text-text-tertiary">{t(`${prefixCustomize}.way1.step3Tip`, { ns: 'appOverview' })}</div>
|
||||
<pre className="box-border overflow-x-scroll rounded-lg border-[0.5px] border-components-panel-border bg-background-section px-4 py-3 text-xs font-medium text-text-secondary select-text">
|
||||
NEXT_PUBLIC_APP_ID=
|
||||
{`'${appId}'`}
|
||||
{' '}
|
||||
<br />
|
||||
NEXT_PUBLIC_APP_KEY=
|
||||
{'\'<Web API Key From Dify>\''}
|
||||
{' '}
|
||||
<br />
|
||||
NEXT_PUBLIC_API_URL=
|
||||
{`'${api_base_url}'`}
|
||||
</pre>
|
||||
<div className="flex py-4">
|
||||
<StepNum>3</StepNum>
|
||||
<div className="flex w-full flex-col overflow-hidden">
|
||||
<div className="text-text-primary">{t(`${prefixCustomize}.way1.step3`, { ns: 'appOverview' })}</div>
|
||||
<div className="mt-1 mb-2 text-xs text-text-tertiary">{t(`${prefixCustomize}.way1.step3Tip`, { ns: 'appOverview' })}</div>
|
||||
<pre className="box-border overflow-x-scroll rounded-lg border-[0.5px] border-components-panel-border bg-background-section px-4 py-3 text-xs font-medium text-text-secondary select-text">
|
||||
NEXT_PUBLIC_APP_ID=
|
||||
{`'${appId}'`}
|
||||
{' '}
|
||||
<br />
|
||||
NEXT_PUBLIC_APP_KEY=
|
||||
{'\'<Web API Key From Dify>\''}
|
||||
{' '}
|
||||
<br />
|
||||
NEXT_PUBLIC_API_URL=
|
||||
{`'${api_base_url}'`}
|
||||
</pre>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<div className="mt-4 w-full rounded-lg border-[0.5px] border-components-panel-border px-6 py-5">
|
||||
<Tag bordered={true} hideBg={true} className="border-text-accent-secondary text-text-accent-secondary uppercase">
|
||||
{t(`${prefixCustomize}.way`, { ns: 'appOverview' })}
|
||||
{' '}
|
||||
2
|
||||
</Tag>
|
||||
<p className="my-2 system-sm-medium text-text-secondary">{t(`${prefixCustomize}.way2.name`, { ns: 'appOverview' })}</p>
|
||||
<Button
|
||||
className="mt-2"
|
||||
onClick={() =>
|
||||
window.open(
|
||||
docLink('/use-dify/publish/developing-with-apis'),
|
||||
'_blank',
|
||||
)}
|
||||
>
|
||||
<span className="text-sm text-text-secondary">{t(`${prefixCustomize}.way2.operation`, { ns: 'appOverview' })}</span>
|
||||
<ArrowTopRightOnSquareIcon className="ml-1 h-4 w-4 shrink-0 text-text-secondary" />
|
||||
</Button>
|
||||
</div>
|
||||
</Modal>
|
||||
</div>
|
||||
<div className="mt-4 w-full rounded-lg border-[0.5px] border-components-panel-border px-6 py-5">
|
||||
<Tag bordered={true} hideBg={true} className="border-text-accent-secondary text-text-accent-secondary uppercase">
|
||||
{t(`${prefixCustomize}.way`, { ns: 'appOverview' })}
|
||||
{' '}
|
||||
2
|
||||
</Tag>
|
||||
<p className="my-2 system-sm-medium text-text-secondary">{t(`${prefixCustomize}.way2.name`, { ns: 'appOverview' })}</p>
|
||||
<Button
|
||||
render={<a href={apiDocLink} target="_blank" rel="noopener noreferrer" />}
|
||||
className="mt-2"
|
||||
>
|
||||
<span className="text-sm text-text-secondary">{t(`${prefixCustomize}.way2.operation`, { ns: 'appOverview' })}</span>
|
||||
<span aria-hidden="true" className="ml-1 i-heroicons-arrow-top-right-on-square h-4 w-4 shrink-0 text-text-secondary" />
|
||||
</Button>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@ -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<Option, string> = {
|
||||
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<Option>('iframe')
|
||||
const [isCopied, setIsCopied] = useState<OptionStatus>({ iframe: false, scripts: false, chromePlugin: false })
|
||||
const [copiedOption, setCopiedOption] = useState<Option | null>(null)
|
||||
|
||||
const { langGeniusVersionInfo } = useAppContext()
|
||||
const themeBuilder = useThemeContext()
|
||||
@ -110,97 +108,98 @@ const Embedded = ({ siteInfo, isShow, onClose, appBaseUrl, accessToken, classNam
|
||||
else {
|
||||
copy(OPTION_MAP[option].getContent(appBaseUrl, accessToken, themeBuilder.theme?.primaryColor ?? '#1C64F2', isTestEnv))
|
||||
}
|
||||
setIsCopied({ ...isCopied, [option]: true })
|
||||
}
|
||||
|
||||
// when toggle option, reset then copy status
|
||||
const resetCopyStatus = () => {
|
||||
const cache = { ...isCopied }
|
||||
Object.keys(cache).forEach((key) => {
|
||||
cache[key as keyof OptionStatus] = false
|
||||
})
|
||||
setIsCopied(cache)
|
||||
setCopiedOption(option)
|
||||
}
|
||||
|
||||
const navigateToChromeUrl = () => {
|
||||
window.open('https://chrome.google.com/webstore/detail/dify-chatbot/ceehdapohffmjmkdcifjofadiaoeggaf', '_blank', 'noopener,noreferrer')
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
resetCopyStatus()
|
||||
}, [isShow])
|
||||
|
||||
return (
|
||||
<Modal
|
||||
title={t(`${prefixEmbedded}.title`, { ns: 'appOverview' })}
|
||||
isShow={isShow}
|
||||
onClose={onClose}
|
||||
className="w-[640px] max-w-2xl!"
|
||||
wrapperClassName={className}
|
||||
closable={true}
|
||||
<Dialog
|
||||
open={isShow}
|
||||
onOpenChange={(open) => {
|
||||
if (open)
|
||||
return
|
||||
setCopiedOption(null)
|
||||
onClose()
|
||||
}}
|
||||
>
|
||||
<div className="mt-8 mb-4 system-sm-medium text-text-primary">
|
||||
{t(`${prefixEmbedded}.explanation`, { ns: 'appOverview' })}
|
||||
</div>
|
||||
<div className="flex flex-wrap items-center justify-between gap-y-2">
|
||||
{Object.keys(OPTION_MAP).map((v, index) => {
|
||||
return (
|
||||
<div
|
||||
key={index}
|
||||
className={cn(
|
||||
style.option,
|
||||
style[`${v}Icon`],
|
||||
option === v && style.active,
|
||||
)}
|
||||
onClick={() => {
|
||||
setOption(v as Option)
|
||||
resetCopyStatus()
|
||||
}}
|
||||
>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
{option === 'chromePlugin' && (
|
||||
<div className="mt-6 w-full">
|
||||
<div className={cn('inline-flex w-full items-center justify-center gap-2 rounded-lg py-3', 'shrink-0 cursor-pointer bg-primary-600 text-white hover:bg-primary-600/75 hover:shadow-sm')}>
|
||||
<div className={`relative h-4 w-4 ${style.pluginInstallIcon}`}></div>
|
||||
<div className="font-['Inter'] text-sm leading-tight font-medium text-white" onClick={navigateToChromeUrl}>{t(`${prefixEmbedded}.chromePlugin`, { ns: 'appOverview' })}</div>
|
||||
</div>
|
||||
<DialogContent className={cn('max-h-[calc(100dvh-2rem)] w-[640px] overflow-visible', className)}>
|
||||
<DialogTitle className="title-2xl-semi-bold text-text-primary">
|
||||
{t(`${prefixEmbedded}.title`, { ns: 'appOverview' })}
|
||||
</DialogTitle>
|
||||
<DialogCloseButton />
|
||||
<div className="mt-8 mb-4 system-sm-medium text-text-primary">
|
||||
{t(`${prefixEmbedded}.explanation`, { ns: 'appOverview' })}
|
||||
</div>
|
||||
)}
|
||||
<div className={cn('inline-flex w-full flex-col items-start justify-start rounded-lg border-[0.5px] border-components-panel-border bg-background-section', 'mt-6')}>
|
||||
<div className="inline-flex items-center justify-start gap-2 self-stretch rounded-t-lg bg-background-section-burn py-1 pr-1 pl-3">
|
||||
<div className="shrink-0 grow system-sm-medium text-text-secondary">
|
||||
{t(`${prefixEmbedded}.${option}`, { ns: 'appOverview' })}
|
||||
<div className="flex flex-wrap items-center justify-between gap-y-2">
|
||||
{OPTIONS.map((v) => {
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
key={v}
|
||||
aria-label={t(`${prefixEmbedded}.${v}`, { ns: 'appOverview' }) || v}
|
||||
className={cn(
|
||||
style.option,
|
||||
optionIconClassName[v],
|
||||
option === v && style.active,
|
||||
)}
|
||||
onClick={() => {
|
||||
setOption(v)
|
||||
setCopiedOption(null)
|
||||
}}
|
||||
>
|
||||
</button>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
{option === 'chromePlugin' && (
|
||||
<div className="mt-6 w-full">
|
||||
<button
|
||||
type="button"
|
||||
className={cn('inline-flex w-full items-center justify-center gap-2 rounded-lg py-3', 'shrink-0 bg-primary-600 text-white hover:bg-primary-600/75 hover:shadow-sm')}
|
||||
onClick={navigateToChromeUrl}
|
||||
>
|
||||
<div className={`relative h-4 w-4 ${style.pluginInstallIcon}`}></div>
|
||||
<div className="font-['Inter'] text-sm leading-tight font-medium text-white">{t(`${prefixEmbedded}.chromePlugin`, { ns: 'appOverview' })}</div>
|
||||
</button>
|
||||
</div>
|
||||
<Tooltip>
|
||||
<TooltipTrigger
|
||||
render={(
|
||||
<ActionButton>
|
||||
<div
|
||||
)}
|
||||
<div className={cn('inline-flex w-full flex-col items-start justify-start rounded-lg border-[0.5px] border-components-panel-border bg-background-section', 'mt-6')}>
|
||||
<div className="inline-flex items-center justify-start gap-2 self-stretch rounded-t-lg bg-background-section-burn py-1 pr-1 pl-3">
|
||||
<div className="shrink-0 grow system-sm-medium text-text-secondary">
|
||||
{t(`${prefixEmbedded}.${option}`, { ns: 'appOverview' })}
|
||||
</div>
|
||||
<Tooltip>
|
||||
<TooltipTrigger
|
||||
render={(
|
||||
<ActionButton
|
||||
aria-label={(copiedOption === option
|
||||
? t(`${prefixEmbedded}.copied`, { ns: 'appOverview' })
|
||||
: t(`${prefixEmbedded}.copy`, { ns: 'appOverview' })) || ''}
|
||||
onClick={onClickCopy}
|
||||
>
|
||||
{isCopied[option] && <RiClipboardFill className="h-4 w-4" />}
|
||||
{!isCopied[option] && <RiClipboardLine className="h-4 w-4" />}
|
||||
</div>
|
||||
</ActionButton>
|
||||
)}
|
||||
/>
|
||||
<TooltipContent>
|
||||
{(isCopied[option]
|
||||
? t(`${prefixEmbedded}.copied`, { ns: 'appOverview' })
|
||||
: t(`${prefixEmbedded}.copy`, { ns: 'appOverview' })) || ''}
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</div>
|
||||
<div className="flex w-full items-start justify-start gap-2 overflow-x-auto p-3">
|
||||
<div className="shrink grow basis-0 font-mono text-[13px] leading-tight text-text-secondary">
|
||||
<pre className="select-text">{OPTION_MAP[option].getContent(appBaseUrl, accessToken, themeBuilder.theme?.primaryColor ?? '#1C64F2', isTestEnv)}</pre>
|
||||
{copiedOption === option && <span aria-hidden="true" className="i-ri-clipboard-fill h-4 w-4" />}
|
||||
{copiedOption !== option && <span aria-hidden="true" className="i-ri-clipboard-line h-4 w-4" />}
|
||||
</ActionButton>
|
||||
)}
|
||||
/>
|
||||
<TooltipContent>
|
||||
{(copiedOption === option
|
||||
? t(`${prefixEmbedded}.copied`, { ns: 'appOverview' })
|
||||
: t(`${prefixEmbedded}.copy`, { ns: 'appOverview' })) || ''}
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</div>
|
||||
<div className="flex max-h-[clamp(180px,calc(100dvh-320px),360px)] w-full items-start justify-start gap-2 overflow-auto p-3">
|
||||
<div className="shrink grow basis-0 font-mono text-[13px] leading-tight text-text-secondary">
|
||||
<pre className="select-text">{OPTION_MAP[option].getContent(appBaseUrl, accessToken, themeBuilder.theme?.primaryColor ?? '#1C64F2', isTestEnv)}</pre>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@ -3,7 +3,7 @@ import type { ModalContextState } from '@/context/modal-context'
|
||||
import type { ProviderContextState } from '@/context/provider-context'
|
||||
import type { AppDetailResponse } from '@/models/app'
|
||||
import type { AppSSO } from '@/types/app'
|
||||
import { act, fireEvent, render, screen, waitFor } from '@testing-library/react'
|
||||
import { fireEvent, render, screen, waitFor } from '@testing-library/react'
|
||||
import { Plan } from '@/app/components/billing/type'
|
||||
import { baseProviderContextValue } from '@/context/provider-context'
|
||||
import { AppModeEnum } from '@/types/app'
|
||||
@ -224,54 +224,51 @@ describe('SettingsModal', () => {
|
||||
expect(mockOnClose).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should clear the delayed hide-more timer when the modal unmounts after closing', () => {
|
||||
vi.useFakeTimers()
|
||||
const clearTimeoutSpy = vi.spyOn(globalThis, 'clearTimeout')
|
||||
const { unmount } = renderSettingsModal()
|
||||
|
||||
fireEvent.click(screen.getByText('appOverview.overview.appInfo.settings.more.entry'))
|
||||
fireEvent.click(screen.getByText('common.operation.cancel'))
|
||||
unmount()
|
||||
|
||||
expect(clearTimeoutSpy).toHaveBeenCalled()
|
||||
vi.runAllTimers()
|
||||
})
|
||||
|
||||
it('should replace the pending hide-more timer and clear the ref after the timeout completes', async () => {
|
||||
const hideCallbacks: Array<() => void> = []
|
||||
const originalSetTimeout = globalThis.setTimeout
|
||||
const setTimeoutSpy = vi.spyOn(globalThis, 'setTimeout').mockImplementation(((
|
||||
callback: TimerHandler,
|
||||
delay?: number,
|
||||
...args: unknown[]
|
||||
) => {
|
||||
if (delay === 200) {
|
||||
hideCallbacks.push(() => {
|
||||
if (typeof callback === 'function')
|
||||
callback(...args)
|
||||
})
|
||||
return hideCallbacks.length as unknown as ReturnType<typeof setTimeout>
|
||||
}
|
||||
|
||||
return originalSetTimeout(callback, delay, ...args)
|
||||
}) as unknown as typeof setTimeout)
|
||||
const clearTimeoutSpy = vi.spyOn(globalThis, 'clearTimeout')
|
||||
it('should collapse the expanded settings section immediately when closing', () => {
|
||||
renderSettingsModal()
|
||||
|
||||
act(() => {
|
||||
fireEvent.click(screen.getByText('common.operation.cancel'))
|
||||
fireEvent.click(screen.getByText('common.operation.cancel'))
|
||||
})
|
||||
fireEvent.click(screen.getByText('appOverview.overview.appInfo.settings.more.entry'))
|
||||
expect(screen.getByPlaceholderText('appOverview.overview.appInfo.settings.more.privacyPolicyPlaceholder')).toBeInTheDocument()
|
||||
|
||||
expect(clearTimeoutSpy).toHaveBeenCalled()
|
||||
expect(hideCallbacks.length).toBeGreaterThanOrEqual(2)
|
||||
fireEvent.click(screen.getByText('common.operation.cancel'))
|
||||
|
||||
act(() => {
|
||||
hideCallbacks.at(-1)?.()
|
||||
})
|
||||
expect(screen.getByText('appOverview.overview.appInfo.settings.more.entry')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
setTimeoutSpy.mockRestore()
|
||||
clearTimeoutSpy.mockRestore()
|
||||
it('should reset local form state when the controlled dialog reopens', () => {
|
||||
const { rerender } = render(
|
||||
<SettingsModal
|
||||
isChat
|
||||
isShow={true}
|
||||
appInfo={mockAppInfo}
|
||||
onClose={mockOnClose}
|
||||
onSave={mockOnSave}
|
||||
/>,
|
||||
)
|
||||
|
||||
fireEvent.click(screen.getByText('appOverview.overview.appInfo.settings.more.entry'))
|
||||
expect(screen.getByPlaceholderText('appOverview.overview.appInfo.settings.more.privacyPolicyPlaceholder')).toBeInTheDocument()
|
||||
|
||||
rerender(
|
||||
<SettingsModal
|
||||
isChat
|
||||
isShow={false}
|
||||
appInfo={mockAppInfo}
|
||||
onClose={mockOnClose}
|
||||
onSave={mockOnSave}
|
||||
/>,
|
||||
)
|
||||
rerender(
|
||||
<SettingsModal
|
||||
isChat
|
||||
isShow={true}
|
||||
appInfo={mockAppInfo}
|
||||
onClose={mockOnClose}
|
||||
onSave={mockOnSave}
|
||||
/>,
|
||||
)
|
||||
|
||||
expect(screen.getByText('appOverview.overview.appInfo.settings.more.entry')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should open the pricing modal from the copyright upgrade badge for sandbox plans', async () => {
|
||||
|
||||
@ -5,23 +5,20 @@ import type { AppDetailResponse } from '@/models/app'
|
||||
import type { AppIconType, AppSSO, Language } from '@/types/app'
|
||||
import { Button } from '@langgenius/dify-ui/button'
|
||||
import { cn } from '@langgenius/dify-ui/cn'
|
||||
import { Dialog, DialogCloseButton, DialogContent, DialogTitle } from '@langgenius/dify-ui/dialog'
|
||||
import { Select, SelectContent, SelectItem, SelectItemIndicator, SelectItemText, SelectTrigger } from '@langgenius/dify-ui/select'
|
||||
import { Switch } from '@langgenius/dify-ui/switch'
|
||||
import { toast } from '@langgenius/dify-ui/toast'
|
||||
import { RiArrowRightSLine, RiCloseLine } from '@remixicon/react'
|
||||
import { Tooltip, TooltipContent, TooltipTrigger } from '@langgenius/dify-ui/tooltip'
|
||||
import * as React from 'react'
|
||||
import { useCallback, useEffect, useRef, useState } from 'react'
|
||||
import { useCallback, useState } from 'react'
|
||||
import { Trans, useTranslation } from 'react-i18next'
|
||||
import ActionButton from '@/app/components/base/action-button'
|
||||
import AppIcon from '@/app/components/base/app-icon'
|
||||
import AppIconPicker from '@/app/components/base/app-icon-picker'
|
||||
import Divider from '@/app/components/base/divider'
|
||||
import { SparklesSoft } from '@/app/components/base/icons/src/public/common'
|
||||
import Input from '@/app/components/base/input'
|
||||
import Modal from '@/app/components/base/modal'
|
||||
import PremiumBadge from '@/app/components/base/premium-badge'
|
||||
import Textarea from '@/app/components/base/textarea'
|
||||
import Tooltip from '@/app/components/base/tooltip'
|
||||
import { ACCOUNT_SETTING_TAB } from '@/app/components/header/account-setting/constants'
|
||||
import { useModalContext } from '@/context/modal-context'
|
||||
import { useProviderContext } from '@/context/provider-context'
|
||||
@ -62,31 +59,20 @@ type SelectOption = {
|
||||
name: string
|
||||
}
|
||||
|
||||
const SettingsModal: FC<ISettingsModalProps> = ({
|
||||
isChat,
|
||||
appInfo,
|
||||
isShow = false,
|
||||
onClose,
|
||||
onSave,
|
||||
}) => {
|
||||
const [isShowMore, setIsShowMore] = useState(false)
|
||||
const createInputInfo = (appInfo: ISettingsModalProps['appInfo']) => {
|
||||
const {
|
||||
title,
|
||||
icon_type,
|
||||
icon,
|
||||
icon_background,
|
||||
icon_url,
|
||||
description,
|
||||
chat_color_theme,
|
||||
chat_color_theme_inverted,
|
||||
copyright,
|
||||
privacy_policy,
|
||||
custom_disclaimer,
|
||||
default_language,
|
||||
show_workflow_steps,
|
||||
use_icon_as_answer_icon,
|
||||
} = appInfo.site
|
||||
const [inputInfo, setInputInfo] = useState({
|
||||
|
||||
return {
|
||||
title,
|
||||
desc: description,
|
||||
chatColorTheme: chat_color_theme,
|
||||
@ -98,18 +84,57 @@ const SettingsModal: FC<ISettingsModalProps> = ({
|
||||
show_workflow_steps,
|
||||
use_icon_as_answer_icon,
|
||||
enable_sso: appInfo.enable_sso,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const createAppIcon = (appInfo: ISettingsModalProps['appInfo']): AppIconSelection => {
|
||||
const { icon_type, icon, icon_background, icon_url } = appInfo.site
|
||||
|
||||
return icon_type === 'image'
|
||||
? { type: 'image', url: icon_url!, fileId: icon }
|
||||
: { type: 'emoji', icon, background: icon_background! }
|
||||
}
|
||||
|
||||
const getSettingsResetKey = (appInfo: ISettingsModalProps['appInfo']) => JSON.stringify([
|
||||
appInfo.id,
|
||||
appInfo.enable_sso,
|
||||
appInfo.site.title,
|
||||
appInfo.site.description,
|
||||
appInfo.site.chat_color_theme,
|
||||
appInfo.site.chat_color_theme_inverted,
|
||||
appInfo.site.copyright,
|
||||
appInfo.site.privacy_policy,
|
||||
appInfo.site.custom_disclaimer,
|
||||
appInfo.site.default_language,
|
||||
appInfo.site.icon_type,
|
||||
appInfo.site.icon,
|
||||
appInfo.site.icon_background,
|
||||
appInfo.site.icon_url,
|
||||
appInfo.site.show_workflow_steps,
|
||||
appInfo.site.use_icon_as_answer_icon,
|
||||
])
|
||||
|
||||
const SettingsModal: FC<ISettingsModalProps> = ({
|
||||
isChat,
|
||||
appInfo,
|
||||
isShow = false,
|
||||
onClose,
|
||||
onSave,
|
||||
}) => {
|
||||
const [isShowMore, setIsShowMore] = useState(false)
|
||||
const { default_language } = appInfo.site
|
||||
const nextInputInfo = createInputInfo(appInfo)
|
||||
const nextAppIcon = createAppIcon(appInfo)
|
||||
const settingsResetKey = getSettingsResetKey(appInfo)
|
||||
const [inputInfo, setInputInfo] = useState(nextInputInfo)
|
||||
const [language, setLanguage] = useState(default_language)
|
||||
const [saveLoading, setSaveLoading] = useState(false)
|
||||
const { t } = useTranslation()
|
||||
const hideMoreTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null)
|
||||
|
||||
const [showAppIconPicker, setShowAppIconPicker] = useState(false)
|
||||
const [appIcon, setAppIcon] = useState<AppIconSelection>(
|
||||
icon_type === 'image'
|
||||
? { type: 'image', url: icon_url!, fileId: icon }
|
||||
: { type: 'emoji', icon, background: icon_background! },
|
||||
)
|
||||
const [appIcon, setAppIcon] = useState<AppIconSelection>(nextAppIcon)
|
||||
const [previousIsShow, setPreviousIsShow] = useState(isShow)
|
||||
const [previousSettingsResetKey, setPreviousSettingsResetKey] = useState(settingsResetKey)
|
||||
|
||||
const { enableBilling, plan, webappCopyrightEnabled } = useProviderContext()
|
||||
const { setShowPricingModal, setShowAccountSettingModal } = useModalContext()
|
||||
@ -123,43 +148,21 @@ const SettingsModal: FC<ISettingsModalProps> = ({
|
||||
setShowAccountSettingModal({ payload: ACCOUNT_SETTING_TAB.BILLING })
|
||||
}, [isFreePlan, setShowAccountSettingModal, setShowPricingModal])
|
||||
|
||||
useEffect(() => {
|
||||
setInputInfo({
|
||||
title,
|
||||
desc: description,
|
||||
chatColorTheme: chat_color_theme,
|
||||
chatColorThemeInverted: chat_color_theme_inverted,
|
||||
copyright,
|
||||
copyrightSwitchValue: !!copyright,
|
||||
privacyPolicy: privacy_policy,
|
||||
customDisclaimer: custom_disclaimer,
|
||||
show_workflow_steps,
|
||||
use_icon_as_answer_icon,
|
||||
enable_sso: appInfo.enable_sso,
|
||||
})
|
||||
setLanguage(default_language)
|
||||
setAppIcon(icon_type === 'image'
|
||||
? { type: 'image', url: icon_url!, fileId: icon }
|
||||
: { type: 'emoji', icon, background: icon_background! })
|
||||
}, [appInfo, chat_color_theme, chat_color_theme_inverted, copyright, custom_disclaimer, default_language, description, icon, icon_background, icon_type, icon_url, privacy_policy, show_workflow_steps, title, use_icon_as_answer_icon])
|
||||
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
if (hideMoreTimerRef.current) {
|
||||
clearTimeout(hideMoreTimerRef.current)
|
||||
hideMoreTimerRef.current = null
|
||||
}
|
||||
const shouldResetForm = isShow && (!previousIsShow || settingsResetKey !== previousSettingsResetKey)
|
||||
if (isShow !== previousIsShow || shouldResetForm) {
|
||||
setPreviousIsShow(isShow)
|
||||
if (shouldResetForm) {
|
||||
setInputInfo(nextInputInfo)
|
||||
setLanguage(default_language)
|
||||
setAppIcon(nextAppIcon)
|
||||
setIsShowMore(false)
|
||||
setPreviousSettingsResetKey(settingsResetKey)
|
||||
}
|
||||
}, [])
|
||||
}
|
||||
|
||||
const onHide = () => {
|
||||
onClose()
|
||||
if (hideMoreTimerRef.current)
|
||||
clearTimeout(hideMoreTimerRef.current)
|
||||
hideMoreTimerRef.current = setTimeout(() => {
|
||||
setIsShowMore(false)
|
||||
hideMoreTimerRef.current = null
|
||||
}, 200)
|
||||
setIsShowMore(false)
|
||||
}
|
||||
|
||||
const onClickSave = async () => {
|
||||
@ -172,7 +175,7 @@ const SettingsModal: FC<ISettingsModalProps> = ({
|
||||
if (hex === null || hex?.length === 0)
|
||||
return true
|
||||
|
||||
const regex = /#([A-F0-9]{6})/i
|
||||
const regex = /#[A-F0-9]{6}/i
|
||||
const check = regex.test(hex)
|
||||
return check
|
||||
}
|
||||
@ -240,245 +243,250 @@ const SettingsModal: FC<ISettingsModalProps> = ({
|
||||
|
||||
return (
|
||||
<>
|
||||
<Modal
|
||||
isShow={isShow}
|
||||
closable={false}
|
||||
onClose={onHide}
|
||||
className="max-w-[520px] p-0"
|
||||
>
|
||||
{/* header */}
|
||||
<div className="pt-5 pr-5 pb-3 pl-6">
|
||||
<div className="flex items-center gap-1">
|
||||
<div className="grow title-2xl-semi-bold text-text-primary">{t(`${prefixSettings}.title`, { ns: 'appOverview' })}</div>
|
||||
<ActionButton className="shrink-0" onClick={onHide}>
|
||||
<RiCloseLine className="h-4 w-4" />
|
||||
</ActionButton>
|
||||
<Dialog open={isShow} onOpenChange={open => !open && onHide()}>
|
||||
<DialogContent className="max-h-[calc(100dvh-2rem)] w-[520px] overflow-visible p-0">
|
||||
{/* header */}
|
||||
<div className="pt-5 pr-5 pb-3 pl-6">
|
||||
<div className="flex items-center gap-1">
|
||||
<DialogTitle className="grow title-2xl-semi-bold text-text-primary">{t(`${prefixSettings}.title`, { ns: 'appOverview' })}</DialogTitle>
|
||||
<DialogCloseButton className="relative top-auto right-auto shrink-0" />
|
||||
</div>
|
||||
<div className="mt-0.5 system-xs-regular text-text-tertiary">
|
||||
<span>{t(`${prefixSettings}.modalTip`, { ns: 'appOverview' })}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-0.5 system-xs-regular text-text-tertiary">
|
||||
<span>{t(`${prefixSettings}.modalTip`, { ns: 'appOverview' })}</span>
|
||||
</div>
|
||||
</div>
|
||||
{/* form body */}
|
||||
<div className="space-y-5 px-6 py-3">
|
||||
{/* name & icon */}
|
||||
<div className="flex gap-4">
|
||||
<div className="grow">
|
||||
<div className={cn('mb-1 py-1 system-sm-semibold text-text-secondary')}>{t(`${prefixSettings}.webName`, { ns: 'appOverview' })}</div>
|
||||
<Input
|
||||
className="w-full"
|
||||
value={inputInfo.title}
|
||||
onChange={onChange('title')}
|
||||
placeholder={t('appNamePlaceholder', { ns: 'app' }) || ''}
|
||||
{/* form body */}
|
||||
<div className="space-y-5 px-6 py-3">
|
||||
{/* name & icon */}
|
||||
<div className="flex gap-4">
|
||||
<div className="grow">
|
||||
<div className={cn('mb-1 py-1 system-sm-semibold text-text-secondary')}>{t(`${prefixSettings}.webName`, { ns: 'appOverview' })}</div>
|
||||
<Input
|
||||
className="w-full"
|
||||
value={inputInfo.title}
|
||||
onChange={onChange('title')}
|
||||
placeholder={t('appNamePlaceholder', { ns: 'app' }) || ''}
|
||||
/>
|
||||
</div>
|
||||
<AppIcon
|
||||
size="xxl"
|
||||
onClick={() => { setShowAppIconPicker(true) }}
|
||||
className="mt-2 cursor-pointer"
|
||||
iconType={appIcon.type}
|
||||
icon={appIcon.type === 'image' ? appIcon.fileId : appIcon.icon}
|
||||
background={appIcon.type === 'image' ? undefined : appIcon.background}
|
||||
imageUrl={appIcon.type === 'image' ? appIcon.url : undefined}
|
||||
/>
|
||||
</div>
|
||||
<AppIcon
|
||||
size="xxl"
|
||||
onClick={() => { setShowAppIconPicker(true) }}
|
||||
className="mt-2 cursor-pointer"
|
||||
iconType={appIcon.type}
|
||||
icon={appIcon.type === 'image' ? appIcon.fileId : appIcon.icon}
|
||||
background={appIcon.type === 'image' ? undefined : appIcon.background}
|
||||
imageUrl={appIcon.type === 'image' ? appIcon.url : undefined}
|
||||
/>
|
||||
</div>
|
||||
{/* description */}
|
||||
<div className="relative">
|
||||
<div className={cn('py-1 system-sm-semibold text-text-secondary')}>{t(`${prefixSettings}.webDesc`, { ns: 'appOverview' })}</div>
|
||||
<Textarea
|
||||
className="mt-1"
|
||||
value={inputInfo.desc}
|
||||
onChange={e => onDesChange(e.target.value)}
|
||||
placeholder={t(`${prefixSettings}.webDescPlaceholder`, { ns: 'appOverview' }) as string}
|
||||
/>
|
||||
<p className={cn('pb-0.5 body-xs-regular text-text-tertiary')}>{t(`${prefixSettings}.webDescTip`, { ns: 'appOverview' })}</p>
|
||||
</div>
|
||||
<Divider className="my-0 h-px" />
|
||||
{/* answer icon */}
|
||||
{isChat && (
|
||||
{/* description */}
|
||||
<div className="relative">
|
||||
<div className={cn('py-1 system-sm-semibold text-text-secondary')}>{t(`${prefixSettings}.webDesc`, { ns: 'appOverview' })}</div>
|
||||
<Textarea
|
||||
className="mt-1"
|
||||
value={inputInfo.desc}
|
||||
onChange={e => onDesChange(e.target.value)}
|
||||
placeholder={t(`${prefixSettings}.webDescPlaceholder`, { ns: 'appOverview' }) as string}
|
||||
/>
|
||||
<p className={cn('pb-0.5 body-xs-regular text-text-tertiary')}>{t(`${prefixSettings}.webDescTip`, { ns: 'appOverview' })}</p>
|
||||
</div>
|
||||
<Divider className="my-0 h-px" />
|
||||
{/* answer icon */}
|
||||
{isChat && (
|
||||
<div className="w-full">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className={cn('py-1 system-sm-semibold text-text-secondary')}>{t('answerIcon.title', { ns: 'app' })}</div>
|
||||
<Switch
|
||||
checked={inputInfo.use_icon_as_answer_icon}
|
||||
onCheckedChange={v => setInputInfo({ ...inputInfo, use_icon_as_answer_icon: v })}
|
||||
/>
|
||||
</div>
|
||||
<p className="pb-0.5 body-xs-regular text-text-tertiary">{t('answerIcon.description', { ns: 'app' })}</p>
|
||||
</div>
|
||||
)}
|
||||
{/* language */}
|
||||
<div className="flex items-center">
|
||||
<div className={cn('grow py-1 system-sm-semibold text-text-secondary')}>{t(`${prefixSettings}.language`, { ns: 'appOverview' })}</div>
|
||||
<Select
|
||||
value={selectedLanguage?.value ?? null}
|
||||
onValueChange={(nextValue) => {
|
||||
if (!nextValue)
|
||||
return
|
||||
setLanguage(nextValue as Language)
|
||||
}}
|
||||
>
|
||||
<SelectTrigger size="large" className="w-[200px]">
|
||||
{selectedLanguage?.name ?? t('placeholder.select', { ns: 'common' })}
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{languageOptions.map(item => (
|
||||
<SelectItem key={item.value} value={item.value}>
|
||||
<SelectItemText>{item.name}</SelectItemText>
|
||||
<SelectItemIndicator />
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
{/* theme color */}
|
||||
{isChat && (
|
||||
<div className="flex items-center">
|
||||
<div className="grow">
|
||||
<div className={cn('py-1 system-sm-semibold text-text-secondary')}>{t(`${prefixSettings}.chatColorTheme`, { ns: 'appOverview' })}</div>
|
||||
<div className="pb-0.5 body-xs-regular text-text-tertiary">{t(`${prefixSettings}.chatColorThemeDesc`, { ns: 'appOverview' })}</div>
|
||||
</div>
|
||||
<div className="shrink-0">
|
||||
<Input
|
||||
className="mb-1 w-[200px]"
|
||||
value={inputInfo.chatColorTheme ?? ''}
|
||||
onChange={onChange('chatColorTheme')}
|
||||
placeholder="E.g #A020F0"
|
||||
/>
|
||||
<div className="flex items-center justify-between">
|
||||
<p className={cn('body-xs-regular text-text-tertiary')}>{t(`${prefixSettings}.chatColorThemeInverted`, { ns: 'appOverview' })}</p>
|
||||
<Switch checked={inputInfo.chatColorThemeInverted} onCheckedChange={v => setInputInfo({ ...inputInfo, chatColorThemeInverted: v })}></Switch>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{/* workflow detail */}
|
||||
<div className="w-full">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className={cn('py-1 system-sm-semibold text-text-secondary')}>{t('answerIcon.title', { ns: 'app' })}</div>
|
||||
<div className={cn('py-1 system-sm-semibold text-text-secondary')}>{t(`${prefixSettings}.workflow.subTitle`, { ns: 'appOverview' })}</div>
|
||||
<Switch
|
||||
checked={inputInfo.use_icon_as_answer_icon}
|
||||
onCheckedChange={v => setInputInfo({ ...inputInfo, use_icon_as_answer_icon: v })}
|
||||
disabled={!(appInfo.mode === AppModeEnum.WORKFLOW || appInfo.mode === AppModeEnum.ADVANCED_CHAT)}
|
||||
checked={inputInfo.show_workflow_steps}
|
||||
onCheckedChange={v => setInputInfo({ ...inputInfo, show_workflow_steps: v })}
|
||||
/>
|
||||
</div>
|
||||
<p className="pb-0.5 body-xs-regular text-text-tertiary">{t('answerIcon.description', { ns: 'app' })}</p>
|
||||
<p className="pb-0.5 body-xs-regular text-text-tertiary">{t(`${prefixSettings}.workflow.showDesc`, { ns: 'appOverview' })}</p>
|
||||
</div>
|
||||
)}
|
||||
{/* language */}
|
||||
<div className="flex items-center">
|
||||
<div className={cn('grow py-1 system-sm-semibold text-text-secondary')}>{t(`${prefixSettings}.language`, { ns: 'appOverview' })}</div>
|
||||
<Select
|
||||
value={selectedLanguage?.value ?? null}
|
||||
onValueChange={(nextValue) => {
|
||||
if (!nextValue)
|
||||
return
|
||||
setLanguage(nextValue as Language)
|
||||
}}
|
||||
>
|
||||
<SelectTrigger size="large" className="w-[200px]">
|
||||
{selectedLanguage?.name ?? t('placeholder.select', { ns: 'common' })}
|
||||
</SelectTrigger>
|
||||
<SelectContent popupClassName="w-(--anchor-width)">
|
||||
{languageOptions.map(item => (
|
||||
<SelectItem key={item.value} value={item.value}>
|
||||
<SelectItemText>{item.name}</SelectItemText>
|
||||
<SelectItemIndicator />
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
{/* theme color */}
|
||||
{isChat && (
|
||||
<div className="flex items-center">
|
||||
<div className="grow">
|
||||
<div className={cn('py-1 system-sm-semibold text-text-secondary')}>{t(`${prefixSettings}.chatColorTheme`, { ns: 'appOverview' })}</div>
|
||||
<div className="pb-0.5 body-xs-regular text-text-tertiary">{t(`${prefixSettings}.chatColorThemeDesc`, { ns: 'appOverview' })}</div>
|
||||
</div>
|
||||
<div className="shrink-0">
|
||||
<Input
|
||||
className="mb-1 w-[200px]"
|
||||
value={inputInfo.chatColorTheme ?? ''}
|
||||
onChange={onChange('chatColorTheme')}
|
||||
placeholder="E.g #A020F0"
|
||||
/>
|
||||
<div className="flex items-center justify-between">
|
||||
<p className={cn('body-xs-regular text-text-tertiary')}>{t(`${prefixSettings}.chatColorThemeInverted`, { ns: 'appOverview' })}</p>
|
||||
<Switch checked={inputInfo.chatColorThemeInverted} onCheckedChange={v => setInputInfo({ ...inputInfo, chatColorThemeInverted: v })}></Switch>
|
||||
{/* more settings switch */}
|
||||
<Divider className="my-0 h-px" />
|
||||
{!isShowMore && (
|
||||
<div className="flex cursor-pointer items-center" onClick={() => setIsShowMore(true)}>
|
||||
<div className="grow">
|
||||
<div className={cn('py-1 system-sm-semibold text-text-secondary')}>{t(`${prefixSettings}.more.entry`, { ns: 'appOverview' })}</div>
|
||||
<p className={cn('pb-0.5 body-xs-regular text-text-tertiary')}>
|
||||
{t(`${prefixSettings}.more.copyRightPlaceholder`, { ns: 'appOverview' })}
|
||||
{' '}
|
||||
&
|
||||
{' '}
|
||||
{t(`${prefixSettings}.more.privacyPolicyPlaceholder`, { ns: 'appOverview' })}
|
||||
</p>
|
||||
</div>
|
||||
<span aria-hidden="true" className="ml-1 i-ri-arrow-right-s-line h-4 w-4 shrink-0 text-text-secondary" />
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{/* workflow detail */}
|
||||
<div className="w-full">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className={cn('py-1 system-sm-semibold text-text-secondary')}>{t(`${prefixSettings}.workflow.subTitle`, { ns: 'appOverview' })}</div>
|
||||
<Switch
|
||||
disabled={!(appInfo.mode === AppModeEnum.WORKFLOW || appInfo.mode === AppModeEnum.ADVANCED_CHAT)}
|
||||
checked={inputInfo.show_workflow_steps}
|
||||
onCheckedChange={v => setInputInfo({ ...inputInfo, show_workflow_steps: v })}
|
||||
)}
|
||||
{/* more settings */}
|
||||
{isShowMore && (
|
||||
<>
|
||||
{/* copyright */}
|
||||
<div className="w-full">
|
||||
<div className="flex items-center">
|
||||
<div className="flex grow items-center">
|
||||
<div className={cn('mr-1 py-1 system-sm-semibold text-text-secondary')}>{t(`${prefixSettings}.more.copyright`, { ns: 'appOverview' })}</div>
|
||||
{/* upgrade button */}
|
||||
{enableBilling && isFreePlan && (
|
||||
<div className="h-[18px] select-none">
|
||||
<PremiumBadge size="s" color="blue" allowHover={true} onClick={handlePlanClick}>
|
||||
<span aria-hidden="true" className="i-custom-public-common-sparkles-soft flex h-3.5 w-3.5 items-center py-px pl-[3px] text-components-premium-badge-indigo-text-stop-0" />
|
||||
<div className="system-xs-medium">
|
||||
<span className="p-1">
|
||||
{t('upgradeBtn.encourageShort', { ns: 'billing' })}
|
||||
</span>
|
||||
</div>
|
||||
</PremiumBadge>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{webappCopyrightEnabled
|
||||
? (
|
||||
<Switch
|
||||
checked={inputInfo.copyrightSwitchValue}
|
||||
onCheckedChange={v => setInputInfo({ ...inputInfo, copyrightSwitchValue: v })}
|
||||
/>
|
||||
)
|
||||
: (
|
||||
<Tooltip>
|
||||
<TooltipTrigger
|
||||
render={(
|
||||
<div>
|
||||
<Switch
|
||||
disabled
|
||||
checked={inputInfo.copyrightSwitchValue}
|
||||
onCheckedChange={v => setInputInfo({ ...inputInfo, copyrightSwitchValue: v })}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
/>
|
||||
<TooltipContent className="w-[180px]">
|
||||
{t(`${prefixSettings}.more.copyrightTooltip`, { ns: 'appOverview' })}
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
)}
|
||||
</div>
|
||||
<p className="pb-0.5 body-xs-regular text-text-tertiary">{t(`${prefixSettings}.more.copyrightTip`, { ns: 'appOverview' })}</p>
|
||||
{inputInfo.copyrightSwitchValue && (
|
||||
<Input
|
||||
className="mt-2 h-10"
|
||||
value={inputInfo.copyright}
|
||||
onChange={onChange('copyright')}
|
||||
placeholder={t(`${prefixSettings}.more.copyRightPlaceholder`, { ns: 'appOverview' }) as string}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
{/* privacy policy */}
|
||||
<div className="w-full">
|
||||
<div className={cn('py-1 system-sm-semibold text-text-secondary')}>{t(`${prefixSettings}.more.privacyPolicy`, { ns: 'appOverview' })}</div>
|
||||
<p className={cn('pb-0.5 body-xs-regular text-text-tertiary')}>
|
||||
<Trans
|
||||
i18nKey={`${prefixSettings}.more.privacyPolicyTip`}
|
||||
ns="appOverview"
|
||||
components={{ privacyPolicyLink: <Link href="https://dify.ai/privacy" target="_blank" rel="noopener noreferrer" className="text-text-accent" /> }}
|
||||
/>
|
||||
</p>
|
||||
<Input
|
||||
className="mt-1"
|
||||
value={inputInfo.privacyPolicy}
|
||||
onChange={onChange('privacyPolicy')}
|
||||
placeholder={t(`${prefixSettings}.more.privacyPolicyPlaceholder`, { ns: 'appOverview' }) as string}
|
||||
/>
|
||||
</div>
|
||||
{/* custom disclaimer */}
|
||||
<div className="w-full">
|
||||
<div className={cn('py-1 system-sm-semibold text-text-secondary')}>{t(`${prefixSettings}.more.customDisclaimer`, { ns: 'appOverview' })}</div>
|
||||
<p className={cn('pb-0.5 body-xs-regular text-text-tertiary')}>{t(`${prefixSettings}.more.customDisclaimerTip`, { ns: 'appOverview' })}</p>
|
||||
<Textarea
|
||||
className="mt-1"
|
||||
value={inputInfo.customDisclaimer}
|
||||
onChange={onChange('customDisclaimer')}
|
||||
placeholder={t(`${prefixSettings}.more.customDisclaimerPlaceholder`, { ns: 'appOverview' }) as string}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
{/* footer */}
|
||||
<div className="flex justify-end p-6 pt-5">
|
||||
<Button className="mr-2" onClick={onHide}>{t('operation.cancel', { ns: 'common' })}</Button>
|
||||
<Button variant="primary" onClick={onClickSave} loading={saveLoading}>{t('operation.save', { ns: 'common' })}</Button>
|
||||
</div>
|
||||
{showAppIconPicker && (
|
||||
<div onClick={e => e.stopPropagation()}>
|
||||
<AppIconPicker
|
||||
onSelect={(payload) => {
|
||||
setAppIcon(payload)
|
||||
setShowAppIconPicker(false)
|
||||
}}
|
||||
onClose={() => {
|
||||
setAppIcon(createAppIcon(appInfo))
|
||||
setShowAppIconPicker(false)
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<p className="pb-0.5 body-xs-regular text-text-tertiary">{t(`${prefixSettings}.workflow.showDesc`, { ns: 'appOverview' })}</p>
|
||||
</div>
|
||||
{/* more settings switch */}
|
||||
<Divider className="my-0 h-px" />
|
||||
{!isShowMore && (
|
||||
<div className="flex cursor-pointer items-center" onClick={() => setIsShowMore(true)}>
|
||||
<div className="grow">
|
||||
<div className={cn('py-1 system-sm-semibold text-text-secondary')}>{t(`${prefixSettings}.more.entry`, { ns: 'appOverview' })}</div>
|
||||
<p className={cn('pb-0.5 body-xs-regular text-text-tertiary')}>
|
||||
{t(`${prefixSettings}.more.copyRightPlaceholder`, { ns: 'appOverview' })}
|
||||
{' '}
|
||||
&
|
||||
{' '}
|
||||
{t(`${prefixSettings}.more.privacyPolicyPlaceholder`, { ns: 'appOverview' })}
|
||||
</p>
|
||||
</div>
|
||||
<RiArrowRightSLine className="ml-1 h-4 w-4 shrink-0 text-text-secondary" />
|
||||
</div>
|
||||
)}
|
||||
{/* more settings */}
|
||||
{isShowMore && (
|
||||
<>
|
||||
{/* copyright */}
|
||||
<div className="w-full">
|
||||
<div className="flex items-center">
|
||||
<div className="flex grow items-center">
|
||||
<div className={cn('mr-1 py-1 system-sm-semibold text-text-secondary')}>{t(`${prefixSettings}.more.copyright`, { ns: 'appOverview' })}</div>
|
||||
{/* upgrade button */}
|
||||
{enableBilling && isFreePlan && (
|
||||
<div className="h-[18px] select-none">
|
||||
<PremiumBadge size="s" color="blue" allowHover={true} onClick={handlePlanClick}>
|
||||
<SparklesSoft className="flex h-3.5 w-3.5 items-center py-px pl-[3px] text-components-premium-badge-indigo-text-stop-0" />
|
||||
<div className="system-xs-medium">
|
||||
<span className="p-1">
|
||||
{t('upgradeBtn.encourageShort', { ns: 'billing' })}
|
||||
</span>
|
||||
</div>
|
||||
</PremiumBadge>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<Tooltip
|
||||
disabled={webappCopyrightEnabled}
|
||||
popupContent={
|
||||
<div className="w-[180px]">{t(`${prefixSettings}.more.copyrightTooltip`, { ns: 'appOverview' })}</div>
|
||||
}
|
||||
asChild={false}
|
||||
>
|
||||
<Switch
|
||||
disabled={!webappCopyrightEnabled}
|
||||
checked={inputInfo.copyrightSwitchValue}
|
||||
onCheckedChange={v => setInputInfo({ ...inputInfo, copyrightSwitchValue: v })}
|
||||
/>
|
||||
</Tooltip>
|
||||
</div>
|
||||
<p className="pb-0.5 body-xs-regular text-text-tertiary">{t(`${prefixSettings}.more.copyrightTip`, { ns: 'appOverview' })}</p>
|
||||
{inputInfo.copyrightSwitchValue && (
|
||||
<Input
|
||||
className="mt-2 h-10"
|
||||
value={inputInfo.copyright}
|
||||
onChange={onChange('copyright')}
|
||||
placeholder={t(`${prefixSettings}.more.copyRightPlaceholder`, { ns: 'appOverview' }) as string}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
{/* privacy policy */}
|
||||
<div className="w-full">
|
||||
<div className={cn('py-1 system-sm-semibold text-text-secondary')}>{t(`${prefixSettings}.more.privacyPolicy`, { ns: 'appOverview' })}</div>
|
||||
<p className={cn('pb-0.5 body-xs-regular text-text-tertiary')}>
|
||||
<Trans
|
||||
i18nKey={`${prefixSettings}.more.privacyPolicyTip`}
|
||||
ns="appOverview"
|
||||
components={{ privacyPolicyLink: <Link href="https://dify.ai/privacy" target="_blank" rel="noopener noreferrer" className="text-text-accent" /> }}
|
||||
/>
|
||||
</p>
|
||||
<Input
|
||||
className="mt-1"
|
||||
value={inputInfo.privacyPolicy}
|
||||
onChange={onChange('privacyPolicy')}
|
||||
placeholder={t(`${prefixSettings}.more.privacyPolicyPlaceholder`, { ns: 'appOverview' }) as string}
|
||||
/>
|
||||
</div>
|
||||
{/* custom disclaimer */}
|
||||
<div className="w-full">
|
||||
<div className={cn('py-1 system-sm-semibold text-text-secondary')}>{t(`${prefixSettings}.more.customDisclaimer`, { ns: 'appOverview' })}</div>
|
||||
<p className={cn('pb-0.5 body-xs-regular text-text-tertiary')}>{t(`${prefixSettings}.more.customDisclaimerTip`, { ns: 'appOverview' })}</p>
|
||||
<Textarea
|
||||
className="mt-1"
|
||||
value={inputInfo.customDisclaimer}
|
||||
onChange={onChange('customDisclaimer')}
|
||||
placeholder={t(`${prefixSettings}.more.customDisclaimerPlaceholder`, { ns: 'appOverview' }) as string}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
{/* footer */}
|
||||
<div className="flex justify-end p-6 pt-5">
|
||||
<Button className="mr-2" onClick={onHide}>{t('operation.cancel', { ns: 'common' })}</Button>
|
||||
<Button variant="primary" onClick={onClickSave} loading={saveLoading}>{t('operation.save', { ns: 'common' })}</Button>
|
||||
</div>
|
||||
{showAppIconPicker && (
|
||||
<div onClick={e => e.stopPropagation()}>
|
||||
<AppIconPicker
|
||||
onSelect={(payload) => {
|
||||
setAppIcon(payload)
|
||||
setShowAppIconPicker(false)
|
||||
}}
|
||||
onClose={() => {
|
||||
setAppIcon(icon_type === 'image'
|
||||
? { type: 'image', url: icon_url!, fileId: icon }
|
||||
: { type: 'emoji', icon, background: icon_background! })
|
||||
setShowAppIconPicker(false)
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</Modal>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
@ -24,15 +24,19 @@ import {
|
||||
DropdownMenuTrigger,
|
||||
} from '@langgenius/dify-ui/dropdown-menu'
|
||||
import { toast } from '@langgenius/dify-ui/toast'
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipTrigger,
|
||||
} from '@langgenius/dify-ui/tooltip'
|
||||
import { useSuspenseQuery } from '@tanstack/react-query'
|
||||
import * as React from 'react'
|
||||
import { useCallback, useEffect, useId, useMemo, useState } from 'react'
|
||||
import { useCallback, useId, useMemo, useState } from 'react'
|
||||
import { Trans, useTranslation } from 'react-i18next'
|
||||
import { AppTypeIcon } from '@/app/components/app/type-selector'
|
||||
import AppIcon from '@/app/components/base/app-icon'
|
||||
import Input from '@/app/components/base/input'
|
||||
import TagSelector from '@/app/components/base/tag-management/selector'
|
||||
import Tooltip from '@/app/components/base/tooltip'
|
||||
import { UserAvatarList } from '@/app/components/base/user-avatar-list'
|
||||
import { NEED_REFRESH_APP_LIST_KEY } from '@/config'
|
||||
import { useAppContext } from '@/context/app-context'
|
||||
@ -229,8 +233,9 @@ const AppCard = ({ app, onlineUsers = [], onRefresh }: AppCardProps) => {
|
||||
setShowConfirmDelete(false)
|
||||
setConfirmDeleteInput('')
|
||||
}
|
||||
catch (e: any) {
|
||||
toast.error(`${t('appDeleteFailed', { ns: 'app' })}${'message' in e ? `: ${e.message}` : ''}`)
|
||||
catch (e) {
|
||||
const message = e instanceof Error ? e.message : ''
|
||||
toast.error(`${t('appDeleteFailed', { ns: 'app' })}${message ? `: ${message}` : ''}`)
|
||||
}
|
||||
}, [app.id, mutateDeleteApp, onPlanInfoChanged, t])
|
||||
|
||||
@ -313,8 +318,8 @@ const AppCard = ({ app, onlineUsers = [], onRefresh }: AppCardProps) => {
|
||||
if (onRefresh)
|
||||
onRefresh()
|
||||
}
|
||||
catch (e: any) {
|
||||
toast.error(e.message || t('editFailed', { ns: 'app' }))
|
||||
catch (e) {
|
||||
toast.error(e instanceof Error ? e.message : t('editFailed', { ns: 'app' }))
|
||||
}
|
||||
}, [app.id, onRefresh, t])
|
||||
|
||||
@ -391,10 +396,18 @@ const AppCard = ({ app, onlineUsers = [], onRefresh }: AppCardProps) => {
|
||||
const shouldShowAccessControlOption = systemFeatures.webapp_auth.enabled && isCurrentWorkspaceEditor
|
||||
const operationsMenuWidthClassName = shouldShowSwitchOption ? 'w-[256px]' : 'w-[216px]'
|
||||
|
||||
const [tags, setTags] = useState<Tag[]>(app.tags)
|
||||
useEffect(() => {
|
||||
setTags(app.tags)
|
||||
}, [app.tags])
|
||||
const appTagsKey = useMemo(() => app.tags.map(tag => tag.id).join(','), [app.tags])
|
||||
const [tagState, setTagState] = useState<{ key: string, tags: Tag[] }>(() => ({
|
||||
key: appTagsKey,
|
||||
tags: app.tags,
|
||||
}))
|
||||
const tags = tagState.key === appTagsKey ? tagState.tags : app.tags
|
||||
const handleTagsUpdate = useCallback((nextTags: Tag[]) => {
|
||||
setTagState({
|
||||
key: appTagsKey,
|
||||
tags: nextTags,
|
||||
})
|
||||
}, [appTagsKey])
|
||||
|
||||
const EditTimeText = useMemo(() => {
|
||||
const timeText = formatTime({
|
||||
@ -454,23 +467,39 @@ const AppCard = ({ app, onlineUsers = [], onRefresh }: AppCardProps) => {
|
||||
)}
|
||||
<div className="flex h-5 w-5 items-center justify-center">
|
||||
{app.access_mode === AccessMode.PUBLIC && (
|
||||
<Tooltip asChild={false} popupContent={t('accessItemsDescription.anyone', { ns: 'app' })}>
|
||||
<span aria-hidden className="i-ri-global-line h-4 w-4 text-text-quaternary" />
|
||||
<Tooltip>
|
||||
<TooltipTrigger
|
||||
aria-label={t('accessItemsDescription.anyone', { ns: 'app' })}
|
||||
render={<span title={t('accessItemsDescription.anyone', { ns: 'app' })} className="i-ri-global-line h-4 w-4 text-text-quaternary" />}
|
||||
/>
|
||||
<TooltipContent>{t('accessItemsDescription.anyone', { ns: 'app' })}</TooltipContent>
|
||||
</Tooltip>
|
||||
)}
|
||||
{app.access_mode === AccessMode.SPECIFIC_GROUPS_MEMBERS && (
|
||||
<Tooltip asChild={false} popupContent={t('accessItemsDescription.specific', { ns: 'app' })}>
|
||||
<span aria-hidden className="i-ri-lock-line h-4 w-4 text-text-quaternary" />
|
||||
<Tooltip>
|
||||
<TooltipTrigger
|
||||
aria-label={t('accessItemsDescription.specific', { ns: 'app' })}
|
||||
render={<span title={t('accessItemsDescription.specific', { ns: 'app' })} className="i-ri-lock-line h-4 w-4 text-text-quaternary" />}
|
||||
/>
|
||||
<TooltipContent>{t('accessItemsDescription.specific', { ns: 'app' })}</TooltipContent>
|
||||
</Tooltip>
|
||||
)}
|
||||
{app.access_mode === AccessMode.ORGANIZATION && (
|
||||
<Tooltip asChild={false} popupContent={t('accessItemsDescription.organization', { ns: 'app' })}>
|
||||
<span aria-hidden className="i-ri-building-line h-4 w-4 text-text-quaternary" />
|
||||
<Tooltip>
|
||||
<TooltipTrigger
|
||||
aria-label={t('accessItemsDescription.organization', { ns: 'app' })}
|
||||
render={<span title={t('accessItemsDescription.organization', { ns: 'app' })} className="i-ri-building-line h-4 w-4 text-text-quaternary" />}
|
||||
/>
|
||||
<TooltipContent>{t('accessItemsDescription.organization', { ns: 'app' })}</TooltipContent>
|
||||
</Tooltip>
|
||||
)}
|
||||
{app.access_mode === AccessMode.EXTERNAL_MEMBERS && (
|
||||
<Tooltip asChild={false} popupContent={t('accessItemsDescription.external', { ns: 'app' })}>
|
||||
<span aria-hidden className="i-ri-verified-badge-line h-4 w-4 text-text-quaternary" />
|
||||
<Tooltip>
|
||||
<TooltipTrigger
|
||||
aria-label={t('accessItemsDescription.external', { ns: 'app' })}
|
||||
render={<span title={t('accessItemsDescription.external', { ns: 'app' })} className="i-ri-verified-badge-line h-4 w-4 text-text-quaternary" />}
|
||||
/>
|
||||
<TooltipContent>{t('accessItemsDescription.external', { ns: 'app' })}</TooltipContent>
|
||||
</Tooltip>
|
||||
)}
|
||||
</div>
|
||||
@ -501,7 +530,7 @@ const AppCard = ({ app, onlineUsers = [], onRefresh }: AppCardProps) => {
|
||||
targetID={app.id}
|
||||
value={tags.map(tag => tag.id)}
|
||||
selectedTags={tags}
|
||||
onCacheUpdate={setTags}
|
||||
onCacheUpdate={handleTagsUpdate}
|
||||
onChange={onRefresh}
|
||||
/>
|
||||
</div>
|
||||
@ -532,42 +561,40 @@ const AppCard = ({ app, onlineUsers = [], onRefresh }: AppCardProps) => {
|
||||
<span aria-hidden className="i-ri-more-fill h-4 w-4 text-text-tertiary" />
|
||||
</div>
|
||||
</DropdownMenuTrigger>
|
||||
{isOperationsMenuOpen && (
|
||||
<DropdownMenuContent
|
||||
placement="bottom-end"
|
||||
sideOffset={4}
|
||||
popupClassName={operationsMenuWidthClassName}
|
||||
>
|
||||
{systemFeatures.webapp_auth.enabled
|
||||
? (
|
||||
<AppCardOperationsMenuContent
|
||||
app={app}
|
||||
shouldShowSwitchOption={shouldShowSwitchOption}
|
||||
shouldShowAccessControlOption={shouldShowAccessControlOption}
|
||||
onEdit={handleShowEditModal}
|
||||
onDuplicate={handleShowDuplicateModal}
|
||||
onExport={exportCheck}
|
||||
onSwitch={handleShowSwitchModal}
|
||||
onDelete={handleShowDeleteConfirm}
|
||||
onAccessControl={handleShowAccessControl}
|
||||
/>
|
||||
)
|
||||
: (
|
||||
<AppCardOperationsMenu
|
||||
app={app}
|
||||
shouldShowSwitchOption={shouldShowSwitchOption}
|
||||
shouldShowOpenInExploreOption={!app.has_draft_trigger}
|
||||
shouldShowAccessControlOption={shouldShowAccessControlOption}
|
||||
onEdit={handleShowEditModal}
|
||||
onDuplicate={handleShowDuplicateModal}
|
||||
onExport={exportCheck}
|
||||
onSwitch={handleShowSwitchModal}
|
||||
onDelete={handleShowDeleteConfirm}
|
||||
onAccessControl={handleShowAccessControl}
|
||||
/>
|
||||
)}
|
||||
</DropdownMenuContent>
|
||||
)}
|
||||
<DropdownMenuContent
|
||||
placement="bottom-end"
|
||||
sideOffset={4}
|
||||
popupClassName={operationsMenuWidthClassName}
|
||||
>
|
||||
{systemFeatures.webapp_auth.enabled
|
||||
? (
|
||||
<AppCardOperationsMenuContent
|
||||
app={app}
|
||||
shouldShowSwitchOption={shouldShowSwitchOption}
|
||||
shouldShowAccessControlOption={shouldShowAccessControlOption}
|
||||
onEdit={handleShowEditModal}
|
||||
onDuplicate={handleShowDuplicateModal}
|
||||
onExport={exportCheck}
|
||||
onSwitch={handleShowSwitchModal}
|
||||
onDelete={handleShowDeleteConfirm}
|
||||
onAccessControl={handleShowAccessControl}
|
||||
/>
|
||||
)
|
||||
: (
|
||||
<AppCardOperationsMenu
|
||||
app={app}
|
||||
shouldShowSwitchOption={shouldShowSwitchOption}
|
||||
shouldShowOpenInExploreOption={!app.has_draft_trigger}
|
||||
shouldShowAccessControlOption={shouldShowAccessControlOption}
|
||||
onEdit={handleShowEditModal}
|
||||
onDuplicate={handleShowDuplicateModal}
|
||||
onExport={exportCheck}
|
||||
onSwitch={handleShowSwitchModal}
|
||||
onDelete={handleShowDeleteConfirm}
|
||||
onAccessControl={handleShowAccessControl}
|
||||
/>
|
||||
)}
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</div>
|
||||
</>
|
||||
|
||||
@ -95,7 +95,7 @@ const ImageInput: FC<UploaderProps> = ({
|
||||
return (
|
||||
<div className={cn(className, 'w-full px-3 py-1.5')}>
|
||||
<div
|
||||
className={cn(isDragActive && 'border-primary-600', 'relative flex aspect-square flex-col items-center justify-center rounded-lg border-[1.5px] border-dashed text-gray-500')}
|
||||
className={cn('relative flex aspect-square flex-col items-center justify-center rounded-lg border-[1.5px] border-dashed border-components-input-border-hover text-gray-500', isDragActive && 'border-primary-600')}
|
||||
onDragEnter={handleDragEnter}
|
||||
onDragOver={handleDragOver}
|
||||
onDragLeave={handleDragLeave}
|
||||
|
||||
@ -248,7 +248,7 @@ describe('InputsFormContent', () => {
|
||||
expect(mockSetCurrentConversationInputs).toHaveBeenCalledWith(expect.objectContaining({ sel: 'A' }))
|
||||
})
|
||||
|
||||
it('renders select dropdown above the settings dialog layer', async () => {
|
||||
it('renders select dropdown on the shared dify-ui overlay layer', async () => {
|
||||
const user = userEvent.setup()
|
||||
const context = createMockContext({
|
||||
inputsForms: [{ variable: 'sel', type: InputVarType.select, label: 'Sel', options: ['A', 'B'], default: 'B' }],
|
||||
@ -258,7 +258,7 @@ describe('InputsFormContent', () => {
|
||||
renderWithContext(<InputsFormContent />, context)
|
||||
await user.click(screen.getByText('B'))
|
||||
|
||||
expect(screen.getByText('A').closest('.z-\\[60\\]')).not.toBeNull()
|
||||
expect(screen.getByText('A').closest('.z-1002')).not.toBeNull()
|
||||
})
|
||||
|
||||
it('handles select input with existing value (value not in options -> shows placeholder)', () => {
|
||||
|
||||
@ -92,7 +92,7 @@ const InputsFormContent = ({ showTip }: Props) => {
|
||||
<SelectTrigger className="w-full">
|
||||
{String(inputsFormValue?.[form.variable] ?? form.default ?? form.label)}
|
||||
</SelectTrigger>
|
||||
<SelectContent popupClassName="z-[60] w-(--anchor-width)">
|
||||
<SelectContent>
|
||||
{form.options.map((option: string) => (
|
||||
<SelectItem key={option} value={option}>
|
||||
<SelectItemText>{option}</SelectItemText>
|
||||
|
||||
@ -146,7 +146,7 @@ const ChatInputArea = ({ readonly, botName, showFeatureBar, showFileUpload, feat
|
||||
<div ref={textValueRef} className="pointer-events-none invisible absolute h-auto w-auto p-1 body-lg-regular leading-6 whitespace-pre">
|
||||
{query}
|
||||
</div>
|
||||
<Textarea ref={ref => textareaRef.current = ref as any} className={cn('w-full resize-none bg-transparent p-1 body-lg-regular leading-6 text-text-primary outline-none')} placeholder={decode(t(readonly ? 'chat.inputDisabledPlaceholder' : 'chat.inputPlaceholder', { ns: 'common', botName }) || '')} autoFocus minRows={1} value={query} onChange={e => handleQueryChange(e.target.value)} onKeyDown={handleKeyDown} onCompositionStart={handleCompositionStart} onCompositionEnd={handleCompositionEnd} onPaste={handleClipboardPasteFile} onDragEnter={handleDragFileEnter} onDragLeave={handleDragFileLeave} onDragOver={handleDragFileOver} onDrop={handleDropFile} readOnly={readonly} />
|
||||
<Textarea ref={ref => textareaRef.current = ref as any} className={cn('w-full resize-none bg-transparent p-1 body-lg-regular leading-6 text-text-primary outline-hidden')} placeholder={decode(t(readonly ? 'chat.inputDisabledPlaceholder' : 'chat.inputPlaceholder', { ns: 'common', botName }) || '')} autoFocus minRows={1} value={query} onChange={e => handleQueryChange(e.target.value)} onKeyDown={handleKeyDown} onCompositionStart={handleCompositionStart} onCompositionEnd={handleCompositionEnd} onPaste={handleClipboardPasteFile} onDragEnter={handleDragFileEnter} onDragLeave={handleDragFileLeave} onDragOver={handleDragFileOver} onDrop={handleDropFile} readOnly={readonly} />
|
||||
</div>
|
||||
{!isMultipleLine && operation}
|
||||
</div>
|
||||
|
||||
@ -200,7 +200,7 @@ describe('InputsFormContent', () => {
|
||||
expect(mockContextValue.handleNewConversationInputsChange).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should render select dropdown above the settings dialog layer', async () => {
|
||||
it('should render select dropdown on the shared dify-ui overlay layer', async () => {
|
||||
render(<InputsFormContent />)
|
||||
const selectTrigger = screen.getAllByText(/Select Label/i).find(el => el.tagName === 'SPAN')
|
||||
if (!selectTrigger)
|
||||
@ -208,7 +208,7 @@ describe('InputsFormContent', () => {
|
||||
|
||||
await user.click(selectTrigger)
|
||||
|
||||
expect(screen.getByText('Option 1').closest('.z-\\[60\\]')).not.toBeNull()
|
||||
expect(screen.getByText('Option 1').closest('.z-1002')).not.toBeNull()
|
||||
})
|
||||
|
||||
it('should handle single file upload change', async () => {
|
||||
|
||||
@ -92,7 +92,7 @@ const InputsFormContent = ({ showTip }: Props) => {
|
||||
<SelectTrigger className="w-full">
|
||||
{String(inputsFormValue?.[form.variable] ?? form.default ?? form.label)}
|
||||
</SelectTrigger>
|
||||
<SelectContent popupClassName="z-[60] w-(--anchor-width)">
|
||||
<SelectContent>
|
||||
{form.options.map((option: string) => (
|
||||
<SelectItem key={option} value={option}>
|
||||
<SelectItemText>{option}</SelectItemText>
|
||||
|
||||
@ -69,7 +69,7 @@ const FormGeneration: FC<FormGenerationProps> = ({
|
||||
<SelectTrigger className="w-full">
|
||||
{selectedOption?.name ?? form.placeholder}
|
||||
</SelectTrigger>
|
||||
<SelectContent popupClassName="z-102 w-(--anchor-width)">
|
||||
<SelectContent>
|
||||
{selectOptions.map(option => (
|
||||
<SelectItem key={option.value} value={option.value}>
|
||||
<SelectItemText>{option.name}</SelectItemText>
|
||||
|
||||
@ -80,7 +80,7 @@ const FileImageItem = ({
|
||||
}
|
||||
{
|
||||
showDownloadAction && (
|
||||
<div className="bg-opacity-[0.3] absolute inset-0.5 z-10 hidden bg-background-overlay-alt group-hover/file-image:block">
|
||||
<div className="absolute inset-0.5 z-10 hidden bg-background-overlay-alt group-hover/file-image:block">
|
||||
<div
|
||||
className="absolute right-0.5 bottom-0.5 flex h-6 w-6 items-center justify-center rounded-lg bg-components-actionbar-bg shadow-md"
|
||||
onClick={(e) => {
|
||||
|
||||
@ -267,7 +267,7 @@ const BaseField = ({
|
||||
: translatedPlaceholder}
|
||||
</SelectValue>
|
||||
</SelectTrigger>
|
||||
<SelectContent popupClassName="max-h-[320px] w-(--anchor-width) bg-components-panel-bg-blur">
|
||||
<SelectContent popupClassName="max-h-[320px] bg-components-panel-bg-blur">
|
||||
{memorizedOptions.map(option => (
|
||||
<SelectItem key={option.value} value={option.value}>
|
||||
<SelectItemText>{option.label}</SelectItemText>
|
||||
@ -293,7 +293,7 @@ const BaseField = ({
|
||||
{nextValue => getSingleSelectLabel(nextValue, memorizedOptions, translatedPlaceholder)}
|
||||
</SelectValue>
|
||||
</SelectTrigger>
|
||||
<SelectContent popupClassName="max-h-[320px] w-(--anchor-width) bg-components-panel-bg-blur">
|
||||
<SelectContent popupClassName="max-h-[320px] bg-components-panel-bg-blur">
|
||||
{memorizedOptions.map(option => (
|
||||
<SelectItem key={option.value} value={option.value}>
|
||||
<SelectItemText>{option.label}</SelectItemText>
|
||||
@ -332,7 +332,7 @@ const BaseField = ({
|
||||
: dynamicPlaceholder}
|
||||
</SelectValue>
|
||||
</SelectTrigger>
|
||||
<SelectContent popupClassName="w-(--anchor-width) bg-components-panel-bg-blur">
|
||||
<SelectContent popupClassName="bg-components-panel-bg-blur">
|
||||
{dynamicNoticeTitle && (
|
||||
<div className={cn(
|
||||
'flex h-[22px] items-center px-3 system-xs-medium-uppercase text-text-tertiary',
|
||||
@ -367,7 +367,7 @@ const BaseField = ({
|
||||
{nextValue => getSingleSelectLabel(nextValue, dynamicOptions, dynamicPlaceholder)}
|
||||
</SelectValue>
|
||||
</SelectTrigger>
|
||||
<SelectContent popupClassName="w-(--anchor-width) bg-components-panel-bg-blur">
|
||||
<SelectContent popupClassName="bg-components-panel-bg-blur">
|
||||
{dynamicNoticeTitle && (
|
||||
<div className={cn(
|
||||
'flex h-[22px] items-center px-3 system-xs-medium-uppercase text-text-tertiary',
|
||||
|
||||
@ -80,7 +80,7 @@ const SelectField = ({
|
||||
{(nextValue: string | null) => getDisplayLabel(nextValue, options, placeholderText)}
|
||||
</SelectValue>
|
||||
</SelectTrigger>
|
||||
<SelectContent popupClassName={cn('w-(--anchor-width) bg-components-panel-bg-blur', popupProps?.className)}>
|
||||
<SelectContent popupClassName={cn('bg-components-panel-bg-blur', popupProps?.className)}>
|
||||
{popupProps?.title && (
|
||||
<div
|
||||
className={cn(
|
||||
|
||||
@ -77,7 +77,7 @@ const TagInput: FC<TagInputProps> = ({ items, onChange, disableAdd, disableRemov
|
||||
<div className={cn('group/tag-add mt-1 flex items-center gap-x-0.5', !isSpecialMode ? 'rounded-md border border-dashed border-divider-deep px-1.5' : '')}>
|
||||
{!isSpecialMode && !focused && <span className="i-ri-add-line h-3.5 w-3.5 text-text-placeholder group-hover/tag-add:text-text-secondary" />}
|
||||
<AutosizeInput
|
||||
inputClassName={cn('appearance-none text-text-primary caret-[#295EFF] outline-none placeholder:text-text-placeholder group-hover/tag-add:placeholder:text-text-secondary', isSpecialMode ? 'bg-transparent' : '', inputClassName)}
|
||||
inputClassName={cn('appearance-none text-text-primary caret-[#295EFF] outline-hidden placeholder:text-text-placeholder group-hover/tag-add:placeholder:text-text-secondary', isSpecialMode ? 'bg-transparent' : '', inputClassName)}
|
||||
className={cn(!isInWorkflow && 'max-w-[300px]', isInWorkflow && 'max-w-[146px]', 'overflow-hidden rounded-md py-1 system-xs-regular', isSpecialMode && 'border border-transparent px-1.5', focused && isSpecialMode && 'border-dashed border-divider-deep')}
|
||||
onFocus={() => setFocused(true)}
|
||||
onBlur={handleBlur}
|
||||
|
||||
@ -53,7 +53,7 @@ const TagManagementModal = ({ show, type }: TagManagementModalProps) => {
|
||||
<span className="i-ri-close-line h-4 w-4 text-text-tertiary" data-testid="tag-management-modal-close-button" />
|
||||
</div>
|
||||
<div className="mt-3 flex flex-wrap gap-2">
|
||||
<input className="w-[100px] shrink-0 appearance-none rounded-lg border border-dashed border-divider-regular bg-transparent px-2 py-1 text-sm leading-5 text-text-secondary caret-primary-600 outline-none placeholder:text-text-quaternary focus:border-solid" placeholder={t('tag.addNew', { ns: 'common' }) || ''} autoFocus value={name} onChange={e => setName(e.target.value)} onKeyDown={e => e.key === 'Enter' && !e.nativeEvent.isComposing && createNewTag()} onBlur={createNewTag} />
|
||||
<input className="w-[100px] shrink-0 appearance-none rounded-lg border border-dashed border-divider-regular bg-transparent px-2 py-1 text-sm leading-5 text-text-secondary caret-primary-600 outline-hidden placeholder:text-text-quaternary focus:border-solid" placeholder={t('tag.addNew', { ns: 'common' }) || ''} autoFocus value={name} onChange={e => setName(e.target.value)} onKeyDown={e => e.key === 'Enter' && !e.nativeEvent.isComposing && createNewTag()} onBlur={createNewTag} />
|
||||
{tagList.map(tag => (<TagItemEditor key={tag.id} tag={tag} />))}
|
||||
</div>
|
||||
</Modal>
|
||||
|
||||
@ -124,7 +124,7 @@ const TagItemEditor: FC<TagItemEditorProps> = ({ tag }) => {
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
{isEditing && (<input className="shrink-0 appearance-none caret-primary-600 outline-none placeholder:text-text-quaternary" autoFocus value={name} onChange={e => setName(e.target.value)} onKeyDown={e => e.key === 'Enter' && editTag(tag.id, name)} onBlur={() => editTag(tag.id, name)} />)}
|
||||
{isEditing && (<input className="shrink-0 appearance-none caret-primary-600 outline-hidden placeholder:text-text-quaternary" autoFocus value={name} onChange={e => setName(e.target.value)} onKeyDown={e => e.key === 'Enter' && editTag(tag.id, name)} onBlur={() => editTag(tag.id, name)} />)}
|
||||
</div>
|
||||
<AlertDialog open={showRemoveModal} onOpenChange={open => !open && setShowRemoveModal(false)}>
|
||||
<AlertDialogContent>
|
||||
|
||||
@ -31,7 +31,7 @@ const COLOR_MAP = {
|
||||
export default function Tag({ children, color = 'green', className = '', bordered = false, hideBg = false }: ITagProps) {
|
||||
return (
|
||||
<div className={
|
||||
cn('inline-flex shrink-0 items-center rounded-md px-2.5 py-px text-xs leading-5', COLOR_MAP[color] ? `${COLOR_MAP[color].text} ${COLOR_MAP[color].bg}` : '', bordered ? 'border' : '', hideBg ? 'bg-transparent' : '', className)
|
||||
cn('inline-flex shrink-0 items-center rounded-md px-2.5 py-px text-xs leading-5', COLOR_MAP[color] ? `${COLOR_MAP[color].text} ${COLOR_MAP[color].bg}` : '', bordered ? 'border border-divider-regular' : '', hideBg ? 'bg-transparent' : '', className)
|
||||
}
|
||||
>
|
||||
{children}
|
||||
|
||||
@ -48,8 +48,8 @@ const HeaderBillingBtn: FC<Props> = ({
|
||||
<div
|
||||
onClick={handleClick}
|
||||
className={cn(
|
||||
'flex h-[22px] items-center rounded-md border border-divider-regular px-2 text-xs font-semibold uppercase',
|
||||
classNames,
|
||||
'flex h-[22px] items-center rounded-md border px-2 text-xs font-semibold uppercase',
|
||||
isDisplayOnly ? 'cursor-default' : 'cursor-pointer',
|
||||
)}
|
||||
>
|
||||
|
||||
@ -55,7 +55,7 @@ const FieldInfo: FC<FieldInfoProps> = ({
|
||||
<SelectTrigger className={cn(s.select, s.selectWrapper)}>
|
||||
{selectedOption?.name ?? `${t('metadata.placeholder.select', { ns: 'datasetDocuments' })}${label}`}
|
||||
</SelectTrigger>
|
||||
<SelectContent popupClassName="w-(--anchor-width)">
|
||||
<SelectContent>
|
||||
{selectOptions.map(option => (
|
||||
<SelectItem key={option.value} value={option.value}>
|
||||
<SelectItemText>{option.name}</SelectItemText>
|
||||
|
||||
@ -26,7 +26,7 @@
|
||||
@apply text-text-secondary text-sm;
|
||||
}
|
||||
.addFileBtn {
|
||||
@apply mt-4 w-fit !text-[13px] font-medium border-[0.5px];
|
||||
@apply mt-4 w-fit !text-[13px] font-medium border-[0.5px] border-components-button-secondary-border;
|
||||
}
|
||||
.plusIcon {
|
||||
@apply w-4 h-4 mr-2 stroke-current stroke-[1.5px];
|
||||
|
||||
@ -79,7 +79,7 @@ const ExternalApiSelect: React.FC<ExternalApiSelectProps> = ({ items, value, onS
|
||||
<RiArrowDownSLine className={`h-4 w-4 text-text-quaternary transition-transform ${isOpen ? 'text-text-secondary' : ''}`} />
|
||||
</div>
|
||||
{isOpen && (
|
||||
<div className="absolute z-10 mt-1 w-full rounded-xl border bg-components-panel-bg-blur shadow-lg">
|
||||
<div className="absolute z-10 mt-1 w-full rounded-xl border border-components-panel-border bg-components-panel-bg-blur shadow-lg">
|
||||
{items.map(item => (
|
||||
<div
|
||||
key={item.value}
|
||||
|
||||
@ -85,7 +85,7 @@ export default function LanguagePage() {
|
||||
<SelectTrigger size="large">
|
||||
{selectedLanguage?.name ?? t('placeholder.select', { ns: 'common' })}
|
||||
</SelectTrigger>
|
||||
<SelectContent popupClassName="w-(--anchor-width)">
|
||||
<SelectContent>
|
||||
{languageOptions.map(item => (
|
||||
<SelectItem key={item.value} value={item.value}>
|
||||
<SelectItemText>{item.name}</SelectItemText>
|
||||
@ -111,7 +111,7 @@ export default function LanguagePage() {
|
||||
<SelectTrigger size="large">
|
||||
{selectedTimezone?.name ?? t('placeholder.select', { ns: 'common' })}
|
||||
</SelectTrigger>
|
||||
<SelectContent popupClassName="w-(--anchor-width)">
|
||||
<SelectContent>
|
||||
{timezones.map(item => (
|
||||
<SelectItem key={item.value} value={String(item.value)}>
|
||||
<SelectItemText>{item.name}</SelectItemText>
|
||||
|
||||
@ -47,7 +47,7 @@ const InvitationLink = ({
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</div>
|
||||
<div className="h-4 shrink-0 border bg-divider-regular" />
|
||||
<div className="h-4 shrink-0 border border-divider-regular bg-divider-regular" />
|
||||
<Tooltip>
|
||||
<TooltipTrigger
|
||||
render={(
|
||||
|
||||
@ -76,7 +76,6 @@ const MemberSelector: FC<Props> = ({
|
||||
placement="bottom"
|
||||
sideOffset={4}
|
||||
popupClassName="border-none bg-transparent p-0 shadow-none backdrop-blur-none"
|
||||
positionerProps={{ style: { zIndex: 1002 } }}
|
||||
>
|
||||
<div className="min-w-[372px] rounded-xl border-[0.5px] border-components-panel-border bg-components-panel-bg-blur shadow-lg backdrop-blur-xs">
|
||||
<div className="p-2 pb-1">
|
||||
|
||||
@ -302,7 +302,7 @@ function Form<
|
||||
<SelectTrigger size="medium" className={cn(inputClassName)}>
|
||||
{selectedOption?.name ?? placeholder?.[language] ?? placeholder?.en_US}
|
||||
</SelectTrigger>
|
||||
<SelectContent popupClassName="w-(--anchor-width)">
|
||||
<SelectContent>
|
||||
{filteredOptions.map(option => (
|
||||
<SelectItem key={option.value} value={option.value}>
|
||||
<SelectItemText>{option.name}</SelectItemText>
|
||||
@ -494,7 +494,6 @@ function Form<
|
||||
{infotipContent}
|
||||
</div>
|
||||
<VarReferencePicker
|
||||
zIndex={1001}
|
||||
readonly={false}
|
||||
isShowNodeName
|
||||
nodeId={nodeId || ''}
|
||||
|
||||
@ -141,7 +141,7 @@ const NavSelector = ({ curNav, navigationItems, createText, isApp, onCreate, onL
|
||||
className="
|
||||
absolute right-0 -left-11 mt-1.5 w-60 max-w-80
|
||||
origin-top-right divide-y divide-divider-regular rounded-lg bg-components-panel-bg-blur
|
||||
shadow-lg outline-none
|
||||
shadow-lg outline-hidden
|
||||
"
|
||||
>
|
||||
<div className="overflow-auto px-1 py-1" style={{ maxHeight: '50vh' }} onScroll={handleScroll}>
|
||||
|
||||
@ -109,7 +109,7 @@ const SelectPackage: React.FC<SelectPackageProps> = ({
|
||||
)}
|
||||
</div>
|
||||
</SelectTrigger>
|
||||
<SelectContent popupClassName="z-1001 w-[512px]">
|
||||
<SelectContent popupClassName="w-[512px]">
|
||||
{versions.map(item => (
|
||||
<SelectItem key={item.value} value={String(item.value)}>
|
||||
<SelectItemText>{item.name}</SelectItemText>
|
||||
@ -141,7 +141,7 @@ const SelectPackage: React.FC<SelectPackageProps> = ({
|
||||
<SelectTrigger className="h-9 text-components-input-text-filled">
|
||||
{selectedPackageOption?.name ?? t(`${i18nPrefix}.selectPackagePlaceholder`, { ns: 'plugin' }) ?? ''}
|
||||
</SelectTrigger>
|
||||
<SelectContent popupClassName="z-1001 w-[512px]">
|
||||
<SelectContent popupClassName="w-[512px]">
|
||||
{packages.map(item => (
|
||||
<SelectItem key={item.value} value={String(item.value)}>
|
||||
<SelectItemText>{item.name}</SelectItemText>
|
||||
|
||||
@ -70,7 +70,7 @@ const AppInputsForm = ({
|
||||
<SelectTrigger className="w-full">
|
||||
{selectedOption?.name ?? label}
|
||||
</SelectTrigger>
|
||||
<SelectContent popupClassName="z-1050 w-(--anchor-width)">
|
||||
<SelectContent>
|
||||
{selectOptions.map(option => (
|
||||
<SelectItem key={option.value} value={option.value}>
|
||||
<SelectItemText>{option.name}</SelectItemText>
|
||||
|
||||
@ -80,19 +80,30 @@ vi.mock('@/hooks/use-oauth', () => ({
|
||||
}))
|
||||
|
||||
vi.mock('../common-modal', () => ({
|
||||
CommonCreateModal: ({ createType, onClose, builder }: {
|
||||
CommonCreateModal: ({ open, createType, onClose, builder }: {
|
||||
open?: boolean
|
||||
createType: SupportedCreationMethods
|
||||
onClose: () => void
|
||||
builder?: TriggerSubscriptionBuilder
|
||||
}) => (
|
||||
<div
|
||||
data-testid="common-create-modal"
|
||||
data-create-type={createType}
|
||||
data-has-builder={!!builder}
|
||||
>
|
||||
<button data-testid="close-modal" onClick={onClose}>Close</button>
|
||||
</div>
|
||||
),
|
||||
}) => {
|
||||
if (open === false)
|
||||
return null
|
||||
|
||||
return (
|
||||
<div
|
||||
data-testid="common-create-modal"
|
||||
data-create-type={createType}
|
||||
data-has-builder={!!builder}
|
||||
>
|
||||
<button
|
||||
data-testid="close-modal"
|
||||
onClick={onClose}
|
||||
>
|
||||
Close
|
||||
</button>
|
||||
</div>
|
||||
)
|
||||
},
|
||||
}))
|
||||
|
||||
vi.mock('../oauth-client', () => ({
|
||||
|
||||
@ -23,12 +23,39 @@ import {
|
||||
} from './hooks/use-common-modal-state'
|
||||
|
||||
type Props = {
|
||||
open?: boolean
|
||||
onClose: () => void
|
||||
createType: SupportedCreationMethods
|
||||
builder?: TriggerSubscriptionBuilder
|
||||
}
|
||||
|
||||
export const CommonCreateModal = ({ onClose, createType, builder }: Props) => {
|
||||
export const CommonCreateModal = ({ open = true, onClose, createType, builder }: Props) => {
|
||||
return (
|
||||
<Dialog
|
||||
open={open}
|
||||
onOpenChange={nextOpen => !nextOpen && onClose()}
|
||||
disablePointerDismissal
|
||||
>
|
||||
<DialogContent
|
||||
backdropProps={{ forceRender: true }}
|
||||
className={cn(
|
||||
'flex max-h-[80%] min-h-[360px] flex-col overflow-hidden p-0 shadow-xs',
|
||||
createType === SupportedCreationMethods.MANUAL
|
||||
? 'w-[640px] max-w-[calc(100vw-2rem)]'
|
||||
: 'w-[480px] max-w-[calc(100vw-2rem)]',
|
||||
)}
|
||||
>
|
||||
<CommonCreateModalContent
|
||||
createType={createType}
|
||||
builder={builder}
|
||||
onClose={onClose}
|
||||
/>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
)
|
||||
}
|
||||
|
||||
function CommonCreateModalContent({ onClose, createType, builder }: Omit<Props, 'open'>) {
|
||||
const { t } = useTranslation()
|
||||
|
||||
const {
|
||||
@ -59,89 +86,77 @@ export const CommonCreateModal = ({ onClose, createType, builder }: Props) => {
|
||||
const modalSize = createType === SupportedCreationMethods.MANUAL ? 'md' : 'sm'
|
||||
|
||||
return (
|
||||
<Dialog open disablePointerDismissal>
|
||||
<DialogContent
|
||||
backdropProps={{ forceRender: true }}
|
||||
className={cn(
|
||||
'flex max-h-[80%] min-h-[360px] flex-col overflow-hidden p-0 shadow-xs',
|
||||
modalSize === 'md'
|
||||
? 'w-[640px] max-w-[calc(100vw-2rem)]'
|
||||
: 'w-[480px] max-w-[calc(100vw-2rem)]',
|
||||
<div
|
||||
className="flex min-h-0 flex-1 flex-col"
|
||||
data-testid="modal"
|
||||
data-size={modalSize}
|
||||
data-disabled={isDisabled}
|
||||
>
|
||||
<div className="relative shrink-0 p-6 pr-14 pb-3">
|
||||
<DialogTitle className="title-2xl-semi-bold text-text-primary" data-testid="modal-title">
|
||||
{t(MODAL_TITLE_KEY_MAP[createType], { ns: 'pluginTrigger' })}
|
||||
</DialogTitle>
|
||||
<DialogCloseButton
|
||||
className="top-5 right-5 h-8 w-8 rounded-lg [&>span]:h-5 [&>span]:w-5"
|
||||
data-testid="modal-close"
|
||||
onClick={onClose}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="min-h-0 flex-1 overflow-y-auto px-6 py-3">
|
||||
{isApiKeyType && <MultiSteps currentStep={currentStep} />}
|
||||
|
||||
{isVerifyStep && (
|
||||
<VerifyStepContent
|
||||
apiKeyCredentialsSchema={apiKeyCredentialsSchema}
|
||||
apiKeyCredentialsFormRef={formRefs.apiKeyCredentialsFormRef}
|
||||
onChange={handleApiKeyCredentialsChange}
|
||||
/>
|
||||
)}
|
||||
>
|
||||
<div
|
||||
className="flex min-h-0 flex-1 flex-col"
|
||||
data-testid="modal"
|
||||
data-size={modalSize}
|
||||
data-disabled={isDisabled}
|
||||
>
|
||||
<div className="relative shrink-0 p-6 pr-14 pb-3">
|
||||
<DialogTitle className="title-2xl-semi-bold text-text-primary" data-testid="modal-title">
|
||||
{t(MODAL_TITLE_KEY_MAP[createType], { ns: 'pluginTrigger' })}
|
||||
</DialogTitle>
|
||||
<DialogCloseButton
|
||||
className="top-5 right-5 h-8 w-8 rounded-lg [&>span]:h-5 [&>span]:w-5"
|
||||
data-testid="modal-close"
|
||||
onClick={onClose}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="min-h-0 flex-1 overflow-y-auto px-6 py-3">
|
||||
{isApiKeyType && <MultiSteps currentStep={currentStep} />}
|
||||
{isConfigurationStep && (
|
||||
<ConfigurationStepContent
|
||||
createType={createType}
|
||||
subscriptionBuilder={subscriptionBuilder}
|
||||
subscriptionFormRef={formRefs.subscriptionFormRef}
|
||||
autoCommonParametersSchema={autoCommonParametersSchema}
|
||||
autoCommonParametersFormRef={formRefs.autoCommonParametersFormRef}
|
||||
manualPropertiesSchema={manualPropertiesSchema}
|
||||
manualPropertiesFormRef={formRefs.manualPropertiesFormRef}
|
||||
onManualPropertiesChange={handleManualPropertiesChange}
|
||||
logs={logData?.logs || []}
|
||||
pluginId={detail?.plugin_id || ''}
|
||||
pluginName={detail?.name || ''}
|
||||
provider={detail?.provider || ''}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{isVerifyStep && (
|
||||
<VerifyStepContent
|
||||
apiKeyCredentialsSchema={apiKeyCredentialsSchema}
|
||||
apiKeyCredentialsFormRef={formRefs.apiKeyCredentialsFormRef}
|
||||
onChange={handleApiKeyCredentialsChange}
|
||||
/>
|
||||
)}
|
||||
|
||||
{isConfigurationStep && (
|
||||
<ConfigurationStepContent
|
||||
createType={createType}
|
||||
subscriptionBuilder={subscriptionBuilder}
|
||||
subscriptionFormRef={formRefs.subscriptionFormRef}
|
||||
autoCommonParametersSchema={autoCommonParametersSchema}
|
||||
autoCommonParametersFormRef={formRefs.autoCommonParametersFormRef}
|
||||
manualPropertiesSchema={manualPropertiesSchema}
|
||||
manualPropertiesFormRef={formRefs.manualPropertiesFormRef}
|
||||
onManualPropertiesChange={handleManualPropertiesChange}
|
||||
logs={logData?.logs || []}
|
||||
pluginId={detail?.plugin_id || ''}
|
||||
pluginName={detail?.name || ''}
|
||||
provider={detail?.provider || ''}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="flex shrink-0 justify-end p-6 pt-5">
|
||||
<div className="flex items-center">
|
||||
<Button
|
||||
disabled={isDisabled}
|
||||
onClick={onClose}
|
||||
>
|
||||
{t('operation.cancel', { ns: 'common' })}
|
||||
</Button>
|
||||
<Button
|
||||
className="ml-2"
|
||||
variant="primary"
|
||||
disabled={isDisabled}
|
||||
data-testid="modal-confirm"
|
||||
onClick={handleConfirm}
|
||||
>
|
||||
{confirmButtonText}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{isVerifyStep && (
|
||||
<div className="shrink-0">
|
||||
<EncryptedBottom />
|
||||
</div>
|
||||
)}
|
||||
<div className="flex shrink-0 justify-end p-6 pt-5">
|
||||
<div className="flex items-center">
|
||||
<Button
|
||||
disabled={isDisabled}
|
||||
onClick={onClose}
|
||||
>
|
||||
{t('operation.cancel', { ns: 'common' })}
|
||||
</Button>
|
||||
<Button
|
||||
className="ml-2"
|
||||
variant="primary"
|
||||
disabled={isDisabled}
|
||||
data-testid="modal-confirm"
|
||||
onClick={handleConfirm}
|
||||
>
|
||||
{confirmButtonText}
|
||||
</Button>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</div>
|
||||
|
||||
{isVerifyStep && (
|
||||
<div className="shrink-0">
|
||||
<EncryptedBottom />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@ -4,7 +4,6 @@ import { cn } from '@langgenius/dify-ui/cn'
|
||||
import { Select, SelectContent, SelectItem, SelectItemIndicator, SelectTrigger } from '@langgenius/dify-ui/select'
|
||||
import { toast } from '@langgenius/dify-ui/toast'
|
||||
import { Tooltip, TooltipContent, TooltipTrigger } from '@langgenius/dify-ui/tooltip'
|
||||
import { RiAddLine, RiEqualizer2Line } from '@remixicon/react'
|
||||
import { useBoolean } from 'ahooks'
|
||||
import { useCallback, useMemo, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
@ -40,6 +39,7 @@ export const CreateSubscriptionButton = ({ buttonType = CreateButtonType.FULL_BU
|
||||
const { subscriptions } = useSubscriptionList()
|
||||
const subscriptionCount = subscriptions?.length || 0
|
||||
const [selectedCreateInfo, setSelectedCreateInfo] = useState<{ type: SupportedCreationMethods, builder?: TriggerSubscriptionBuilder } | null>(null)
|
||||
const [isCreateModalOpen, setIsCreateModalOpen] = useState(false)
|
||||
|
||||
const detail = usePluginStore(state => state.detail)
|
||||
const [isMenuOpen, setIsMenuOpen] = useState(false)
|
||||
@ -89,8 +89,11 @@ export const CreateSubscriptionButton = ({ buttonType = CreateButtonType.FULL_BU
|
||||
<Tooltip>
|
||||
<TooltipTrigger
|
||||
render={(
|
||||
<ActionButton onClick={onClickClientSettings}>
|
||||
<RiEqualizer2Line className="h-4 w-4 text-text-tertiary" />
|
||||
<ActionButton
|
||||
aria-label={t('subscription.addType.options.oauth.clientSettings', { ns: 'pluginTrigger' })}
|
||||
onClick={onClickClientSettings}
|
||||
>
|
||||
<span aria-hidden className="i-ri-equalizer-2-line h-4 w-4 text-text-tertiary" />
|
||||
</ActionButton>
|
||||
)}
|
||||
/>
|
||||
@ -126,12 +129,21 @@ export const CreateSubscriptionButton = ({ buttonType = CreateButtonType.FULL_BU
|
||||
show: supportedMethods.includes(SupportedCreationMethods.MANUAL),
|
||||
},
|
||||
]
|
||||
}, [t, oauthConfig, supportedMethods, methodType, onClickClientSettings])
|
||||
}, [t, oauthConfig, supportedMethods, onClickClientSettings])
|
||||
const visibleOptions = useMemo(() => {
|
||||
return allOptions.filter(option => option.show)
|
||||
}, [allOptions])
|
||||
const shouldAllowSelect = methodType === DEFAULT_METHOD || (methodType === SupportedCreationMethods.OAUTH && supportedMethods.length === 1)
|
||||
|
||||
const showCreateModal = useCallback((createInfo: { type: SupportedCreationMethods, builder?: TriggerSubscriptionBuilder }) => {
|
||||
setSelectedCreateInfo(createInfo)
|
||||
setIsCreateModalOpen(true)
|
||||
}, [])
|
||||
|
||||
const hideCreateModal = useCallback(() => {
|
||||
setIsCreateModalOpen(false)
|
||||
}, [])
|
||||
|
||||
const onChooseCreateType = async (type: SupportedCreationMethods) => {
|
||||
if (type === SupportedCreationMethods.OAUTH) {
|
||||
if (oauthConfig?.configured) {
|
||||
@ -140,7 +152,10 @@ export const CreateSubscriptionButton = ({ buttonType = CreateButtonType.FULL_BU
|
||||
openOAuthPopup(response.authorization_url, (callbackData) => {
|
||||
if (callbackData) {
|
||||
toast.success(t('modal.oauth.authorization.authSuccess', { ns: 'pluginTrigger' }))
|
||||
setSelectedCreateInfo({ type: SupportedCreationMethods.OAUTH, builder: response.subscription_builder })
|
||||
showCreateModal({
|
||||
type: SupportedCreationMethods.OAUTH,
|
||||
builder: response.subscription_builder,
|
||||
})
|
||||
}
|
||||
})
|
||||
},
|
||||
@ -154,7 +169,9 @@ export const CreateSubscriptionButton = ({ buttonType = CreateButtonType.FULL_BU
|
||||
}
|
||||
}
|
||||
else {
|
||||
setSelectedCreateInfo({ type })
|
||||
showCreateModal({
|
||||
type,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@ -202,7 +219,7 @@ export const CreateSubscriptionButton = ({ buttonType = CreateButtonType.FULL_BU
|
||||
onClick={onClickCreate}
|
||||
>
|
||||
<div className="flex flex-1 items-center justify-center">
|
||||
<RiAddLine className="mr-2 size-4" />
|
||||
<span aria-hidden className="mr-2 i-ri-add-line size-4" />
|
||||
{buttonTextMap[methodType!]}
|
||||
{methodType === SupportedCreationMethods.OAUTH && oauthConfig?.custom_enabled && oauthConfig?.custom_configured && (
|
||||
<Badge
|
||||
@ -220,7 +237,7 @@ export const CreateSubscriptionButton = ({ buttonType = CreateButtonType.FULL_BU
|
||||
<TooltipTrigger
|
||||
render={(
|
||||
<div onClick={onClickClientSettings} className="p-2">
|
||||
<RiEqualizer2Line className="size-4 text-components-button-primary-text" />
|
||||
<span aria-hidden className="i-ri-equalizer-2-line size-4 text-components-button-primary-text" />
|
||||
</div>
|
||||
)}
|
||||
/>
|
||||
@ -238,6 +255,7 @@ export const CreateSubscriptionButton = ({ buttonType = CreateButtonType.FULL_BU
|
||||
disabled={!(supportedMethods?.length === 1 || subscriptionCount >= MAX_COUNT)}
|
||||
render={(
|
||||
<ActionButton
|
||||
aria-label={buttonTextMap[methodType!]}
|
||||
onClick={onClickCreate}
|
||||
className={cn(
|
||||
'float-right',
|
||||
@ -245,7 +263,7 @@ export const CreateSubscriptionButton = ({ buttonType = CreateButtonType.FULL_BU
|
||||
)}
|
||||
state={subscriptionCount >= MAX_COUNT ? ActionButtonState.Disabled : ActionButtonState.Default}
|
||||
>
|
||||
<RiAddLine className="size-4" />
|
||||
<span aria-hidden className="i-ri-add-line size-4" />
|
||||
</ActionButton>
|
||||
)}
|
||||
/>
|
||||
@ -255,7 +273,7 @@ export const CreateSubscriptionButton = ({ buttonType = CreateButtonType.FULL_BU
|
||||
</Tooltip>
|
||||
)}
|
||||
</SelectTrigger>
|
||||
<SelectContent placement="bottom-start" sideOffset={4} popupClassName={cn('z-1000', buttonType === CreateButtonType.FULL_BUTTON && 'min-w-(--anchor-width)')}>
|
||||
<SelectContent placement="bottom-start" sideOffset={4}>
|
||||
{visibleOptions.map(option => (
|
||||
<SelectItem key={option.value} value={option.value}>
|
||||
<div className="mr-8 flex grow items-center gap-1 truncate px-1">
|
||||
@ -268,23 +286,33 @@ export const CreateSubscriptionButton = ({ buttonType = CreateButtonType.FULL_BU
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
{selectedCreateInfo && (
|
||||
<CommonCreateModal
|
||||
createType={selectedCreateInfo.type}
|
||||
builder={selectedCreateInfo.builder}
|
||||
onClose={() => setSelectedCreateInfo(null)}
|
||||
/>
|
||||
)}
|
||||
{isShowClientSettingsModal && (
|
||||
<OAuthClientSettingsModal
|
||||
oauthConfig={oauthConfig}
|
||||
onClose={() => {
|
||||
hideClientSettingsModal()
|
||||
refetchOAuthConfig()
|
||||
}}
|
||||
showOAuthCreateModal={builder => setSelectedCreateInfo({ type: SupportedCreationMethods.OAUTH, builder })}
|
||||
/>
|
||||
)}
|
||||
{selectedCreateInfo
|
||||
? (
|
||||
<CommonCreateModal
|
||||
open={isCreateModalOpen}
|
||||
createType={selectedCreateInfo.type}
|
||||
builder={selectedCreateInfo.builder}
|
||||
onClose={hideCreateModal}
|
||||
/>
|
||||
)
|
||||
: null}
|
||||
{isShowClientSettingsModal
|
||||
? (
|
||||
<OAuthClientSettingsModal
|
||||
oauthConfig={oauthConfig}
|
||||
onClose={() => {
|
||||
hideClientSettingsModal()
|
||||
refetchOAuthConfig()
|
||||
}}
|
||||
showOAuthCreateModal={(builder) => {
|
||||
showCreateModal({
|
||||
type: SupportedCreationMethods.OAUTH,
|
||||
builder,
|
||||
})
|
||||
}}
|
||||
/>
|
||||
)
|
||||
: null}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
@ -110,7 +110,6 @@ export const SubscriptionSelectorEntry = ({ selectedId, onSelect }: {
|
||||
placement="bottom-start"
|
||||
sideOffset={4}
|
||||
popupClassName="border-none bg-transparent p-0 shadow-none backdrop-blur-none"
|
||||
positionerProps={{ style: { zIndex: 11 } }}
|
||||
>
|
||||
<div className="rounded-xl border border-components-panel-border bg-components-panel-bg shadow-lg">
|
||||
<SubscriptionList
|
||||
|
||||
@ -233,7 +233,7 @@ const ReasoningConfigForm: React.FC<Props> = ({
|
||||
<SelectTrigger className="h-8 grow">
|
||||
{selectedOption?.name ?? placeholder?.[language] ?? placeholder?.en_US}
|
||||
</SelectTrigger>
|
||||
<SelectContent popupClassName="w-(--anchor-width)">
|
||||
<SelectContent>
|
||||
{pickerProps.selectItems.map(item => (
|
||||
<SelectItem key={item.value} value={String(item.value)}>
|
||||
<SelectItemText>{item.name}</SelectItemText>
|
||||
@ -278,7 +278,6 @@ const ReasoningConfigForm: React.FC<Props> = ({
|
||||
)}
|
||||
{showVariableSelector && (
|
||||
<VarReferencePicker
|
||||
zIndex={1001}
|
||||
className="h-8 grow"
|
||||
readonly={false}
|
||||
isShowNodeName
|
||||
|
||||
@ -58,7 +58,6 @@ const StrategyPicker = ({
|
||||
<DropdownMenuContent
|
||||
placement="top-end"
|
||||
sideOffset={4}
|
||||
className="z-99"
|
||||
popupClassName="w-[280px] p-1"
|
||||
>
|
||||
<DropdownMenuRadioGroup
|
||||
|
||||
@ -101,7 +101,6 @@ const ToolPicker: FC<Props> = ({
|
||||
placement="top"
|
||||
sideOffset={0}
|
||||
popupClassName="border-none bg-transparent p-0 shadow-none backdrop-blur-none"
|
||||
positionerProps={{ style: { zIndex: 1000 } }}
|
||||
>
|
||||
<div className="relative min-h-20 w-full rounded-xl border-[0.5px] border-components-panel-border bg-components-panel-bg-blur pb-2 shadow-lg backdrop-blur-xs">
|
||||
<div className="p-2 pb-1">
|
||||
|
||||
@ -138,7 +138,7 @@ const RunOnce: FC<IRunOnceProps> = ({
|
||||
<SelectTrigger className="w-full">
|
||||
{String(inputs[item.key] || item.default || t('placeholder.select', { ns: 'common' }))}
|
||||
</SelectTrigger>
|
||||
<SelectContent popupClassName="w-(--anchor-width)">
|
||||
<SelectContent>
|
||||
{(item.options || []).map(option => (
|
||||
<SelectItem key={option} value={option}>
|
||||
<SelectItemText>{option}</SelectItemText>
|
||||
|
||||
@ -80,7 +80,6 @@ const LabelSelector: FC<LabelSelectorProps> = ({
|
||||
placement="bottom-start"
|
||||
sideOffset={4}
|
||||
popupClassName="border-none bg-transparent p-0 shadow-none backdrop-blur-none"
|
||||
positionerProps={{ style: { zIndex: 1040 } }}
|
||||
>
|
||||
<div className="relative w-[591px] rounded-lg border-[0.5px] border-components-panel-border bg-components-panel-bg-blur shadow-lg backdrop-blur-[5px]">
|
||||
<div className="border-b-[0.5px] border-divider-regular p-2">
|
||||
|
||||
@ -51,7 +51,6 @@ const MethodSelector: FC<MethodSelectorProps> = ({
|
||||
<PopoverContent
|
||||
placement="bottom-start"
|
||||
sideOffset={4}
|
||||
positionerProps={{ style: { zIndex: 1040 } }}
|
||||
>
|
||||
<div className="relative w-[320px]">
|
||||
<div className="p-1">
|
||||
|
||||
@ -535,7 +535,7 @@ const MentionInputInner = forwardRef<HTMLTextAreaElement, MentionInputProps>(({
|
||||
<Textarea
|
||||
ref={textareaRef}
|
||||
className={cn(
|
||||
'relative z-10 w-full resize-none bg-transparent p-1 body-lg-regular leading-6 text-transparent caret-primary-500 outline-none',
|
||||
'relative z-10 w-full resize-none bg-transparent p-1 body-lg-regular leading-6 text-transparent caret-primary-500 outline-hidden',
|
||||
'placeholder:text-text-tertiary',
|
||||
)}
|
||||
style={{ paddingRight, paddingBottom }}
|
||||
|
||||
@ -545,7 +545,7 @@ export const CommentThread: FC<CommentThreadProps> = memo(({
|
||||
<DropdownMenuContent
|
||||
placement="bottom-end"
|
||||
sideOffset={4}
|
||||
popupClassName="z-[100] w-36 rounded-xl border-[0.5px] border-components-panel-border bg-components-panel-bg-blur shadow-lg backdrop-blur-[10px]"
|
||||
popupClassName="w-36 rounded-xl border-[0.5px] border-components-panel-border bg-components-panel-bg-blur shadow-lg backdrop-blur-[10px]"
|
||||
>
|
||||
<button
|
||||
className="flex w-full items-center justify-start rounded-xl px-3 py-2 text-left text-sm text-text-secondary hover:bg-state-base-hover"
|
||||
@ -635,7 +635,7 @@ export const CommentThread: FC<CommentThreadProps> = memo(({
|
||||
<DropdownMenuContent
|
||||
placement="bottom-end"
|
||||
sideOffset={4}
|
||||
popupClassName="z-[100] w-36 rounded-xl border-[0.5px] border-components-panel-border bg-components-panel-bg-blur shadow-lg backdrop-blur-[10px]"
|
||||
popupClassName="w-36 rounded-xl border-[0.5px] border-components-panel-border bg-components-panel-bg-blur shadow-lg backdrop-blur-[10px]"
|
||||
data-reply-menu
|
||||
>
|
||||
<div className={cn(deletingReplyId === reply.id ? 'hidden' : 'block')}>
|
||||
|
||||
@ -1,15 +1,18 @@
|
||||
'use client'
|
||||
import type { EnvironmentVariable } from '@/app/components/workflow/types'
|
||||
import { Button } from '@langgenius/dify-ui/button'
|
||||
import {
|
||||
AlertDialog,
|
||||
AlertDialogActions,
|
||||
AlertDialogCancelButton,
|
||||
AlertDialogConfirmButton,
|
||||
AlertDialogContent,
|
||||
AlertDialogTitle,
|
||||
} from '@langgenius/dify-ui/alert-dialog'
|
||||
import { cn } from '@langgenius/dify-ui/cn'
|
||||
import { RiCloseLine, RiLock2Line } from '@remixicon/react'
|
||||
import { noop } from 'es-toolkit/function'
|
||||
import * as React from 'react'
|
||||
import { useCallback, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import Checkbox from '@/app/components/base/checkbox'
|
||||
import { Env } from '@/app/components/base/icons/src/vender/line/others'
|
||||
import Modal from '@/app/components/base/modal'
|
||||
|
||||
export type DSLExportConfirmModalProps = {
|
||||
envList: EnvironmentVariable[]
|
||||
@ -17,11 +20,16 @@ export type DSLExportConfirmModalProps = {
|
||||
onClose: () => void
|
||||
}
|
||||
|
||||
const DSLExportConfirmModal = ({
|
||||
type DSLExportConfirmContentProps = DSLExportConfirmModalProps & {
|
||||
onExportingChange?: (isExporting: boolean) => void
|
||||
}
|
||||
|
||||
export const DSLExportConfirmContent = ({
|
||||
envList = [],
|
||||
onConfirm,
|
||||
onClose,
|
||||
}: DSLExportConfirmModalProps) => {
|
||||
onExportingChange,
|
||||
}: DSLExportConfirmContentProps) => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
const [exportSecrets, setExportSecrets] = useState<boolean>(false)
|
||||
@ -32,73 +40,75 @@ const DSLExportConfirmModal = ({
|
||||
return
|
||||
|
||||
setIsExporting(true)
|
||||
onExportingChange?.(true)
|
||||
try {
|
||||
await onConfirm(exportSecrets)
|
||||
onClose()
|
||||
}
|
||||
finally {
|
||||
setIsExporting(false)
|
||||
onExportingChange?.(false)
|
||||
}
|
||||
}, [exportSecrets, isExporting, onClose, onConfirm])
|
||||
}, [exportSecrets, isExporting, onClose, onConfirm, onExportingChange])
|
||||
|
||||
return (
|
||||
<Modal
|
||||
isShow={true}
|
||||
onClose={noop}
|
||||
className={cn('w-[480px] max-w-[480px]')}
|
||||
>
|
||||
<div className="relative pb-6 title-2xl-semi-bold text-text-primary">{t('env.export.title', { ns: 'workflow' })}</div>
|
||||
<div
|
||||
className={cn('absolute top-4 right-4 p-2', !isExporting && 'cursor-pointer')}
|
||||
onClick={() => !isExporting && onClose()}
|
||||
>
|
||||
<RiCloseLine className="h-4 w-4 text-text-tertiary" />
|
||||
</div>
|
||||
<div className="relative">
|
||||
<table className="w-full border-separate border-spacing-0 rounded-lg border border-divider-regular shadow-xs">
|
||||
<thead className="system-xs-medium-uppercase text-text-tertiary">
|
||||
<tr>
|
||||
<td width={220} className="h-7 border-r border-b border-divider-regular pl-3">{t('env.export.name', { ns: 'workflow' })}</td>
|
||||
<td className="h-7 border-b border-divider-regular pl-3">{t('env.export.value', { ns: 'workflow' })}</td>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{envList.map((env, index) => (
|
||||
<tr key={env.name}>
|
||||
<td className={cn('h-7 border-r pl-3 system-xs-medium', index + 1 !== envList.length && 'border-b')}>
|
||||
<div className="flex w-[200px] items-center gap-1">
|
||||
<Env className="h-4 w-4 shrink-0 text-util-colors-violet-violet-600" />
|
||||
<div className="truncate text-text-primary">{env.name}</div>
|
||||
<div className="shrink-0 text-text-tertiary">{t('env.export.secret', { ns: 'workflow' })}</div>
|
||||
<RiLock2Line className="h-3 w-3 shrink-0 text-text-tertiary" />
|
||||
</div>
|
||||
</td>
|
||||
<td className={cn('h-7 pl-3', index + 1 !== envList.length && 'border-b')}>
|
||||
<div className="truncate system-xs-regular text-text-secondary">{env.value}</div>
|
||||
</td>
|
||||
<AlertDialogContent className="w-120 max-w-120">
|
||||
<div className="px-6 pt-6">
|
||||
<AlertDialogTitle className="pb-6 title-2xl-semi-bold text-text-primary">
|
||||
{t('env.export.title', { ns: 'workflow' })}
|
||||
</AlertDialogTitle>
|
||||
<div className="relative">
|
||||
<table className="w-full border-separate border-spacing-0 rounded-lg border border-divider-regular shadow-xs">
|
||||
<thead className="system-xs-medium-uppercase text-text-tertiary">
|
||||
<tr>
|
||||
<td width={220} className="h-7 border-r border-b border-divider-regular pl-3">{t('env.export.name', { ns: 'workflow' })}</td>
|
||||
<td className="h-7 border-b border-divider-regular pl-3">{t('env.export.value', { ns: 'workflow' })}</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div className="mt-4 flex gap-2">
|
||||
<Checkbox
|
||||
className="shrink-0"
|
||||
checked={exportSecrets}
|
||||
disabled={isExporting}
|
||||
onCheck={() => setExportSecrets(!exportSecrets)}
|
||||
/>
|
||||
<div
|
||||
className={cn('system-sm-medium text-text-primary', !isExporting && 'cursor-pointer')}
|
||||
onClick={() => !isExporting && setExportSecrets(!exportSecrets)}
|
||||
>
|
||||
{t('env.export.checkbox', { ns: 'workflow' })}
|
||||
</thead>
|
||||
<tbody>
|
||||
{envList.map((env, index) => (
|
||||
<tr key={env.name}>
|
||||
<td className={cn('h-7 border-r border-divider-regular pl-3 system-xs-medium', index + 1 !== envList.length && 'border-b border-divider-regular')}>
|
||||
<div className="flex w-50 items-center gap-1">
|
||||
<span aria-hidden="true" className="i-custom-vender-line-others-env h-4 w-4 shrink-0 text-util-colors-violet-violet-600" />
|
||||
<div className="truncate text-text-primary">{env.name}</div>
|
||||
<div className="shrink-0 text-text-tertiary">{t('env.export.secret', { ns: 'workflow' })}</div>
|
||||
<span aria-hidden="true" className="i-ri-lock-2-line h-3 w-3 shrink-0 text-text-tertiary" />
|
||||
</div>
|
||||
</td>
|
||||
<td className={cn('h-7 pl-3', index + 1 !== envList.length && 'border-b border-divider-regular')}>
|
||||
<div className="truncate system-xs-regular text-text-secondary">{env.value}</div>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div className="mt-4 flex gap-2">
|
||||
<Checkbox
|
||||
className="shrink-0"
|
||||
checked={exportSecrets}
|
||||
disabled={isExporting}
|
||||
onCheck={() => setExportSecrets(!exportSecrets)}
|
||||
ariaLabelledBy="dsl-export-secrets-checkbox-label"
|
||||
/>
|
||||
<button
|
||||
id="dsl-export-secrets-checkbox-label"
|
||||
type="button"
|
||||
disabled={isExporting}
|
||||
className="cursor-pointer rounded-sm text-left system-sm-medium text-text-primary outline-hidden focus-visible:ring-1 focus-visible:ring-components-input-border-hover disabled:cursor-not-allowed disabled:opacity-50"
|
||||
onClick={() => setExportSecrets(!exportSecrets)}
|
||||
>
|
||||
{t('env.export.checkbox', { ns: 'workflow' })}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-row-reverse pt-6">
|
||||
<Button
|
||||
className="ml-2"
|
||||
variant="primary"
|
||||
<AlertDialogActions>
|
||||
<AlertDialogCancelButton disabled={isExporting}>
|
||||
{t('operation.cancel', { ns: 'common' })}
|
||||
</AlertDialogCancelButton>
|
||||
<AlertDialogConfirmButton
|
||||
tone="default"
|
||||
loading={isExporting}
|
||||
disabled={isExporting}
|
||||
onClick={submit}
|
||||
@ -108,10 +118,28 @@ const DSLExportConfirmModal = ({
|
||||
: exportSecrets
|
||||
? t('env.export.export', { ns: 'workflow' })
|
||||
: t('env.export.ignore', { ns: 'workflow' })}
|
||||
</Button>
|
||||
<Button disabled={isExporting} onClick={onClose}>{t('operation.cancel', { ns: 'common' })}</Button>
|
||||
</div>
|
||||
</Modal>
|
||||
</AlertDialogConfirmButton>
|
||||
</AlertDialogActions>
|
||||
</AlertDialogContent>
|
||||
)
|
||||
}
|
||||
|
||||
const DSLExportConfirmModal = (props: DSLExportConfirmModalProps) => {
|
||||
const { envList, onClose } = props
|
||||
const [isExporting, setIsExporting] = useState(false)
|
||||
const isDialogOpen = envList.length > 0
|
||||
|
||||
const handleOpenChange = useCallback((open: boolean) => {
|
||||
if (open || isExporting)
|
||||
return
|
||||
|
||||
onClose()
|
||||
}, [isExporting, onClose])
|
||||
|
||||
return (
|
||||
<AlertDialog open={isDialogOpen} onOpenChange={handleOpenChange}>
|
||||
<DSLExportConfirmContent {...props} onExportingChange={setIsExporting} />
|
||||
</AlertDialog>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@ -231,7 +231,6 @@ export const AgentStrategySelector = memo((props: AgentStrategySelectorProps) =>
|
||||
placement="bottom"
|
||||
sideOffset={0}
|
||||
popupClassName="border-none bg-transparent p-0 shadow-none backdrop-blur-none"
|
||||
positionerProps={{ style: { zIndex: 10 } }}
|
||||
>
|
||||
<div className="w-[388px] overflow-hidden rounded-md border-[0.5px] border-components-panel-border bg-components-panel-bg-blur shadow">
|
||||
<header className="flex gap-1 p-2">
|
||||
|
||||
@ -191,7 +191,7 @@ const FormItem: FC<Props> = ({
|
||||
<SelectTrigger className="w-full">
|
||||
{String(value || payload.default || t('placeholder.select', { ns: 'common' }))}
|
||||
</SelectTrigger>
|
||||
<SelectContent popupClassName="w-(--anchor-width)">
|
||||
<SelectContent>
|
||||
{(payload.options || []).map(option => (
|
||||
<SelectItem key={option} value={option}>
|
||||
<SelectItemText>{option}</SelectItemText>
|
||||
|
||||
@ -65,7 +65,6 @@ const FormInputItem: FC<Props> = ({
|
||||
schema,
|
||||
value,
|
||||
onChange,
|
||||
inPanel,
|
||||
currentTool,
|
||||
currentProvider,
|
||||
showManageInputField,
|
||||
@ -329,7 +328,7 @@ const FormInputItem: FC<Props> = ({
|
||||
<SelectTrigger className="h-8 grow">
|
||||
{selectedStaticOption?.name ?? placeholder?.[language] ?? placeholder?.en_US}
|
||||
</SelectTrigger>
|
||||
<SelectContent popupClassName="w-(--anchor-width)">
|
||||
<SelectContent>
|
||||
{staticSelectItems.map(item => (
|
||||
<SelectItem key={item.value} value={item.value}>
|
||||
{item.icon && (
|
||||
@ -361,7 +360,7 @@ const FormInputItem: FC<Props> = ({
|
||||
<SelectTrigger className="h-8 grow">
|
||||
{selectedDynamicOption?.name ?? (isLoadingOptions ? 'Loading...' : (placeholder?.[language] ?? placeholder?.en_US))}
|
||||
</SelectTrigger>
|
||||
<SelectContent popupClassName="w-(--anchor-width)">
|
||||
<SelectContent>
|
||||
{dynamicSelectItems.map(item => (
|
||||
<SelectItem key={item.value} value={item.value}>
|
||||
{item.icon && (
|
||||
@ -413,7 +412,6 @@ const FormInputItem: FC<Props> = ({
|
||||
)}
|
||||
{showVariableSelector && (
|
||||
<VarReferencePicker
|
||||
zIndex={inPanel ? 1000 : undefined}
|
||||
className="h-8 grow"
|
||||
readonly={readOnly}
|
||||
isShowNodeName
|
||||
|
||||
@ -66,7 +66,7 @@ const ConstantField: FC<Props> = ({
|
||||
>
|
||||
{selectedOption?.name ?? placeholder?.[language] ?? placeholder?.en_US}
|
||||
</SelectTrigger>
|
||||
<SelectContent popupClassName="w-(--anchor-width)">
|
||||
<SelectContent>
|
||||
{selectOptions.map(option => (
|
||||
<SelectItem key={option.value} value={option.value}>
|
||||
<SelectItemText>{option.name}</SelectItemText>
|
||||
|
||||
@ -43,7 +43,7 @@ const Field: FC<Props> = ({
|
||||
disabled={depth !== MAX_DEPTH + 1}
|
||||
render={(
|
||||
<div
|
||||
className={cn('flex items-center justify-between rounded-md pr-2 outline-none focus:outline-none focus-visible:outline-none', !readonly && 'hover:bg-state-base-hover', depth !== MAX_DEPTH + 1 && 'cursor-pointer')}
|
||||
className={cn('flex items-center justify-between rounded-md pr-2 outline-hidden focus:outline-hidden focus-visible:outline-hidden', !readonly && 'hover:bg-state-base-hover', depth !== MAX_DEPTH + 1 && 'cursor-pointer')}
|
||||
onMouseDown={() => !readonly && onSelect?.([...valueSelector, name])}
|
||||
>
|
||||
<div className="flex grow items-stretch">
|
||||
|
||||
@ -82,7 +82,6 @@ type Props = {
|
||||
placeholder?: string
|
||||
minWidth?: number
|
||||
popupFor?: 'assigned' | 'toAssigned'
|
||||
zIndex?: number
|
||||
currentTool?: Tool
|
||||
currentProvider?: ToolWithProvider | TriggerWithProvider
|
||||
preferSchemaType?: boolean
|
||||
@ -117,7 +116,6 @@ const VarReferencePicker: FC<Props> = ({
|
||||
placeholder,
|
||||
minWidth,
|
||||
popupFor,
|
||||
zIndex,
|
||||
currentTool,
|
||||
currentProvider,
|
||||
preferSchemaType,
|
||||
@ -415,11 +413,6 @@ const VarReferencePicker: FC<Props> = ({
|
||||
sideOffset={0}
|
||||
className="mt-1"
|
||||
popupClassName="border-none bg-transparent p-0 shadow-none backdrop-blur-none"
|
||||
positionerProps={{
|
||||
style: {
|
||||
zIndex: zIndex || 100,
|
||||
},
|
||||
}}
|
||||
>
|
||||
{!isConstant && (
|
||||
<VarReferencePopup
|
||||
@ -428,7 +421,6 @@ const VarReferencePicker: FC<Props> = ({
|
||||
onChange={handleVarReferenceChange}
|
||||
itemWidth={isAddBtnTrigger ? 260 : (minWidth || triggerWidth)}
|
||||
isSupportFileVar={isSupportFileVar}
|
||||
zIndex={zIndex}
|
||||
preferSchemaType={preferSchemaType}
|
||||
/>
|
||||
)}
|
||||
|
||||
@ -14,7 +14,6 @@ type Props = {
|
||||
onChange: (value: ValueSelector, varDetail: Var) => void
|
||||
itemWidth?: number
|
||||
isSupportFileVar?: boolean
|
||||
zIndex?: number
|
||||
preferSchemaType?: boolean
|
||||
}
|
||||
const VarReferencePopup: FC<Props> = ({
|
||||
@ -23,7 +22,6 @@ const VarReferencePopup: FC<Props> = ({
|
||||
onChange,
|
||||
itemWidth,
|
||||
isSupportFileVar = true,
|
||||
zIndex,
|
||||
preferSchemaType,
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
@ -68,7 +66,6 @@ const VarReferencePopup: FC<Props> = ({
|
||||
onChange={onChange}
|
||||
itemWidth={itemWidth}
|
||||
isSupportFileVar={isSupportFileVar}
|
||||
zIndex={zIndex}
|
||||
showManageInputField={showManageRagInputFields}
|
||||
onManageInputField={() => setShowInputFieldPanel?.(true)}
|
||||
preferSchemaType={preferSchemaType}
|
||||
|
||||
@ -78,7 +78,6 @@ type ItemProps = {
|
||||
isLoopVar?: boolean
|
||||
isFlat?: boolean
|
||||
isInCodeGeneratorInstructionEditor?: boolean
|
||||
zIndex?: number
|
||||
className?: string
|
||||
preferSchemaType?: boolean
|
||||
isSelected?: boolean
|
||||
@ -97,7 +96,6 @@ const Item: FC<ItemProps> = ({
|
||||
isLoopVar,
|
||||
isFlat,
|
||||
isInCodeGeneratorInstructionEditor,
|
||||
zIndex,
|
||||
className,
|
||||
preferSchemaType,
|
||||
isSelected,
|
||||
@ -211,7 +209,7 @@ const Item: FC<ItemProps> = ({
|
||||
className={cn(
|
||||
(isObj || isStructureOutput) ? 'pr-1' : 'pr-[18px]',
|
||||
(isHovering || isSelected) && ((isObj || isStructureOutput) ? 'bg-components-panel-on-panel-item-bg-hover' : 'bg-state-base-hover'),
|
||||
'relative flex h-6 w-full cursor-pointer items-center rounded-md pl-3 outline-none focus:outline-none focus-visible:outline-none',
|
||||
'relative flex h-6 w-full cursor-pointer items-center rounded-md pl-3 outline-hidden focus:outline-hidden focus-visible:outline-hidden',
|
||||
className,
|
||||
)}
|
||||
data-selected={isSelected ? 'true' : 'false'}
|
||||
@ -265,11 +263,6 @@ const Item: FC<ItemProps> = ({
|
||||
placement="left-start"
|
||||
sideOffset={0}
|
||||
popupClassName={cn(VAR_REFERENCE_CHILD_POPUP_CLASS_NAME, 'border-none bg-transparent p-0 shadow-none backdrop-blur-none')}
|
||||
positionerProps={{
|
||||
style: {
|
||||
zIndex: zIndex || 100,
|
||||
},
|
||||
}}
|
||||
>
|
||||
{(isStructureOutput || isObj) && (
|
||||
<PickerStructurePanel
|
||||
@ -297,7 +290,6 @@ type Props = {
|
||||
maxHeightClass?: string
|
||||
onClose?: () => void
|
||||
onBlur?: () => void
|
||||
zIndex?: number
|
||||
isInCodeGeneratorInstructionEditor?: boolean
|
||||
showManageInputField?: boolean
|
||||
onManageInputField?: () => void
|
||||
@ -315,7 +307,6 @@ const VarReferenceVars: FC<Props> = ({
|
||||
maxHeightClass,
|
||||
onClose,
|
||||
onBlur,
|
||||
zIndex,
|
||||
isInCodeGeneratorInstructionEditor,
|
||||
showManageInputField,
|
||||
onManageInputField,
|
||||
@ -490,7 +481,6 @@ const VarReferenceVars: FC<Props> = ({
|
||||
isLoopVar={item.isLoop}
|
||||
isFlat={item.isFlat}
|
||||
isInCodeGeneratorInstructionEditor={isInCodeGeneratorInstructionEditor}
|
||||
zIndex={zIndex}
|
||||
preferSchemaType={preferSchemaType}
|
||||
isSelected={effectiveSelectedIndex === optionIndex}
|
||||
onActivate={() => setSelectedIndex(optionIndex)}
|
||||
|
||||
@ -51,7 +51,6 @@ const DependencyPicker: FC<Props> = ({
|
||||
placement="bottom-start"
|
||||
sideOffset={4}
|
||||
popupClassName="border-none bg-transparent p-0 shadow-none backdrop-blur-none"
|
||||
positionerProps={{ style: { zIndex: 100 } }}
|
||||
>
|
||||
<div
|
||||
className="rounded-lg bg-white p-1 shadow-sm"
|
||||
|
||||
@ -155,12 +155,7 @@ const EmailInput = ({
|
||||
sideOffset={4}
|
||||
alignOffset={-40}
|
||||
popupClassName="border-none bg-transparent p-0 shadow-none backdrop-blur-none"
|
||||
positionerProps={{
|
||||
anchor: inputRef,
|
||||
style: {
|
||||
zIndex: 1000,
|
||||
},
|
||||
}}
|
||||
positionerProps={{ anchor: inputRef }}
|
||||
>
|
||||
<MemberList
|
||||
searchValue={searchKey}
|
||||
|
||||
@ -58,7 +58,6 @@ const MemberSelector: FC<Props> = ({
|
||||
sideOffset={4}
|
||||
alignOffset={35}
|
||||
popupClassName="border-none bg-transparent p-0 shadow-none backdrop-blur-none"
|
||||
positionerProps={{ style: { zIndex: 1000 } }}
|
||||
>
|
||||
<MemberList
|
||||
searchValue={searchValue}
|
||||
|
||||
@ -63,7 +63,6 @@ const ConditionAdd = ({
|
||||
placement="bottom-start"
|
||||
sideOffset={4}
|
||||
popupClassName="border-none bg-transparent p-0 shadow-none backdrop-blur-none"
|
||||
positionerProps={{ style: { zIndex: 1000 } }}
|
||||
>
|
||||
<div className="w-[296px] rounded-lg border-[0.5px] border-components-panel-border bg-components-panel-bg-blur shadow-lg">
|
||||
<VarReferenceVars
|
||||
|
||||
@ -373,7 +373,7 @@ const ConditionItem = ({
|
||||
<SelectTrigger className="h-8 rounded-t-none border-0 px-2 text-xs hover:bg-components-input-bg-normal focus-visible:bg-components-input-bg-normal">
|
||||
{selectedSelectOption?.name ?? t('placeholder.select', { ns: 'common' })}
|
||||
</SelectTrigger>
|
||||
<SelectContent popupClassName="w-(--anchor-width)">
|
||||
<SelectContent>
|
||||
{selectOptions.map(option => (
|
||||
<SelectItem key={option.value} value={option.value} className="text-xs">
|
||||
<SelectItemText>{option.name}</SelectItemText>
|
||||
|
||||
@ -44,7 +44,6 @@ const ConditionVarSelector = ({
|
||||
placement="bottom-start"
|
||||
sideOffset={4}
|
||||
popupClassName="border-none bg-transparent p-0 shadow-none backdrop-blur-none"
|
||||
positionerProps={{ style: { zIndex: 1000 } }}
|
||||
>
|
||||
<div className="w-[296px] rounded-lg border-[0.5px] border-components-panel-border bg-components-panel-bg-blur shadow-lg">
|
||||
<VarReferenceVars
|
||||
|
||||
@ -101,7 +101,7 @@ const Panel: FC<NodePanelProps<IterationNodeType>> = ({
|
||||
<div className="px-4 pb-2">
|
||||
<Field title={t(`${i18nPrefix}.MaxParallelismTitle`, { ns: 'workflow' })} isSubTitle tooltip={<div className="w-[230px]">{t(`${i18nPrefix}.MaxParallelismDesc`, { ns: 'workflow' })}</div>}>
|
||||
<div className="row flex">
|
||||
<Input type="number" wrapperClassName="w-18 mr-4 " max={MAX_PARALLEL_LIMIT} min={MIN_ITERATION_PARALLEL_NUM} value={inputs.parallel_nums} onChange={(e) => { changeParallelNums(Number(e.target.value)) }} />
|
||||
<Input type="number" wrapperClassName="w-18 mr-4" max={MAX_PARALLEL_LIMIT} min={MIN_ITERATION_PARALLEL_NUM} value={inputs.parallel_nums} onChange={(e) => { changeParallelNums(Number(e.target.value)) }} />
|
||||
<Slider
|
||||
value={inputs.parallel_nums}
|
||||
onValueChange={changeParallelNums}
|
||||
@ -133,7 +133,7 @@ const Panel: FC<NodePanelProps<IterationNodeType>> = ({
|
||||
<SelectTrigger className="w-full">
|
||||
{selectedResponseMethod?.name ?? t('placeholder.select', { ns: 'common' })}
|
||||
</SelectTrigger>
|
||||
<SelectContent popupClassName="w-(--anchor-width)">
|
||||
<SelectContent>
|
||||
{responseMethod.map(item => (
|
||||
<SelectItem key={item.value} value={String(item.value)}>
|
||||
<SelectItemText>{item.name}</SelectItemText>
|
||||
|
||||
@ -26,17 +26,20 @@ const AddDataset: FC<Props> = ({
|
||||
}, [onChange, hideModal])
|
||||
return (
|
||||
<div>
|
||||
<div className="cursor-pointer rounded-md p-1 select-none hover:bg-state-base-hover" onClick={showModal} data-testid="add-button">
|
||||
<span className="i-ri-add-line h-4 w-4 text-text-tertiary" />
|
||||
</div>
|
||||
{isShowModal && (
|
||||
<SelectDataset
|
||||
isShow={isShowModal}
|
||||
onClose={hideModal}
|
||||
selectedIds={selectedIds}
|
||||
onSelect={handleSelect}
|
||||
/>
|
||||
)}
|
||||
<button
|
||||
type="button"
|
||||
className="cursor-pointer rounded-md p-1 outline-hidden select-none hover:bg-state-base-hover focus-visible:ring-2 focus-visible:ring-state-accent-solid"
|
||||
onClick={showModal}
|
||||
data-testid="add-button"
|
||||
>
|
||||
<span aria-hidden="true" className="i-ri-add-line h-4 w-4 text-text-tertiary" />
|
||||
</button>
|
||||
<SelectDataset
|
||||
isShow={isShowModal}
|
||||
onClose={hideModal}
|
||||
selectedIds={selectedIds}
|
||||
onSelect={handleSelect}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@ -50,7 +50,6 @@ const AddCondition = ({
|
||||
placement="bottom-start"
|
||||
sideOffset={12}
|
||||
popupClassName="border-none bg-transparent p-0 shadow-none backdrop-blur-none"
|
||||
positionerProps={{ style: { zIndex: 1002 } }}
|
||||
>
|
||||
<div className="w-[320px] rounded-xl border-[0.5px] border-components-panel-border bg-components-panel-bg-blur shadow-lg">
|
||||
<div className="p-2 pb-1">
|
||||
|
||||
@ -63,7 +63,6 @@ const ConditionCommonVariableSelector = ({
|
||||
placement="bottom-start"
|
||||
sideOffset={4}
|
||||
popupClassName="border-none bg-transparent p-0 shadow-none backdrop-blur-none"
|
||||
positionerProps={{ style: { zIndex: 1000 } }}
|
||||
>
|
||||
<div className="w-[200px] rounded-lg border-[0.5px] border-components-panel-border bg-components-panel-bg-blur p-1 shadow-lg">
|
||||
{variables.map(v => (
|
||||
|
||||
@ -4,17 +4,17 @@ import type {
|
||||
} from '@/app/components/workflow/nodes/knowledge-retrieval/types'
|
||||
import { Button } from '@langgenius/dify-ui/button'
|
||||
import { cn } from '@langgenius/dify-ui/cn'
|
||||
import {
|
||||
Popover,
|
||||
PopoverContent,
|
||||
PopoverTrigger,
|
||||
} from '@langgenius/dify-ui/popover'
|
||||
import { RiArrowDownSLine } from '@remixicon/react'
|
||||
import {
|
||||
useMemo,
|
||||
useState,
|
||||
} from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import {
|
||||
PortalToFollowElem,
|
||||
PortalToFollowElemContent,
|
||||
PortalToFollowElemTrigger,
|
||||
} from '@/app/components/base/portal-to-follow-elem'
|
||||
import {
|
||||
getOperators,
|
||||
isComparisonOperatorNeedTranslate,
|
||||
@ -49,31 +49,32 @@ const ConditionOperator = ({
|
||||
}, [t, variableType])
|
||||
const selectedOption = options.find(o => Array.isArray(value) ? o.value === value[0] : o.value === value)
|
||||
return (
|
||||
<PortalToFollowElem
|
||||
<Popover
|
||||
open={open}
|
||||
onOpenChange={setOpen}
|
||||
placement="bottom-end"
|
||||
offset={{
|
||||
mainAxis: 4,
|
||||
crossAxis: 0,
|
||||
}}
|
||||
>
|
||||
<PortalToFollowElemTrigger onClick={() => setOpen(v => !v)}>
|
||||
<Button
|
||||
className={cn('shrink-0', !selectedOption && 'opacity-50', className)}
|
||||
size="small"
|
||||
variant="ghost"
|
||||
disabled={disabled}
|
||||
>
|
||||
{
|
||||
selectedOption
|
||||
? selectedOption.label
|
||||
: t(`${i18nPrefix}.select`, { ns: 'workflow' })
|
||||
}
|
||||
<RiArrowDownSLine className="ml-1 h-3.5 w-3.5" />
|
||||
</Button>
|
||||
</PortalToFollowElemTrigger>
|
||||
<PortalToFollowElemContent className="z-10">
|
||||
<PopoverTrigger
|
||||
render={(
|
||||
<Button
|
||||
className={cn('shrink-0', !selectedOption && 'opacity-50', className)}
|
||||
size="small"
|
||||
variant="ghost"
|
||||
disabled={disabled}
|
||||
>
|
||||
{
|
||||
selectedOption
|
||||
? selectedOption.label
|
||||
: t(`${i18nPrefix}.select`, { ns: 'workflow' })
|
||||
}
|
||||
<RiArrowDownSLine className="ml-1 h-3.5 w-3.5" />
|
||||
</Button>
|
||||
)}
|
||||
/>
|
||||
<PopoverContent
|
||||
placement="bottom-end"
|
||||
sideOffset={4}
|
||||
popupClassName="border-none bg-transparent p-0 shadow-none backdrop-blur-none"
|
||||
>
|
||||
<div className="rounded-xl border-[0.5px] border-components-panel-border bg-components-panel-bg-blur p-1 shadow-lg">
|
||||
{
|
||||
options.map(option => (
|
||||
@ -90,8 +91,8 @@ const ConditionOperator = ({
|
||||
))
|
||||
}
|
||||
</div>
|
||||
</PortalToFollowElemContent>
|
||||
</PortalToFollowElem>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@ -70,7 +70,6 @@ const ConditionVariableSelector = ({
|
||||
placement="bottom-start"
|
||||
sideOffset={4}
|
||||
popupClassName="border-none bg-transparent p-0 shadow-none backdrop-blur-none"
|
||||
positionerProps={{ style: { zIndex: 1000 } }}
|
||||
>
|
||||
<div className="w-[296px] rounded-lg border-[0.5px] border-components-panel-border bg-components-panel-bg-blur shadow-lg">
|
||||
<VarReferenceVars
|
||||
|
||||
@ -134,7 +134,7 @@ const ValueInput = ({
|
||||
<SelectTrigger className="h-8 grow text-[13px]">
|
||||
{selectedOption?.name ?? 'Select value'}
|
||||
</SelectTrigger>
|
||||
<SelectContent popupClassName="w-(--anchor-width)">
|
||||
<SelectContent>
|
||||
{selectOptions.map(option => (
|
||||
<SelectItem key={option.value} value={option.value}>
|
||||
<SelectItemText>{option.name}</SelectItemText>
|
||||
|
||||
@ -61,7 +61,6 @@ const ConditionAdd = ({
|
||||
placement="bottom-start"
|
||||
sideOffset={4}
|
||||
popupClassName="border-none bg-transparent p-0 shadow-none backdrop-blur-none"
|
||||
positionerProps={{ style: { zIndex: 1000 } }}
|
||||
>
|
||||
<div className="w-[296px] rounded-lg border-[0.5px] border-components-panel-border bg-components-panel-bg-blur shadow-lg">
|
||||
<VarReferenceVars
|
||||
|
||||
@ -317,7 +317,7 @@ const ConditionItem = ({
|
||||
<SelectTrigger className="h-8 rounded-t-none border-0 px-2 text-xs hover:bg-components-input-bg-normal focus-visible:bg-components-input-bg-normal">
|
||||
{selectedSelectOption?.name ?? t('placeholder.select', { ns: 'common' })}
|
||||
</SelectTrigger>
|
||||
<SelectContent popupClassName="w-(--anchor-width)">
|
||||
<SelectContent>
|
||||
{selectOptions.map(option => (
|
||||
<SelectItem key={option.value} value={option.value} className="text-xs">
|
||||
<SelectItemText>{option.name}</SelectItemText>
|
||||
|
||||
@ -44,7 +44,6 @@ const ConditionVarSelector = ({
|
||||
placement="bottom-start"
|
||||
sideOffset={4}
|
||||
popupClassName="border-none bg-transparent p-0 shadow-none backdrop-blur-none"
|
||||
positionerProps={{ style: { zIndex: 1000 } }}
|
||||
>
|
||||
<div className="w-[296px] rounded-lg border-[0.5px] border-components-panel-border bg-components-panel-bg-blur shadow-lg">
|
||||
<VarReferenceVars
|
||||
|
||||
@ -27,7 +27,7 @@ const InputModeSelect = ({
|
||||
<SelectTrigger className="w-full">
|
||||
{selectedOption?.label ?? t('nodes.loop.inputMode', { ns: 'workflow' })}
|
||||
</SelectTrigger>
|
||||
<SelectContent popupClassName="w-(--anchor-width)">
|
||||
<SelectContent>
|
||||
{options.map(option => (
|
||||
<SelectItem key={option.value} value={option.value}>
|
||||
<SelectItemText>{option.label}</SelectItemText>
|
||||
|
||||
@ -50,7 +50,7 @@ const VariableTypeSelect = ({
|
||||
<SelectTrigger className="w-full">
|
||||
{selectedOption?.label}
|
||||
</SelectTrigger>
|
||||
<SelectContent popupClassName="w-(--anchor-width)">
|
||||
<SelectContent>
|
||||
{options.map(option => (
|
||||
<SelectItem key={option.value} value={option.value}>
|
||||
<SelectItemText>{option.label}</SelectItemText>
|
||||
|
||||
@ -147,7 +147,7 @@ const AddExtractParameter: FC<Props> = ({
|
||||
<SelectTrigger className="w-full capitalize">
|
||||
{param.type}
|
||||
</SelectTrigger>
|
||||
<SelectContent popupClassName="w-(--anchor-width)">
|
||||
<SelectContent>
|
||||
{TYPES.map(type => (
|
||||
<SelectItem key={type} value={type} className="capitalize">
|
||||
<SelectItemText className="capitalize">{type}</SelectItemText>
|
||||
|
||||
@ -44,7 +44,7 @@ const FrequencySelector = ({ frequency, onChange }: FrequencySelectorProps) => {
|
||||
<SelectTrigger className="w-full py-2">
|
||||
{selectedFrequency?.name ?? t('nodes.triggerSchedule.selectFrequency', { ns: 'workflow' })}
|
||||
</SelectTrigger>
|
||||
<SelectContent popupClassName="w-(--anchor-width)">
|
||||
<SelectContent>
|
||||
<SelectGroup>
|
||||
<SelectLabel>{groupLabel}</SelectLabel>
|
||||
{frequencies.map(item => (
|
||||
|
||||
@ -147,7 +147,7 @@ const renderSelectCell = (
|
||||
>
|
||||
{selectedOption?.name ?? column.placeholder}
|
||||
</SelectTrigger>
|
||||
<SelectContent className="-translate-x-3" popupClassName="z-60 w-26 min-w-26">
|
||||
<SelectContent className="-translate-x-3" popupClassName="w-26 min-w-26">
|
||||
{options.map(option => (
|
||||
<SelectItem key={option.value} value={option.value}>
|
||||
<SelectItemText>{option.name}</SelectItemText>
|
||||
|
||||
@ -95,7 +95,7 @@ const Panel: FC<NodePanelProps<WebhookTriggerNodeType>> = ({
|
||||
<SelectTrigger className="h-8 pr-8 text-sm">
|
||||
{selectedMethod?.name}
|
||||
</SelectTrigger>
|
||||
<SelectContent popupClassName="z-5 w-26 min-w-26">
|
||||
<SelectContent popupClassName="w-26 min-w-26">
|
||||
{HTTP_METHODS.map(item => (
|
||||
<SelectItem key={item.value} value={item.value}>
|
||||
<SelectItemText>{item.name}</SelectItemText>
|
||||
@ -167,7 +167,7 @@ const Panel: FC<NodePanelProps<WebhookTriggerNodeType>> = ({
|
||||
<SelectTrigger className="h-8 w-full text-sm">
|
||||
{selectedContentType?.name}
|
||||
</SelectTrigger>
|
||||
<SelectContent popupClassName="z-5">
|
||||
<SelectContent>
|
||||
{CONTENT_TYPES.map(item => (
|
||||
<SelectItem key={item.value} value={item.value}>
|
||||
<SelectItemText>{item.name}</SelectItemText>
|
||||
|
||||
@ -18,7 +18,7 @@ const StatusContainer: FC<Props> = ({
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
'relative rounded-lg border px-3 py-2.5 system-xs-regular break-all',
|
||||
'relative rounded-lg border border-workflow-display-disabled-border-1 px-3 py-2.5 system-xs-regular break-all',
|
||||
status === 'succeeded' && 'border-[rgba(23,178,106,0.8)] bg-workflow-display-success-bg bg-[url(~@/app/components/workflow/run/assets/bg-line-success.svg)] text-text-success',
|
||||
status === 'succeeded' && theme === Theme.light && 'shadow-[inset_2px_2px_0_0_rgba(255,255,255,0.5),inset_0_1px_3px_0_rgba(0,0,0,0.12),inset_0_2px_24px_0_rgba(23,178,106,0.2),0_1px_2px_0_rgba(9,9,11,0.05),0_0_0_1px_rgba(0,0,0,0.05)]',
|
||||
status === 'succeeded' && theme === Theme.dark && 'shadow-[inset_2px_2px_0_0_rgba(255,255,255,0.12),inset_0_1px_3px_0_rgba(0,0,0,0.4),inset_0_2px_24px_0_rgba(23,178,106,0.25),0_1px_2px_0_rgba(0,0,0,0.1),0_0_0_1px_rgba(24,24,27,0.95)]',
|
||||
|
||||
@ -87,33 +87,34 @@ const SearchInput = ({
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
{open && !!schools.length && !!value && (
|
||||
<PopoverContent
|
||||
placement="bottom"
|
||||
sideOffset={4}
|
||||
popupClassName="w-[var(--anchor-width)] rounded-xl border-[0.5px] border-components-panel-border bg-components-panel-bg-blur p-1 shadow-lg"
|
||||
positionerProps={{ style: { zIndex: 32 } }}
|
||||
>
|
||||
<div
|
||||
className="max-h-[330px] overflow-y-auto"
|
||||
onScroll={handleScroll}
|
||||
>
|
||||
{schools.map(school => (
|
||||
{!!schools.length && !!value
|
||||
? (
|
||||
<PopoverContent
|
||||
placement="bottom"
|
||||
sideOffset={4}
|
||||
popupClassName="w-[var(--anchor-width)] rounded-xl border-[0.5px] border-components-panel-border bg-components-panel-bg-blur p-1 shadow-lg"
|
||||
>
|
||||
<div
|
||||
key={school}
|
||||
className="flex h-8 cursor-pointer items-center truncate rounded-lg px-2 py-1.5 system-md-regular text-text-secondary hover:bg-state-base-hover"
|
||||
title={school}
|
||||
onClick={() => {
|
||||
onChange(school)
|
||||
setOpen(false)
|
||||
}}
|
||||
className="max-h-[330px] overflow-y-auto"
|
||||
onScroll={handleScroll}
|
||||
>
|
||||
{school}
|
||||
{schools.map(school => (
|
||||
<div
|
||||
key={school}
|
||||
className="flex h-8 cursor-pointer items-center truncate rounded-lg px-2 py-1.5 system-md-regular text-text-secondary hover:bg-state-base-hover"
|
||||
title={school}
|
||||
onClick={() => {
|
||||
onChange(school)
|
||||
setOpen(false)
|
||||
}}
|
||||
>
|
||||
{school}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</PopoverContent>
|
||||
)}
|
||||
</PopoverContent>
|
||||
)
|
||||
: null}
|
||||
</Popover>
|
||||
)
|
||||
}
|
||||
|
||||
@ -138,7 +138,7 @@ export default function InviteSettingsPage() {
|
||||
<SelectTrigger size="large">
|
||||
{selectedLanguage?.name ?? t('placeholder.select', { ns: 'common' })}
|
||||
</SelectTrigger>
|
||||
<SelectContent popupClassName="w-(--anchor-width)">
|
||||
<SelectContent>
|
||||
{languageOptions.map(item => (
|
||||
<SelectItem key={item.value} value={item.value}>
|
||||
<SelectItemText>{item.name}</SelectItemText>
|
||||
|
||||
@ -151,7 +151,7 @@ const OneMoreStep = () => {
|
||||
<SelectTrigger size="large">
|
||||
{selectedLanguage?.name ?? t('placeholder.select', { ns: 'common' })}
|
||||
</SelectTrigger>
|
||||
<SelectContent popupClassName="w-(--anchor-width)">
|
||||
<SelectContent>
|
||||
{languageOptions.map(item => (
|
||||
<SelectItem key={item.value} value={item.value}>
|
||||
<SelectItemText>{item.name}</SelectItemText>
|
||||
@ -178,7 +178,7 @@ const OneMoreStep = () => {
|
||||
<SelectTrigger size="large">
|
||||
{selectedTimezone?.name ?? t('placeholder.select', { ns: 'common' })}
|
||||
</SelectTrigger>
|
||||
<SelectContent popupClassName="w-(--anchor-width)">
|
||||
<SelectContent>
|
||||
{timezones.map(item => (
|
||||
<SelectItem key={item.value} value={String(item.value)}>
|
||||
<SelectItemText>{item.name}</SelectItemText>
|
||||
|
||||
@ -12,7 +12,7 @@
|
||||
@import 'tailwindcss/theme.css' layer(theme);
|
||||
@import 'tailwindcss/utilities.css' layer(utilities);
|
||||
|
||||
/* Local preflight (replaces v3 `corePlugins.preflight: false`). */
|
||||
/* Local preflight keeps the browser baseline controlled across app entries. */
|
||||
@import './preflight.css' layer(base);
|
||||
|
||||
/* Design system: palette overrides, semantic tokens, light/dark themes,
|
||||
@ -98,19 +98,6 @@
|
||||
--background-image-chat-answer-human-input-form-divider-bg: var(--color-chat-answer-human-input-form-divider-bg);
|
||||
}
|
||||
|
||||
/* ---------- Backwards-compat: gray-200 default border color ----------- *
|
||||
* v4 changed the default border color to `currentColor`. Preserve the v3
|
||||
* baseline used throughout the codebase. */
|
||||
@layer base {
|
||||
*,
|
||||
::after,
|
||||
::before,
|
||||
::backdrop,
|
||||
::file-selector-button {
|
||||
border-color: var(--color-gray-200, currentcolor);
|
||||
}
|
||||
}
|
||||
|
||||
/* ---------- App-level component CSS ----------------------------------- */
|
||||
@layer components {
|
||||
[class*='code-'] {
|
||||
|
||||
@ -102,7 +102,7 @@ export const CheckModal = () => {
|
||||
|
||||
const confirmInfo = anthropicConfirmInfo || notionConfirmInfo || billingConfirmInfo
|
||||
|
||||
if (!confirmInfo || !showPayStatusModal)
|
||||
if (!confirmInfo)
|
||||
return null
|
||||
|
||||
const description = (confirmInfo as { desc?: string }).desc || ''
|
||||
|
||||
Loading…
Reference in New Issue
Block a user