Merge branch 'main' into 4-27-app-deploy

This commit is contained in:
Stephen Zhou 2026-05-06 16:51:23 +08:00
commit 0a028faae6
No known key found for this signature in database
99 changed files with 1509 additions and 1230 deletions

View File

@ -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()

View File

@ -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)

View File

@ -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,
)

View File

@ -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",
}
}

View File

@ -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(
{

View File

@ -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

View File

@ -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

View File

@ -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}
/>
</>
)
}

View File

@ -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>

View File

@ -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()

View File

@ -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([])}
/>
)}
</>
)
}

View File

@ -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"
/>
)}
>

View File

@ -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}

View File

@ -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)',
}}

View File

@ -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

View File

@ -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
? (

View File

@ -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)

View File

@ -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>

View File

@ -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>

View File

@ -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 () => {

View File

@ -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>
)
}

View File

@ -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>
)
}

View File

@ -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 () => {

View File

@ -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>
</>
)
}

View File

@ -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>
</>

View File

@ -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}

View File

@ -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)', () => {

View File

@ -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>

View File

@ -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>

View File

@ -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 () => {

View File

@ -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>

View File

@ -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>

View File

@ -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) => {

View File

@ -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',

View File

@ -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(

View File

@ -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}

View File

@ -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>

View File

@ -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>

View File

@ -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}

View File

@ -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',
)}
>

View File

@ -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>

View File

@ -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];

View File

@ -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}

View File

@ -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>

View File

@ -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={(

View File

@ -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">

View File

@ -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 || ''}

View File

@ -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}>

View File

@ -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>

View File

@ -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>

View File

@ -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', () => ({

View File

@ -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>
)
}

View File

@ -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}
</>
)
}

View File

@ -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

View File

@ -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

View File

@ -58,7 +58,6 @@ const StrategyPicker = ({
<DropdownMenuContent
placement="top-end"
sideOffset={4}
className="z-99"
popupClassName="w-[280px] p-1"
>
<DropdownMenuRadioGroup

View File

@ -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">

View File

@ -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>

View File

@ -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">

View File

@ -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">

View File

@ -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 }}

View File

@ -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')}>

View File

@ -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>
)
}

View File

@ -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">

View File

@ -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>

View File

@ -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

View File

@ -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>

View File

@ -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">

View File

@ -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}
/>
)}

View File

@ -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}

View File

@ -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)}

View File

@ -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"

View File

@ -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}

View File

@ -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}

View File

@ -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

View File

@ -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>

View File

@ -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

View File

@ -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>

View File

@ -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>
)
}

View File

@ -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">

View File

@ -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 => (

View File

@ -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>
)
}

View File

@ -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

View File

@ -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>

View File

@ -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

View File

@ -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>

View File

@ -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

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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 => (

View File

@ -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>

View File

@ -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>

View File

@ -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)]',

View File

@ -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>
)
}

View File

@ -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>

View File

@ -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>

View File

@ -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-'] {

View File

@ -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 || ''