Select: Allow custom value for selects (#19775)

* WIP: simple poc of allow custom value for selects

* Add support for custom value in segment

* Update snapshots
This commit is contained in:
Torkel Ödegaard
2019-10-15 15:08:56 +02:00
committed by GitHub
parent 7963422df0
commit d0d59b8001
12 changed files with 128 additions and 3 deletions

View File

@@ -80,6 +80,38 @@ SegmentStories.add('Grouped Array Options', () => {
); );
}); });
SegmentStories.add('With custom options allowed', () => {
const options = ['Option1', 'Option2', 'OptionWithLooongLabel', 'Option4'].map(toOption);
return (
<UseState initialState={options[0].value}>
{(value, updateValue) => (
<>
<div className="gf-form-inline">
<div className="gf-form">
<span className="gf-form-label width-8 query-keyword">Segment Name</span>
</div>
<Segment
allowCustomValue
value={value}
options={options}
onChange={(value: SelectableValue<string>) => {
updateValue(value);
action('Segment value changed')(value);
}}
/>
<Segment
allowCustomValue
Component={AddButton}
onChange={(value: SelectableValue<string>) => action('New value added')(value)}
options={options}
/>
</div>
</>
)}
</UseState>
);
});
const CustomLabelComponent = ({ value }: any) => <div className="gf-form-label">custom({value})</div>; const CustomLabelComponent = ({ value }: any) => <div className="gf-form-label">custom({value})</div>;
SegmentStories.add('Custom Label Field', () => { SegmentStories.add('Custom Label Field', () => {

View File

@@ -13,6 +13,7 @@ export function Segment<T>({
onChange, onChange,
Component, Component,
className, className,
allowCustomValue,
}: React.PropsWithChildren<SegmentSyncProps<T>>) { }: React.PropsWithChildren<SegmentSyncProps<T>>) {
const [Label, width, expanded, setExpanded] = useExpandableLabel(false); const [Label, width, expanded, setExpanded] = useExpandableLabel(false);
@@ -25,6 +26,7 @@ export function Segment<T>({
width={width} width={width}
options={options} options={options}
onClickOutside={() => setExpanded(false)} onClickOutside={() => setExpanded(false)}
allowCustomValue={allowCustomValue}
onChange={value => { onChange={value => {
setExpanded(false); setExpanded(false);
onChange(value); onChange(value);

View File

@@ -81,8 +81,39 @@ SegmentStories.add('Grouped Array Options', () => {
); );
}); });
const CustomLabelComponent = ({ value }: any) => <div className="gf-form-label">custom({value})</div>; SegmentStories.add('With custom options allowed', () => {
const options = ['Option1', 'Option2', 'OptionWithLooongLabel', 'Option4'].map(toOption);
return (
<UseState initialState={options[0].value}>
{(value, updateValue) => (
<>
<div className="gf-form-inline">
<div className="gf-form">
<span className="gf-form-label width-8 query-keyword">Segment Name</span>
</div>
<SegmentAsync
allowCustomValue
value={value}
loadOptions={() => loadOptions(options)}
onChange={value => {
updateValue(value);
action('Segment value changed')(value);
}}
/>
<SegmentAsync
allowCustomValue
Component={AddButton}
onChange={value => action('New value added')(value)}
loadOptions={() => loadOptions(options)}
/>
</div>
</>
)}
</UseState>
);
});
const CustomLabelComponent = ({ value }: any) => <div className="gf-form-label">custom({value})</div>;
SegmentStories.add('Custom Label Field', () => { SegmentStories.add('Custom Label Field', () => {
return ( return (
<UseState initialState={groupedOptions[0].options[0].value}> <UseState initialState={groupedOptions[0].options[0].value}>

View File

@@ -14,6 +14,7 @@ export function SegmentAsync<T>({
loadOptions, loadOptions,
Component, Component,
className, className,
allowCustomValue,
}: React.PropsWithChildren<SegmentAsyncProps<T>>) { }: React.PropsWithChildren<SegmentAsyncProps<T>>) {
const [selectPlaceholder, setSelectPlaceholder] = useState<string>(''); const [selectPlaceholder, setSelectPlaceholder] = useState<string>('');
const [loadedOptions, setLoadedOptions] = useState<Array<SelectableValue<T>>>([]); const [loadedOptions, setLoadedOptions] = useState<Array<SelectableValue<T>>>([]);
@@ -38,6 +39,7 @@ export function SegmentAsync<T>({
width={width} width={width}
options={loadedOptions} options={loadedOptions}
noOptionsMessage={selectPlaceholder} noOptionsMessage={selectPlaceholder}
allowCustomValue={allowCustomValue}
onClickOutside={() => { onClickOutside={() => {
setSelectPlaceholder(''); setSelectPlaceholder('');
setLoadedOptions([]); setLoadedOptions([]);

View File

@@ -10,6 +10,7 @@ export interface Props<T> {
onClickOutside: () => void; onClickOutside: () => void;
width: number; width: number;
noOptionsMessage?: string; noOptionsMessage?: string;
allowCustomValue?: boolean;
} }
export function SegmentSelect<T>({ export function SegmentSelect<T>({
@@ -18,6 +19,7 @@ export function SegmentSelect<T>({
onClickOutside, onClickOutside,
width, width,
noOptionsMessage = '', noOptionsMessage = '',
allowCustomValue = false,
}: React.PropsWithChildren<Props<T>>) { }: React.PropsWithChildren<Props<T>>) {
const ref = useRef(null); const ref = useRef(null);
@@ -39,6 +41,7 @@ export function SegmentSelect<T>({
isOpen={true} isOpen={true}
onChange={({ value }) => onChange(value!)} onChange={({ value }) => onChange(value!)}
options={options} options={options}
allowCustomValue={allowCustomValue}
/> />
</div> </div>
); );

View File

@@ -5,4 +5,5 @@ export interface SegmentProps<T> {
value?: T; value?: T;
Component?: ReactElement; Component?: ReactElement;
className?: string; className?: string;
allowCustomValue?: boolean;
} }

View File

@@ -36,3 +36,30 @@ SelectStories.add('default', () => {
</UseState> </UseState>
); );
}); });
SelectStories.add('With allowCustomValue', () => {
const intialState: SelectableValue<string> = { label: 'A label', value: 'A value' };
const value = object<SelectableValue<string>>('Selected Value:', intialState);
const options = object<Array<SelectableValue<string>>>('Options:', [
intialState,
{ label: 'Another label', value: 'Another value' },
]);
return (
<UseState initialState={value}>
{(value, updateValue) => {
return (
<Select
value={value}
options={options}
allowCustomValue={true}
onChange={value => {
action('onChanged fired')(value);
updateValue(value);
}}
/>
);
}}
</UseState>
);
});

View File

@@ -4,7 +4,9 @@ import React, { PureComponent } from 'react';
// Ignoring because I couldn't get @types/react-select work wih Torkel's fork // Ignoring because I couldn't get @types/react-select work wih Torkel's fork
// @ts-ignore // @ts-ignore
import { default as ReactSelect } from '@torkelo/react-select'; import { default as ReactSelect, Creatable } from '@torkelo/react-select';
// @ts-ignore
import { Creatable } from '@torkelo/react-select/lib/creatable';
// @ts-ignore // @ts-ignore
import { default as ReactAsyncSelect } from '@torkelo/react-select/lib/Async'; import { default as ReactAsyncSelect } from '@torkelo/react-select/lib/Async';
// @ts-ignore // @ts-ignore
@@ -48,6 +50,7 @@ export interface CommonProps<T> {
onOpenMenu?: () => void; onOpenMenu?: () => void;
onCloseMenu?: () => void; onCloseMenu?: () => void;
tabSelectsValue?: boolean; tabSelectsValue?: boolean;
allowCustomValue: boolean;
} }
export interface SelectProps<T> extends CommonProps<T> { export interface SelectProps<T> extends CommonProps<T> {
@@ -83,6 +86,7 @@ export class Select<T> extends PureComponent<SelectProps<T>> {
backspaceRemovesValue: true, backspaceRemovesValue: true,
maxMenuHeight: 300, maxMenuHeight: 300,
tabSelectsValue: true, tabSelectsValue: true,
allowCustomValue: false,
components: { components: {
Option: SelectOption, Option: SelectOption,
SingleValue, SingleValue,
@@ -120,6 +124,7 @@ export class Select<T> extends PureComponent<SelectProps<T>> {
tabSelectsValue, tabSelectsValue,
onCloseMenu, onCloseMenu,
onOpenMenu, onOpenMenu,
allowCustomValue,
} = this.props; } = this.props;
let widthClass = ''; let widthClass = '';
@@ -127,6 +132,14 @@ export class Select<T> extends PureComponent<SelectProps<T>> {
widthClass = 'width-' + width; widthClass = 'width-' + width;
} }
let SelectComponent: ReactSelect | Creatable = ReactSelect;
const creatableOptions: any = {};
if (allowCustomValue) {
SelectComponent = Creatable;
creatableOptions.formatCreateLabel = (input: string) => input;
}
const selectClassNames = classNames('gf-form-input', 'gf-form-input--form-dropdown', widthClass, className); const selectClassNames = classNames('gf-form-input', 'gf-form-input--form-dropdown', widthClass, className);
const selectComponents = { ...Select.defaultProps.components, ...components }; const selectComponents = { ...Select.defaultProps.components, ...components };
@@ -134,7 +147,7 @@ export class Select<T> extends PureComponent<SelectProps<T>> {
<WrapInTooltip onCloseMenu={onCloseMenu} onOpenMenu={onOpenMenu} tooltipContent={tooltipContent} isOpen={isOpen}> <WrapInTooltip onCloseMenu={onCloseMenu} onOpenMenu={onOpenMenu} tooltipContent={tooltipContent} isOpen={isOpen}>
{(onOpenMenuInternal, onCloseMenuInternal) => { {(onOpenMenuInternal, onCloseMenuInternal) => {
return ( return (
<ReactSelect <SelectComponent
classNamePrefix="gf-form-select-box" classNamePrefix="gf-form-select-box"
className={selectClassNames} className={selectClassNames}
components={selectComponents} components={selectComponents}
@@ -162,6 +175,7 @@ export class Select<T> extends PureComponent<SelectProps<T>> {
onMenuOpen={onOpenMenuInternal} onMenuOpen={onOpenMenuInternal}
onMenuClose={onCloseMenuInternal} onMenuClose={onCloseMenuInternal}
tabSelectsValue={tabSelectsValue} tabSelectsValue={tabSelectsValue}
{...creatableOptions}
/> />
); );
}} }}

View File

@@ -81,6 +81,7 @@ exports[`Render when feature toggle editorsCanAdmin is turned off should not ren
className="gf-form" className="gf-form"
> >
<Select <Select
allowCustomValue={false}
autoFocus={false} autoFocus={false}
backspaceRemovesValue={true} backspaceRemovesValue={true}
className="gf-form-select-box__control--menu-right" className="gf-form-select-box__control--menu-right"
@@ -166,6 +167,7 @@ exports[`Render when feature toggle editorsCanAdmin is turned on should render p
className="gf-form" className="gf-form"
> >
<Select <Select
allowCustomValue={false}
autoFocus={false} autoFocus={false}
backspaceRemovesValue={true} backspaceRemovesValue={true}
className="gf-form-select-box__control--menu-right" className="gf-form-select-box__control--menu-right"

View File

@@ -43,6 +43,7 @@ exports[`Render should disable log analytics credentials form 1`] = `
className="width-25" className="width-25"
> >
<Select <Select
allowCustomValue={false}
autoFocus={false} autoFocus={false}
backspaceRemovesValue={true} backspaceRemovesValue={true}
className="" className=""
@@ -138,6 +139,7 @@ exports[`Render should enable azure log analytics load workspaces button 1`] = `
className="width-25" className="width-25"
> >
<Select <Select
allowCustomValue={false}
autoFocus={false} autoFocus={false}
backspaceRemovesValue={true} backspaceRemovesValue={true}
className="" className=""
@@ -233,6 +235,7 @@ exports[`Render should render component 1`] = `
className="width-25" className="width-25"
> >
<Select <Select
allowCustomValue={false}
autoFocus={false} autoFocus={false}
backspaceRemovesValue={true} backspaceRemovesValue={true}
className="" className=""

View File

@@ -18,6 +18,7 @@ exports[`Render should disable azure monitor secret input 1`] = `
Azure Cloud Azure Cloud
</Component> </Component>
<Select <Select
allowCustomValue={false}
autoFocus={false} autoFocus={false}
backspaceRemovesValue={true} backspaceRemovesValue={true}
className="width-15" className="width-15"
@@ -163,6 +164,7 @@ exports[`Render should disable azure monitor secret input 1`] = `
className="width-25" className="width-25"
> >
<Select <Select
allowCustomValue={false}
autoFocus={false} autoFocus={false}
backspaceRemovesValue={true} backspaceRemovesValue={true}
className="" className=""
@@ -233,6 +235,7 @@ exports[`Render should enable azure monitor load subscriptions button 1`] = `
Azure Cloud Azure Cloud
</Component> </Component>
<Select <Select
allowCustomValue={false}
autoFocus={false} autoFocus={false}
backspaceRemovesValue={true} backspaceRemovesValue={true}
className="width-15" className="width-15"
@@ -368,6 +371,7 @@ exports[`Render should enable azure monitor load subscriptions button 1`] = `
className="width-25" className="width-25"
> >
<Select <Select
allowCustomValue={false}
autoFocus={false} autoFocus={false}
backspaceRemovesValue={true} backspaceRemovesValue={true}
className="" className=""
@@ -438,6 +442,7 @@ exports[`Render should render component 1`] = `
Azure Cloud Azure Cloud
</Component> </Component>
<Select <Select
allowCustomValue={false}
autoFocus={false} autoFocus={false}
backspaceRemovesValue={true} backspaceRemovesValue={true}
className="width-15" className="width-15"
@@ -573,6 +578,7 @@ exports[`Render should render component 1`] = `
className="width-25" className="width-25"
> >
<Select <Select
allowCustomValue={false}
autoFocus={false} autoFocus={false}
backspaceRemovesValue={true} backspaceRemovesValue={true}
className="" className=""

View File

@@ -68,6 +68,7 @@ exports[`Render PromQueryEditor with basic options should render 1`] = `
Resolution Resolution
</div> </div>
<Select <Select
allowCustomValue={false}
autoFocus={false} autoFocus={false}
backspaceRemovesValue={true} backspaceRemovesValue={true}
className="" className=""
@@ -134,6 +135,7 @@ exports[`Render PromQueryEditor with basic options should render 1`] = `
Format Format
</div> </div>
<Select <Select
allowCustomValue={false}
autoFocus={false} autoFocus={false}
backspaceRemovesValue={true} backspaceRemovesValue={true}
className="" className=""