fix: preserve select migration behavior

This commit is contained in:
yyh 2026-04-30 13:13:55 +08:00
parent df76733ed2
commit 3c99026328
No known key found for this signature in database
6 changed files with 75 additions and 13 deletions

View File

@ -118,6 +118,25 @@ describe('BaseField', () => {
expect(screen.queryByText('Beta')).not.toBeInTheDocument()
})
it('should not render current select value when it is filtered out by show_on conditions', () => {
renderBaseField({
formSchema: {
type: FormTypeEnum.select,
name: 'mode',
label: 'Mode',
required: false,
options: [
{ label: 'Alpha', value: 'alpha' },
{ label: 'Beta', value: 'beta', show_on: [{ variable: 'enabled', value: 'yes' }] },
],
},
defaultValues: { mode: 'beta', enabled: 'no' },
})
expect(screen.getByRole('combobox', { name: 'Mode' })).not.toHaveTextContent('beta')
expect(screen.getByRole('combobox', { name: 'Mode' })).toHaveTextContent('common.placeholder.input')
})
it('should render dynamic select loading state', () => {
mockDynamicOptions.mockReturnValue({
data: undefined,

View File

@ -51,6 +51,19 @@ const getTranslatedContent = ({ content, render }: {
return ''
}
type SelectOption = {
label: string
value: string
}
const getSingleSelectValue = (value: unknown, options: SelectOption[]) => {
return options.find(option => option.value === value)?.value ?? null
}
const getSingleSelectLabel = (value: unknown, options: SelectOption[], placeholder: string | undefined) => {
return options.find(option => option.value === value)?.label ?? placeholder
}
const VALIDATE_STATUS_STYLE_MAP: Record<FormItemValidateStatusEnum, { componentClassName: string, textClassName: string, infoFieldName: string }> = {
[FormItemValidateStatusEnum.Error]: {
componentClassName: 'border-components-input-border-destructive focus:border-components-input-border-destructive',
@ -267,7 +280,7 @@ const BaseField = ({
: (
<Select
items={memorizedOptions}
value={value || null}
value={getSingleSelectValue(value, memorizedOptions)}
disabled={disabled}
onValueChange={(next) => {
if (next == null)
@ -276,7 +289,9 @@ const BaseField = ({
}}
>
<SelectTrigger id={field.name} aria-label={translatedLabel || field.name} className="px-2">
<SelectValue placeholder={translatedPlaceholder} />
<SelectValue placeholder={translatedPlaceholder}>
{nextValue => getSingleSelectLabel(nextValue, memorizedOptions, translatedPlaceholder)}
</SelectValue>
</SelectTrigger>
<SelectContent popupClassName="max-h-[320px] w-(--anchor-width) bg-components-panel-bg-blur">
{memorizedOptions.map(option => (
@ -339,7 +354,7 @@ const BaseField = ({
: (
<Select
items={dynamicOptions}
value={value || null}
value={getSingleSelectValue(value, dynamicOptions)}
disabled={disabled || isDynamicOptionsLoading}
onValueChange={(next) => {
if (next == null)
@ -348,7 +363,9 @@ const BaseField = ({
}}
>
<SelectTrigger id={field.name} aria-label={translatedLabel || field.name} className="px-2">
<SelectValue placeholder={dynamicPlaceholder} />
<SelectValue placeholder={dynamicPlaceholder}>
{nextValue => getSingleSelectLabel(nextValue, dynamicOptions, dynamicPlaceholder)}
</SelectValue>
</SelectTrigger>
<SelectContent popupClassName="w-(--anchor-width) bg-components-panel-bg-blur">
{dynamicNoticeTitle && (

View File

@ -33,6 +33,22 @@ describe('SelectField', () => {
expect(screen.getByRole('combobox', { name: 'Mode' })).toHaveTextContent('Alpha')
})
it('should render the option label when selected value is an empty string', () => {
mockField.state.value = ''
render(
<SelectField
label="Mode"
options={[
{ label: 'No default selected', value: '' },
{ label: 'Alpha', value: 'alpha' },
]}
/>,
)
expect(screen.getByRole('combobox', { name: 'Mode' })).toHaveTextContent('No default selected')
})
it('should update value when users select another option', async () => {
const user = userEvent.setup()
render(

View File

@ -21,10 +21,11 @@ describe('InputTypeSelectField', () => {
})
it('should render label and selected option', () => {
render(<InputTypeSelectField label="Input type" supportFile={true} />)
const { container } = render(<InputTypeSelectField label="Input type" supportFile={true} />)
expect(screen.getByText('Input type')).toBeInTheDocument()
expect(screen.getByText('appDebug.variableConfig.text-input')).toBeInTheDocument()
expect(container.querySelector('[role="combobox"] span > div')).not.toBeInTheDocument()
})
it('should update value when users choose another input type', async () => {

View File

@ -1,7 +1,6 @@
import type { FileTypeSelectOption } from './types'
import * as React from 'react'
import { useTranslation } from 'react-i18next'
import Badge from '@/app/components/base/badge'
type TriggerProps = {
option: FileTypeSelectOption | undefined
@ -16,13 +15,13 @@ const Trigger = ({
return <span className="grow p-1">{t('placeholder.select', { ns: 'common' })}</span>
return (
<div className="flex min-w-0 items-center gap-x-0.5">
<>
<option.Icon className="h-4 w-4 shrink-0 text-text-tertiary" />
<span className="min-w-0 grow truncate p-1">{option.label}</span>
<div className="pr-0.5">
<Badge text={option.type} uppercase={false} />
</div>
</div>
<span className="relative inline-flex h-5 shrink-0 items-center rounded-[5px] border border-divider-deep px-[5px] system-xs-medium leading-3 whitespace-nowrap text-text-tertiary">
{option.type}
</span>
</>
)
}

View File

@ -18,6 +18,14 @@ export type Option = {
value: string
}
const getSelectedValue = (value: string | undefined, options: Option[]) => {
return options.some(option => option.value === value) ? value : null
}
const getDisplayLabel = (value: string | null, options: Option[], placeholder: string) => {
return options.find(option => option.value === value)?.label ?? placeholder
}
type SelectFieldPopupProps = {
className?: string
title?: string
@ -58,7 +66,7 @@ const SelectField = ({
/>
<Select
items={options}
value={field.state.value ?? null}
value={getSelectedValue(field.state.value, options)}
disabled={disabled}
onValueChange={(next) => {
if (next == null)
@ -68,7 +76,9 @@ const SelectField = ({
}}
>
<SelectTrigger id={field.name} className="px-2">
<SelectValue placeholder={placeholderText} />
<SelectValue placeholder={placeholderText}>
{(nextValue: string | null) => getDisplayLabel(nextValue, options, placeholderText)}
</SelectValue>
</SelectTrigger>
<SelectContent popupClassName={cn('w-(--anchor-width) bg-components-panel-bg-blur', popupProps?.className)}>
{popupProps?.title && (