Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 10 additions & 4 deletions packages/ui/src/ui-component/dropdown/AsyncDropdown.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { useTheme, styled } from '@mui/material/styles'

// API
import credentialsApi from '@/api/credentials'
import { getOptionLabel, getSelectionValue, getSingleAutocompleteValue, isOptionEqualToValue } from './dropdownUtils'

// const
import { baseURL } from '@/store/constant'
Expand Down Expand Up @@ -92,7 +93,10 @@ export const AsyncDropdown = ({
}
return options.find((option) => option.name === value)
}
const getDefaultOptionValue = () => (multiple ? [] : '')
const getAutocompleteValue = () => {
if (multiple) return findMatchingOptions(options, internalValue) || []
return getSingleAutocompleteValue({ options, value: internalValue, freeSolo })
}
const addNewOption = [{ label: '- Create New -', name: '-create-' }]
let [internalValue, setInternalValue] = useState(value ?? 'choose an option')
const { reactFlowInstance } = useContext(flowContext)
Expand Down Expand Up @@ -183,7 +187,10 @@ export const AsyncDropdown = ({
disabled={disabled}
disableClearable={disableClearable}
multiple={multiple}
autoSelect={freeSolo && !multiple}
filterSelectedOptions={multiple}
getOptionLabel={getOptionLabel}
isOptionEqualToValue={isOptionEqualToValue}
size='small'
sx={{ mt: 1, width: fullWidth ? '100%' : multiple ? '90%' : '100%' }}
open={open}
Expand All @@ -194,7 +201,7 @@ export const AsyncDropdown = ({
setOpen(false)
}}
options={options}
value={findMatchingOptions(options, internalValue) || getDefaultOptionValue()}
value={getAutocompleteValue()}
onChange={(e, selection) => {
if (multiple) {
let value = ''
Expand All @@ -205,7 +212,7 @@ export const AsyncDropdown = ({
setInternalValue(value)
onSelect(value)
} else {
const value = selection ? selection.name : ''
const value = getSelectionValue(selection)
if (isCreateNewOption && value === '-create-') {
onCreateNew()
} else {
Expand All @@ -224,7 +231,6 @@ export const AsyncDropdown = ({
const textField = (
<TextField
{...params}
value={internalValue}
sx={{
height: '100%',
'& .MuiInputBase-root': {
Expand Down
10 changes: 6 additions & 4 deletions packages/ui/src/ui-component/dropdown/Dropdown.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { Popper, FormControl, TextField, Box, Typography } from '@mui/material'
import Autocomplete, { autocompleteClasses } from '@mui/material/Autocomplete'
import { useTheme, styled } from '@mui/material/styles'
import PropTypes from 'prop-types'
import { getOptionLabel, getSelectionValue, getSingleAutocompleteValue, isOptionEqualToValue } from './dropdownUtils'

const StyledPopper = styled(Popper)({
boxShadow: '0px 8px 10px -5px rgb(0 0 0 / 20%), 0px 16px 24px 2px rgb(0 0 0 / 14%), 0px 6px 30px 5px rgb(0 0 0 / 12%)',
Expand All @@ -21,7 +22,6 @@ const StyledPopper = styled(Popper)({
export const Dropdown = ({ name, value, loading, options, onSelect, disabled = false, freeSolo = false, disableClearable = false }) => {
const customization = useSelector((state) => state.customization)
const findMatchingOptions = (options = [], value) => options.find((option) => option.name === value)
const getDefaultOptionValue = () => ''
let [internalValue, setInternalValue] = useState(value ?? 'choose an option')
const theme = useTheme()

Expand All @@ -32,12 +32,15 @@ export const Dropdown = ({ name, value, loading, options, onSelect, disabled = f
disabled={disabled}
freeSolo={freeSolo}
disableClearable={disableClearable}
autoSelect={freeSolo}
getOptionLabel={getOptionLabel}
isOptionEqualToValue={isOptionEqualToValue}
size='small'
loading={loading}
options={options || []}
value={findMatchingOptions(options, internalValue) || getDefaultOptionValue()}
value={getSingleAutocompleteValue({ options, value: internalValue, freeSolo })}
onChange={(e, selection) => {
const value = selection ? selection.name : ''
const value = getSelectionValue(selection)
setInternalValue(value)
onSelect(value)
}}
Expand All @@ -47,7 +50,6 @@ export const Dropdown = ({ name, value, loading, options, onSelect, disabled = f
return (
<TextField
{...params}
value={internalValue}
sx={{
height: '100%',
'& .MuiInputBase-root': {
Expand Down
31 changes: 31 additions & 0 deletions packages/ui/src/ui-component/dropdown/dropdownUtils.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
export const DEFAULT_DROPDOWN_VALUE = 'choose an option'

export const getOptionName = (option) => {
if (typeof option === 'string') return option
return option?.name ?? ''
}

export const getOptionLabel = (option) => {
if (typeof option === 'string') return option
return option?.label ?? option?.name ?? ''
}

export const isEmptyDropdownValue = (value) => value === undefined || value === null || value === '' || value === DEFAULT_DROPDOWN_VALUE

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

According to the repository's general rules, loose equality (== null) should be used as a standard idiom for a 'nullish' check that covers both null and undefined in JavaScript/TypeScript.

Suggested change
export const isEmptyDropdownValue = (value) => value === undefined || value === null || value === '' || value === DEFAULT_DROPDOWN_VALUE
export const isEmptyDropdownValue = (value) => value == null || value === '' || value === DEFAULT_DROPDOWN_VALUE
References
  1. In JavaScript/TypeScript, use loose equality (== null) as a standard idiom for a 'nullish' check that covers both null and undefined.


export const findMatchingOption = (options = [], value) => (options || []).find((option) => option.name === value)

export const getSingleAutocompleteValue = ({ options = [], value, freeSolo = false }) => {
const matchingOption = findMatchingOption(options, value)
if (matchingOption) return matchingOption

if (freeSolo && !isEmptyDropdownValue(value)) return value

return ''
}

export const getSelectionValue = (selection) => {
if (typeof selection === 'string') return selection
return selection?.name ?? ''
}

export const isOptionEqualToValue = (option, value) => getOptionName(option) === getOptionName(value)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

If value is empty (e.g., '' or null when cleared) and an option does not have a name property (or has an empty name), isOptionEqualToValue will return true because both getOptionName(option) and getOptionName(value) evaluate to ''. This causes MUI Autocomplete to incorrectly treat that option as selected. To prevent this, we should ensure that the option name is truthy before performing the equality check.

Suggested change
export const isOptionEqualToValue = (option, value) => getOptionName(option) === getOptionName(value)
export const isOptionEqualToValue = (option, value) => {
const optionName = getOptionName(option)
const valueName = getOptionName(value)
return !!optionName && optionName === valueName
}

48 changes: 48 additions & 0 deletions packages/ui/src/ui-component/dropdown/dropdownUtils.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import {
DEFAULT_DROPDOWN_VALUE,
getOptionLabel,
getSelectionValue,
getSingleAutocompleteValue,
isOptionEqualToValue
} from './dropdownUtils'

describe('dropdownUtils', () => {
const options = [
{ label: 'GPT 5', name: 'gpt-5' },
{ label: 'GPT 5 Mini', name: 'gpt-5-mini' }
]

it('keeps a typed free-solo value when it is not in the option list', () => {
expect(
getSingleAutocompleteValue({
options,
value: 'gpt-5.4-nano-suporte-juridico',
freeSolo: true
})
).toBe('gpt-5.4-nano-suporte-juridico')
})

it('does not keep unknown values when free-solo entry is disabled', () => {
expect(
getSingleAutocompleteValue({
options,
value: 'gpt-5.4-nano-suporte-juridico',
freeSolo: false
})
).toBe('')
})

it('treats the default placeholder as empty for free-solo fields', () => {
expect(getSingleAutocompleteValue({ options, value: DEFAULT_DROPDOWN_VALUE, freeSolo: true })).toBe('')
})

it('extracts typed free-solo selections and option selections', () => {
expect(getSelectionValue('gpt-5.4-nano-suporte-juridico')).toBe('gpt-5.4-nano-suporte-juridico')
expect(getSelectionValue(options[0])).toBe('gpt-5')
})

it('handles option labels and equality for string values', () => {
expect(getOptionLabel('gpt-5.4-nano-suporte-juridico')).toBe('gpt-5.4-nano-suporte-juridico')
expect(isOptionEqualToValue(options[0], 'gpt-5')).toBe(true)
})
Comment on lines +44 to +47

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Add a test case to verify that isOptionEqualToValue correctly returns false when comparing an option with a missing/empty name against an empty value.

    it('handles option labels and equality for string values', () => {
        expect(getOptionLabel('gpt-5.4-nano-suporte-juridico')).toBe('gpt-5.4-nano-suporte-juridico')
        expect(isOptionEqualToValue(options[0], 'gpt-5')).toBe(true)
        expect(isOptionEqualToValue({ label: 'Select' }, '')).toBe(false)
    })

})