fix(dify-ui): improve picker type inference (#38428)

This commit is contained in:
yyh 2026-07-05 22:01:21 +08:00 committed by GitHub
parent acfef79a8f
commit e742e0ec17
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
21 changed files with 243 additions and 187 deletions

View File

@ -17,7 +17,30 @@ import { parsePlacement } from '../placement'
export type { Placement }
export const Autocomplete = BaseAutocomplete.Root
export type AutocompleteRootProps<ItemValue> = BaseAutocomplete.Root.Props<ItemValue>
export type AutocompleteRootGroupedProps<
Items extends readonly { items: readonly unknown[] }[],
> = Omit<AutocompleteRootProps<Items[number]['items'][number]>, 'items'> & {
items: Items
}
export type AutocompleteRootFlatProps<ItemValue>
= Omit<AutocompleteRootProps<ItemValue>, 'items'>
& {
items?: readonly ItemValue[]
}
export function Autocomplete<Items extends readonly { items: readonly unknown[] }[]>(
props: AutocompleteRootGroupedProps<Items>,
): React.JSX.Element
export function Autocomplete<ItemValue>(
props: AutocompleteRootFlatProps<ItemValue>,
): React.JSX.Element
export function Autocomplete(
props: AutocompleteRootProps<unknown>,
): React.JSX.Element {
return <BaseAutocomplete.Root {...props} />
}
export const AutocompleteValue = BaseAutocomplete.Value
export const AutocompleteGroup = BaseAutocomplete.Group
export const AutocompleteCollection = BaseAutocomplete.Collection
@ -25,7 +48,6 @@ export const AutocompleteRow = BaseAutocomplete.Row
export const useAutocompleteFilter = BaseAutocomplete.useFilter
export const useAutocompleteFilteredItems = BaseAutocomplete.useFilteredItems
export type AutocompleteRootProps<ItemValue> = BaseAutocomplete.Root.Props<ItemValue>
export type AutocompleteRootChangeEventDetails = BaseAutocomplete.Root.ChangeEventDetails
export type AutocompleteRootHighlightEventDetails = BaseAutocomplete.Root.HighlightEventDetails

View File

@ -17,7 +17,15 @@ import { parsePlacement } from '../placement'
export type { Placement }
export const Combobox = BaseCombobox.Root
export type ComboboxRootProps<Value, Multiple extends boolean | undefined = false>
= BaseCombobox.Root.Props<Value, Multiple>
export function Combobox<Value, Multiple extends boolean | undefined = false>(
props: ComboboxRootProps<Value, Multiple>,
): React.JSX.Element {
return <BaseCombobox.Root {...props} />
}
export const ComboboxValue = BaseCombobox.Value
export const ComboboxGroup = BaseCombobox.Group
export const ComboboxCollection = BaseCombobox.Collection
@ -25,8 +33,6 @@ export const ComboboxRow = BaseCombobox.Row
export const useComboboxFilter = BaseCombobox.useFilter
export const useComboboxFilteredItems = BaseCombobox.useFilteredItems
export type ComboboxRootProps<Value, Multiple extends boolean | undefined = false>
= BaseCombobox.Root.Props<Value, Multiple>
export type ComboboxRootChangeEventDetails = BaseCombobox.Root.ChangeEventDetails
export type ComboboxRootHighlightEventDetails = BaseCombobox.Root.HighlightEventDetails

View File

@ -16,7 +16,17 @@ import { parsePlacement } from '../placement'
export type { Placement }
export const Select = BaseSelect.Root
export type SelectRootProps<
Value,
Multiple extends boolean | undefined = false,
> = BaseSelect.Root.Props<Value, Multiple>
export function Select<Value, Multiple extends boolean | undefined = false>(
props: SelectRootProps<Value, Multiple>,
): React.JSX.Element {
return <BaseSelect.Root {...props} />
}
export const SelectValue = BaseSelect.Value
export const SelectGroup = BaseSelect.Group

View File

@ -36,9 +36,9 @@ const RangeSelector: FC<Props> = ({
name: t(`filter.period.${range.name}`, { ns: 'appLog' }),
}))
}, [ranges, t])
const [value, setValue] = useState('0')
const [value, setValue] = useState(0)
const selectedItem = useMemo(() => {
return items.find(item => String(item.value) === value) ?? null
return items.find(item => item.value === value) ?? null
}, [items, value])
const handleSelectRange = useCallback((item: TimePeriodOption) => {
@ -50,20 +50,20 @@ const RangeSelector: FC<Props> = ({
period = { start: startOfToday, end: endOfToday }
}
else {
period = { start: today.subtract(item.value as number, 'day').startOf('day'), end: today.endOf('day') }
period = { start: today.subtract(item.value, 'day').startOf('day'), end: today.endOf('day') }
}
onSelect({ query: period!, name })
}, [onSelect])
return (
<Select
value={selectedItem ? String(selectedItem.value) : null}
<Select<number>
value={selectedItem?.value ?? null}
open={open}
onOpenChange={setOpen}
onValueChange={(nextValue) => {
if (!nextValue)
if (nextValue == null)
return
const nextItem = items.find(item => String(item.value) === nextValue)
const nextItem = items.find(item => item.value === nextValue)
if (!nextItem)
return
setValue(nextValue)
@ -80,7 +80,7 @@ const RangeSelector: FC<Props> = ({
</SelectTrigger>
<SelectContent className="translate-x-[-24px]" popupClassName="w-[200px]" listClassName="p-1">
{items.map(item => (
<SelectItem key={item.value} value={String(item.value)} className="h-8 py-0 pr-2 pl-7 system-md-regular">
<SelectItem key={item.value} value={item.value} className="h-8 py-0 pr-2 pl-7 system-md-regular">
<SelectItemText className="px-0">{item.name}</SelectItemText>
<SelectItemIndicator className="absolute top-[8px] left-2 ml-0" />
</SelectItem>

View File

@ -33,7 +33,7 @@ import TypeSelector from './type-select'
import { CHECKBOX_DEFAULT_FALSE_VALUE, CHECKBOX_DEFAULT_TRUE_VALUE, TEXT_MAX_LENGTH } from './utils'
type Translate = (key: string, options?: Record<string, unknown>) => string
const EMPTY_SELECT_VALUE = '__empty__'
const EMPTY_SELECT_VALUE = '__empty__' as const
type ConfigModalFormFieldsProps = {
checkboxDefaultSelectValue: string
@ -169,10 +169,14 @@ const ConfigModalFormFields: FC<ConfigModalFormFieldsProps> = ({
</Field>
{options && options.length > 0 && (
<Field title={t('variableConfig.defaultValue', { ns: 'appDebug' })}>
<Select
<Select<string>
key={`default-select-${options.join('-')}`}
value={tempPayload.default ? String(tempPayload.default) : EMPTY_SELECT_VALUE}
onValueChange={value => onPayloadChange('default')(value === EMPTY_SELECT_VALUE ? undefined : value)}
value={typeof tempPayload.default === 'string' ? tempPayload.default : EMPTY_SELECT_VALUE}
onValueChange={(value) => {
if (value == null)
return
onPayloadChange('default')(value === EMPTY_SELECT_VALUE ? undefined : value)
}}
>
<SelectTrigger size="large" className="w-full">
<SelectValue placeholder={t('variableConfig.selectDefaultValue', { ns: 'appDebug' })} />

View File

@ -105,17 +105,17 @@ const ChatUserInput = ({
/>
)}
{type === 'select' && (
<Select
value={inputs[key] ? String(inputs[key]) : null}
<Select<string>
value={typeof inputs[key] === 'string' && inputs[key] !== '' ? inputs[key] : null}
disabled={debugInputReadonly}
onValueChange={(nextValue) => {
if (!nextValue)
if (nextValue == null || nextValue === '')
return
handleInputValueChange(key, nextValue)
}}
>
<SelectTrigger className="w-full">
{String(inputs[key] || t('placeholder.select', { ns: 'common' }))}
{typeof inputs[key] === 'string' && inputs[key] !== '' ? inputs[key] : t('placeholder.select', { ns: 'common' })}
</SelectTrigger>
<SelectContent>
{(options || []).map(option => (

View File

@ -165,17 +165,17 @@ const PromptValuePanel: FC<IPromptValuePanelProps> = ({
/>
)}
{type === 'select' && (
<Select
value={inputs[key] ? String(inputs[key]) : null}
<Select<string>
value={typeof inputs[key] === 'string' && inputs[key] !== '' ? inputs[key] : null}
disabled={debugInputReadonly}
onValueChange={(nextValue) => {
if (!nextValue)
if (nextValue == null || nextValue === '')
return
handleInputValueChange(key, nextValue)
}}
>
<SelectTrigger className="w-full bg-gray-50">
{String(inputs[key] || t('placeholder.select', { ns: 'common' }))}
{typeof inputs[key] === 'string' && inputs[key] !== '' ? inputs[key] : t('placeholder.select', { ns: 'common' })}
</SelectTrigger>
<SelectContent>
{(options || []).map(option => (

View File

@ -21,7 +21,7 @@ import { useAppVoices } from '@/service/use-apps'
import { TtsAutoPlay } from '@/types/app'
type SelectOption = {
value: string | number
value: string
name: string
}
@ -40,7 +40,7 @@ const VoiceParamConfig = ({
const text2speech = useFeatures(state => state.features.text2speech)
const featuresStore = useFeaturesStore()
const formatLanguageName = (item: SelectOption) => {
return t(`voice.language.${replace(String(item.value), '-', '')}`, item.name, { ns: 'common' as const })
return t(`voice.language.${replace(item.value, '-', '')}`, item.name, { ns: 'common' as const })
}
let languageItem = languages.find(item => item.value === text2speech?.language)
@ -101,9 +101,9 @@ const VoiceParamConfig = ({
</Infotip>
</div>
<Select
value={languageItem ? String(languageItem.value) : null}
value={languageItem?.value ?? null}
onValueChange={(nextValue) => {
if (!nextValue)
if (nextValue == null)
return
handleChange({
language: nextValue,
@ -115,7 +115,7 @@ const VoiceParamConfig = ({
</SelectTrigger>
<SelectContent listClassName="max-h-60">
{languages.map(item => (
<SelectItem key={item.value} value={String(item.value)}>
<SelectItem key={item.value} value={item.value}>
<SelectItemText>
{formatLanguageName(item)}
</SelectItemText>
@ -131,10 +131,10 @@ const VoiceParamConfig = ({
</div>
<div className="flex items-center gap-1">
<Select
value={voiceItem ? String(voiceItem.value) : null}
value={voiceItem?.value ?? null}
disabled={!languageItem}
onValueChange={(nextValue) => {
if (!nextValue)
if (nextValue == null || nextValue === '')
return
handleChange({
voice: nextValue,
@ -147,7 +147,7 @@ const VoiceParamConfig = ({
</SelectTrigger>
<SelectContent listClassName="max-h-60">
{voiceItems?.map((item: SelectOption) => (
<SelectItem key={item.value} value={String(item.value)}>
<SelectItem key={item.value} value={item.value}>
<SelectItemText>
{item.name}
</SelectItemText>

View File

@ -1,3 +1,4 @@
import type { ComponentProps } from 'react'
import { CheckboxGroup } from '@langgenius/dify-ui/checkbox-group'
import { fireEvent, render, screen } from '@testing-library/react'
import { beforeEach, describe, expect, it, vi } from 'vitest'
@ -12,7 +13,7 @@ vi.mock('../../display-toggle', () => ({
}))
describe('MenuBar', () => {
const defaultProps = {
const defaultProps: ComponentProps<typeof MenuBar> = {
hasSelectableSegments: true,
isLoading: false,
totalText: '10 Chunks',
@ -21,7 +22,7 @@ describe('MenuBar', () => {
{ value: 0, name: 'Enabled' },
{ value: 1, name: 'Disabled' },
],
selectDefaultValue: 'all' as const,
selectDefaultValue: 'all',
onChangeStatus: vi.fn(),
inputValue: '',
onInputChange: vi.fn(),

View File

@ -1,4 +1,5 @@
'use client'
import type { SegmentStatusFilterOption, SegmentStatusFilterValue } from '../hooks/use-search-filter'
import { Checkbox } from '@langgenius/dify-ui/checkbox'
import { Select, SelectContent, SelectItem, SelectItemIndicator, SelectItemText, SelectTrigger } from '@langgenius/dify-ui/select'
import { useTranslation } from 'react-i18next'
@ -7,18 +8,13 @@ import Input from '@/app/components/base/input'
import DisplayToggle from '../display-toggle'
import s from '../style.module.css'
type Item = {
value: number | string
name: string
} & Record<string, unknown>
type MenuBarProps = {
hasSelectableSegments: boolean
isLoading: boolean
totalText: string
statusList: Item[]
selectDefaultValue: 'all' | 0 | 1
onChangeStatus: (item: Item) => void
statusList: SegmentStatusFilterOption[]
selectDefaultValue: SegmentStatusFilterValue
onChangeStatus: (item: SegmentStatusFilterOption) => void
inputValue: string
onInputChange: (value: string) => void
isCollapsed: boolean
@ -55,12 +51,12 @@ function MenuBar({
<span className="size-4 shrink-0" aria-hidden />
)}
<div className="flex-1 pl-5 system-sm-semibold-uppercase text-text-secondary">{totalText}</div>
<Select
value={selectedStatus ? String(selectedStatus.value) : null}
<Select<SegmentStatusFilterValue>
value={selectedStatus?.value ?? null}
onValueChange={(nextValue) => {
if (!nextValue)
if (nextValue == null)
return
const nextItem = statusList.find(item => String(item.value) === nextValue)
const nextItem = statusList.find(item => item.value === nextValue)
if (nextItem)
onChangeStatus(nextItem)
}}
@ -70,7 +66,7 @@ function MenuBar({
</SelectTrigger>
<SelectContent popupClassName="w-[160px]">
{statusList.map(item => (
<SelectItem key={item.value} value={String(item.value)}>
<SelectItem key={item.value} value={item.value}>
<SelectItemText>{item.name}</SelectItemText>
<SelectItemIndicator />
</SelectItem>

View File

@ -2,8 +2,10 @@ import { useDebounceFn } from 'ahooks'
import { useCallback, useMemo, useRef, useState } from 'react'
import { useTranslation } from 'react-i18next'
type SelectOption = {
value: string | number
export type SegmentStatusFilterValue = 'all' | 0 | 1
export type SegmentStatusFilterOption = {
value: SegmentStatusFilterValue
name: string
}
@ -11,10 +13,10 @@ type UseSearchFilterReturn = {
inputValue: string
searchValue: string
selectedStatus: boolean | 'all'
statusList: SelectOption[]
selectDefaultValue: 'all' | 0 | 1
statusList: SegmentStatusFilterOption[]
selectDefaultValue: SegmentStatusFilterValue
handleInputChange: (value: string) => void
onChangeStatus: (item: SelectOption) => void
onChangeStatus: (item: SegmentStatusFilterOption) => void
onClearFilter: () => void
resetPage: () => void
}
@ -31,7 +33,7 @@ export const useSearchFilter = (options: UseSearchFilterOptions): UseSearchFilte
const [searchValue, setSearchValue] = useState<string>('')
const [selectedStatus, setSelectedStatus] = useState<boolean | 'all'>('all')
const statusList = useRef<SelectOption[]>([
const statusList = useRef<SegmentStatusFilterOption[]>([
{ value: 'all', name: t('list.index.all', { ns: 'datasetDocuments' }) },
{ value: 0, name: t('list.status.disabled', { ns: 'datasetDocuments' }) },
{ value: 1, name: t('list.status.enabled', { ns: 'datasetDocuments' }) },
@ -47,7 +49,7 @@ export const useSearchFilter = (options: UseSearchFilterOptions): UseSearchFilte
handleSearch()
}, [handleSearch])
const onChangeStatus = useCallback(({ value }: SelectOption) => {
const onChangeStatus = useCallback(({ value }: SegmentStatusFilterOption) => {
setSelectedStatus(value === 'all' ? 'all' : !!value)
onPageChange(1)
}, [onPageChange])

View File

@ -19,7 +19,7 @@ type SelectOption = {
}
type TimezoneOption = {
value: string | number
value: string
name: string
}
@ -119,7 +119,7 @@ export default function PreferencePage() {
value={selectedLanguage?.value ?? null}
disabled={editing}
onValueChange={(nextValue) => {
if (!nextValue)
if (nextValue == null)
return
const nextItem = languageOptions.find(item => item.value === nextValue)
if (nextItem)
@ -142,12 +142,12 @@ export default function PreferencePage() {
<div className="mb-6">
<div className={titleClassName}>{t('language.timezone', { ns: 'common' })}</div>
<Select
value={selectedTimezone ? String(selectedTimezone.value) : null}
value={selectedTimezone?.value ?? null}
disabled={editing}
onValueChange={(nextValue) => {
if (!nextValue)
return
const nextItem = timezones.find(item => String(item.value) === nextValue)
const nextItem = timezones.find(item => item.value === nextValue)
if (nextItem)
handleSelectTimezone(nextItem)
}}
@ -157,7 +157,7 @@ export default function PreferencePage() {
</SelectTrigger>
<SelectContent>
{timezones.map(item => (
<SelectItem key={item.value} value={String(item.value)}>
<SelectItem key={item.value} value={item.value}>
<SelectItemText>{item.name}</SelectItemText>
<SelectItemIndicator />
</SelectItem>

View File

@ -24,7 +24,7 @@ import SelectPackage from './steps/selectPackage'
const i18nPrefix = 'installFromGitHub'
type SelectOption = {
value: string | number
value: string
name: string
}

View File

@ -5,7 +5,7 @@ import { PluginCategoryEnum } from '../../../../types'
import SelectPackage from '../selectPackage'
type SelectOption = {
value: string | number
value: string
name: string
}

View File

@ -12,7 +12,7 @@ import { handleUpload } from '../../hooks'
const i18nPrefix = 'installFromGitHub'
type SelectOption = {
value: string | number
value: string
name: string
}
@ -49,8 +49,8 @@ const SelectPackage: React.FC<SelectPackageProps> = ({
const { t } = useTranslation()
const isEdit = Boolean(updatePayload)
const [isUploading, setIsUploading] = React.useState(false)
const selectedVersionOption = versions.find(item => String(item.value) === selectedVersion) ?? null
const selectedPackageOption = packages.find(item => String(item.value) === selectedPackage) ?? null
const selectedVersionOption = versions.find(item => item.value === selectedVersion) ?? null
const selectedPackageOption = packages.find(item => item.value === selectedPackage) ?? null
const handleUploadPackage = async () => {
if (isUploading)
@ -80,11 +80,11 @@ const SelectPackage: React.FC<SelectPackageProps> = ({
<>
<FieldRoot name="version" className="gap-4 self-stretch">
<Select
value={selectedVersionOption ? String(selectedVersionOption.value) : null}
value={selectedVersionOption?.value ?? null}
onValueChange={(value) => {
if (!value)
if (value == null)
return
const selectedItem = versions.find(item => String(item.value) === value)
const selectedItem = versions.find(item => item.value === value)
if (selectedItem)
onSelectVersion(selectedItem)
}}
@ -110,7 +110,7 @@ const SelectPackage: React.FC<SelectPackageProps> = ({
</SelectTrigger>
<SelectContent popupClassName="w-[512px]">
{versions.map(item => (
<SelectItem key={item.value} value={String(item.value)}>
<SelectItem key={item.value} value={item.value}>
<SelectItemText>{item.name}</SelectItemText>
{item.value === updatePayload?.originalPackageInfo.version && (
<Badge uppercase={true} className="ml-1 shrink-0">INSTALLED</Badge>
@ -123,12 +123,12 @@ const SelectPackage: React.FC<SelectPackageProps> = ({
</FieldRoot>
<FieldRoot name="package" className="gap-4 self-stretch">
<Select
value={selectedPackageOption ? String(selectedPackageOption.value) : null}
value={selectedPackageOption?.value ?? null}
readOnly={!selectedVersion}
onValueChange={(value) => {
if (!value)
if (value == null)
return
const selectedItem = packages.find(item => String(item.value) === value)
const selectedItem = packages.find(item => item.value === value)
if (selectedItem)
onSelectPackage(selectedItem)
}}
@ -141,7 +141,7 @@ const SelectPackage: React.FC<SelectPackageProps> = ({
</SelectTrigger>
<SelectContent popupClassName="w-[512px]">
{packages.map(item => (
<SelectItem key={item.value} value={String(item.value)}>
<SelectItem key={item.value} value={item.value}>
<SelectItemText>{item.name}</SelectItemText>
<SelectItemIndicator />
</SelectItem>

View File

@ -156,8 +156,9 @@ const ReasoningConfigForm: React.FC<Props> = ({
language,
schema,
})
const selectValue = typeof varInput?.value === 'string' ? varInput.value : null
const selectedOption = isSelect && options
? pickerProps.selectItems.find(item => item.value === (varInput?.value as string | number | undefined)) ?? null
? pickerProps.selectItems.find(item => item.value === selectValue) ?? null
: null
return (
@ -230,13 +231,20 @@ const ReasoningConfigForm: React.FC<Props> = ({
/>
)}
{isSelect && options && (
<Select value={selectedOption ? String(selectedOption.value) : null} onValueChange={value => value && handleValueChange(variable, type)(value)}>
<Select<string>
value={selectedOption?.value ?? null}
onValueChange={(value) => {
if (value == null || value === '')
return
handleValueChange(variable, type)(value)
}}
>
<SelectTrigger className="h-8 grow">
{selectedOption?.name ?? placeholder?.[language] ?? placeholder?.en_US}
</SelectTrigger>
<SelectContent>
{pickerProps.selectItems.map(item => (
<SelectItem key={item.value} value={String(item.value)}>
<SelectItem key={item.value} value={item.value}>
<SelectItemText>{item.name}</SelectItemText>
<SelectItemIndicator />
</SelectItem>

View File

@ -38,6 +38,7 @@ type IRunOnceProps = {
isStopping: boolean
} | null
}
const RunOnce: FC<IRunOnceProps> = ({
promptConfig,
inputs,
@ -117,109 +118,115 @@ const RunOnce: FC<IRunOnceProps> = ({
<form onSubmit={onSubmit}>
{(inputs === null || inputs === undefined || Object.keys(inputs).length === 0) || !isInitialized
? null
: promptConfig.prompt_variables.filter(item => item.hide !== true).map(item => (
<div className="mt-4 w-full" key={item.key}>
{item.type !== 'checkbox' && (
<div className="flex h-6 items-center gap-1 system-md-semibold text-text-secondary">
<div className="truncate">{item.name}</div>
{!item.required && <span className="system-xs-regular text-text-tertiary">{t('panel.optional', { ns: 'workflow' })}</span>}
: promptConfig.prompt_variables.filter(item => item.hide !== true).map((item) => {
const inputValue = inputs[item.key]
const selectValue = typeof inputValue === 'string' && inputValue !== '' ? inputValue : null
const defaultSelectValue = typeof item.default === 'string' && item.default !== '' ? item.default : null
return (
<div className="mt-4 w-full" key={item.key}>
{item.type !== 'checkbox' && (
<div className="flex h-6 items-center gap-1 system-md-semibold text-text-secondary">
<div className="truncate">{item.name}</div>
{!item.required && <span className="system-xs-regular text-text-tertiary">{t('panel.optional', { ns: 'workflow' })}</span>}
</div>
)}
<div className="mt-1">
{item.type === 'select' && (
<Select<string>
value={selectValue}
onValueChange={(nextValue) => {
if (nextValue == null || nextValue === '')
return
handleInputsChange({ ...inputsRef.current, [item.key]: nextValue })
}}
>
<SelectTrigger className="w-full">
{selectValue ?? defaultSelectValue ?? t('placeholder.select', { ns: 'common' })}
</SelectTrigger>
<SelectContent>
{(item.options || []).map(option => (
<SelectItem key={option} value={option}>
<SelectItemText>{option}</SelectItemText>
<SelectItemIndicator />
</SelectItem>
))}
</SelectContent>
</Select>
)}
{item.type === 'string' && (
<Input
type="text"
placeholder={item.name}
value={inputs[item.key] as string}
onChange={(e: ChangeEvent<HTMLInputElement>) => { handleInputsChange({ ...inputsRef.current, [item.key]: e.target.value }) }}
maxLength={item.max_length}
/>
)}
{item.type === 'paragraph' && (
<Textarea
aria-label={item.name}
className="h-[104px] sm:text-xs"
placeholder={item.name}
value={inputs[item.key] as string}
onValueChange={(value) => { handleInputsChange({ ...inputsRef.current, [item.key]: value }) }}
/>
)}
{item.type === 'number' && (
<Input
type="number"
placeholder={item.name}
value={inputs[item.key] as number}
onChange={(e: ChangeEvent<HTMLInputElement>) => { handleInputsChange({ ...inputsRef.current, [item.key]: e.target.value }) }}
/>
)}
{item.type === 'checkbox' && (
<BoolInput
name={item.name || item.key}
value={!!inputs[item.key] as boolean}
required={item.required}
onChange={(value) => { handleInputsChange({ ...inputsRef.current, [item.key]: value }) }}
/>
)}
{item.type === 'file' && (
<FileUploaderInAttachmentWrapper
value={inputs[item.key] && typeof inputs[item.key] === 'object' && !Array.isArray(inputs[item.key])
? [inputs[item.key] as FileEntity]
: []}
onChange={(files) => { handleInputsChange({ ...inputsRef.current, [item.key]: files[0] }) }}
fileConfig={{
...item.config,
fileUploadConfig: (visionConfig as any).fileUploadConfig,
}}
/>
)}
{item.type === 'file-list' && (
<FileUploaderInAttachmentWrapper
value={Array.isArray(inputs[item.key]) ? inputs[item.key] as FileEntity[] : []}
onChange={(files) => { handleInputsChange({ ...inputsRef.current, [item.key]: files }) }}
fileConfig={{
...item.config,
// eslint-disable-next-line ts/no-explicit-any
fileUploadConfig: (visionConfig as any).fileUploadConfig,
}}
/>
)}
{item.type === 'json_object' && (
<CodeEditor
language={CodeLanguage.json}
value={inputs[item.key] as string}
onChange={(value) => { handleInputsChange({ ...inputsRef.current, [item.key]: value }) }}
noWrapper
className="bg h-[80px] overflow-y-auto rounded-[10px] bg-components-input-bg-normal p-1"
placeholder={
<div className="whitespace-pre">{typeof item.json_schema === 'string' ? item.json_schema : JSON.stringify(item.json_schema || '', null, 2)}</div>
}
/>
)}
</div>
)}
<div className="mt-1">
{item.type === 'select' && (
<Select
value={inputs[item.key] ? String(inputs[item.key]) : null}
onValueChange={(nextValue) => {
if (!nextValue)
return
handleInputsChange({ ...inputsRef.current, [item.key]: nextValue })
}}
>
<SelectTrigger className="w-full">
{String(inputs[item.key] || item.default || t('placeholder.select', { ns: 'common' }))}
</SelectTrigger>
<SelectContent>
{(item.options || []).map(option => (
<SelectItem key={option} value={option}>
<SelectItemText>{option}</SelectItemText>
<SelectItemIndicator />
</SelectItem>
))}
</SelectContent>
</Select>
)}
{item.type === 'string' && (
<Input
type="text"
placeholder={item.name}
value={inputs[item.key] as string}
onChange={(e: ChangeEvent<HTMLInputElement>) => { handleInputsChange({ ...inputsRef.current, [item.key]: e.target.value }) }}
maxLength={item.max_length}
/>
)}
{item.type === 'paragraph' && (
<Textarea
aria-label={item.name}
className="h-[104px] sm:text-xs"
placeholder={item.name}
value={inputs[item.key] as string}
onValueChange={(value) => { handleInputsChange({ ...inputsRef.current, [item.key]: value }) }}
/>
)}
{item.type === 'number' && (
<Input
type="number"
placeholder={item.name}
value={inputs[item.key] as number}
onChange={(e: ChangeEvent<HTMLInputElement>) => { handleInputsChange({ ...inputsRef.current, [item.key]: e.target.value }) }}
/>
)}
{item.type === 'checkbox' && (
<BoolInput
name={item.name || item.key}
value={!!inputs[item.key] as boolean}
required={item.required}
onChange={(value) => { handleInputsChange({ ...inputsRef.current, [item.key]: value }) }}
/>
)}
{item.type === 'file' && (
<FileUploaderInAttachmentWrapper
value={inputs[item.key] && typeof inputs[item.key] === 'object' && !Array.isArray(inputs[item.key])
? [inputs[item.key] as FileEntity]
: []}
onChange={(files) => { handleInputsChange({ ...inputsRef.current, [item.key]: files[0] }) }}
fileConfig={{
...item.config,
fileUploadConfig: (visionConfig as any).fileUploadConfig,
}}
/>
)}
{item.type === 'file-list' && (
<FileUploaderInAttachmentWrapper
value={Array.isArray(inputs[item.key]) ? inputs[item.key] as FileEntity[] : []}
onChange={(files) => { handleInputsChange({ ...inputsRef.current, [item.key]: files }) }}
fileConfig={{
...item.config,
// eslint-disable-next-line ts/no-explicit-any
fileUploadConfig: (visionConfig as any).fileUploadConfig,
}}
/>
)}
{item.type === 'json_object' && (
<CodeEditor
language={CodeLanguage.json}
value={inputs[item.key] as string}
onChange={(value) => { handleInputsChange({ ...inputsRef.current, [item.key]: value }) }}
noWrapper
className="bg h-[80px] overflow-y-auto rounded-[10px] bg-components-input-bg-normal p-1"
placeholder={
<div className="whitespace-pre">{typeof item.json_schema === 'string' ? item.json_schema : JSON.stringify(item.json_schema || '', null, 2)}</div>
}
/>
)}
</div>
</div>
))}
)
})}
{
visionConfig?.enabled && (
<div className="mt-4 w-full">

View File

@ -74,7 +74,7 @@ const createVar = (type: VarType, variable = 'test.variable'): Var => ({
type SelectOption = {
name: string
value: string | number
value: ErrorHandleMode
}
describe('iteration/use-config', () => {

View File

@ -124,12 +124,12 @@ const Panel: FC<NodePanelProps<IterationNodeType>> = ({
<div className="px-4 py-2">
<Field title={errorResponseMethodLabel}>
<Select
value={selectedResponseMethod ? String(selectedResponseMethod.value) : null}
<Select<ErrorHandleMode>
value={selectedResponseMethod?.value ?? null}
onValueChange={(nextValue) => {
if (!nextValue)
if (nextValue == null)
return
const nextItem = responseMethod.find(item => String(item.value) === nextValue)
const nextItem = responseMethod.find(item => item.value === nextValue)
if (nextItem)
changeErrorResponseMode(nextItem)
}}
@ -140,7 +140,7 @@ const Panel: FC<NodePanelProps<IterationNodeType>> = ({
</SelectTrigger>
<SelectContent>
{responseMethod.map(item => (
<SelectItem key={item.value} value={String(item.value)}>
<SelectItem key={item.value} value={item.value}>
<SelectItemText>{item.name}</SelectItemText>
<SelectItemIndicator />
</SelectItem>

View File

@ -22,7 +22,7 @@ import { toNodeOutputVars } from '../_base/components/variable/utils'
import useNodeCrud from '../_base/hooks/use-node-crud'
type SelectOption = {
value: string | number
value: ErrorHandleMode
name: string
}
@ -98,7 +98,7 @@ const useConfig = (id: string, payload: IterationNodeType) => {
const changeErrorResponseMode = useCallback((item: SelectOption) => {
const newInputs = produce(inputs, (draft) => {
draft.error_handle_mode = item.value as ErrorHandleMode
draft.error_handle_mode = item.value
})
setInputs(newInputs)
}, [inputs, setInputs])

View File

@ -1,7 +1,7 @@
import tz from './timezone.json'
type Item = {
value: number | string
value: string
name: string
}
export const timezones: Item[] = tz