Skip to content

Commit

Permalink
fix(Dropdown): fix showing selected option when value is numeric (#1439)
Browse files Browse the repository at this point in the history
  • Loading branch information
vtsvetkov-splunk authored Nov 12, 2024
1 parent c743559 commit 9e7394a
Show file tree
Hide file tree
Showing 7 changed files with 143 additions and 22 deletions.
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import React from 'react';
import userEvent from '@testing-library/user-event';
import { http, HttpResponse } from 'msw';
import { render, screen } from '@testing-library/react';
import { render, screen, within } from '@testing-library/react';

import MultiInputComponent, { MultiInputComponentProps } from './MultiInputComponent';
import { server } from '../../mocks/server';
Expand Down Expand Up @@ -160,3 +160,80 @@ describe.each(mockedEntries)('handler endpoint data loading', (mockedEntry) => {
);
});
});

it('should render label (boolean-like)', () => {
renderFeature({
value: 'true',
controlOptions: {
items: [
{
label: 'truevalue',
value: true,
},
{
label: 'falsevalue',
value: false,
},
{
label: 'optionone',
value: 1,
},
],
},
});
const inputComponent = screen.getByTestId('multiselect');

expect(within(inputComponent).getByText('truevalue')).toBeInTheDocument();
expect(within(inputComponent).queryByText('falsevalue')).not.toBeInTheDocument();
expect(within(inputComponent).queryByText('optionone')).not.toBeInTheDocument();
});

it('should render singe value (numeric)', () => {
renderFeature({
value: 1,
controlOptions: {
items: [
{
label: 'label1',
value: 1,
},
{
label: 'label2',
value: 2,
},
],
},
});
const inputComponent = screen.getByTestId('multiselect');

expect(within(inputComponent).getByText('label1')).toBeInTheDocument();
expect(within(inputComponent).queryByText('label2')).not.toBeInTheDocument();
});

it('should render two values (number + boolean)', () => {
renderFeature({
value: '1;false',
controlOptions: {
delimiter: ';',
items: [
{
label: 'label1',
value: 1,
},
{
label: 'label2',
value: false,
},
{
label: 'label3',
value: 3,
},
],
},
});
const inputComponent = screen.getByTestId('multiselect');

expect(within(inputComponent).getByText('label1')).toBeInTheDocument();
expect(within(inputComponent).getByText('label2')).toBeInTheDocument();
expect(within(inputComponent).queryByText('label3')).not.toBeInTheDocument();
});
7 changes: 4 additions & 3 deletions ui/src/components/MultiInputComponent/MultiInputComponent.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { AxiosCallType, axiosCallWrapper, generateEndPointUrl } from '../../util
import { filterResponse } from '../../util/util';
import { MultipleSelectCommonOptions } from '../../types/globalConfig/entities';
import { invariant } from '../../util/invariant';
import { AcceptableFormValue } from '../../types/components/shareableTypes';

const MultiSelectWrapper = styled(Multiselect)`
width: 320px !important;
Expand All @@ -23,7 +24,7 @@ export interface MultiInputComponentProps {
field: string;
controlOptions: z.TypeOf<typeof MultipleSelectCommonOptions>;
disabled?: boolean;
value?: string;
value?: AcceptableFormValue;
error?: boolean;
dependencyValues?: Record<string, unknown>;
}
Expand Down Expand Up @@ -62,7 +63,7 @@ function MultiInputComponent(props: MultiInputComponentProps) {
return itemList.map((item) => (
<Multiselect.Option
label={item.label}
value={item.value}
value={String(item.value)}
key={typeof item.value === 'boolean' ? String(item.value) : item.value}
/>
));
Expand Down Expand Up @@ -133,7 +134,7 @@ function MultiInputComponent(props: MultiInputComponentProps) {
const effectiveDisabled = loading ? true : disabled;
const loadingIndicator = loading ? <WaitSpinnerWrapper /> : null;

const valueList = value ? value.split(delimiter) : [];
const valueList = value ? String(value).split(delimiter) : [];

return (
<>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import React from 'react';
import userEvent from '@testing-library/user-event';
import { http, HttpResponse } from 'msw';
import { render, screen } from '@testing-library/react';
import { render, screen, within } from '@testing-library/react';

import SingleInputComponent, { SingleInputComponentProps } from './SingleInputComponent';
import { setUnifiedConfig } from '../../util/util';
Expand Down Expand Up @@ -132,3 +132,38 @@ describe.each(mockedEntries)('handler endpoint data loading', (entry) => {
);
});
});

it('should render Select... when value does not exist in autoCompleteFields', () => {
renderFeature({
value: 'notExistingValue',
controlOptions: {
autoCompleteFields: [
{
label: 'label1',
value: 'value1',
},
],
},
});
const inputComponent = screen.getByRole('combobox');
expect(inputComponent).toBeInTheDocument();
expect(within(inputComponent).getByText('Select...')).toBeInTheDocument();
});

it.each([
{ value: true, autoCompleteFields: [{ label: 'trueLabel', value: true }] },
{
value: false,
autoCompleteFields: [{ label: 'falseLabel', value: false }],
},
{ value: 0, autoCompleteFields: [{ label: 'falseLabel', value: '0' }] },
{ value: 0, autoCompleteFields: [{ label: 'falseLabel', value: 0 }] },
])('should render label with value $value', ({ value, autoCompleteFields }) => {
renderFeature({
value,
controlOptions: { autoCompleteFields },
});
const inputComponent = screen.getByRole('combobox');
const { label } = autoCompleteFields[0];
expect(within(inputComponent).getByText(label)).toBeInTheDocument();
});
32 changes: 20 additions & 12 deletions ui/src/components/SingleInputComponent/SingleInputComponent.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import { AxiosCallType, axiosCallWrapper, generateEndPointUrl } from '../../util
import { SelectCommonOptions } from '../../types/globalConfig/entities';
import { filterResponse } from '../../util/util';
import { getValueMapTruthyFalse } from '../../util/considerFalseAndTruthy';
import { StandardPages } from '../../types/components/shareableTypes';
import { AcceptableFormValue, StandardPages } from '../../types/components/shareableTypes';

const SelectWrapper = styled(Select)`
width: 320px !important;
Expand All @@ -27,7 +27,7 @@ const StyledDiv = styled.div`
}
`;

