Compare commits

...

1 Commits

Author SHA1 Message Date
yyh
5d7d0a4fda fix(select): align read-only interaction with Base UI 1.8 2026-09-05 11:14:12 +08:00
12 changed files with 352 additions and 80 deletions

View File

@@ -37,6 +37,15 @@ Import the complete family from its single public subpath:
import { Radio, RadioControl, RadioGroup, RadioItem } from '@langgenius/dify-ui/radio-group'
```
## Read-only and disabled selection
Follow the [`Select`] and [`Autocomplete`] contracts: `readOnly` locks the value while allowing
popup browsing; `disabled` makes the control unavailable.
Dify UI triggers show a decorative lock alongside their popup indicator when read-only.
Disabled appearance takes precedence when both states apply. `AutocompleteClear` is hidden while
read-only. Keep these state indicators in Dify UI rather than duplicating them in consumers.
## Typed values
Do not widen domain values to `string`. Use `Select<Value, Multiple>`, `RadioGroup<Value>`,

View File

@@ -2,6 +2,7 @@ import type { Meta, StoryObj } from '@storybook/react-vite'
import type { Virtualizer } from '@tanstack/react-virtual'
import { useVirtualizer } from '@tanstack/react-virtual'
import * as React from 'react'
import { expect, waitFor, within } from 'storybook/test'
import {
Autocomplete,
AutocompleteClear,
@@ -960,6 +961,7 @@ export const DisabledAndReadOnly: Story = {
defaultValue="feature"
mode="list"
disabled
readOnly
>
<AutocompleteInputGroup>
<AutocompleteInput aria-label="Disabled tag autocomplete" />
@@ -976,28 +978,69 @@ export const DisabledAndReadOnly: Story = {
</AutocompletePositioner>
</AutocompletePortal>
</Autocomplete>
<Autocomplete
items={promptCompletions}
itemToStringValue={getSuggestionLabel}
defaultValue="summarize this conversation"
mode="both"
readOnly
>
<AutocompleteInputGroup>
<AutocompleteInput aria-label="Read-only prompt autocomplete" />
<AutocompleteClear />
<AutocompleteTrigger />
</AutocompleteInputGroup>
<AutocompletePortal>
<AutocompletePositioner>
<AutocompletePopup>
<AutocompleteList<Suggestion>>
{(item) => <SuggestionItem key={item.value} item={item} />}
</AutocompleteList>
</AutocompletePopup>
</AutocompletePositioner>
</AutocompletePortal>
</Autocomplete>
{(['small', 'medium', 'large'] as const).map((size) => (
<Autocomplete
key={size}
items={promptCompletions}
itemToStringValue={getSuggestionLabel}
defaultValue="summarize this"
mode="both"
readOnly
>
<AutocompleteInputGroup size={size}>
<AutocompleteInput size={size} aria-label={`Read-only ${size} prompt autocomplete`} />
<AutocompleteClear size={size} aria-label={`Clear read-only ${size} prompt`} />
<AutocompleteTrigger
size={size}
aria-label={`Browse read-only ${size} prompt suggestions`}
/>
</AutocompleteInputGroup>
<AutocompletePortal>
<AutocompletePositioner>
<AutocompletePopup>
<AutocompleteList<Suggestion>>
{(item) => <SuggestionItem key={item.value} item={item} />}
</AutocompleteList>
</AutocompletePopup>
</AutocompletePositioner>
</AutocompletePortal>
</Autocomplete>
))}
</div>
),
play: async ({ canvas, canvasElement, userEvent }) => {
const input = canvas.getByRole('combobox', { name: 'Read-only medium prompt autocomplete' })
const body = within(canvasElement.ownerDocument.body)
await expect(canvas.getByRole('combobox', { name: 'Disabled tag autocomplete' })).toBeDisabled()
await expect(input).toHaveAttribute('readonly')
await expect(
canvas.queryByRole('button', { name: 'Clear read-only medium prompt' }),
).not.toBeInTheDocument()
const trigger = canvas.getByRole('button', {
name: 'Browse read-only medium prompt suggestions',
})
await expect(trigger).toBeVisible()
await userEvent.click(trigger)
const list = await body.findByRole('listbox')
await expect(list).toHaveAttribute('aria-readonly', 'true')
await expect(input).toHaveValue('summarize this')
await userEvent.keyboard('{ArrowDown}')
const firstHighlightedId = input.getAttribute('aria-activedescendant')
await expect(firstHighlightedId).toBeTruthy()
await expect(input).toHaveValue('summarize this')
await userEvent.keyboard('{ArrowDown}')
await expect(input).not.toHaveAttribute('aria-activedescendant', firstHighlightedId)
await expect(input).toHaveValue('summarize this')
await userEvent.keyboard('{Enter}')
await expect(input).toHaveValue('summarize this')
await userEvent.click(
within(list).getByRole('option', { name: 'summarize this dataset with citations' }),
)
await expect(input).toHaveValue('summarize this')
await userEvent.keyboard('{Escape}')
await waitFor(() => expect(body.queryByRole('listbox')).not.toBeInTheDocument())
await waitFor(() => expect(input).toHaveFocus())
},
}

View File

@@ -166,7 +166,6 @@ const autocompleteControlVariants = cva(
'focus-visible:inset-ring-2 focus-visible:inset-ring-state-accent-solid',
'disabled:cursor-not-allowed disabled:hover:bg-transparent disabled:hover:text-text-tertiary disabled:focus-visible:bg-transparent disabled:focus-visible:ring-0',
'group-data-disabled/autocomplete:cursor-not-allowed group-data-disabled/autocomplete:hover:bg-transparent group-data-disabled/autocomplete:focus-visible:bg-transparent group-data-disabled/autocomplete:focus-visible:ring-0',
'group-data-readonly/autocomplete:hidden',
'motion-reduce:transition-none',
],
{
@@ -200,9 +199,17 @@ function AutocompleteTrigger({
props['aria-label'] ??
(props['aria-labelledby'] ? undefined : 'Open autocomplete suggestions')
}
className={cn(autocompleteControlVariants({ size }), className)}
className={cn(
autocompleteControlVariants({ size }),
'group/autocomplete-trigger [&[data-readonly]:not([data-disabled])]:w-auto [&[data-readonly]:not([data-disabled])]:gap-1 [&[data-readonly]:not([data-disabled])]:px-1',
className,
)}
{...props}
>
<span
className="i-ri-lock-line hidden size-4 shrink-0 group-[[data-readonly]:not([data-disabled])]/autocomplete-trigger:block"
aria-hidden="true"
/>
{children ?? <span className="i-ri-arrow-down-s-line size-4" aria-hidden="true" />}
</BaseAutocomplete.Trigger>
)
@@ -226,6 +233,7 @@ function AutocompleteClear({
}
className={cn(
autocompleteControlVariants({ size }),
'group-data-readonly/autocomplete:hidden',
'data-ending-style:opacity-0 data-starting-style:opacity-0',
className,
)}

View File

@@ -257,8 +257,8 @@ export const WithDisabledItem: Story = {
export const Disabled: Story = {
render: () => (
<div className={triggerWidth}>
<Select defaultValue="seattle">
<SelectTrigger aria-label="City" disabled>
<Select defaultValue="seattle" disabled>
<SelectTrigger aria-label="City">
<SelectValue />
</SelectTrigger>
<SelectContent>
@@ -296,6 +296,121 @@ export const ReadOnly: Story = {
</Select>
</div>
),
play: async ({ canvas, canvasElement, userEvent }) => {
const trigger = canvas.getByRole('combobox', { name: 'City' })
const body = within(canvasElement.ownerDocument.body)
await expect(trigger).toHaveAttribute('aria-readonly', 'true')
await userEvent.click(trigger)
const list = await body.findByRole('listbox')
await expect(list).toHaveAttribute('aria-readonly', 'true')
const newYork = within(list).getByRole('option', { name: 'New York' })
await userEvent.click(newYork)
await expect(trigger).toHaveTextContent('Seattle')
await expect(newYork).toHaveAttribute('aria-selected', 'false')
await expect(trigger).toHaveAttribute('aria-expanded', 'true')
await userEvent.keyboard('{End}{Enter}')
await expect(newYork).toHaveFocus()
await expect(trigger).toHaveTextContent('Seattle')
await userEvent.keyboard('{Escape}')
await waitFor(() => expect(body.queryByRole('listbox')).not.toBeInTheDocument())
await waitFor(() => expect(trigger).toHaveFocus())
await userEvent.keyboard('n')
await expect(trigger).toHaveTextContent('Seattle')
await userEvent.keyboard('{ArrowDown}')
await waitFor(() => expect(body.getByRole('listbox')).toBeVisible())
await userEvent.keyboard('{Escape}')
await waitFor(() => expect(trigger).toHaveFocus())
},
}
export const InteractionStates: Story = {
render: () => (
<div className="group flex flex-wrap gap-6">
{[
{ label: 'Editable', readOnly: false, disabled: false },
{ label: 'Read-only', readOnly: true, disabled: false },
{ label: 'Disabled', readOnly: false, disabled: true },
{ label: 'Read-only and disabled', readOnly: true, disabled: true },
].map(({ label, readOnly, disabled }) => (
<div key={label} className="flex w-52 flex-col gap-3">
<span className="system-xs-medium text-text-tertiary">{label}</span>
{(['small', 'medium', 'large'] as const).map((size) => (
<Select
key={size}
items={cityItems}
defaultValue="seattle"
readOnly={readOnly}
disabled={disabled}
>
<SelectTrigger size={size} aria-label={`${label} ${size} city`}>
<SelectValue />
</SelectTrigger>
<SelectContent>
{cityItems.map(({ label: city, value }) => (
<SelectItem key={value} value={value}>
<SelectItemText>{city}</SelectItemText>
<SelectItemIndicator />
</SelectItem>
))}
</SelectContent>
</Select>
))}
</div>
))}
</div>
),
}
export const ReadOnlyMultiple: Story = {
render: () => (
<div className={triggerWidth}>
<Select<string, true> multiple defaultValue={['seattle', 'tokyo']} items={cityItems} readOnly>
<SelectTrigger aria-label="Cities">
<SelectValue />
</SelectTrigger>
<SelectContent>
{cityItems.map((item) => (
<SelectItem key={item.value} value={item.value}>
<SelectItemText>{item.label}</SelectItemText>
<SelectItemIndicator />
</SelectItem>
))}
</SelectContent>
</Select>
</div>
),
play: async ({ canvas, canvasElement, userEvent }) => {
const trigger = canvas.getByRole('combobox', { name: 'Cities' })
const body = within(canvasElement.ownerDocument.body)
trigger.focus()
await userEvent.keyboard('{ArrowDown}')
const list = await body.findByRole('listbox')
const options = within(list)
await expect(list).toHaveAttribute('aria-multiselectable', 'true')
await expect(list).toHaveAttribute('aria-readonly', 'true')
await userEvent.click(options.getByRole('option', { name: 'Seattle' }))
await userEvent.click(options.getByRole('option', { name: 'New York' }))
await userEvent.keyboard('{End}{Enter}')
await expect(options.getAllByRole('option', { selected: true })).toHaveLength(2)
await expect(options.getByRole('option', { name: 'Seattle' })).toHaveAttribute(
'aria-selected',
'true',
)
await expect(options.getByRole('option', { name: 'Tokyo' })).toHaveAttribute(
'aria-selected',
'true',
)
await expect(options.getByRole('option', { name: 'Paris' })).toHaveFocus()
await expect(trigger).toHaveAttribute('aria-expanded', 'true')
await userEvent.keyboard('{Escape}')
await waitFor(() => expect(body.queryByRole('listbox')).not.toBeInTheDocument())
await waitFor(() => expect(trigger).toHaveFocus())
},
}
const ControlledDemo = () => {

View File

@@ -62,11 +62,10 @@ type SelectGroupProps = BaseSelect.Group.Props
const selectTriggerVariants = cva(
[
'group flex w-full items-center border-0 bg-components-input-bg-normal text-start text-components-input-text-filled outline-hidden',
'group/select-trigger flex w-full items-center border-0 bg-components-input-bg-normal text-start text-components-input-text-filled outline-hidden',
'hover:bg-state-base-hover-alt focus-visible:bg-state-base-hover-alt data-popup-open:bg-state-base-hover-alt',
'focus-visible:ring-2 focus-visible:ring-state-accent-solid',
'data-placeholder:text-components-input-text-placeholder',
'data-readonly:cursor-default data-readonly:bg-components-input-bg-normal data-readonly:hover:bg-components-input-bg-normal',
'data-disabled:cursor-not-allowed data-disabled:bg-components-input-bg-disabled data-disabled:text-components-input-text-filled-disabled data-disabled:hover:bg-components-input-bg-disabled',
'data-disabled:data-placeholder:text-components-input-text-disabled',
],
@@ -94,7 +93,11 @@ function SelectTrigger({ className, children, size, ...props }: SelectTriggerPro
return (
<BaseSelect.Trigger className={cn(selectTriggerVariants({ size, className }))} {...props}>
<span className="min-w-0 grow truncate">{children}</span>
<BaseSelect.Icon className="shrink-0 text-text-quaternary transition-colors group-hover:text-text-secondary group-data-readonly:hidden data-popup-open:text-text-secondary">
<span
className="me-1 i-ri-lock-line hidden size-4 shrink-0 text-text-tertiary group-[[data-readonly]:not([data-disabled])]/select-trigger:block"
aria-hidden="true"
/>
<BaseSelect.Icon className="shrink-0 text-text-quaternary transition-colors group-[:hover:not([data-disabled])]/select-trigger:text-text-secondary data-popup-open:text-text-secondary">
<span className="i-ri-arrow-down-s-line h-4 w-4" aria-hidden="true" />
</BaseSelect.Icon>
</BaseSelect.Trigger>

View File

@@ -12,8 +12,8 @@ import {
SelectPortal,
SelectPositioner,
SelectTrigger,
SelectValue,
} from '@langgenius/dify-ui/select'
import { RiArrowDownSLine } from '@remixicon/react'
import dayjs from 'dayjs'
import * as React from 'react'
import { useCallback, useMemo, useState } from 'react'
@@ -35,7 +35,6 @@ type Props = Readonly<{
const RangeSelector: FC<Props> = ({ isCustomRange, ranges, onSelect }) => {
const { t } = useTranslation()
const [open, setOpen] = useState(false)
const items = useMemo<TimePeriodOption[]>(() => {
return ranges.map((range) => ({
...range,
@@ -69,8 +68,6 @@ const RangeSelector: FC<Props> = ({ isCustomRange, ranges, onSelect }) => {
return (
<Select<number>
value={selectedItem?.value ?? null}
open={open}
onOpenChange={setOpen}
onValueChange={(nextValue) => {
if (nextValue == null) return
const nextItem = items.find((item) => item.value === nextValue)
@@ -79,15 +76,12 @@ const RangeSelector: FC<Props> = ({ isCustomRange, ranges, onSelect }) => {
handleSelectRange(nextItem)
}}
>
<SelectTrigger className="h-auto w-fit max-w-none border-0 bg-transparent p-0 hover:bg-transparent focus-visible:bg-transparent [&>*:last-child]:hidden">
<div className="flex h-8 cursor-pointer items-center space-x-1.5 rounded-lg bg-components-input-bg-normal pr-2 pl-3 group-data-popup-open:bg-state-base-hover-alt">
<div className="system-sm-regular text-components-input-text-filled">
{isCustomRange
? t(($) => $['filter.period.custom'], { ns: 'appLog' })
: selectedItem?.name}
</div>
<RiArrowDownSLine className="size-4 text-text-quaternary group-data-popup-open:text-text-secondary" />
</div>
<SelectTrigger className="w-fit">
<SelectValue>
{isCustomRange
? t(($) => $['filter.period.custom'], { ns: 'appLog' })
: selectedItem?.name}
</SelectValue>
</SelectTrigger>
<SelectPortal>
<SelectPositioner className="-translate-x-6">

View File

@@ -262,16 +262,28 @@ describe('SelectPackage', () => {
expect(onSelectVersion).toHaveBeenCalledWith({ value: 'v0.9.0', name: 'v0.9.0' })
})
it('should select a valid package option', async () => {
it('should only allow package selection after a version is selected', async () => {
const user = userEvent.setup()
const onSelectPackage = vi.fn()
renderSelectPackage({
selectedVersion: 'v1.0.0',
const props = {
...createDefaultProps(),
updatePayload: createUpdatePayload(),
onSelectPackage,
})
}
const { rerender } = render(<SelectPackage {...props} packages={[]} />)
const section = getSection('plugin.installFromGitHub.selectPackage')
await user.click(within(section).getByRole('combobox'))
const packageTrigger = screen.getByRole('combobox', {
name: 'plugin.installFromGitHub.selectPackage',
})
expect(packageTrigger).toBeDisabled()
await user.click(packageTrigger)
expect(screen.queryByRole('listbox')).not.toBeInTheDocument()
expect(onSelectPackage).not.toHaveBeenCalled()
rerender(<SelectPackage {...props} selectedVersion="v1.0.0" />)
expect(packageTrigger).toBeEnabled()
await user.click(packageTrigger)
await user.click(await screen.findByRole('option', { name: 'plugin.tar.gz' }))
expect(onSelectPackage).toHaveBeenCalledWith({
@@ -472,27 +484,6 @@ describe('SelectPackage', () => {
})
})
// ================================
// PortalSelect Readonly State Tests
// ================================
describe('PortalSelect Readonly State', () => {
it('should make package select readonly when no version selected', () => {
renderSelectPackage({ selectedVersion: '' })
// When no version is selected, package select should be readonly
const trigger = screen.getAllByRole('combobox')[1]
expect(trigger).toHaveAttribute('aria-readonly', 'true')
})
it('should make package select active when version is selected', () => {
renderSelectPackage({ selectedVersion: 'v1.0.0' })
// When version is selected, package select should be active
const trigger = screen.getAllByRole('combobox')[1]
expect(trigger).not.toHaveAttribute('aria-readonly', 'true')
})
})
// ================================
// installedValue Props Tests
// ================================

View File

@@ -128,7 +128,7 @@ const SelectPackage: React.FC<SelectPackageProps> = ({
<Field name="package" className="gap-4 self-stretch">
<Select
value={selectedPackageOption?.value ?? null}
readOnly={!selectedVersion}
disabled={!selectedVersion}
onValueChange={(value) => {
if (value == null) return
const selectedItem = packages.find((item) => item.value === value)

View File

@@ -4,7 +4,7 @@ import type {
FormOption,
} from '@/app/components/header/account-setting/model-provider-page/declarations'
import type { AppSelectorValue } from '@/app/components/plugins/plugin-detail-panel/app-selector'
import { fireEvent, screen, waitFor } from '@testing-library/react'
import { act, fireEvent, screen, waitFor } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import { FormTypeEnum } from '@/app/components/header/account-setting/model-provider-page/declarations'
import { PluginCategoryEnum } from '@/app/components/plugins/types'
@@ -292,6 +292,108 @@ describe('FormInputItem branches', () => {
})
})
it.each([
{ mode: 'single', multiple: false },
{ mode: 'multiple', multiple: true },
])(
'should let users inspect read-only static $mode selections without changing them',
async ({ multiple }) => {
const user = userEvent.setup()
const { onChange } = renderFormInputItem({
readOnly: true,
schema: createSchema({
multiple,
type: FormTypeEnum.select,
options: ['alpha', 'beta', 'gamma', 'delta'].map((value) => createOption(value)),
}),
value: {
field: {
type: VarKindType.constant,
value: multiple ? ['alpha', 'beta', 'gamma'] : 'alpha',
},
},
})
const trigger = screen.getByRole('combobox')
expect(trigger).toHaveTextContent(multiple ? '3 selected' : 'alpha')
await user.click(trigger)
expect(await screen.findByRole('listbox')).toHaveAttribute('aria-readonly', 'true')
for (const name of multiple ? ['alpha', 'beta', 'gamma'] : ['alpha'])
expect(screen.getByRole('option', { name })).toHaveAttribute('aria-selected', 'true')
await user.click(screen.getByRole('option', { name: 'alpha' }))
await user.click(screen.getByRole('option', { name: 'delta' }))
expect(onChange).not.toHaveBeenCalled()
expect(screen.getByRole('option', { name: 'alpha' })).toHaveAttribute('aria-selected', 'true')
expect(screen.getByRole('option', { name: 'delta' })).toHaveAttribute(
'aria-selected',
'false',
)
await user.keyboard('{Escape}')
await waitFor(() => expect(screen.queryByRole('listbox')).not.toBeInTheDocument())
expect(trigger).toHaveTextContent(multiple ? '3 selected' : 'alpha')
},
)
it.each([
{ mode: 'single', multiple: false },
{ mode: 'multiple', multiple: true },
])(
'should disable loading dynamic $mode selections then allow read-only inspection',
async ({ multiple }) => {
const user = userEvent.setup()
let resolveOptions!: (result: { options: FormOption[] }) => void
mockFetchDynamicOptions.mockReturnValueOnce(
new Promise<{ options: FormOption[] }>((resolve) => {
resolveOptions = resolve
}),
)
const { onChange } = renderFormInputItem({
readOnly: true,
schema: createSchema({ multiple, type: FormTypeEnum.dynamicSelect }),
currentProvider: { plugin_id: 'provider-1', name: 'provider-1' } as never,
currentTool: { name: 'tool-1' } as never,
providerType: PluginCategoryEnum.tool,
value: {
field: {
type: VarKindType.constant,
value: multiple ? ['alpha', 'beta', 'gamma'] : 'alpha',
},
},
})
const trigger = screen.getByRole('combobox')
expect(trigger).toBeDisabled()
await user.click(trigger)
expect(screen.queryByRole('listbox')).not.toBeInTheDocument()
await act(async () => {
resolveOptions({
options: ['alpha', 'beta', 'gamma', 'delta'].map((value) => createOption(value)),
})
})
expect(trigger).not.toBeDisabled()
expect(trigger).toHaveTextContent(multiple ? '3 selected' : 'alpha')
await user.click(trigger)
expect(await screen.findByRole('listbox')).toHaveAttribute('aria-readonly', 'true')
for (const name of multiple ? ['alpha', 'beta', 'gamma'] : ['alpha'])
expect(screen.getByRole('option', { name })).toHaveAttribute('aria-selected', 'true')
await user.click(screen.getByRole('option', { name: 'alpha' }))
await user.click(screen.getByRole('option', { name: 'delta' }))
expect(onChange).not.toHaveBeenCalled()
expect(screen.getByRole('option', { name: 'alpha' })).toHaveAttribute('aria-selected', 'true')
expect(screen.getByRole('option', { name: 'delta' })).toHaveAttribute(
'aria-selected',
'false',
)
},
)
it('should recover when fetching dynamic tool options fails', async () => {
const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {})
mockFetchDynamicOptions.mockRejectedValueOnce(new Error('network'))

View File

@@ -7,7 +7,7 @@ describe('form-input-item sections', () => {
it('should render a loading multi-select label', () => {
renderWorkflowComponent(
<MultiSelectField
disabled={false}
readOnly={false}
isLoading
items={[{ name: 'Alpha', value: 'alpha' }]}
onChange={vi.fn()}
@@ -37,7 +37,7 @@ describe('form-input-item sections', () => {
renderWorkflowComponent(
<MultiSelectField
disabled={false}
readOnly={false}
items={[
{ name: 'Alpha', value: 'alpha', icon: '/alpha.svg' },
{ name: 'Beta', value: 'beta' },

View File

@@ -19,7 +19,7 @@ import CodeEditor from '@/app/components/workflow/nodes/_base/components/editor/
import { CodeLanguage } from '@/app/components/workflow/nodes/code/types'
type MultiSelectFieldProps = {
disabled: boolean
readOnly: boolean
isLoading?: boolean
items: SelectItem[]
onChange: (value: string[]) => void
@@ -36,7 +36,7 @@ const LoadingIndicator = () => (
)
export const MultiSelectField: FC<MultiSelectFieldProps> = ({
disabled,
readOnly,
isLoading = false,
items,
onChange,
@@ -60,7 +60,13 @@ export const MultiSelectField: FC<MultiSelectFieldProps> = ({
}
return (
<Select multiple value={value} onValueChange={onChange} disabled={disabled || isLoading}>
<Select
multiple
value={value}
onValueChange={onChange}
readOnly={readOnly}
disabled={isLoading}
>
<div className="min-w-0 grow">
<SelectTrigger aria-label={placeholder || selectedLabel || 'Options'}>
<span className={cn('flex min-w-0 items-center', textClassName)}>

View File

@@ -392,7 +392,7 @@ const FormInputItem: FC<Props> = ({
{isSelect && isConstant && !isMultipleSelect && (
<Select
value={selectedStaticOption?.value ?? null}
disabled={readOnly}
readOnly={readOnly}
onValueChange={(value) => value && handleValueChange(value)}
>
<SelectTrigger className="h-8 min-w-0 grow">
@@ -411,7 +411,7 @@ const FormInputItem: FC<Props> = ({
)}
{isSelect && isConstant && isMultipleSelect && (
<MultiSelectField
disabled={readOnly}
readOnly={readOnly}
value={(varInput?.value as string[] | undefined) || []}
items={staticSelectItems}
onChange={handleValueChange}
@@ -422,7 +422,8 @@ const FormInputItem: FC<Props> = ({
{isDynamicSelect && !isMultipleSelect && (
<Select
value={selectedDynamicOption?.value ?? null}
disabled={readOnly || isLoadingOptions}
readOnly={readOnly}
disabled={isLoadingOptions}
onValueChange={(value) => value && handleValueChange(value)}
>
<SelectTrigger className="h-8 min-w-0 grow">
@@ -442,7 +443,7 @@ const FormInputItem: FC<Props> = ({
)}
{isDynamicSelect && isMultipleSelect && (
<MultiSelectField
disabled={readOnly || isLoadingOptions}
readOnly={readOnly}
isLoading={isLoadingOptions}
value={(varInput?.value as string[] | undefined) || []}
items={dynamicSelectItems}