Skip to content

Commit

Permalink
Merge branch 'master' into john-bodley-flake8-F8
Browse files Browse the repository at this point in the history
  • Loading branch information
john-bodley authored Nov 7, 2017
2 parents 3cfe735 + ccb87d3 commit b174215
Show file tree
Hide file tree
Showing 21 changed files with 470 additions and 125 deletions.
File renamed without changes
File renamed without changes
File renamed without changes
87 changes: 87 additions & 0 deletions superset/assets/javascripts/components/OnPasteSelect.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
import React from 'react';
import PropTypes from 'prop-types';
import Select from 'react-select';

export default class OnPasteSelect extends React.Component {
onPaste(evt) {
if (!this.props.multi) {
return;
}
evt.preventDefault();
const clipboard = evt.clipboardData.getData('Text');
if (!clipboard) {
return;
}
const regex = `[${this.props.separator}]+`;
const values = clipboard.split(new RegExp(regex)).map(v => v.trim());
const validator = this.props.isValidNewOption;
const selected = this.props.value || [];
const existingOptions = {};
const existing = {};
this.props.options.forEach((v) => {
existingOptions[v[this.props.valueKey]] = 1;
});
let options = [];
selected.forEach((v) => {
options.push({ [this.props.labelKey]: v, [this.props.valueKey]: v });
existing[v] = 1;
});
options = options.concat(values
.filter((v) => {
const notExists = !existing[v];
existing[v] = 1;
return notExists && (validator ? validator({ [this.props.labelKey]: v }) : !!v);
})
.map((v) => {
const opt = { [this.props.labelKey]: v, [this.props.valueKey]: v };
if (!existingOptions[v]) {
this.props.options.unshift(opt);
}
return opt;
}),
);
if (options.length) {
if (this.props.onChange) {
this.props.onChange(options);
}
}
}
render() {
const SelectComponent = this.props.selectWrap;
const refFunc = (ref) => {
if (this.props.ref) {
this.props.ref(ref);
}
this.pasteInput = ref;
};
const inputProps = { onPaste: this.onPaste.bind(this) };
return (
<SelectComponent
{...this.props}
ref={refFunc}
inputProps={inputProps}
/>
);
}
}

OnPasteSelect.propTypes = {
separator: PropTypes.string.isRequired,
selectWrap: PropTypes.func.isRequired,
ref: PropTypes.func,
onChange: PropTypes.func.isRequired,
valueKey: PropTypes.string.isRequired,
labelKey: PropTypes.string.isRequired,
options: PropTypes.array,
multi: PropTypes.bool.isRequired,
value: PropTypes.any,
isValidNewOption: PropTypes.func,
};
OnPasteSelect.defaultProps = {
separator: ',',
selectWrap: Select,
valueKey: 'value',
labelKey: 'label',
options: [],
multi: false,
};
56 changes: 56 additions & 0 deletions superset/assets/javascripts/components/VirtualizedRendererWrap.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import React from 'react';
import PropTypes from 'prop-types';

export default function VirtualizedRendererWrap(renderer) {
function WrapperRenderer({
focusedOption,
focusOption,
key,
option,
selectValue,
style,
valueArray,
}) {
if (!option) {
return null;
}
const className = ['VirtualizedSelectOption'];
if (option === focusedOption) {
className.push('VirtualizedSelectFocusedOption');
}
if (option.disabled) {
className.push('VirtualizedSelectDisabledOption');
}
if (valueArray && valueArray.indexOf(option) >= 0) {
className.push('VirtualizedSelectSelectedOption');
}
if (option.className) {
className.push(option.className);
}
const events = option.disabled ? {} : {
onClick: () => selectValue(option),
onMouseEnter: () => focusOption(option),
};
return (
<div
className={className.join(' ')}
key={key}
style={Object.assign(option.style || {}, style)}
title={option.title}
{...events}
>
{renderer(option)}
</div>
);
}
WrapperRenderer.propTypes = {
focusedOption: PropTypes.object.isRequired,
focusOption: PropTypes.func.isRequired,
key: PropTypes.string,
option: PropTypes.object,
selectValue: PropTypes.func.isRequired,
style: PropTypes.object,
valueArray: PropTypes.array,
};
return WrapperRenderer;
}
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
import React from 'react';
import PropTypes from 'prop-types';
import VirtualizedSelect from 'react-virtualized-select';
import Select, { Creatable } from 'react-select';
import ControlHeader from '../ControlHeader';
import { t } from '../../../locales';
import VirtualizedRendererWrap from '../../../components/VirtualizedRendererWrap';
import OnPasteSelect from '../../../components/OnPasteSelect';

