Relation search widget (#186)
* search action/reducer refactor * Relation widget skeleton * search clearing * query action + reducer * Autocomplete component for RelationControl
This commit is contained in:
@ -21,6 +21,9 @@ import SelectControl from './Widgets/SelectControl';
|
||||
import SelectPreview from './Widgets/SelectPreview';
|
||||
import ObjectControl from './Widgets/ObjectControl';
|
||||
import ObjectPreview from './Widgets/ObjectPreview';
|
||||
import RelationControl from './Widgets/RelationControl';
|
||||
import RelationPreview from './Widgets/RelationPreview';
|
||||
|
||||
|
||||
registry.registerWidget('string', StringControl, StringPreview);
|
||||
registry.registerWidget('text', TextControl, TextPreview);
|
||||
@ -32,6 +35,7 @@ registry.registerWidget('date', DateControl, DatePreview);
|
||||
registry.registerWidget('datetime', DateTimeControl, DateTimePreview);
|
||||
registry.registerWidget('select', SelectControl, SelectPreview);
|
||||
registry.registerWidget('object', ObjectControl, ObjectPreview);
|
||||
registry.registerWidget('relation', RelationControl, RelationPreview);
|
||||
registry.registerWidget('unknown', UnknownControl, UnknownPreview);
|
||||
|
||||
export function resolveWidget(name) { // eslint-disable-line
|
||||
|
108
src/components/Widgets/RelationControl.js
Normal file
108
src/components/Widgets/RelationControl.js
Normal file
@ -0,0 +1,108 @@
|
||||
import React, { Component, PropTypes } from 'react';
|
||||
import Autosuggest from 'react-autosuggest';
|
||||
import { connect } from 'react-redux';
|
||||
import { debounce } from 'lodash';
|
||||
import { Loader } from '../../components/UI/index';
|
||||
import { query, clearSearch } from '../../actions/search';
|
||||
|
||||
|
||||
function escapeRegexCharacters(str) {
|
||||
return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
||||
}
|
||||
|
||||
class RelationControl extends Component {
|
||||
static propTypes = {
|
||||
onChange: PropTypes.func.isRequired,
|
||||
value: PropTypes.node,
|
||||
field: PropTypes.node,
|
||||
isFetching: PropTypes.bool,
|
||||
query: PropTypes.func.isRequired,
|
||||
clearSearch: PropTypes.func.isRequired,
|
||||
queryHits: PropTypes.array, // eslint-disable-line
|
||||
};
|
||||
|
||||
onChange = (event, { newValue }) => {
|
||||
this.props.onChange(newValue);
|
||||
};
|
||||
|
||||
onSuggestionsFetchRequested = debounce(({ value }) => {
|
||||
if (value.length < 3) return;
|
||||
const { field } = this.props;
|
||||
const collection = field.get('collection');
|
||||
const searchFields = field.get('searchFields').map(f => `data.${ f }`).toJS();
|
||||
this.props.query(collection, searchFields, value);
|
||||
}, 80);
|
||||
|
||||
onSuggestionsClearRequested = () => {
|
||||
this.props.clearSearch();
|
||||
};
|
||||
|
||||
getMatchingHits = (value) => {
|
||||
const { field, queryHits } = this.props;
|
||||
const searchFields = field.get('searchFields').toJS();
|
||||
const escapedValue = escapeRegexCharacters(value.trim());
|
||||
const regex = new RegExp(`^ ${ escapedValue }`, 'i');
|
||||
|
||||
if (escapedValue === '') {
|
||||
return [];
|
||||
}
|
||||
|
||||
return queryHits.filter((hit) => {
|
||||
let testResult = false;
|
||||
searchFields.forEach((f) => {
|
||||
testResult = testResult || regex.test(hit.data[f]);
|
||||
});
|
||||
return testResult;
|
||||
});
|
||||
};
|
||||
|
||||
getSuggestionValue = (suggestion) => {
|
||||
const { field } = this.props;
|
||||
const valueField = field.get('valueField');
|
||||
return suggestion.data[valueField];
|
||||
};
|
||||
|
||||
renderSuggestion = (suggestion) => {
|
||||
const { field } = this.props;
|
||||
const valueField = field.get('valueField');
|
||||
return <span>{suggestion.data[valueField]}</span>;
|
||||
};
|
||||
|
||||
render() {
|
||||
const { value, isFetching, queryHits } = this.props;
|
||||
|
||||
const inputProps = {
|
||||
placeholder: '',
|
||||
value: value || '',
|
||||
onChange: this.onChange,
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Autosuggest
|
||||
suggestions={queryHits}
|
||||
onSuggestionsFetchRequested={this.onSuggestionsFetchRequested} // eslint-disable-line
|
||||
onSuggestionsClearRequested={this.onSuggestionsClearRequested} // eslint-disable-line
|
||||
getSuggestionValue={this.getSuggestionValue}
|
||||
renderSuggestion={this.renderSuggestion}
|
||||
inputProps={inputProps}
|
||||
/>
|
||||
<Loader active={isFetching} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function mapStateToProps(state) {
|
||||
const isFetching = state.search.get('isFetching');
|
||||
const queryHits = state.search.get('queryHits');
|
||||
return { isFetching, queryHits };
|
||||
}
|
||||
|
||||
export default connect(
|
||||
mapStateToProps,
|
||||
{
|
||||
query,
|
||||
clearSearch,
|
||||
}
|
||||
)(RelationControl);
|
10
src/components/Widgets/RelationPreview.js
Normal file
10
src/components/Widgets/RelationPreview.js
Normal file
@ -0,0 +1,10 @@
|
||||
import React, { PropTypes } from 'react';
|
||||
import previewStyle from './defaultPreviewStyle';
|
||||
|
||||
export default function RelationPreview({ value }) {
|
||||
return <div style={previewStyle}>{ value }</div>;
|
||||
}
|
||||
|
||||
RelationPreview.propTypes = {
|
||||
value: PropTypes.node,
|
||||
};
|
Reference in New Issue
Block a user