mirror of
https://github.com/grafana/grafana.git
synced 2024-11-22 00:47:38 -06:00
Grafana-UI: allow custom validity checks for creatable selects (#36118)
* add actions handling to Select in storybook * Grafana-UI: allow custom validity checks for creatable selects
This commit is contained in:
parent
b8b90ec74f
commit
58bbe17d84
@ -5,10 +5,10 @@ import { Icon, Select, AsyncSelect, MultiSelect, AsyncMultiSelect } from '@grafa
|
||||
import { getAvailableIcons, IconName } from '../../types';
|
||||
import { SelectCommonProps } from './types';
|
||||
import { Meta, Story } from '@storybook/react';
|
||||
import { kebabCase } from 'lodash';
|
||||
import { generateOptions } from './mockOptions';
|
||||
import mdx from './Select.mdx';
|
||||
import { auto } from '@popperjs/core';
|
||||
import { action } from '@storybook/addon-actions';
|
||||
|
||||
export default {
|
||||
title: 'Forms/Select',
|
||||
@ -94,6 +94,7 @@ export const Basic: Story<StoryProps> = (args) => {
|
||||
value={value}
|
||||
onChange={(v) => {
|
||||
setValue(v);
|
||||
action('onChange')(v);
|
||||
}}
|
||||
prefix={getPrefix(args.icon)}
|
||||
{...args}
|
||||
@ -113,6 +114,7 @@ export const BasicSelectPlainValue: Story<StoryProps> = (args) => {
|
||||
value={value}
|
||||
onChange={(v) => {
|
||||
setValue(v.value);
|
||||
action('onChange')(v);
|
||||
}}
|
||||
prefix={getPrefix(args.icon)}
|
||||
{...args}
|
||||
@ -145,6 +147,7 @@ export const SelectWithOptionDescriptions: Story = (args) => {
|
||||
value={value}
|
||||
onChange={(v) => {
|
||||
setValue(v.value);
|
||||
action('onChange')(v);
|
||||
}}
|
||||
prefix={getPrefix(args.icon)}
|
||||
{...args}
|
||||
@ -187,6 +190,7 @@ export const MultiSelectWithOptionGroups: Story = (args) => {
|
||||
value={value}
|
||||
onChange={(v) => {
|
||||
setValue(v.map((v: any) => v.value));
|
||||
action('onChange')(v);
|
||||
}}
|
||||
prefix={getPrefix(args.icon)}
|
||||
{...args}
|
||||
@ -205,6 +209,7 @@ export const MultiSelectBasic: Story = (args) => {
|
||||
value={value}
|
||||
onChange={(v) => {
|
||||
setValue(v);
|
||||
action('onChange')(v);
|
||||
}}
|
||||
prefix={getPrefix(args.icon)}
|
||||
{...args}
|
||||
@ -228,6 +233,7 @@ export const MultiSelectAsync: Story = (args) => {
|
||||
value={value}
|
||||
onChange={(v) => {
|
||||
setValue(v);
|
||||
action('onChange')(v);
|
||||
}}
|
||||
prefix={getPrefix(args.icon)}
|
||||
{...args}
|
||||
@ -248,6 +254,7 @@ export const BasicSelectAsync: Story = (args) => {
|
||||
value={value}
|
||||
onChange={(v) => {
|
||||
setValue(v);
|
||||
action('onChange')(v);
|
||||
}}
|
||||
prefix={getPrefix(args.icon)}
|
||||
{...args}
|
||||
@ -266,6 +273,7 @@ export const AutoMenuPlacement: Story = (args) => {
|
||||
value={value}
|
||||
onChange={(v) => {
|
||||
setValue(v);
|
||||
action('onChange')(v);
|
||||
}}
|
||||
prefix={getPrefix(args.icon)}
|
||||
{...args}
|
||||
@ -289,12 +297,14 @@ export const CustomValueCreation: Story = (args) => {
|
||||
value={value}
|
||||
onChange={(v) => {
|
||||
setValue(v);
|
||||
action('onChange')(v);
|
||||
}}
|
||||
allowCustomValue={args.allowCustomValue}
|
||||
onCreateOption={(v) => {
|
||||
const customValue: SelectableValue<string> = { value: kebabCase(v), label: v };
|
||||
const customValue: SelectableValue<string> = { value: v, label: v };
|
||||
setCustomOptions([...customOptions, customValue]);
|
||||
setValue(customValue);
|
||||
action('onCreateOption')(v);
|
||||
}}
|
||||
prefix={getPrefix(args.icon)}
|
||||
{...args}
|
||||
|
@ -4,6 +4,7 @@ import { render, screen } from '@testing-library/react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import selectEvent from 'react-select-event';
|
||||
import { SelectBase } from './SelectBase';
|
||||
import { SelectBaseProps } from './types';
|
||||
import { SelectableValue } from '@grafana/data';
|
||||
import { MultiValueContainer } from './MultiValue';
|
||||
|
||||
@ -213,4 +214,53 @@ describe('SelectBase', () => {
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('When allowCustomValue is set to true', () => {
|
||||
it('Should allow creating a new option', async () => {
|
||||
const valueIsStrictlyEqual: SelectBaseProps<string>['filterOption'] = (option, value) => option.value === value;
|
||||
const valueIsStrictlyNotEqual: SelectBaseProps<string>['isValidNewOption'] = (newOption, _, options) =>
|
||||
options.every(({ value }) => value !== newOption);
|
||||
|
||||
const spy = jest.fn();
|
||||
render(
|
||||
<SelectBase
|
||||
onChange={spy}
|
||||
isOpen
|
||||
allowCustomValue
|
||||
filterOption={valueIsStrictlyEqual}
|
||||
isValidNewOption={valueIsStrictlyNotEqual}
|
||||
/>
|
||||
);
|
||||
|
||||
const textBox = screen.getByRole('textbox');
|
||||
userEvent.type(textBox, 'NOT AN OPTION');
|
||||
|
||||
let creatableOption = screen.getByLabelText('Select option');
|
||||
expect(creatableOption).toBeInTheDocument();
|
||||
expect(creatableOption).toHaveTextContent('Create: NOT AN OPTION');
|
||||
|
||||
userEvent.click(creatableOption);
|
||||
expect(spy).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
label: 'NOT AN OPTION',
|
||||
value: 'NOT AN OPTION',
|
||||
})
|
||||
);
|
||||
|
||||
// Should also create options in a case-insensitive way.
|
||||
userEvent.type(textBox, 'not an option');
|
||||
|
||||
creatableOption = screen.getByLabelText('Select option');
|
||||
expect(creatableOption).toBeInTheDocument();
|
||||
expect(creatableOption).toHaveTextContent('Create: not an option');
|
||||
|
||||
userEvent.click(creatableOption);
|
||||
expect(spy).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
label: 'not an option',
|
||||
value: 'not an option',
|
||||
})
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -1,4 +1,4 @@
|
||||
import React, { useCallback } from 'react';
|
||||
import React, { ComponentProps, useCallback } from 'react';
|
||||
import { default as ReactSelect } from 'react-select';
|
||||
import Creatable from 'react-select/creatable';
|
||||
import { default as ReactAsyncSelect } from 'react-select/async';
|
||||
@ -134,6 +134,7 @@ export function SelectBase<T>({
|
||||
tabSelectsValue = true,
|
||||
value,
|
||||
width,
|
||||
isValidNewOption,
|
||||
}: SelectBaseProps<T>) {
|
||||
const theme = useTheme2();
|
||||
const styles = getSelectStyles(theme);
|
||||
@ -149,7 +150,7 @@ export function SelectBase<T>({
|
||||
|
||||
let ReactSelectComponent = ReactSelect;
|
||||
|
||||
const creatableProps: any = {};
|
||||
const creatableProps: ComponentProps<typeof Creatable> = {};
|
||||
let asyncSelectProps: any = {};
|
||||
let selectedValue;
|
||||
if (isMulti && loadOptions) {
|
||||
@ -218,6 +219,7 @@ export function SelectBase<T>({
|
||||
ReactSelectComponent = Creatable as any;
|
||||
creatableProps.formatCreateLabel = formatCreateLabel ?? ((input: string) => `Create: ${input}`);
|
||||
creatableProps.onCreateOption = onCreateOption;
|
||||
creatableProps.isValidNewOption = isValidNewOption;
|
||||
}
|
||||
|
||||
// Instead of having AsyncSelect, as a separate component we render ReactAsyncSelect
|
||||
|
@ -19,7 +19,7 @@ export interface SelectCommonProps<T> {
|
||||
components?: any;
|
||||
defaultValue?: any;
|
||||
disabled?: boolean;
|
||||
filterOption?: (option: SelectableValue, searchQuery: string) => boolean;
|
||||
filterOption?: (option: SelectableValue<T>, searchQuery: string) => boolean;
|
||||
/** Function for formatting the text that is displayed when creating a new value*/
|
||||
formatCreateLabel?: (input: string) => string;
|
||||
getOptionLabel?: (item: SelectableValue<T>) => React.ReactNode;
|
||||
@ -64,6 +64,12 @@ export interface SelectCommonProps<T> {
|
||||
/** Sets the width to a multiple of 8px. Should only be used with inline forms. Setting width of the container is preferred in other cases.*/
|
||||
width?: number;
|
||||
isOptionDisabled?: () => boolean;
|
||||
/** allowCustomValue must be enabled. Determines whether the "create new" option should be displayed based on the current input value, select value and options array. */
|
||||
isValidNewOption?: (
|
||||
inputValue: string,
|
||||
value: SelectableValue<T> | null,
|
||||
options: Readonly<Array<SelectableValue<T>>>
|
||||
) => boolean;
|
||||
}
|
||||
|
||||
export interface SelectAsyncProps<T> {
|
||||
|
Loading…
Reference in New Issue
Block a user