const propTypes = {
choices: PropTypes.array,
Expand Down Expand Up @@ -37,55 +40,6 @@ const defaultProps = {
valueKey: 'value',
};

// Handle `onPaste` so that users may paste in
// options as comma-delimited, slightly modified from
// https://github.com/JedWatson/react-select/issues/1672
function pasteSelect(props) {
let pasteInput;
return (
<Select
{...props}
ref={(ref) => {
// Creatable requires a reference to its Select child
if (props.ref) {
props.ref(ref);
}
pasteInput = ref;
}}
inputProps={{
onPaste: (evt) => {
if (!props.multi) {
return;
}
evt.preventDefault();
// pull text from the clipboard and split by comma
const clipboard = evt.clipboardData.getData('Text');
if (!clipboard) {
return;
}
const values = clipboard.split(/[,]+/).map(v => v.trim());
const options = values
.filter(value =>
// Creatable validates options
props.isValidNewOption ? props.isValidNewOption({ label: value }) : !!value,
)
.map(value => ({
[props.labelKey]: value,
[props.valueKey]: value,
}));
if (options.length) {
pasteInput.selectValue(options);
}
},
}}
/>
);
}
pasteSelect.propTypes = {
multi: PropTypes.bool,
ref: PropTypes.func,
};

export default class SelectControl extends React.PureComponent {
constructor(props) {
super(props);
Expand Down Expand Up @@ -161,23 +115,16 @@ export default class SelectControl extends React.PureComponent {
clearable: this.props.clearable,
isLoading: this.props.isLoading,
onChange: this.onChange,
optionRenderer: this.props.optionRenderer,
optionRenderer: VirtualizedRendererWrap(this.props.optionRenderer),
valueRenderer: this.props.valueRenderer,
selectComponent: this.props.freeForm ? Creatable : Select,
};
// Tab, comma or Enter will trigger a new option created for FreeFormSelect
const selectWrap = this.props.freeForm ? (
<Creatable {...selectProps}>
{pasteSelect}
</Creatable>
) : (
pasteSelect(selectProps)
);
return (
<div>
{this.props.showHeader &&
<ControlHeader {...this.props} />
}
{selectWrap}
<OnPasteSelect {...selectProps} selectWrap={VirtualizedSelect} />
</div>
);
}
Expand Down
25 changes: 18 additions & 7 deletions superset/assets/javascripts/explore/stores/controls.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -568,17 +568,28 @@ export const controls = {
granularity_sqla: {
type: 'SelectControl',
label: t('Time Column'),
default: control =>
control.choices && control.choices.length > 0 ? control.choices[0][0] : null,
description: t('The time column for the visualization. Note that you ' +
'can define arbitrary expression that return a DATETIME ' +
'column in the table or. Also note that the ' +
'column in the table. Also note that the ' +
'filter below is applied against this column or ' +
'expression'),
mapStateToProps: state => ({
choices: (state.datasource) ? state.datasource.granularity_sqla : [],
}),
freeForm: true,
default: (c) => {
if (c.options && c.options.length > 0) {
return c.options[0].column_name;
}
return null;
},
clearable: false,
optionRenderer: c => <ColumnOption column={c} />,
valueRenderer: c => <ColumnOption column={c} />,
valueKey: 'column_name',
mapStateToProps: (state) => {
const newState = {};
if (state.datasource) {
newState.options = state.datasource.columns.filter(c => c.is_dttm);
}
return newState;
},
},

time_grain_sqla: {
Expand Down
105 changes: 105 additions & 0 deletions superset/assets/spec/javascripts/components/OnPasteSelect_spec.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
/* eslint-disable no-unused-expressions */
import React from 'react';
import sinon from 'sinon';
import { expect } from 'chai';
import { shallow } from 'enzyme';
import { describe, it } from 'mocha';
import VirtualizedSelect from 'react-virtualized-select';
import Select, { Creatable } from 'react-select';

import OnPasteSelect from '../../../javascripts/components/OnPasteSelect';

const defaultProps = {
onChange: sinon.spy(),
multi: true,
isValidNewOption: sinon.spy(s => !!s.label),
value: [],
options: [
{ value: 'United States', label: 'United States' },
{ value: 'China', label: 'China' },
{ value: 'India', label: 'India' },
{ value: 'Canada', label: 'Canada' },
{ value: 'Russian Federation', label: 'Russian Federation' },
{ value: 'Japan', label: 'Japan' },
{ value: 'Mexico', label: 'Mexico' },
],
};

const defaultEvt = {
preventDefault: sinon.spy(),
clipboardData: {
getData: sinon.spy(() => ' United States, China , India, Canada, '),
},
};

describe('OnPasteSelect', () => {
let wrapper;
let props;
let evt;
let expected;
beforeEach(() => {
props = Object.assign({}, defaultProps);
wrapper = shallow(<OnPasteSelect {...props} />);
evt = Object.assign({}, defaultEvt);
});

it('renders the supplied selectWrap component', () => {
const select = wrapper.find(Select);
expect(select).to.have.lengthOf(1);
});

it('renders custom selectWrap components', () => {
props.selectWrap = Creatable;
wrapper = shallow(<OnPasteSelect {...props} />);
expect(wrapper.find(Creatable)).to.have.lengthOf(1);
props.selectWrap = VirtualizedSelect;
wrapper = shallow(<OnPasteSelect {...props} />);
expect(wrapper.find(VirtualizedSelect)).to.have.lengthOf(1);
});

describe('onPaste', () => {
it('calls onChange with pasted values', () => {
wrapper.instance().onPaste(evt);
expected = props.options.slice(0, 4);
expect(props.onChange.calledWith(expected)).to.be.true;
expect(evt.preventDefault.called).to.be.true;
expect(props.isValidNewOption.callCount).to.equal(5);
});

it('calls onChange without any duplicate values and adds new values', () => {
evt.clipboardData.getData = sinon.spy(() =>
'China, China, China, China, Mexico, Mexico, Chi na, Mexico, ',
);
expected = [
props.options[1],
props.options[6],
{ label: 'Chi na', value: 'Chi na' },
];
wrapper.instance().onPaste(evt);
expect(props.onChange.calledWith(expected)).to.be.true;
expect(evt.preventDefault.called).to.be.true;
expect(props.isValidNewOption.callCount).to.equal(9);
expect(props.options[0].value).to.equal(expected[2].value);
props.options.splice(0, 1);
});

it('calls onChange with currently selected values and new values', () => {
props.value = ['United States', 'Canada', 'Mexico'];
evt.clipboardData.getData = sinon.spy(() =>
'United States, Canada, Japan, India',
);
wrapper = shallow(<OnPasteSelect {...props} />);
expected = [
props.options[0],
props.options[3],
props.options[6],
props.options[5],
props.options[2],
];
wrapper.instance().onPaste(evt);
expect(props.onChange.calledWith(expected)).to.be.true;
expect(evt.preventDefault.called).to.be.true;
expect(props.isValidNewOption.callCount).to.equal(11);
});
});
});
Loading

0 comments on commit b174215

Please sign in to comment.