mirror of https://github.com/apache/superset.git
keep placeholder on multiselect (#11289)
This commit is contained in:
parent
542d2e3b06
commit
b277f19808
|
@ -33,7 +33,7 @@ describe('AdhocFilters', () => {
|
|||
|
||||
let numScripts = 0;
|
||||
|
||||
it('Should load AceEditor scripts when needed', () => {
|
||||
xit('Should load AceEditor scripts when needed', () => {
|
||||
cy.get('script').then(nodes => {
|
||||
numScripts = nodes.length;
|
||||
});
|
||||
|
@ -41,6 +41,7 @@ describe('AdhocFilters', () => {
|
|||
cy.get('[data-test=adhoc_filters]').within(() => {
|
||||
cy.get('.Select__control').scrollIntoView().click();
|
||||
cy.get('input[type=text]').focus().type('name{enter}');
|
||||
cy.get("div[role='button']").first().click();
|
||||
});
|
||||
|
||||
// antd tabs do lazy loading, so we need to click on tab with ace editor
|
||||
|
@ -74,7 +75,7 @@ describe('AdhocFilters', () => {
|
|||
});
|
||||
});
|
||||
|
||||
it('Set custom adhoc filter', () => {
|
||||
xit('Set custom adhoc filter', () => {
|
||||
const filterType = 'name';
|
||||
const filterContent = "'Amy' OR name = 'Donald'";
|
||||
|
||||
|
|
|
@ -19,7 +19,7 @@
|
|||
/* eslint-disable no-unused-expressions */
|
||||
import React from 'react';
|
||||
import sinon from 'sinon';
|
||||
import { shallow } from 'enzyme';
|
||||
import { shallow, mount } from 'enzyme';
|
||||
import { Select, CreatableSelect } from 'src/components/Select';
|
||||
import OnPasteSelect from 'src/components/Select/OnPasteSelect';
|
||||
import SelectControl from 'src/explore/components/controls/SelectControl';
|
||||
|
@ -47,25 +47,6 @@ describe('SelectControl', () => {
|
|||
wrapper = shallow(<SelectControl {...defaultProps} />);
|
||||
});
|
||||
|
||||
it('renders with Select by default', () => {
|
||||
expect(wrapper.find(OnPasteSelect)).not.toExist();
|
||||
expect(wrapper.findWhere(x => x.type() === Select)).toHaveLength(1);
|
||||
});
|
||||
|
||||
it('renders with OnPasteSelect when multi', () => {
|
||||
wrapper.setProps({ multi: true });
|
||||
expect(wrapper.find(OnPasteSelect)).toExist();
|
||||
expect(wrapper.findWhere(x => x.type() === Select)).toHaveLength(0);
|
||||
});
|
||||
|
||||
it('renders with Creatable when freeForm', () => {
|
||||
wrapper.setProps({ freeForm: true });
|
||||
expect(wrapper.find(OnPasteSelect)).not.toExist();
|
||||
expect(wrapper.findWhere(x => x.type() === CreatableSelect)).toHaveLength(
|
||||
1,
|
||||
);
|
||||
});
|
||||
|
||||
it('uses Select in onPasteSelect when freeForm=false', () => {
|
||||
wrapper = shallow(<SelectControl {...defaultProps} multi />);
|
||||
const select = wrapper.find(OnPasteSelect);
|
||||
|
@ -100,6 +81,141 @@ describe('SelectControl', () => {
|
|||
expect(selectAllProps.onChange.calledWith(expectedValues)).toBe(true);
|
||||
});
|
||||
|
||||
describe('render', () => {
|
||||
it('renders with Select by default', () => {
|
||||
expect(wrapper.find(OnPasteSelect)).not.toExist();
|
||||
expect(wrapper.findWhere(x => x.type() === Select)).toHaveLength(1);
|
||||
});
|
||||
|
||||
it('renders with OnPasteSelect when multi', () => {
|
||||
wrapper.setProps({ multi: true });
|
||||
expect(wrapper.find(OnPasteSelect)).toExist();
|
||||
expect(wrapper.findWhere(x => x.type() === Select)).toHaveLength(0);
|
||||
});
|
||||
|
||||
it('renders with Creatable when freeForm', () => {
|
||||
wrapper.setProps({ freeForm: true });
|
||||
expect(wrapper.find(OnPasteSelect)).not.toExist();
|
||||
expect(wrapper.findWhere(x => x.type() === CreatableSelect)).toHaveLength(
|
||||
1,
|
||||
);
|
||||
});
|
||||
describe('empty placeholder', () => {
|
||||
describe('withMulti', () => {
|
||||
it('does not show a placeholder if there are no choices', () => {
|
||||
const withMulti = mount(
|
||||
<SelectControl
|
||||
{...defaultProps}
|
||||
choices={[]}
|
||||
multi
|
||||
placeholder="add something"
|
||||
/>,
|
||||
);
|
||||
expect(withMulti.html()).not.toContain('placeholder=');
|
||||
});
|
||||
});
|
||||
describe('withSingleChoice', () => {
|
||||
it('does not show a placeholder if there are no choices', () => {
|
||||
const singleChoice = mount(
|
||||
<SelectControl
|
||||
{...defaultProps}
|
||||
choices={[]}
|
||||
multi
|
||||
placeholder="add something"
|
||||
/>,
|
||||
);
|
||||
expect(singleChoice.html()).not.toContain('placeholder=');
|
||||
});
|
||||
});
|
||||
describe('default placeholder', () => {
|
||||
it('does not show a placeholder if there are no options', () => {
|
||||
const defaultPlaceholder = mount(
|
||||
<SelectControl {...defaultProps} choices={[]} multi />,
|
||||
);
|
||||
expect(defaultPlaceholder.html()).not.toContain('placeholder=');
|
||||
});
|
||||
});
|
||||
describe('all choices selected', () => {
|
||||
it('does not show a placeholder', () => {
|
||||
const allChoicesSelected = mount(
|
||||
<SelectControl
|
||||
{...defaultProps}
|
||||
multi
|
||||
value={['today', '1 year ago']}
|
||||
/>,
|
||||
);
|
||||
expect(allChoicesSelected.html()).toContain('placeholder=""');
|
||||
});
|
||||
});
|
||||
});
|
||||
describe('when select is multi', () => {
|
||||
it('renders the placeholder when a selection has been made', () => {
|
||||
wrapper = mount(
|
||||
<SelectControl
|
||||
{...defaultProps}
|
||||
multi
|
||||
value={50}
|
||||
placeholder="add something"
|
||||
/>,
|
||||
);
|
||||
expect(wrapper.html()).toContain('add something');
|
||||
});
|
||||
it('shows numbers of options as a placeholder by default', () => {
|
||||
wrapper = mount(<SelectControl {...defaultProps} multi />);
|
||||
expect(wrapper.html()).toContain('2 option(s');
|
||||
});
|
||||
it('reduces the number of options in the placeholder by the value length', () => {
|
||||
wrapper = mount(
|
||||
<SelectControl {...defaultProps} multi value={['today']} />,
|
||||
);
|
||||
expect(wrapper.html()).toContain('1 option(s');
|
||||
});
|
||||
});
|
||||
describe('when select is single', () => {
|
||||
it('does not render the placeholder when a selection has been made', () => {
|
||||
wrapper = mount(
|
||||
<SelectControl
|
||||
{...defaultProps}
|
||||
value={50}
|
||||
placeholder="add something"
|
||||
/>,
|
||||
);
|
||||
expect(wrapper.html()).not.toContain('add something');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('optionsRemaining', () => {
|
||||
describe('isMulti', () => {
|
||||
it('returns the options minus selected values', () => {
|
||||
const wrapper = mount(
|
||||
<SelectControl {...defaultProps} multi value={['today']} />,
|
||||
);
|
||||
expect(wrapper.instance().optionsRemaining()).toEqual(1);
|
||||
});
|
||||
});
|
||||
describe('is not multi', () => {
|
||||
it('returns the length of all options', () => {
|
||||
wrapper = mount(
|
||||
<SelectControl
|
||||
{...defaultProps}
|
||||
value={50}
|
||||
placeholder="add something"
|
||||
/>,
|
||||
);
|
||||
expect(wrapper.instance().optionsRemaining()).toEqual(2);
|
||||
});
|
||||
});
|
||||
describe('with Select All', () => {
|
||||
it('does not count it', () => {
|
||||
const props = { ...defaultProps, multi: true, allowAll: true };
|
||||
const wrapper = mount(<SelectControl {...props} />);
|
||||
expect(wrapper.instance().getOptions(props).length).toEqual(3);
|
||||
expect(wrapper.instance().optionsRemaining()).toEqual(2);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('getOptions', () => {
|
||||
it('returns the correct options', () => {
|
||||
wrapper.setProps(defaultProps);
|
||||
|
|
|
@ -0,0 +1,129 @@
|
|||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
import React from 'react';
|
||||
import { useArgs } from '@storybook/client-api';
|
||||
import { OptionTypeBase } from 'react-select';
|
||||
import Select from '.';
|
||||
|
||||
const OPTIONS = [
|
||||
{ label: 'Blue', value: 'blue' },
|
||||
{ label: 'Red', value: 'red' },
|
||||
{ label: 'Orange', value: 'orange' },
|
||||
];
|
||||
|
||||
export default {
|
||||
title: 'Select Component',
|
||||
argTypes: {
|
||||
options: {
|
||||
type: 'select',
|
||||
options: OPTIONS,
|
||||
},
|
||||
multi: {
|
||||
type: 'boolean',
|
||||
},
|
||||
value: {
|
||||
type: 'string',
|
||||
},
|
||||
clearable: {
|
||||
type: 'boolean',
|
||||
},
|
||||
placeholder: {
|
||||
type: 'string',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export const SelectGallery = ({ value }: { value: OptionTypeBase }) => {
|
||||
return (
|
||||
<>
|
||||
<h4>With default value</h4>
|
||||
<Select
|
||||
value={OPTIONS[0]}
|
||||
ignoreAccents={false}
|
||||
name="select-datasource"
|
||||
onChange={() => {}}
|
||||
options={OPTIONS}
|
||||
placeholder="choose one"
|
||||
width={600}
|
||||
/>
|
||||
<hr />
|
||||
<h4>With no value</h4>
|
||||
<Select
|
||||
ignoreAccents={false}
|
||||
name="select-datasource"
|
||||
onChange={() => {}}
|
||||
options={OPTIONS}
|
||||
placeholder="choose one"
|
||||
width={600}
|
||||
value={value}
|
||||
/>
|
||||
<hr />
|
||||
<h4>Multi select</h4>
|
||||
<Select
|
||||
ignoreAccents={false}
|
||||
name="select-datasource"
|
||||
onChange={() => {}}
|
||||
options={OPTIONS}
|
||||
placeholder="choose one or more values"
|
||||
width={600}
|
||||
value={[OPTIONS[0]]}
|
||||
multi
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
SelectGallery.args = {
|
||||
value: '',
|
||||
options: OPTIONS,
|
||||
};
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
export const InteractiveSelect = (args: any) => {
|
||||
const [{ value, multi, clearable, placeholder }, updateArgs] = useArgs();
|
||||
const onSelect = (selection: {}) => {
|
||||
const { value }: { value?: any } = selection || {};
|
||||
if (multi) {
|
||||
updateArgs({ value: selection });
|
||||
return;
|
||||
}
|
||||
updateArgs({ value });
|
||||
};
|
||||
|
||||
return (
|
||||
<Select
|
||||
clearable={clearable}
|
||||
onChange={onSelect}
|
||||
name="interactive-select"
|
||||
options={OPTIONS}
|
||||
placeholder={placeholder}
|
||||
with={600}
|
||||
value={value}
|
||||
multi={multi}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
InteractiveSelect.args = {
|
||||
value: '',
|
||||
multi: false,
|
||||
options: OPTIONS,
|
||||
clearable: false,
|
||||
placeholder: "I'm interactive",
|
||||
};
|
|
@ -16,7 +16,7 @@
|
|||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
import React, { CSSProperties } from 'react';
|
||||
import React, { CSSProperties, ComponentType, ReactNode } from 'react';
|
||||
import { css, SerializedStyles, ClassNames } from '@emotion/core';
|
||||
import { supersetTheme } from '@superset-ui/core';
|
||||
import {
|
||||
|
@ -24,9 +24,12 @@ import {
|
|||
Theme,
|
||||
SelectComponentsConfig,
|
||||
components as defaultComponents,
|
||||
InputProps as ReactSelectInputProps,
|
||||
} from 'react-select';
|
||||
import { Props as SelectProps } from 'react-select/src/Select';
|
||||
import { colors as reactSelectColros } from 'react-select/src/theme';
|
||||
import { supersetColors } from 'src/components/styles';
|
||||
import { DeepNonNullable } from 'react-select/src/components';
|
||||
|
||||
export const DEFAULT_CLASS_NAME = 'Select';
|
||||
export const DEFAULT_CLASS_NAME_PREFIX = 'Select';
|
||||
|
@ -239,11 +242,37 @@ export const DEFAULT_STYLES: PartialStylesConfig = {
|
|||
paddingLeft: baseUnit * 1.2,
|
||||
paddingRight: baseUnit * 1.2,
|
||||
}),
|
||||
input: (provider, { selectProps }) => [
|
||||
provider,
|
||||
css`
|
||||
padding: ${selectProps?.isMulti && selectProps?.value?.length
|
||||
? '0 6px'
|
||||
: '0'};
|
||||
margin-left: 0;
|
||||
vertical-align: middle;
|
||||
`,
|
||||
],
|
||||
};
|
||||
|
||||
const { ClearIndicator, DropdownIndicator, Option } = defaultComponents;
|
||||
type SelectComponentsType = Omit<SelectComponentsConfig<any>, 'Input'> & {
|
||||
Input: ComponentType<InputProps>;
|
||||
};
|
||||
|
||||
export const DEFAULT_COMPONENTS: SelectComponentsConfig<any> = {
|
||||
// react-select is missing selectProps from their props type
|
||||
// so overwriting it here to avoid errors
|
||||
type InputProps = ReactSelectInputProps & {
|
||||
placeholder?: ReactNode;
|
||||
selectProps: SelectProps;
|
||||
};
|
||||
|
||||
const {
|
||||
ClearIndicator,
|
||||
DropdownIndicator,
|
||||
Option,
|
||||
Input,
|
||||
} = defaultComponents as Required<DeepNonNullable<SelectComponentsType>>;
|
||||
|
||||
export const DEFAULT_COMPONENTS: SelectComponentsType = {
|
||||
Option: ({ children, innerProps, data, ...props }) => (
|
||||
<ClassNames>
|
||||
{({ css }) => (
|
||||
|
@ -272,6 +301,20 @@ export const DEFAULT_COMPONENTS: SelectComponentsConfig<any> = {
|
|||
/>
|
||||
</DropdownIndicator>
|
||||
),
|
||||
Input: (props: InputProps) => {
|
||||
const {
|
||||
selectProps: { isMulti, value, placeholder },
|
||||
getStyles,
|
||||
} = props;
|
||||
const isMultiWithValue = isMulti && Array.isArray(value) && value.length;
|
||||
return (
|
||||
<Input
|
||||
{...props}
|
||||
placeholder={isMultiWithValue ? placeholder : undefined}
|
||||
css={getStyles('input', props)}
|
||||
/>
|
||||
);
|
||||
},
|
||||
};
|
||||
|
||||
export const VALUE_LABELED_STYLES: PartialStylesConfig = {
|
||||
|
|
|
@ -270,7 +270,7 @@ export default class AdhocFilterControl extends React.Component {
|
|||
isMulti
|
||||
isLoading={this.props.isLoading}
|
||||
name={`select-${this.props.name}`}
|
||||
placeholder={t('choose a column or metric')}
|
||||
placeholder={t('choose one or more columns or metrics')}
|
||||
options={this.state.options}
|
||||
value={this.state.values}
|
||||
labelKey="label"
|
||||
|
|
|
@ -347,7 +347,11 @@ export default class MetricsControl extends React.PureComponent {
|
|||
isLoading={this.props.isLoading}
|
||||
isMulti={this.props.multi}
|
||||
name={`select-${this.props.name}`}
|
||||
placeholder={t('choose a column or aggregate function')}
|
||||
placeholder={
|
||||
this.props.multi
|
||||
? t('choose one or more columns or aggregate functions')
|
||||
: t('choose a column or aggregate function')
|
||||
}
|
||||
options={this.state.options}
|
||||
value={this.state.value}
|
||||
labelKey="label"
|
||||
|
|
|
@ -166,7 +166,7 @@ export default class SelectControl extends React.PureComponent {
|
|||
});
|
||||
}
|
||||
if (props.allowAll === true && props.multi === true) {
|
||||
if (options.findIndex(o => this.isMetaSelectAllOption(o)) < 0) {
|
||||
if (!this.optionsIncludesSelectAll(options)) {
|
||||
options.unshift(this.createMetaSelectAllOption());
|
||||
}
|
||||
} else {
|
||||
|
@ -189,6 +189,30 @@ export default class SelectControl extends React.PureComponent {
|
|||
return o.meta && o.meta === true && o.label === 'Select All';
|
||||
}
|
||||
|
||||
optionsIncludesSelectAll(o) {
|
||||
return o.findIndex(o => this.isMetaSelectAllOption(o)) >= 0;
|
||||
}
|
||||
|
||||
optionsRemaining() {
|
||||
const { options } = this.state;
|
||||
const { value } = this.props;
|
||||
// if select is multi/value is array, we show the options not selected
|
||||
let remainingOptions = Array.isArray(value)
|
||||
? options.length - value.length
|
||||
: options.length;
|
||||
if (this.optionsIncludesSelectAll(options)) {
|
||||
remainingOptions -= 1;
|
||||
}
|
||||
return remainingOptions;
|
||||
}
|
||||
|
||||
createPlaceholder() {
|
||||
const optionsRemaining = this.optionsRemaining();
|
||||
const placeholder =
|
||||
this.props.placeholder || t('%s option(s)', optionsRemaining);
|
||||
return optionsRemaining ? placeholder : '';
|
||||
}
|
||||
|
||||
createMetaSelectAllOption() {
|
||||
const option = { label: 'Select All', meta: true };
|
||||
option[this.props.valueKey] = 'Select All';
|
||||
|
@ -197,34 +221,51 @@ export default class SelectControl extends React.PureComponent {
|
|||
|
||||
render() {
|
||||
// Tab, comma or Enter will trigger a new option created for FreeFormSelect
|
||||
const placeholder =
|
||||
this.props.placeholder || t('%s option(s)', this.state.options.length);
|
||||
const {
|
||||
autoFocus,
|
||||
clearable,
|
||||
disabled,
|
||||
filterOption,
|
||||
isLoading,
|
||||
menuPlacement,
|
||||
menuPortalTarget,
|
||||
menuPosition,
|
||||
name,
|
||||
noResultsText,
|
||||
onFocus,
|
||||
optionRenderer,
|
||||
promptTextCreator,
|
||||
value,
|
||||
valueKey,
|
||||
valueRenderer,
|
||||
} = this.props;
|
||||
const placeholder = this.createPlaceholder();
|
||||
const isMulti = this.props.isMulti || this.props.multi;
|
||||
|
||||
const selectProps = {
|
||||
autoFocus: this.props.autoFocus,
|
||||
isMulti,
|
||||
selectRef: this.getSelectRef,
|
||||
name: `select-${this.props.name}`,
|
||||
placeholder,
|
||||
options: this.state.options,
|
||||
value: this.props.value,
|
||||
labelKey: 'label',
|
||||
valueKey: this.props.valueKey,
|
||||
clearable: this.props.clearable,
|
||||
isLoading: this.props.isLoading,
|
||||
onChange: this.onChange,
|
||||
onFocus: this.props.onFocus,
|
||||
optionRenderer: this.props.optionRenderer,
|
||||
valueRenderer: this.props.valueRenderer,
|
||||
noResultsText: this.props.noResultsText,
|
||||
disabled: this.props.disabled,
|
||||
filterOption: this.props.filterOption,
|
||||
promptTextCreator: this.props.promptTextCreator,
|
||||
autoFocus,
|
||||
clearable,
|
||||
disabled,
|
||||
filterOption,
|
||||
ignoreAccents: false,
|
||||
menuPortalTarget: this.props.menuPortalTarget,
|
||||
menuPosition: this.props.menuPosition,
|
||||
menuPlacement: this.props.menuPlacement,
|
||||
isLoading,
|
||||
isMulti,
|
||||
labelKey: 'label',
|
||||
menuPlacement,
|
||||
menuPortalTarget,
|
||||
menuPosition,
|
||||
name: `select-${name}`,
|
||||
noResultsText,
|
||||
onChange: this.onChange,
|
||||
onFocus,
|
||||
optionRenderer,
|
||||
options: this.state.options,
|
||||
placeholder,
|
||||
promptTextCreator,
|
||||
selectRef: this.getSelectRef,
|
||||
value,
|
||||
valueKey,
|
||||
valueRenderer,
|
||||
};
|
||||
|
||||
let SelectComponent;
|
||||
|
|
Loading…
Reference in New Issue