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 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 AutocompleteValue = BaseAutocomplete.Value
export const AutocompleteGroup = BaseAutocomplete.Group export const AutocompleteGroup = BaseAutocomplete.Group
export const AutocompleteCollection = BaseAutocomplete.Collection export const AutocompleteCollection = BaseAutocomplete.Collection
@ -25,7 +48,6 @@ export const AutocompleteRow = BaseAutocomplete.Row
export const useAutocompleteFilter = BaseAutocomplete.useFilter export const useAutocompleteFilter = BaseAutocomplete.useFilter
export const useAutocompleteFilteredItems = BaseAutocomplete.useFilteredItems export const useAutocompleteFilteredItems = BaseAutocomplete.useFilteredItems
export type AutocompleteRootProps<ItemValue> = BaseAutocomplete.Root.Props<ItemValue>
export type AutocompleteRootChangeEventDetails = BaseAutocomplete.Root.ChangeEventDetails export type AutocompleteRootChangeEventDetails = BaseAutocomplete.Root.ChangeEventDetails
export type AutocompleteRootHighlightEventDetails = BaseAutocomplete.Root.HighlightEventDetails export type AutocompleteRootHighlightEventDetails = BaseAutocomplete.Root.HighlightEventDetails

View File

@ -17,7 +17,15 @@ import { parsePlacement } from '../placement'
export type { 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 ComboboxValue = BaseCombobox.Value
export const ComboboxGroup = BaseCombobox.Group export const ComboboxGroup = BaseCombobox.Group
export const ComboboxCollection = BaseCombobox.Collection export const ComboboxCollection = BaseCombobox.Collection
@ -25,8 +33,6 @@ export const ComboboxRow = BaseCombobox.Row
export const useComboboxFilter = BaseCombobox.useFilter export const useComboboxFilter = BaseCombobox.useFilter
export const useComboboxFilteredItems = BaseCombobox.useFilteredItems 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 ComboboxRootChangeEventDetails = BaseCombobox.Root.ChangeEventDetails
export type ComboboxRootHighlightEventDetails = BaseCombobox.Root.HighlightEventDetails export type ComboboxRootHighlightEventDetails = BaseCombobox.Root.HighlightEventDetails

View File

@ -16,7 +16,17 @@ import { parsePlacement } from '../placement'
export type { 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 SelectValue = BaseSelect.Value
export const SelectGroup = BaseSelect.Group export const SelectGroup = BaseSelect.Group

View File

@ -36,9 +36,9 @@ const RangeSelector: FC<Props> = ({
name: t(`filter.period.${range.name}`, { ns: 'appLog' }), name: t(`filter.period.${range.name}`, { ns: 'appLog' }),
})) }))
}, [ranges, t]) }, [ranges, t])
const [value, setValue] = useState('0') const [value, setValue] = useState(0)
const selectedItem = useMemo(() => { const selectedItem = useMemo(() => {
return items.find(item => String(item.value) === value) ?? null return items.find(item => item.value === value) ?? null
}, [items, value]) }, [items, value])
const handleSelectRange = useCallback((item: TimePeriodOption) => { const handleSelectRange = useCallback((item: TimePeriodOption) => {
@ -50,20 +50,20 @@ const RangeSelector: FC<Props> = ({
period = { start: startOfToday, end: endOfToday } period = { start: startOfToday, end: endOfToday }
} }
else { 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({ query: period!, name })
}, [onSelect]) }, [onSelect])
return ( return (
<Select <Select<number>
value={selectedItem ? String(selectedItem.value) : null} value={selectedItem?.value ?? null}
open={open} open={open}
onOpenChange={setOpen} onOpenChange={setOpen}
onValueChange={(nextValue) => { onValueChange={(nextValue) => {
if (!nextValue) if (nextValue == null)
return return
const nextItem = items.find(item => String(item.value) === nextValue) const nextItem = items.find(item => item.value === nextValue)
if (!nextItem) if (!nextItem)
return return
setValue(nextValue) setValue(nextValue)
@ -80,7 +80,7 @@ const RangeSelector: FC<Props> = ({
</SelectTrigger> </SelectTrigger>
<SelectContent className="translate-x-[-24px]" popupClassName="w-[200px]" listClassName="p-1"> <SelectContent className="translate-x-[-24px]" popupClassName="w-[200px]" listClassName="p-1">
{items.map(item => ( {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> <SelectItemText className="px-0">{item.name}</SelectItemText>
<SelectItemIndicator className="absolute top-[8px] left-2 ml-0" /> <SelectItemIndicator className="absolute top-[8px] left-2 ml-0" />
</SelectItem> </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' import { CHECKBOX_DEFAULT_FALSE_VALUE, CHECKBOX_DEFAULT_TRUE_VALUE, TEXT_MAX_LENGTH } from './utils'
type Translate = (key: string, options?: Record<string, unknown>) => string type Translate = (key: string, options?: Record<string, unknown>) => string
const EMPTY_SELECT_VALUE = '__empty__' const EMPTY_SELECT_VALUE = '__empty__' as const
type ConfigModalFormFieldsProps = { type ConfigModalFormFieldsProps = {
checkboxDefaultSelectValue: string checkboxDefaultSelectValue: string
@ -169,10 +169,14 @@ const ConfigModalFormFields: FC<ConfigModalFormFieldsProps> = ({
</Field> </Field>
{options && options.length > 0 && ( {options && options.length > 0 && (
<Field title={t('variableConfig.defaultValue', { ns: 'appDebug' })}> <Field title={t('variableConfig.defaultValue', { ns: 'appDebug' })}>
<Select <Select<string>
key={`default-select-${options.join('-')}`} key={`default-select-${options.join('-')}`}
value={tempPayload.default ? String(tempPayload.default) : EMPTY_SELECT_VALUE} value={typeof tempPayload.default === 'string' ? tempPayload.default : EMPTY_SELECT_VALUE}
onValueChange={value => onPayloadChange('default')(value === EMPTY_SELECT_VALUE ? undefined : value)} onValueChange={(value) => {
if (value == null)
return
onPayloadChange('default')(value === EMPTY_SELECT_VALUE ? undefined : value)
}}
> >
<SelectTrigger size="large" className="w-full"> <SelectTrigger size="large" className="w-full">
<SelectValue placeholder={t('variableConfig.selectDefaultValue', { ns: 'appDebug' })} /> <SelectValue placeholder={t('variableConfig.selectDefaultValue', { ns: 'appDebug' })} />

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -38,6 +38,7 @@ type IRunOnceProps = {
isStopping: boolean isStopping: boolean
} | null } | null
} }
const RunOnce: FC<IRunOnceProps> = ({ const RunOnce: FC<IRunOnceProps> = ({
promptConfig, promptConfig,
inputs, inputs,
@ -117,109 +118,115 @@ const RunOnce: FC<IRunOnceProps> = ({
<form onSubmit={onSubmit}> <form onSubmit={onSubmit}>
{(inputs === null || inputs === undefined || Object.keys(inputs).length === 0) || !isInitialized {(inputs === null || inputs === undefined || Object.keys(inputs).length === 0) || !isInitialized
? null ? null
: promptConfig.prompt_variables.filter(item => item.hide !== true).map(item => ( : promptConfig.prompt_variables.filter(item => item.hide !== true).map((item) => {
<div className="mt-4 w-full" key={item.key}> const inputValue = inputs[item.key]
{item.type !== 'checkbox' && ( const selectValue = typeof inputValue === 'string' && inputValue !== '' ? inputValue : null
<div className="flex h-6 items-center gap-1 system-md-semibold text-text-secondary"> const defaultSelectValue = typeof item.default === 'string' && item.default !== '' ? item.default : null
<div className="truncate">{item.name}</div>
{!item.required && <span className="system-xs-regular text-text-tertiary">{t('panel.optional', { ns: 'workflow' })}</span>} 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>
)}
<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>
</div> )
))} })}
{ {
visionConfig?.enabled && ( visionConfig?.enabled && (
<div className="mt-4 w-full"> <div className="mt-4 w-full">

View File

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

View File

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

View File

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

View File

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