From 5e6154fdd1a68ea0ef8d915cf263dbf795870f5f Mon Sep 17 00:00:00 2001 From: yyh <92089059+lyzno1@users.noreply.github.com> Date: Thu, 30 Jul 2026 21:37:04 +0800 Subject: [PATCH] refactor(web): align Base UI composition contracts (#39820) --- .../debug/__tests__/chat-user-input.spec.tsx | 9 ++- .../configuration/debug/chat-user-input.tsx | 7 +- .../__tests__/index.spec.tsx | 9 ++- .../prompt-value-panel/index.tsx | 7 +- .../chat-with-history/inputs-form/content.tsx | 3 +- .../__tests__/field-renderer.spec.tsx | 80 +++++++++++-------- .../human-input-content/field-renderer.tsx | 3 +- .../submitted-content-item.tsx | 3 +- .../embedded-chatbot/inputs-form/content.tsx | 3 +- .../language-select/__tests__/index.spec.tsx | 1 + .../create/step-two/language-select/index.tsx | 3 +- .../model-selector/popup.tsx | 5 +- .../workspace-role-checkbox-list.tsx | 1 + .../share/text-generation/run-once/index.tsx | 9 ++- .../workflow/block-selector/blocks.tsx | 4 +- .../block-selector/featured-tools.tsx | 2 +- .../block-selector/featured-triggers.tsx | 10 +-- .../rag-tool-recommendations/list.tsx | 4 +- .../block-selector/snippets/index.tsx | 2 +- .../workflow/block-selector/start-blocks.tsx | 4 +- .../workflow/block-selector/tools.tsx | 4 +- .../block-selector/trigger-plugin/list.tsx | 7 +- .../components/before-run-form/form-item.tsx | 5 +- .../components/variable/output-var-list.tsx | 4 +- .../components/variable/var-type-picker.tsx | 11 +-- .../components/variable-in-markdown.tsx | 3 +- .../visual-editor/edit-card/type-selector.tsx | 5 +- .../components/extract-parameter/update.tsx | 9 ++- .../logs/__tests__/source-picker.spec.tsx | 77 ++++++++++++++++++ .../logs/components/source-picker.tsx | 22 +++-- .../create-guide/ui/source-step.tsx | 1 + .../create-guide/ui/target-step.tsx | 1 + .../ui/__tests__/source-app-picker.spec.tsx | 13 +++ .../create-release/ui/source-app-picker.tsx | 7 ++ 34 files changed, 239 insertions(+), 99 deletions(-) create mode 100644 web/features/agent-v2/agent-detail/logs/__tests__/source-picker.spec.tsx diff --git a/web/app/components/app/configuration/debug/__tests__/chat-user-input.spec.tsx b/web/app/components/app/configuration/debug/__tests__/chat-user-input.spec.tsx index 401afe403de..b4506f33dcb 100644 --- a/web/app/components/app/configuration/debug/__tests__/chat-user-input.spec.tsx +++ b/web/app/components/app/configuration/debug/__tests__/chat-user-input.spec.tsx @@ -46,6 +46,7 @@ vi.mock('@langgenius/dify-ui/select', async () => { const SelectContext = React.createContext<{ disabled?: boolean onValueChange?: (value: string) => void + value?: string | null }>({}) return { @@ -53,15 +54,21 @@ vi.mock('@langgenius/dify-ui/select', async () => { children, disabled, onValueChange, + value, }: { children: React.ReactNode disabled?: boolean onValueChange?: (value: string) => void + value?: string | null }) => ( - +
{children}
), + SelectValue: ({ placeholder }: { placeholder?: React.ReactNode }) => { + const context = React.use(SelectContext) + return <>{context.value || placeholder} + }, SelectTrigger: ({ children, className }: { children: React.ReactNode; className?: string }) => { const context = React.useContext(SelectContext) return ( diff --git a/web/app/components/app/configuration/debug/chat-user-input.tsx b/web/app/components/app/configuration/debug/chat-user-input.tsx index 9268a2b77ba..3ad495da64a 100644 --- a/web/app/components/app/configuration/debug/chat-user-input.tsx +++ b/web/app/components/app/configuration/debug/chat-user-input.tsx @@ -7,6 +7,7 @@ import { SelectItemIndicator, SelectItemText, SelectTrigger, + SelectValue, } from '@langgenius/dify-ui/select' import { Textarea } from '@langgenius/dify-ui/textarea' import * as React from 'react' @@ -130,9 +131,9 @@ const ChatUserInput = ({ inputs }: Props) => { }} > - {typeof inputs[key] === 'string' && inputs[key] !== '' - ? inputs[key] - : t(($) => $['placeholder.select'], { ns: 'common' })} + $['placeholder.select'], { ns: 'common' })} + /> {(options || []).map((option) => ( diff --git a/web/app/components/app/configuration/prompt-value-panel/__tests__/index.spec.tsx b/web/app/components/app/configuration/prompt-value-panel/__tests__/index.spec.tsx index 66ba4f14491..4fcb36ba44a 100644 --- a/web/app/components/app/configuration/prompt-value-panel/__tests__/index.spec.tsx +++ b/web/app/components/app/configuration/prompt-value-panel/__tests__/index.spec.tsx @@ -69,20 +69,27 @@ vi.mock('@langgenius/dify-ui/select', async () => { const React = await import('react') const SelectContext = React.createContext<{ onValueChange?: (value: string) => void + value?: string | null }>({}) return { Select: ({ children, onValueChange, + value, }: { children: React.ReactNode onValueChange?: (value: string) => void + value?: string | null }) => ( - +
{children}
), + SelectValue: ({ placeholder }: { placeholder?: React.ReactNode }) => { + const context = React.use(SelectContext) + return <>{context.value || placeholder} + }, SelectTrigger: ({ children }: { children: React.ReactNode }) => { const context = React.useContext(SelectContext) return ( diff --git a/web/app/components/app/configuration/prompt-value-panel/index.tsx b/web/app/components/app/configuration/prompt-value-panel/index.tsx index d409a76c12d..c7522902272 100644 --- a/web/app/components/app/configuration/prompt-value-panel/index.tsx +++ b/web/app/components/app/configuration/prompt-value-panel/index.tsx @@ -11,6 +11,7 @@ import { SelectItemIndicator, SelectItemText, SelectTrigger, + SelectValue, } from '@langgenius/dify-ui/select' import { Textarea } from '@langgenius/dify-ui/textarea' import { Tooltip, TooltipContent, TooltipTrigger } from '@langgenius/dify-ui/tooltip' @@ -208,9 +209,9 @@ const PromptValuePanel: FC = ({ }} > - {typeof inputs[key] === 'string' && inputs[key] !== '' - ? inputs[key] - : t(($) => $['placeholder.select'], { ns: 'common' })} + $['placeholder.select'], { ns: 'common' })} + /> {(options || []).map((option) => ( diff --git a/web/app/components/base/chat/chat-with-history/inputs-form/content.tsx b/web/app/components/base/chat/chat-with-history/inputs-form/content.tsx index 087bb972a56..b53e238e24d 100644 --- a/web/app/components/base/chat/chat-with-history/inputs-form/content.tsx +++ b/web/app/components/base/chat/chat-with-history/inputs-form/content.tsx @@ -5,6 +5,7 @@ import { SelectItemIndicator, SelectItemText, SelectTrigger, + SelectValue, } from '@langgenius/dify-ui/select' import { Textarea } from '@langgenius/dify-ui/textarea' import * as React from 'react' @@ -108,7 +109,7 @@ const InputsFormContent = ({ showTip }: Props) => { onValueChange={(value) => value && handleFormChange(form.variable, value)} > - {String(inputsFormValue?.[form.variable] ?? form.default ?? form.label)} + {form.options.map((option: string) => ( diff --git a/web/app/components/base/chat/chat/answer/human-input-content/__tests__/field-renderer.spec.tsx b/web/app/components/base/chat/chat/answer/human-input-content/__tests__/field-renderer.spec.tsx index 1ea31ba7d10..b450277debf 100644 --- a/web/app/components/base/chat/chat/answer/human-input-content/__tests__/field-renderer.spec.tsx +++ b/web/app/components/base/chat/chat/answer/human-input-content/__tests__/field-renderer.spec.tsx @@ -32,42 +32,52 @@ vi.mock('@langgenius/dify-ui/textarea', () => ({ Textarea: MockTextarea, })) -vi.mock('@langgenius/dify-ui/select', () => ({ - Select: ({ - children, - onValueChange, - }: { - children: React.ReactNode - onValueChange: (value: string | null) => void - }) => ( -
- + + {children} +
+ + ), + SelectValue: () => <>{React.use(SelectValueContext)}, + SelectTrigger: ({ children }: { children: React.ReactNode }) => ( + - - {children} - - ), - SelectTrigger: ({ children }: { children: React.ReactNode }) => ( - - ), - SelectContent: ({ children }: { children: React.ReactNode }) =>
{children}
, - SelectItem: ({ children }: { children: React.ReactNode }) =>
{children}
, - SelectItemText: ({ children }: { children: React.ReactNode }) => {children}, - SelectItemIndicator: () => selected, -})) + ), + SelectContent: ({ children }: { children: React.ReactNode }) =>
{children}
, + SelectItem: ({ children }: { children: React.ReactNode }) =>
{children}
, + SelectItemText: ({ children }: { children: React.ReactNode }) => {children}, + SelectItemIndicator: () => selected, + } +}) vi.mock('@/app/components/base/file-uploader', () => ({ FileUploaderInAttachmentWrapper: ({ diff --git a/web/app/components/base/chat/chat/answer/human-input-content/field-renderer.tsx b/web/app/components/base/chat/chat/answer/human-input-content/field-renderer.tsx index 6b09c9d5430..9f1cf6dbe70 100644 --- a/web/app/components/base/chat/chat/answer/human-input-content/field-renderer.tsx +++ b/web/app/components/base/chat/chat/answer/human-input-content/field-renderer.tsx @@ -7,6 +7,7 @@ import { SelectItemIndicator, SelectItemText, SelectTrigger, + SelectValue, } from '@langgenius/dify-ui/select' import { Textarea } from '@langgenius/dify-ui/textarea' import * as React from 'react' @@ -54,7 +55,7 @@ const HumanInputFieldRenderer = ({ field, value, onChange }: Props) => { }} > - {typeof value === 'string' ? value : ''} + {options.map((option) => ( diff --git a/web/app/components/base/chat/chat/answer/human-input-content/submitted-content-item.tsx b/web/app/components/base/chat/chat/answer/human-input-content/submitted-content-item.tsx index 5983efbbbcf..a429e02691d 100644 --- a/web/app/components/base/chat/chat/answer/human-input-content/submitted-content-item.tsx +++ b/web/app/components/base/chat/chat/answer/human-input-content/submitted-content-item.tsx @@ -7,6 +7,7 @@ import { SelectItemIndicator, SelectItemText, SelectTrigger, + SelectValue, } from '@langgenius/dify-ui/select' import * as React from 'react' import { FileList } from '@/app/components/base/file-uploader' @@ -68,7 +69,7 @@ const SubmittedContentItem = ({ content, formInputFields, values }: SubmittedCon aria-label={field.output_variable_name} disabled > - {selectedValue} + {field.option_source.value.map((option) => ( diff --git a/web/app/components/base/chat/embedded-chatbot/inputs-form/content.tsx b/web/app/components/base/chat/embedded-chatbot/inputs-form/content.tsx index b83a720a789..5e1540c6d53 100644 --- a/web/app/components/base/chat/embedded-chatbot/inputs-form/content.tsx +++ b/web/app/components/base/chat/embedded-chatbot/inputs-form/content.tsx @@ -5,6 +5,7 @@ import { SelectItemIndicator, SelectItemText, SelectTrigger, + SelectValue, } from '@langgenius/dify-ui/select' import { Textarea } from '@langgenius/dify-ui/textarea' import * as React from 'react' @@ -112,7 +113,7 @@ const InputsFormContent = ({ showTip }: Props) => { onValueChange={(value) => value && handleFormChange(form.variable, value)} > - {String(inputsFormValue?.[form.variable] ?? form.default ?? form.label)} + {form.options.map((option: string) => ( diff --git a/web/app/components/datasets/create/step-two/language-select/__tests__/index.spec.tsx b/web/app/components/datasets/create/step-two/language-select/__tests__/index.spec.tsx index 1419ce18f3f..948d62158dd 100644 --- a/web/app/components/datasets/create/step-two/language-select/__tests__/index.spec.tsx +++ b/web/app/components/datasets/create/step-two/language-select/__tests__/index.spec.tsx @@ -128,6 +128,7 @@ describe('LanguageSelect', () => { {children} ), + SelectValue: ({ placeholder }: { placeholder?: React.ReactNode }) => <>{placeholder}, SelectContent: ({ children }: { children: React.ReactNode }) =>
{children}
, SelectItem: ({ children }: { children: React.ReactNode }) =>
{children}
, SelectItemText: ({ children }: { children: React.ReactNode }) => {children}, diff --git a/web/app/components/datasets/create/step-two/language-select/index.tsx b/web/app/components/datasets/create/step-two/language-select/index.tsx index 486b436a6d4..96a281807b0 100644 --- a/web/app/components/datasets/create/step-two/language-select/index.tsx +++ b/web/app/components/datasets/create/step-two/language-select/index.tsx @@ -8,6 +8,7 @@ import { SelectItemIndicator, SelectItemText, SelectTrigger, + SelectValue, } from '@langgenius/dify-ui/select' import * as React from 'react' import { languages } from '@/i18n-config/language' @@ -39,7 +40,7 @@ const LanguageSelect: FC = ({ currentLanguage, onSelect, d 'cursor-not-allowed bg-components-button-tertiary-bg-disabled text-components-button-tertiary-text-disabled hover:bg-components-button-tertiary-bg-disabled', )} > - {currentLanguage ||  } +  } /> {supportedLanguages.map(({ prompt_name }) => ( diff --git a/web/app/components/header/account-setting/model-provider-page/model-selector/popup.tsx b/web/app/components/header/account-setting/model-provider-page/model-selector/popup.tsx index dcde8341b05..bd20c530e26 100644 --- a/web/app/components/header/account-setting/model-provider-page/model-selector/popup.tsx +++ b/web/app/components/header/account-setting/model-provider-page/model-selector/popup.tsx @@ -89,10 +89,7 @@ function Popup({ ) const { theme } = useTheme() const language = useLanguage() - const previewCardHandle = useMemo( - () => createPreviewCardHandle(), - [], - ) + const [previewCardHandle] = useState(() => createPreviewCardHandle()) const [marketplaceCollapsed, setMarketplaceCollapsed] = useState(false) const [showIncompatibleModels, setShowIncompatibleModels] = useState(false) const { modelProviders } = useProviderContext() diff --git a/web/app/components/header/account-setting/workspace-role-checkbox-list.tsx b/web/app/components/header/account-setting/workspace-role-checkbox-list.tsx index 763d4a76f9a..e3c40d67b3b 100644 --- a/web/app/components/header/account-setting/workspace-role-checkbox-list.tsx +++ b/web/app/components/header/account-setting/workspace-role-checkbox-list.tsx @@ -282,6 +282,7 @@ const WorkspaceRoleCheckboxList = ({ ) : ( $['role.workspaceRoles.title'], { ns: 'permission' })} value={selectedRoleIds[0] ?? ''} onValueChange={handleRadioValueChange} className="flex-col items-stretch gap-0.5 pb-2" diff --git a/web/app/components/share/text-generation/run-once/index.tsx b/web/app/components/share/text-generation/run-once/index.tsx index 5223fbe9324..a800957fe77 100644 --- a/web/app/components/share/text-generation/run-once/index.tsx +++ b/web/app/components/share/text-generation/run-once/index.tsx @@ -13,6 +13,7 @@ import { SelectItemIndicator, SelectItemText, SelectTrigger, + SelectValue, } from '@langgenius/dify-ui/select' import { Textarea } from '@langgenius/dify-ui/textarea' import { RiLoader2Line, RiPlayLargeLine } from '@remixicon/react' @@ -143,16 +144,16 @@ const RunOnce: FC = ({
{item.type === 'select' && ( - value={selectValue} + value={selectValue ?? defaultSelectValue} onValueChange={(nextValue) => { if (nextValue == null || nextValue === '') return handleInputsChange({ ...inputsRef.current, [item.key]: nextValue }) }} > - {selectValue ?? - defaultSelectValue ?? - t(($) => $['placeholder.select'], { ns: 'common' })} + $['placeholder.select'], { ns: 'common' })} + /> {(item.options || []).map((option) => ( diff --git a/web/app/components/workflow/block-selector/blocks.tsx b/web/app/components/workflow/block-selector/blocks.tsx index 2526be204b8..a36fde13c81 100644 --- a/web/app/components/workflow/block-selector/blocks.tsx +++ b/web/app/components/workflow/block-selector/blocks.tsx @@ -6,7 +6,7 @@ import { PreviewCardTrigger, } from '@langgenius/dify-ui/preview-card' import { groupBy } from 'es-toolkit/compat' -import { Fragment, memo, useCallback, useId, useMemo } from 'react' +import { Fragment, memo, useCallback, useId, useMemo, useState } from 'react' import { useTranslation } from 'react-i18next' import { useStoreApi } from 'reactflow' import Badge from '@/app/components/base/badge' @@ -36,7 +36,7 @@ const Blocks = ({ const { t } = useTranslation() const store = useStoreApi() const blocksFromHooks = useBlocks() - const previewCardHandle = useMemo(() => createPreviewCardHandle(), []) + const [previewCardHandle] = useState(() => createPreviewCardHandle()) const previewDescriptionBaseId = useId() // Use external blocks if provided, otherwise fallback to hook-based blocks diff --git a/web/app/components/workflow/block-selector/featured-tools.tsx b/web/app/components/workflow/block-selector/featured-tools.tsx index 23972a1a417..225ca1c2823 100644 --- a/web/app/components/workflow/block-selector/featured-tools.tsx +++ b/web/app/components/workflow/block-selector/featured-tools.tsx @@ -60,7 +60,7 @@ const FeaturedTools = ({ }: FeaturedToolsProps) => { const { t } = useTranslation() const language = useGetLanguage() - const previewCardHandle = useMemo(() => createPreviewCardHandle(), []) + const [previewCardHandle] = useState(() => createPreviewCardHandle()) const [visibleCount, setVisibleCount] = useState(INITIAL_VISIBLE_COUNT) const [visibleCountPlugins, setVisibleCountPlugins] = useState(plugins) const [isCollapsed, setIsCollapsed] = useFeaturedToolsCollapsed() diff --git a/web/app/components/workflow/block-selector/featured-triggers.tsx b/web/app/components/workflow/block-selector/featured-triggers.tsx index 6398b4fde75..97ed6fa9cbb 100644 --- a/web/app/components/workflow/block-selector/featured-triggers.tsx +++ b/web/app/components/workflow/block-selector/featured-triggers.tsx @@ -58,13 +58,11 @@ const FeaturedTriggers = ({ }: FeaturedTriggersProps) => { const { t } = useTranslation() const language = useGetLanguage() - const previewCardHandle = useMemo( - () => createPreviewCardHandle(), - [], + const [previewCardHandle] = useState(() => + createPreviewCardHandle(), ) - const triggerActionPreviewCardHandle = useMemo( - () => createPreviewCardHandle(), - [], + const [triggerActionPreviewCardHandle] = useState(() => + createPreviewCardHandle(), ) const [visibleCount, setVisibleCount] = useState(INITIAL_VISIBLE_COUNT) const [visibleCountPlugins, setVisibleCountPlugins] = useState(plugins) diff --git a/web/app/components/workflow/block-selector/rag-tool-recommendations/list.tsx b/web/app/components/workflow/block-selector/rag-tool-recommendations/list.tsx index 5ffea50c7e8..124f5cef802 100644 --- a/web/app/components/workflow/block-selector/rag-tool-recommendations/list.tsx +++ b/web/app/components/workflow/block-selector/rag-tool-recommendations/list.tsx @@ -5,7 +5,7 @@ import type { Plugin } from '@/app/components/plugins/types' import type { OnSelectBlock } from '@/app/components/workflow/types' import { cn } from '@langgenius/dify-ui/cn' import { createPreviewCardHandle, PreviewCard } from '@langgenius/dify-ui/preview-card' -import { useCallback, useMemo, useRef } from 'react' +import { useCallback, useMemo, useRef, useState } from 'react' import { useGetLanguage } from '@/context/i18n' import { createToolListData } from '../tool-list-data' import { ToolActionPreviewCard } from '../tool/action-item' @@ -24,7 +24,7 @@ type ListProps = { const List = ({ onSelect, tools, viewType, unInstalledPlugins, className }: ListProps) => { const language = useGetLanguage() - const previewCardHandle = useMemo(() => createPreviewCardHandle(), []) + const [previewCardHandle] = useState(() => createPreviewCardHandle()) const isFlatView = viewType === ViewType.flat const { letters, flatTools, treeGroups } = useMemo( diff --git a/web/app/components/workflow/block-selector/snippets/index.tsx b/web/app/components/workflow/block-selector/snippets/index.tsx index e19875d1483..fd94f43f155 100644 --- a/web/app/components/workflow/block-selector/snippets/index.tsx +++ b/web/app/components/workflow/block-selector/snippets/index.tsx @@ -68,7 +68,7 @@ const Snippets = ({ searchText, onSearchTextChange, insertPayload, onInserted }: const deferredSearchText = useDeferredValue(searchText) const viewportRef = useRef(null) const [tagIds, setTagIds] = useState([]) - const previewCardHandle = useMemo(() => createPreviewCardHandle(), []) + const [previewCardHandle] = useState(() => createPreviewCardHandle()) const keyword = deferredSearchText.trim() || undefined diff --git a/web/app/components/workflow/block-selector/start-blocks.tsx b/web/app/components/workflow/block-selector/start-blocks.tsx index 6e3db52bb16..c754c4c78e0 100644 --- a/web/app/components/workflow/block-selector/start-blocks.tsx +++ b/web/app/components/workflow/block-selector/start-blocks.tsx @@ -7,7 +7,7 @@ import { PreviewCardTrigger, } from '@langgenius/dify-ui/preview-card' import { Tooltip, TooltipContent, TooltipTrigger } from '@langgenius/dify-ui/tooltip' -import { Fragment, memo, useCallback, useEffect, useId, useMemo } from 'react' +import { Fragment, memo, useCallback, useEffect, useId, useMemo, useState } from 'react' import { useTranslation } from 'react-i18next' import useNodes from '@/app/components/workflow/store/workflow/use-nodes' import BlockIcon from '../block-icon' @@ -46,7 +46,7 @@ const StartBlocks = ({ }: StartBlocksProps) => { const { t } = useTranslation() const nodes = useNodes() - const previewCardHandle = useMemo(() => createPreviewCardHandle(), []) + const [previewCardHandle] = useState(() => createPreviewCardHandle()) const previewDescriptionBaseId = useId() const filteredBlocks = useMemo(() => { diff --git a/web/app/components/workflow/block-selector/tools.tsx b/web/app/components/workflow/block-selector/tools.tsx index 43f0eca71e7..91b28861710 100644 --- a/web/app/components/workflow/block-selector/tools.tsx +++ b/web/app/components/workflow/block-selector/tools.tsx @@ -3,7 +3,7 @@ import type { ToolActionPreviewPayload } from './tool/action-item' import type { ToolDefaultValue, ToolType, ToolValue } from './types' import { cn } from '@langgenius/dify-ui/cn' import { createPreviewCardHandle, PreviewCard } from '@langgenius/dify-ui/preview-card' -import { memo, useMemo, useRef } from 'react' +import { memo, useMemo, useRef, useState } from 'react' import Empty from '@/app/components/tools/provider/empty' import { useGetLanguage } from '@/context/i18n' import { IndexBar } from './index-bar' @@ -40,7 +40,7 @@ const Tools = ({ selectedTools, }: ToolsProps) => { const language = useGetLanguage() - const previewCardHandle = useMemo(() => createPreviewCardHandle(), []) + const [previewCardHandle] = useState(() => createPreviewCardHandle()) const isFlatView = viewType === ViewType.flat const isShowLetterIndex = isFlatView && tools.length > 10 diff --git a/web/app/components/workflow/block-selector/trigger-plugin/list.tsx b/web/app/components/workflow/block-selector/trigger-plugin/list.tsx index e4c76cccba2..256ec1402b8 100644 --- a/web/app/components/workflow/block-selector/trigger-plugin/list.tsx +++ b/web/app/components/workflow/block-selector/trigger-plugin/list.tsx @@ -3,7 +3,7 @@ import type { BlockEnum } from '../../types' import type { TriggerDefaultValue, TriggerWithProvider } from '../types' import type { TriggerPluginActionPreviewPayload } from './action-item' import { createPreviewCardHandle, PreviewCard } from '@langgenius/dify-ui/preview-card' -import { memo, useEffect, useMemo } from 'react' +import { memo, useEffect, useMemo, useState } from 'react' import { useGetLanguage } from '@/context/i18n' import { useAllTriggerPlugins } from '@/service/use-triggers' import { TriggerPluginActionPreviewCard } from './action-item' @@ -25,9 +25,8 @@ const TriggerPluginList = ({ }: TriggerPluginListProps) => { const { data: triggerPluginsData } = useAllTriggerPlugins() const language = useGetLanguage() - const previewCardHandle = useMemo( - () => createPreviewCardHandle(), - [], + const [previewCardHandle] = useState(() => + createPreviewCardHandle(), ) const normalizedSearch = searchText.trim().toLowerCase() diff --git a/web/app/components/workflow/nodes/_base/components/before-run-form/form-item.tsx b/web/app/components/workflow/nodes/_base/components/before-run-form/form-item.tsx index 13edee51ce1..3e53916b6e4 100644 --- a/web/app/components/workflow/nodes/_base/components/before-run-form/form-item.tsx +++ b/web/app/components/workflow/nodes/_base/components/before-run-form/form-item.tsx @@ -10,6 +10,7 @@ import { SelectItemIndicator, SelectItemText, SelectTrigger, + SelectValue, } from '@langgenius/dify-ui/select' import { Textarea } from '@langgenius/dify-ui/textarea' import { RiDeleteBinLine } from '@remixicon/react' @@ -198,9 +199,7 @@ const FormItem: FC = ({ }} > - {String( - value || payload.default || t(($) => $['placeholder.select'], { ns: 'common' }), - )} + $['placeholder.select'], { ns: 'common' })} /> {(payload.options || []).map((option) => ( diff --git a/web/app/components/workflow/nodes/_base/components/variable/output-var-list.tsx b/web/app/components/workflow/nodes/_base/components/variable/output-var-list.tsx index 1e276f20cce..71f260082e5 100644 --- a/web/app/components/workflow/nodes/_base/components/variable/output-var-list.tsx +++ b/web/app/components/workflow/nodes/_base/components/variable/output-var-list.tsx @@ -76,10 +76,10 @@ const OutputVarList: FC = ({ readonly, outputs, outputKeyOrders, onChange const handleVarTypeChange = useCallback( (index: number) => { - return (value: string) => { + return (value: VarType) => { const key = list[index]!.variable const newOutputs = produce(outputs, (draft) => { - draft[key]!.type = value as VarType + draft[key]!.type = value }) onChange(newOutputs) } diff --git a/web/app/components/workflow/nodes/_base/components/variable/var-type-picker.tsx b/web/app/components/workflow/nodes/_base/components/variable/var-type-picker.tsx index 26cff721527..b1fb87e1db5 100644 --- a/web/app/components/workflow/nodes/_base/components/variable/var-type-picker.tsx +++ b/web/app/components/workflow/nodes/_base/components/variable/var-type-picker.tsx @@ -8,6 +8,7 @@ import { SelectItemIndicator, SelectItemText, SelectTrigger, + SelectValue, } from '@langgenius/dify-ui/select' import * as React from 'react' import { VarType } from '@/app/components/workflow/types' @@ -15,8 +16,8 @@ import { VarType } from '@/app/components/workflow/types' type Props = Readonly<{ className?: string readonly: boolean - value: string - onChange: (value: string) => void + value: VarType + onChange: (value: VarType) => void }> const TYPES = [ @@ -32,7 +33,7 @@ const TYPES = [ const VarReferencePicker: FC = ({ readonly, className, value, onChange }) => { return (
- value={param.type} onValueChange={(value) => value && handleParamChange('type')(value)} > - {param.type} + + + {TYPES.map((type) => ( - + key={type} value={type} className="capitalize"> {type} diff --git a/web/features/agent-v2/agent-detail/logs/__tests__/source-picker.spec.tsx b/web/features/agent-v2/agent-detail/logs/__tests__/source-picker.spec.tsx new file mode 100644 index 00000000000..c6395efeb9c --- /dev/null +++ b/web/features/agent-v2/agent-detail/logs/__tests__/source-picker.spec.tsx @@ -0,0 +1,77 @@ +import type { + AgentLogSourceGroupResponse, + AgentLogSourceResponse, +} from '@dify/contracts/api/console/agent/types.gen' +import { render, screen } from '@testing-library/react' +import userEvent from '@testing-library/user-event' +import { AgentLogSourcePicker } from '../components/source-picker' + +const sources = { + webapp: { + app_id: 'webapp-app-id', + app_name: 'Book Translation', + id: 'webapp:webapp-app-id', + type: 'webapp', + }, + workflow: { + app_id: 'workflow-app-id', + app_name: 'SVG Logo Design', + id: 'workflow:workflow-app-id:workflow-id:v3:agent-node-id', + node_id: 'agent-node-id', + type: 'workflow', + workflow_id: 'workflow-id', + workflow_version: 'v3', + }, +} satisfies Record + +const groups: AgentLogSourceGroupResponse[] = [ + { + label: 'Webapp', + type: 'webapp', + sources: [sources.webapp], + }, + { + label: 'Workflow', + type: 'workflow', + sources: [sources.workflow], + }, +] + +describe('AgentLogSourcePicker', () => { + it('should filter sources across groups and only show empty when no source matches', async () => { + const user = userEvent.setup() + + render( + , + ) + + await user.click( + screen.getByRole('combobox', { + name: 'agentV2.agentDetail.logs.filters.source.label', + }), + ) + const searchInput = screen.getByRole('combobox', { + name: 'agentV2.agentDetail.logs.filters.source.searchLabel', + }) + await user.type(searchInput, 'Book') + + expect(screen.getByRole('option', { name: /Book Translation/ })).toBeInTheDocument() + expect(screen.queryByRole('option', { name: /SVG Logo Design/ })).not.toBeInTheDocument() + expect( + screen.queryByText('agentV2.agentDetail.logs.filters.source.empty'), + ).not.toBeInTheDocument() + + await user.clear(searchInput) + await user.type(searchInput, 'Missing source') + + expect(screen.queryByRole('option')).not.toBeInTheDocument() + expect(screen.getByText('agentV2.agentDetail.logs.filters.source.empty')).toBeInTheDocument() + }) +}) diff --git a/web/features/agent-v2/agent-detail/logs/components/source-picker.tsx b/web/features/agent-v2/agent-detail/logs/components/source-picker.tsx index 971dab79a1b..db188fef3bf 100644 --- a/web/features/agent-v2/agent-detail/logs/components/source-picker.tsx +++ b/web/features/agent-v2/agent-detail/logs/components/source-picker.tsx @@ -21,7 +21,7 @@ import { ComboboxTrigger, ComboboxValue, } from '@langgenius/dify-ui/combobox' -import { useState } from 'react' +import { useMemo, useState } from 'react' import { useTranslation } from 'react-i18next' import { LogSourceIcon } from './source-icon' @@ -35,6 +35,10 @@ const getSourceGroupLabel = (group: AgentLogSourceGroupResponse, t: TFunction<'a const getSourceLabel = (source: AgentLogSourceResponse) => source.app_name +type AgentLogSourceComboboxGroup = Omit & { + items: AgentLogSourceResponse[] +} + export function AgentLogSourcePicker({ value, groups, @@ -53,13 +57,17 @@ export function AgentLogSourcePicker({ const { t } = useTranslation('agentV2') const { t: tCommon } = useTranslation('common') const [inputValue, setInputValue] = useState('') - const sources = groups.flatMap((group) => group.sources ?? []) + const sourceGroups = useMemo( + () => groups.map(({ sources, ...group }) => ({ ...group, items: sources ?? [] })), + [groups], + ) + const sources = sourceGroups.flatMap((group) => group.items) const selectedSources = sources.filter((source) => value.includes(source.id)) return ( multiple - items={groups} + items={sourceGroups} value={selectedSources} itemToStringLabel={getSourceLabel} onValueChange={(nextSources) => { @@ -112,9 +120,9 @@ export function AgentLogSourcePicker({ )} {!isLoading && !isError && ( <> - - {groups.map((group) => ( - + className="max-h-69 p-2 pt-1"> + {(group) => ( + {getSourceGroupLabel(group, t)} @@ -134,7 +142,7 @@ export function AgentLogSourcePicker({ )} - ))} + )} {t(($) => $['agentDetail.logs.filters.source.empty'])} diff --git a/web/features/deployments/create-guide/ui/source-step.tsx b/web/features/deployments/create-guide/ui/source-step.tsx index ad943e83dd3..d18f8736592 100644 --- a/web/features/deployments/create-guide/ui/source-step.tsx +++ b/web/features/deployments/create-guide/ui/source-step.tsx @@ -77,6 +77,7 @@ function SourceMethodSection() { hideHeader > + aria-label={t(($) => $['createGuide.steps.method'])} value={method} onValueChange={selectMethod} className="flex flex-col items-stretch gap-2 sm:flex-row" diff --git a/web/features/deployments/create-guide/ui/target-step.tsx b/web/features/deployments/create-guide/ui/target-step.tsx index 78281844506..f50cff3f941 100644 --- a/web/features/deployments/create-guide/ui/target-step.tsx +++ b/web/features/deployments/create-guide/ui/target-step.tsx @@ -90,6 +90,7 @@ function TargetEnvironmentSection() {
{hasEnvironmentOptions ? ( + aria-label={t(($) => $['createGuide.target.environment'])} value={effectiveSelectedEnvironmentId} onValueChange={selectEnvironment} className="grid grid-cols-1 items-stretch gap-3 lg:grid-cols-2" diff --git a/web/features/deployments/create-release/ui/__tests__/source-app-picker.spec.tsx b/web/features/deployments/create-release/ui/__tests__/source-app-picker.spec.tsx index 98445eafbae..9c625a019fe 100644 --- a/web/features/deployments/create-release/ui/__tests__/source-app-picker.spec.tsx +++ b/web/features/deployments/create-release/ui/__tests__/source-app-picker.spec.tsx @@ -155,4 +155,17 @@ describe('SourceAppPicker', () => { screen.queryByRole('button', { name: /createModal\.loadMoreApps/ }), ).not.toBeInTheDocument() }) + + it('should restore the selected app by business identity', async () => { + const user = userEvent.setup() + + renderSourceAppPicker(false) + + await user.click(screen.getByRole('combobox', { name: 'deployments.versions.sourceAppOption' })) + + expect(screen.getByRole('option', { name: /Workflow App/ })).toHaveAttribute( + 'aria-selected', + 'true', + ) + }) }) diff --git a/web/features/deployments/create-release/ui/source-app-picker.tsx b/web/features/deployments/create-release/ui/source-app-picker.tsx index f34cfe6ff60..9f07fa902ff 100644 --- a/web/features/deployments/create-release/ui/source-app-picker.tsx +++ b/web/features/deployments/create-release/ui/source-app-picker.tsx @@ -41,6 +41,10 @@ function sourceAppSearchText(app: App) { return `${app.name} ${app.id}`.toLowerCase() } +function isSameApp(app: App, selectedApp: App) { + return app.id === selectedApp.id +} + function SourceAppTrigger({ app }: { app?: SourceAppPickerValue }) { const { t } = useTranslation('deployments') @@ -144,6 +148,7 @@ export function SourceAppPicker({ const sourceAppsIsFetching = useAtomValue(createReleaseSourceAppsIsFetchingAtom) const sourceAppsIsFetchingNextPage = useAtomValue(createReleaseSourceAppsIsFetchingNextPageAtom) const sourceAppsIsLoading = useAtomValue(createReleaseSourceAppsIsLoadingAtom) + const selectedApp = apps.find((app) => app.id === value?.id) ?? null const { rootRef, sentinelRef } = useInfiniteScroll( { error: sourceAppsError, @@ -163,8 +168,10 @@ export function SourceAppPicker({ return ( items={apps} + value={selectedApp} open={!disabled && isShow} inputValue={searchText} + isItemEqualToValue={isSameApp} onOpenChange={(open) => { setIsShow(disabled ? false : open) }}