mirror of
https://github.com/langgenius/dify.git
synced 2026-07-31 17:29:37 +08:00
refactor(web): align Base UI composition contracts (#39820)
This commit is contained in:
parent
69a16d8319
commit
5e6154fdd1
@ -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
|
||||
}) => (
|
||||
<SelectContext.Provider value={{ disabled, onValueChange }}>
|
||||
<SelectContext.Provider value={{ disabled, onValueChange, value }}>
|
||||
<div>{children}</div>
|
||||
</SelectContext.Provider>
|
||||
),
|
||||
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 (
|
||||
|
||||
@ -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) => {
|
||||
}}
|
||||
>
|
||||
<SelectTrigger className="w-full">
|
||||
{typeof inputs[key] === 'string' && inputs[key] !== ''
|
||||
? inputs[key]
|
||||
: t(($) => $['placeholder.select'], { ns: 'common' })}
|
||||
<SelectValue
|
||||
placeholder={t(($) => $['placeholder.select'], { ns: 'common' })}
|
||||
/>
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{(options || []).map((option) => (
|
||||
|
||||
@ -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
|
||||
}) => (
|
||||
<SelectContext.Provider value={{ onValueChange }}>
|
||||
<SelectContext.Provider value={{ onValueChange, value }}>
|
||||
<div>{children}</div>
|
||||
</SelectContext.Provider>
|
||||
),
|
||||
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 (
|
||||
|
||||
@ -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<IPromptValuePanelProps> = ({
|
||||
}}
|
||||
>
|
||||
<SelectTrigger className="w-full bg-gray-50">
|
||||
{typeof inputs[key] === 'string' && inputs[key] !== ''
|
||||
? inputs[key]
|
||||
: t(($) => $['placeholder.select'], { ns: 'common' })}
|
||||
<SelectValue
|
||||
placeholder={t(($) => $['placeholder.select'], { ns: 'common' })}
|
||||
/>
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{(options || []).map((option) => (
|
||||
|
||||
@ -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)}
|
||||
>
|
||||
<SelectTrigger className="w-full">
|
||||
{String(inputsFormValue?.[form.variable] ?? form.default ?? form.label)}
|
||||
<SelectValue placeholder={form.label} />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{form.options.map((option: string) => (
|
||||
|
||||
@ -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
|
||||
}) => (
|
||||
<div>
|
||||
<button
|
||||
type="button"
|
||||
data-testid="content-item-select-root"
|
||||
onClick={() => onValueChange('alice')}
|
||||
>
|
||||
select alice
|
||||
vi.mock('@langgenius/dify-ui/select', async () => {
|
||||
const React = await import('react')
|
||||
const SelectValueContext = React.createContext<string | null>(null)
|
||||
|
||||
return {
|
||||
Select: ({
|
||||
children,
|
||||
onValueChange,
|
||||
value,
|
||||
}: {
|
||||
children: React.ReactNode
|
||||
onValueChange: (value: string | null) => void
|
||||
value: string | null
|
||||
}) => (
|
||||
<SelectValueContext value={value}>
|
||||
<div>
|
||||
<button
|
||||
type="button"
|
||||
data-testid="content-item-select-root"
|
||||
onClick={() => onValueChange('alice')}
|
||||
>
|
||||
select alice
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
data-testid="content-item-select-null"
|
||||
onClick={() => onValueChange(null)}
|
||||
>
|
||||
select null
|
||||
</button>
|
||||
{children}
|
||||
</div>
|
||||
</SelectValueContext>
|
||||
),
|
||||
SelectValue: () => <>{React.use(SelectValueContext)}</>,
|
||||
SelectTrigger: ({ children }: { children: React.ReactNode }) => (
|
||||
<button type="button" data-testid="content-item-select">
|
||||
{children}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
data-testid="content-item-select-null"
|
||||
onClick={() => onValueChange(null)}
|
||||
>
|
||||
select null
|
||||
</button>
|
||||
{children}
|
||||
</div>
|
||||
),
|
||||
SelectTrigger: ({ children }: { children: React.ReactNode }) => (
|
||||
<button type="button" data-testid="content-item-select">
|
||||
{children}
|
||||
</button>
|
||||
),
|
||||
SelectContent: ({ children }: { children: React.ReactNode }) => <div>{children}</div>,
|
||||
SelectItem: ({ children }: { children: React.ReactNode }) => <div>{children}</div>,
|
||||
SelectItemText: ({ children }: { children: React.ReactNode }) => <span>{children}</span>,
|
||||
SelectItemIndicator: () => <span>selected</span>,
|
||||
}))
|
||||
),
|
||||
SelectContent: ({ children }: { children: React.ReactNode }) => <div>{children}</div>,
|
||||
SelectItem: ({ children }: { children: React.ReactNode }) => <div>{children}</div>,
|
||||
SelectItemText: ({ children }: { children: React.ReactNode }) => <span>{children}</span>,
|
||||
SelectItemIndicator: () => <span>selected</span>,
|
||||
}
|
||||
})
|
||||
|
||||
vi.mock('@/app/components/base/file-uploader', () => ({
|
||||
FileUploaderInAttachmentWrapper: ({
|
||||
|
||||
@ -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) => {
|
||||
}}
|
||||
>
|
||||
<SelectTrigger size="large" className="w-full" aria-label={field.output_variable_name}>
|
||||
{typeof value === 'string' ? value : ''}
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent listClassName="max-h-[140px] overflow-y-auto">
|
||||
{options.map((option) => (
|
||||
|
||||
@ -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}
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent listClassName="max-h-[140px] overflow-y-auto">
|
||||
{field.option_source.value.map((option) => (
|
||||
|
||||
@ -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)}
|
||||
>
|
||||
<SelectTrigger className="w-full">
|
||||
{String(inputsFormValue?.[form.variable] ?? form.default ?? form.label)}
|
||||
<SelectValue placeholder={form.label} />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{form.options.map((option: string) => (
|
||||
|
||||
@ -128,6 +128,7 @@ describe('LanguageSelect', () => {
|
||||
{children}
|
||||
</button>
|
||||
),
|
||||
SelectValue: ({ placeholder }: { placeholder?: React.ReactNode }) => <>{placeholder}</>,
|
||||
SelectContent: ({ children }: { children: React.ReactNode }) => <div>{children}</div>,
|
||||
SelectItem: ({ children }: { children: React.ReactNode }) => <div>{children}</div>,
|
||||
SelectItemText: ({ children }: { children: React.ReactNode }) => <span>{children}</span>,
|
||||
|
||||
@ -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<ILanguageSelectProps> = ({ 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 || <span> </span>}
|
||||
<SelectValue placeholder={<span> </span>} />
|
||||
</SelectTrigger>
|
||||
<SelectContent placement="bottom-start" sideOffset={4} popupClassName="w-max">
|
||||
{supportedLanguages.map(({ prompt_name }) => (
|
||||
|
||||
@ -89,10 +89,7 @@ function Popup({
|
||||
)
|
||||
const { theme } = useTheme()
|
||||
const language = useLanguage()
|
||||
const previewCardHandle = useMemo(
|
||||
() => createPreviewCardHandle<ModelSelectorPreviewPayload>(),
|
||||
[],
|
||||
)
|
||||
const [previewCardHandle] = useState(() => createPreviewCardHandle<ModelSelectorPreviewPayload>())
|
||||
const [marketplaceCollapsed, setMarketplaceCollapsed] = useState(false)
|
||||
const [showIncompatibleModels, setShowIncompatibleModels] = useState(false)
|
||||
const { modelProviders } = useProviderContext()
|
||||
|
||||
@ -282,6 +282,7 @@ const WorkspaceRoleCheckboxList = ({
|
||||
</ul>
|
||||
) : (
|
||||
<RadioGroup
|
||||
aria-label={t(($) => $['role.workspaceRoles.title'], { ns: 'permission' })}
|
||||
value={selectedRoleIds[0] ?? ''}
|
||||
onValueChange={handleRadioValueChange}
|
||||
className="flex-col items-stretch gap-0.5 pb-2"
|
||||
|
||||
@ -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<IRunOnceProps> = ({
|
||||
<div className="mt-1">
|
||||
{item.type === 'select' && (
|
||||
<Select<string>
|
||||
value={selectValue}
|
||||
value={selectValue ?? defaultSelectValue}
|
||||
onValueChange={(nextValue) => {
|
||||
if (nextValue == null || nextValue === '') return
|
||||
handleInputsChange({ ...inputsRef.current, [item.key]: nextValue })
|
||||
}}
|
||||
>
|
||||
<SelectTrigger className="w-full">
|
||||
{selectValue ??
|
||||
defaultSelectValue ??
|
||||
t(($) => $['placeholder.select'], { ns: 'common' })}
|
||||
<SelectValue
|
||||
placeholder={t(($) => $['placeholder.select'], { ns: 'common' })}
|
||||
/>
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{(item.options || []).map((option) => (
|
||||
|
||||
@ -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<BlockPreviewPayload>(), [])
|
||||
const [previewCardHandle] = useState(() => createPreviewCardHandle<BlockPreviewPayload>())
|
||||
const previewDescriptionBaseId = useId()
|
||||
|
||||
// Use external blocks if provided, otherwise fallback to hook-based blocks
|
||||
|
||||
@ -60,7 +60,7 @@ const FeaturedTools = ({
|
||||
}: FeaturedToolsProps) => {
|
||||
const { t } = useTranslation()
|
||||
const language = useGetLanguage()
|
||||
const previewCardHandle = useMemo(() => createPreviewCardHandle<FeaturedToolPreviewPayload>(), [])
|
||||
const [previewCardHandle] = useState(() => createPreviewCardHandle<FeaturedToolPreviewPayload>())
|
||||
const [visibleCount, setVisibleCount] = useState(INITIAL_VISIBLE_COUNT)
|
||||
const [visibleCountPlugins, setVisibleCountPlugins] = useState(plugins)
|
||||
const [isCollapsed, setIsCollapsed] = useFeaturedToolsCollapsed()
|
||||
|
||||
@ -58,13 +58,11 @@ const FeaturedTriggers = ({
|
||||
}: FeaturedTriggersProps) => {
|
||||
const { t } = useTranslation()
|
||||
const language = useGetLanguage()
|
||||
const previewCardHandle = useMemo(
|
||||
() => createPreviewCardHandle<FeaturedTriggerPreviewPayload>(),
|
||||
[],
|
||||
const [previewCardHandle] = useState(() =>
|
||||
createPreviewCardHandle<FeaturedTriggerPreviewPayload>(),
|
||||
)
|
||||
const triggerActionPreviewCardHandle = useMemo(
|
||||
() => createPreviewCardHandle<TriggerPluginActionPreviewPayload>(),
|
||||
[],
|
||||
const [triggerActionPreviewCardHandle] = useState(() =>
|
||||
createPreviewCardHandle<TriggerPluginActionPreviewPayload>(),
|
||||
)
|
||||
const [visibleCount, setVisibleCount] = useState(INITIAL_VISIBLE_COUNT)
|
||||
const [visibleCountPlugins, setVisibleCountPlugins] = useState(plugins)
|
||||
|
||||
@ -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<ToolActionPreviewPayload>(), [])
|
||||
const [previewCardHandle] = useState(() => createPreviewCardHandle<ToolActionPreviewPayload>())
|
||||
const isFlatView = viewType === ViewType.flat
|
||||
|
||||
const { letters, flatTools, treeGroups } = useMemo(
|
||||
|
||||
@ -68,7 +68,7 @@ const Snippets = ({ searchText, onSearchTextChange, insertPayload, onInserted }:
|
||||
const deferredSearchText = useDeferredValue(searchText)
|
||||
const viewportRef = useRef<HTMLDivElement>(null)
|
||||
const [tagIds, setTagIds] = useState<string[]>([])
|
||||
const previewCardHandle = useMemo(() => createPreviewCardHandle<SnippetListItemData>(), [])
|
||||
const [previewCardHandle] = useState(() => createPreviewCardHandle<SnippetListItemData>())
|
||||
|
||||
const keyword = deferredSearchText.trim() || undefined
|
||||
|
||||
|
||||
@ -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<StartBlockPreviewPayload>(), [])
|
||||
const [previewCardHandle] = useState(() => createPreviewCardHandle<StartBlockPreviewPayload>())
|
||||
const previewDescriptionBaseId = useId()
|
||||
|
||||
const filteredBlocks = useMemo(() => {
|
||||
|
||||
@ -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<ToolActionPreviewPayload>(), [])
|
||||
const [previewCardHandle] = useState(() => createPreviewCardHandle<ToolActionPreviewPayload>())
|
||||
const isFlatView = viewType === ViewType.flat
|
||||
const isShowLetterIndex = isFlatView && tools.length > 10
|
||||
|
||||
|
||||
@ -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<TriggerPluginActionPreviewPayload>(),
|
||||
[],
|
||||
const [previewCardHandle] = useState(() =>
|
||||
createPreviewCardHandle<TriggerPluginActionPreviewPayload>(),
|
||||
)
|
||||
|
||||
const normalizedSearch = searchText.trim().toLowerCase()
|
||||
|
||||
@ -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<Props> = ({
|
||||
}}
|
||||
>
|
||||
<SelectTrigger className="w-full">
|
||||
{String(
|
||||
value || payload.default || t(($) => $['placeholder.select'], { ns: 'common' }),
|
||||
)}
|
||||
<SelectValue placeholder={t(($) => $['placeholder.select'], { ns: 'common' })} />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{(payload.options || []).map((option) => (
|
||||
|
||||
@ -76,10 +76,10 @@ const OutputVarList: FC<Props> = ({ 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)
|
||||
}
|
||||
|
||||
@ -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<Props> = ({ readonly, className, value, onChange }) => {
|
||||
return (
|
||||
<div className={cn(className, !readonly && 'cursor-pointer select-none')}>
|
||||
<Select
|
||||
<Select<VarType>
|
||||
value={value}
|
||||
readOnly={readonly}
|
||||
onValueChange={(type) => {
|
||||
@ -43,7 +44,7 @@ const VarReferencePicker: FC<Props> = ({ readonly, className, value, onChange })
|
||||
className="h-8 w-30 cursor-pointer rounded-lg px-2.5 text-[13px] text-text-primary"
|
||||
title={value}
|
||||
>
|
||||
<span className="capitalize">{value}</span>
|
||||
<SelectValue className="capitalize" />
|
||||
</SelectTrigger>
|
||||
<SelectContent
|
||||
sideOffset={4}
|
||||
@ -51,7 +52,7 @@ const VarReferencePicker: FC<Props> = ({ readonly, className, value, onChange })
|
||||
listClassName="p-0"
|
||||
>
|
||||
{TYPES.map((type) => (
|
||||
<SelectItem
|
||||
<SelectItem<VarType>
|
||||
key={type}
|
||||
value={type}
|
||||
className="h-7.5 rounded-lg pr-2 pl-3 text-[13px] text-text-primary"
|
||||
|
||||
@ -9,6 +9,7 @@ import {
|
||||
SelectItemIndicator,
|
||||
SelectItemText,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from '@langgenius/dify-ui/select'
|
||||
import * as React from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
@ -156,7 +157,7 @@ const SelectPreview: React.FC<{ label: string; options: string[] }> = ({ label,
|
||||
className="w-full rounded-[10px]"
|
||||
aria-label="human-input-note-select"
|
||||
>
|
||||
{value}
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent listClassName="max-h-[140px] overflow-y-auto">
|
||||
{options.map((option) => (
|
||||
|
||||
@ -8,6 +8,7 @@ import {
|
||||
SelectItemIndicator,
|
||||
SelectItemText,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from '@langgenius/dify-ui/select'
|
||||
import { RiCheckLine } from '@remixicon/react'
|
||||
import { useState } from 'react'
|
||||
@ -43,7 +44,7 @@ const TypeSelector: FC<TypeSelectorProps> = ({ items, currentValue, onSelect, po
|
||||
open && 'bg-state-base-hover',
|
||||
)}
|
||||
>
|
||||
<span className="system-xs-medium text-text-tertiary">{currentValue}</span>
|
||||
<SelectValue className="system-xs-medium text-text-tertiary" />
|
||||
</SelectTrigger>
|
||||
<SelectContent
|
||||
sideOffset={4}
|
||||
@ -54,7 +55,7 @@ const TypeSelector: FC<TypeSelectorProps> = ({ items, currentValue, onSelect, po
|
||||
{items.map((item) => {
|
||||
const isSelected = item.value === currentValue
|
||||
return (
|
||||
<SelectItem
|
||||
<SelectItem<Type | ArrayType>
|
||||
key={item.value}
|
||||
value={item.value}
|
||||
className="gap-x-1 rounded-lg px-2 py-1"
|
||||
|
||||
@ -11,6 +11,7 @@ import {
|
||||
SelectItemIndicator,
|
||||
SelectItemText,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from '@langgenius/dify-ui/select'
|
||||
import { Switch } from '@langgenius/dify-ui/switch'
|
||||
import { Textarea } from '@langgenius/dify-ui/textarea'
|
||||
@ -189,14 +190,16 @@ const AddExtractParameter: FC<Props> = ({ type, payload, onSave, onCancel }) =>
|
||||
ns: 'workflow',
|
||||
})}
|
||||
>
|
||||
<Select
|
||||
<Select<ParamType>
|
||||
value={param.type}
|
||||
onValueChange={(value) => value && handleParamChange('type')(value)}
|
||||
>
|
||||
<SelectTrigger className="w-full capitalize">{param.type}</SelectTrigger>
|
||||
<SelectTrigger className="w-full capitalize">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{TYPES.map((type) => (
|
||||
<SelectItem key={type} value={type} className="capitalize">
|
||||
<SelectItem<ParamType> key={type} value={type} className="capitalize">
|
||||
<SelectItemText className="capitalize">{type}</SelectItemText>
|
||||
<SelectItemIndicator />
|
||||
</SelectItem>
|
||||
|
||||
@ -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<string, AgentLogSourceResponse>
|
||||
|
||||
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(
|
||||
<AgentLogSourcePicker
|
||||
value={[]}
|
||||
groups={groups}
|
||||
isLoading={false}
|
||||
isError={false}
|
||||
onRetry={vi.fn()}
|
||||
onChange={vi.fn()}
|
||||
/>,
|
||||
)
|
||||
|
||||
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()
|
||||
})
|
||||
})
|
||||
@ -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<AgentLogSourceGroupResponse, 'sources'> & {
|
||||
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<AgentLogSourceComboboxGroup[]>(
|
||||
() => 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 (
|
||||
<Combobox<AgentLogSourceResponse, true>
|
||||
multiple
|
||||
items={groups}
|
||||
items={sourceGroups}
|
||||
value={selectedSources}
|
||||
itemToStringLabel={getSourceLabel}
|
||||
onValueChange={(nextSources) => {
|
||||
@ -112,9 +120,9 @@ export function AgentLogSourcePicker({
|
||||
)}
|
||||
{!isLoading && !isError && (
|
||||
<>
|
||||
<ComboboxList className="max-h-69 p-2 pt-1">
|
||||
{groups.map((group) => (
|
||||
<ComboboxGroup key={group.type} items={group.sources ?? []}>
|
||||
<ComboboxList<AgentLogSourceComboboxGroup> className="max-h-69 p-2 pt-1">
|
||||
{(group) => (
|
||||
<ComboboxGroup key={group.type} items={group.items}>
|
||||
<ComboboxGroupLabel className="px-1 pt-2 pb-1">
|
||||
{getSourceGroupLabel(group, t)}
|
||||
</ComboboxGroupLabel>
|
||||
@ -134,7 +142,7 @@ export function AgentLogSourcePicker({
|
||||
)}
|
||||
</ComboboxCollection>
|
||||
</ComboboxGroup>
|
||||
))}
|
||||
)}
|
||||
</ComboboxList>
|
||||
<ComboboxEmpty className="px-3 py-3 text-center system-xs-regular">
|
||||
{t(($) => $['agentDetail.logs.filters.source.empty'])}
|
||||
|
||||
@ -77,6 +77,7 @@ function SourceMethodSection() {
|
||||
hideHeader
|
||||
>
|
||||
<RadioGroup<GuideMethod>
|
||||
aria-label={t(($) => $['createGuide.steps.method'])}
|
||||
value={method}
|
||||
onValueChange={selectMethod}
|
||||
className="flex flex-col items-stretch gap-2 sm:flex-row"
|
||||
|
||||
@ -90,6 +90,7 @@ function TargetEnvironmentSection() {
|
||||
</div>
|
||||
{hasEnvironmentOptions ? (
|
||||
<RadioGroup<string>
|
||||
aria-label={t(($) => $['createGuide.target.environment'])}
|
||||
value={effectiveSelectedEnvironmentId}
|
||||
onValueChange={selectEnvironment}
|
||||
className="grid grid-cols-1 items-stretch gap-3 lg:grid-cols-2"
|
||||
|
||||
@ -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',
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
@ -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<HTMLDivElement>(
|
||||
{
|
||||
error: sourceAppsError,
|
||||
@ -163,8 +168,10 @@ export function SourceAppPicker({
|
||||
return (
|
||||
<Combobox<App>
|
||||
items={apps}
|
||||
value={selectedApp}
|
||||
open={!disabled && isShow}
|
||||
inputValue={searchText}
|
||||
isItemEqualToValue={isSameApp}
|
||||
onOpenChange={(open) => {
|
||||
setIsShow(disabled ? false : open)
|
||||
}}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user