type BasicFormItem = { value: string | number | boolean; label: string };
type BasicFormItem = { value: AcceptableFormValue; label: string };

type FormItem =
| BasicFormItem
Expand All @@ -39,9 +39,9 @@ type FormItem =
export interface SingleInputComponentProps {
id?: string;
disabled?: boolean;
value: string;
value: AcceptableFormValue;
error?: boolean;
handleChange: (field: string, value: string | number | boolean) => void;
handleChange: (field: string, value: string) => void;
field: string;
dependencyValues?: Record<string, unknown>;
controlOptions: z.TypeOf<typeof SelectCommonOptions> & {
Expand Down Expand Up @@ -74,27 +74,33 @@ function SingleInputComponent(props: SingleInputComponentProps) {
hideClearBtn,
} = controlOptions;

const handleChange = (e: unknown, obj: { value: string | number | boolean }) => {
restProps.handleChange(field, obj.value);
const handleChange = (e: unknown, obj: { value: AcceptableFormValue }) => {
restProps.handleChange(field, String(obj.value));
};
const Option = createSearchChoice ? ComboBox.Option : Select.Option;
const Heading = createSearchChoice ? ComboBox.Heading : Select.Heading;

function generateOptions(items: FormItem[]) {
const data: ReactElement[] = [];
items.forEach((item) => {
if ('value' in item && item.value && item.label) {
// splunk will mape those when sending post form
if (
'value' in item &&
item.value !== null &&
item.value !== undefined &&
item.value !== '' &&
item.label
) {
// splunk will maps those when sending post form
// so worth doing it earlier to keep same state before and after post
const itemValue = getValueMapTruthyFalse(item.value, props.page);
const itemValue = String(getValueMapTruthyFalse(item.value, props.page));
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore JSX element type 'Option' does not have any construct or call signatures.
data.push(<Option label={item.label} value={itemValue} key={item.value} />);
}
if ('children' in item && item.children && item.label) {
data.push(<Heading key={item.label}>{item.label}</Heading>);
item.children.forEach((child) => {
const childValue = getValueMapTruthyFalse(child.value, props.page);
const childValue = String(getValueMapTruthyFalse(child.value, props.page));
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore JSX element type 'Option' does not have any construct or call signatures.
data.push(<Option label={child.label} value={childValue} key={childValue} />);
Expand Down Expand Up @@ -181,7 +187,7 @@ function SingleInputComponent(props: SingleInputComponentProps) {
// if value is empty use empty string as ComboBox accepts only string
props.value === null || typeof props.value === 'undefined'
? ''
: props.value.toString()
: String(props.value)
}
name={field}
error={error}
Expand All @@ -201,7 +207,9 @@ function SingleInputComponent(props: SingleInputComponentProps) {
data-test-loading={loading}
value={
// if value is empty use empty string as Select accepts only string
props.value === null || typeof props.value === 'undefined' ? '' : props.value
props.value === null || typeof props.value === 'undefined'
? ''
: String(props.value)
}
name={field}
error={error}
Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 1 addition & 1 deletion ui/src/pages/Input/stories/globalConfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@
"field": "select2",
"label": "Select 2",
"help": "Default value is Numeric 3",
"defaultValue": 1,
"defaultValue": 3,
"options": {
"autoCompleteFields": [
{
Expand Down

0 comments on commit 9e7394a

Please sign in to comment.