Commit a8cf4d24 authored by Maxym Borodenko's avatar Maxym Borodenko Committed by Matija Obreza
Browse files

AutocompleteFilter component

parent 13b887ae
......@@ -40,6 +40,7 @@ const apiAccessionsMapInfo = createApiCaller(AccessionService.mapInfo, RECEIVE_A
const apiGeoJson = createPureApiCaller(AccessionService.geoJson);
const apiClimateInfo = createPureApiCaller(ClimateService.getCurrentClimate);
const apiAutocomplete = createPureApiCaller(AccessionService.autocomplete);
const apiListAccessionByUuid = createPureApiCaller(AccessionService.listAllByUuid);
const apiAccessionByUuid = createApiCaller(AccessionService.getDetailsByUuid, RECEIVE_ACCESSION);
......@@ -164,6 +165,10 @@ const makeRange = (variable: number, diff: number) => {
};
};
export const autocomplete = (field: string, term: string, filters: string | AccessionFilter) => (dispatch, getState) => {
return dispatch(apiAutocomplete(field, term, filters));
};
export const applyClimateFilters = (climate: TileClimate, extraFilters?: any) => (dispatch) => {
// BIO1 = Annual Mean Temperature !!
// BIO2 = Mean Diurnal Range (Mean of monthly (max temp - min temp))
......
......@@ -63,7 +63,13 @@ class BrowsePage extends BrowsePageTemplate<Accession> {
return (
<PageLayout sidebar={
<AccessionFilters initialValues={ paged && paged.filter || {} } onSubmit={ this.myApplyFilters } terms={ suggestionTerms } crops={ crops }/>
<AccessionFilters
initialValues={ paged && paged.filter || {} }
filterCode={ paged && paged.filterCode || '' }
onSubmit={ this.myApplyFilters }
terms={ suggestionTerms }
crops={ crops }
/>
}>
<PageTitle title={ t('accessions.public.p.browse.title') }/>
<ContentHeader title={ t('accessions.public.p.browse.title') } subTitle={ t('accessions.public.p.browse.subTitle') } />
......
......@@ -356,9 +356,13 @@ class BrowsePage extends React.Component<IMapPageProps, any> {
<ContentHeader title={ t('accessions.public.p.browse.title') } subTitle={ t('accessions.public.p.browse.subTitle') } />
<Drawer variant="temporary" open={ sidebarOpened } onClose={ this.closeSidebar }>
<AccessionsFilters terms={ suggestionTerms } onSubmit={ loadAccessionsMapInfo } initialValues={ mapInfo.data.filter }/>
</Drawer>
<AccessionsFilters
filterCode={ filterCode || '' }
terms={ suggestionTerms }
onSubmit={ loadAccessionsMapInfo }
initialValues={ mapInfo.data.filter }
/>
</Drawer>
<Dialog open={ dialogOpened } onClose={ this.hideDialog } maxWidth="md" fullWidth>
<BioClimateDisplay classes={ {section: classes.climateDialog} } climateData={ climateData }/>
......
......@@ -158,7 +158,12 @@ class BrowsePage extends React.Component<IOverviewPageProps, any> {
return (
<PageLayout
sidebar={
<AccessionFilters terms={ suggestionTerms } initialValues={ overviewWrapper && overviewWrapper.filter || {} } onSubmit={ applyOverviewFilters }/>
<AccessionFilters
filterCode={ filterCode || '' }
terms={ suggestionTerms }
initialValues={ overviewWrapper && overviewWrapper.filter || {} }
onSubmit={ applyOverviewFilters }
/>
}
withFooter
>
......
import * as React from 'react';
import { connect } from 'react-redux';
import { bindActionCreators } from 'redux';
import { reduxForm } from 'redux-form';
import {translate} from 'react-i18next';
......@@ -10,11 +11,13 @@ import CollapsibleComponentSearch from 'ui/common/filter/CollapsibleComponentSea
import NumberFilter from 'ui/common/filter/NumberFilter';
import StringFilter from 'ui/common/filter/StringFilter';
import StringArrFilter from 'ui/common/filter/StringArrFilter';
import AutocompleteFilter from 'ui/common/filter/AutocompleteFilter';
import Accession from 'model/accession/Accession';
import DateFilter from 'ui/common/filter/DateFilter';
import BooleanFilter from 'ui/common/filter/BooleanFilter';
import { autocomplete } from 'accessions/actions/public';
const AccessionFilters = ({handleSubmit, initialize, terms, crops, t, ...other}) => {
const AccessionFilters = ({handleSubmit, initialValues, initialize, filterCode, autocomplete, terms, crops, t, ...other}) => {
// console.log('AccessionFilters', initialValues);
return (
<FiltersBlock title={ t('accessions.public.f.filtersTitle') } handleSubmit={ handleSubmit } initialize={ initialize } { ...other }>
......@@ -26,7 +29,14 @@ const AccessionFilters = ({handleSubmit, initialize, terms, crops, t, ...other})
/>
</CollapsibleComponentSearch>
<CollapsibleComponentSearch title={ t('common:f.textSearch') }>
<StringArrFilter name="institute.code" terms={ terms && terms.get('institute.code') } label={ t('accessions.common.instituteCode') } placeholder="NGA039"/>
<AutocompleteFilter
filterCode={ filterCode }
autocomplete={ autocomplete }
name="institute.code"
terms={ terms && terms.get('institute.code') }
label={ t('accessions.common.instituteCode') }
placeholder="NGA039"
/>
<StringArrFilter name="institute.country.code3" label={ t('accessions.model.institute.country.iso3') } placeholder="NGA"/>
<StringFilter name="accessionNumber" searchType="contains" label={ t('accessions.common.acceNumb') } placeholder="IRGC"/>
<NumberFilter name="seqNo" label={ t('accessions.public.f.seqNumber') } />
......@@ -45,7 +55,9 @@ const AccessionFilters = ({handleSubmit, initialize, terms, crops, t, ...other})
/>
</CollapsibleComponentSearch>
<CollapsibleComponentSearch title={ t('accessions.common.taxonomy') }>
<StringArrFilter
<AutocompleteFilter
filterCode={ filterCode }
autocomplete={ autocomplete }
name="taxonomy.genus"
terms={ terms && terms.get('taxonomy.genus') }
label={ t('accessions.common.genus') }
......@@ -143,11 +155,14 @@ const AccessionFilters = ({handleSubmit, initialize, terms, crops, t, ...other})
const mapStateToProps = (state, ownProps) => ({
crops: state.crop.public.list ? state.crop.public.list.data : undefined,
});
const mapDispatchToProps = (dispatch) => bindActionCreators({
autocomplete,
}, dispatch);
export default translate()(reduxForm({
enableReinitialize: true,
destroyOnUnmount: false,
form: ACCESSION_FILTERFORM,
})(connect(mapStateToProps)(AccessionFilters)));
})(connect(mapStateToProps, mapDispatchToProps)(AccessionFilters)));
/*
* Defined in Swagger as '#/definitions/LabelValue«string»'
*/
class LabelValue<T> {
public label: string;
public value: T;
}
export default LabelValue;
......@@ -11,12 +11,14 @@ import FilteredPage, { IPageRequest } from 'model/FilteredPage';
import {AccessionRef} from 'model/accession/AccessionRef';
import AccessionAuditLog from 'model/accession/AccessionAuditLog';
import AccessionSuggestionPage from 'model/accession/AccessionSuggestionPage';
import LabelValue from 'model/LabelValue';
const URL_GET_BY_DOI = `/api/v1/acn/{doi}`; // UrlTemplate doesn't like the / in DOI
const URL_UUIDS_FROM_ACCE_NUMBERS = `/api/v1/acn/acce-number`;
const URL_UUID_FROM_ACCE_NUMBER = UrlTemplate.parse(`/api/v1/acn/acce-number/{acceNumber}`);
const URL_GET_BY_UUID = UrlTemplate.parse(`/api/v1/acn/{uuid}`);
const URL_GEO_JSON = `/api/v1/acn/geoJson`;
const URL_AUTOCOMPLETE = UrlTemplate.parse(`/api/v1/acn/autocomplete/{field}`);
const URL_UUID_FROM_IDS = `/api/v1/acn/id`;
const URL_LIST_BY_UUID = `/api/v1/acn/for-uuid`;
const URL_GET_ACCESSION_AUDIT_LOG_BY_DOI = `/api/v1/acn/auditlog/{doi}`; // UrlTemplate doesn't like the / in DOI
......@@ -366,6 +368,30 @@ class AccessionService {
...content,
}).then(({ data }) => data);
}
/**
* autocomplete at /api/v1/acn/autocomplete/{field}
*
* @param field field
* @param filter filter
* @param term term
*/
public static autocomplete(field: string, term: string, filter: string | AccessionFilter, xhrConfig?): Promise<Array<LabelValue<string>>> {
const qs = QueryString.stringify({
f: typeof filter === 'string' ? filter : undefined,
term: term || undefined,
}, {});
const apiUrl = URL_AUTOCOMPLETE.expand({ field }) + (qs ? `?${qs}` : '');
const content = { data: typeof filter === 'string' ? null : { ...filter } };
return axiosBackend.request({
...xhrConfig,
url: apiUrl,
method: 'GET',
...content,
}).then(({ data }) => data as Array<LabelValue<string>>);
}
}
export default AccessionService;
import * as React from 'react';
import { Fields, change, Field } from 'redux-form';
import {connect} from 'react-redux';
import {bindActionCreators} from 'redux';
import { translate } from 'react-i18next';
import * as _ from 'lodash';
import Number from 'ui/common/Number';
import { Properties, PropertiesItem } from 'ui/common/Properties';
import LabelValue from 'model/LabelValue';
import AccessionFilter from 'model/accession/AccessionFilter';
import MaterialAutosuggest from 'ui/common/material-autosuggest';
import { debounce } from 'debounce';
interface IStringListProps extends React.ClassAttributes<any> {
input: any;
notInput: any;
removeByIndex: (index: number, isNot: boolean) => void;
}
class StringList extends React.Component<IStringListProps> {
public state = {
renderList: [],
};
private addToNotList = (item, index) => {
const { input, notInput } = this.props;
const {renderList} = this.state;
renderList[index] = {value: item.value, state: false};
this.setState({renderList});
input.onChange(renderList.filter((renderItem) => renderItem.state).map((renderItem) => renderItem.value));
notInput.onChange(renderList.filter((renderItem) => !renderItem.state).map((renderItem) => renderItem.value));
}
private removeFromNotList = (item, index) => {
const { input, notInput } = this.props;
const {renderList} = this.state;
renderList[index] = {value: item.value, state: true};
this.setState({renderList});
input.onChange(renderList.filter((renderItem) => renderItem.state).map((renderItem) => renderItem.value));
notInput.onChange(renderList.filter((renderItem) => !renderItem.state).map((renderItem) => renderItem.value));
}
public componentWillMount() {
let {input: {value: doList}, notInput: {value: notList}} = this.props;
doList = doList || [];
notList = notList || [];
const {renderList} = this.state;
doList
.filter((doItem) => renderList.findIndex((renderItem) => renderItem.value === doItem) === -1)
.map((doItem) => renderList.push({value: doItem, state: true}));
notList
.filter((notItem) => renderList.findIndex((renderItem) => renderItem.value === notItem) === -1)
.map((notItem) => renderList.push({value: notItem, state: false}));
this.setState({renderList});
}
public componentWillReceiveProps(nextProps) {
let {input: {value: doList}, notInput: {value: notList}} = nextProps;
doList = doList || [];
notList = notList || [];
const {renderList} = this.state;
const newRenderList = [];
renderList.map((renderItem) => {
if (doList.indexOf(renderItem.value) !== -1 || notList.indexOf(renderItem.value) !== -1) {
newRenderList.push(renderItem);
}
});
doList
.filter((doItem) => newRenderList.findIndex((renderItem) => renderItem.value === doItem) === -1)
.map((doItem) => newRenderList.push({value: doItem, state: true}));
notList
.filter((notItem) => newRenderList.findIndex((renderItem) => renderItem.value === notItem) === -1)
.map((notItem) => newRenderList.push({value: notItem, state: false}));
this.setState({renderList: newRenderList});
}
public render() {
const {removeByIndex} = this.props;
const {renderList} = this.state;
return (
<div>
{ renderList.map((renderItem, index) => (
<div style={ { margin: '.2rem 0', padding: '.2rem 1rem', backgroundColor: '#e8e5e1', color: '#202222' } } key={ renderItem.value }>
{ renderItem.state ?
<span className="font-bold float-Left mr-5" onClick={ () => this.addToNotList(renderItem, index) }>+</span>
:
<span className="font-bold float-Left mr-5" onClick={ () => this.removeFromNotList(renderItem, index) }>-</span>
}
{ renderItem.value }
<div className="font-bold float-right" onClick={ () => removeByIndex(index, !renderItem.state) }>X</div>
</div>
)) }
</div>
);
}
}
interface IAutocompleteFilterInternal extends React.ClassAttributes<any> {
field: string;
filterCode: string;
input: any;
placeholder?: string;
label?: string;
options?: { [key: string]: any; };
t: any;
addToNotList: (item: any) => void;
notList: any[];
autocomplete: (field: string, term: string, filter: string | AccessionFilter) => Promise<Array<LabelValue<string>>>;
}
class AutocompleteFilterInternal extends React.Component<IAutocompleteFilterInternal & any, any> {
private constructor(props, context) {
super(props, context);
const notValues = _.get(props, `${props.names[0]}.input.value`);
const values = _.get(props, `${props.names[1]}.input.value`);
this.state = {
values,
notValues,
text: '',
autocompleteObj: [],
};
}
public componentWillMount() {
const input = _.get(this.props, `${this.props.names[0]}.input`);
const value = typeof input.value[0] === 'number' ? input.value.map((key) => `${key}`) : input.value;
const notValue = _.get(this.props, `${this.props.names[1]}.input.value`);
this.setState({
values: [ ...value ],
notValues: [ ...notValue ],
text: '',
});
}
public componentWillReceiveProps(nextProps) {
const input = _.get(nextProps, `${nextProps.names[0]}.input`);
const value = typeof input.value[0] === 'number' ? input.value.map((key) => `${key}`) : input.value;
const notValue = _.get(nextProps, `${nextProps.names[1]}.input.value`);
this.setState({
values: [ ...value ],
notValues: [...notValue],
text: '',
});
}
private maybeAdd = (...newValues: string[]) => {
const values = [ ...this.state.values ];
newValues.forEach((text) => {
if (text && text.length > 0) {
if (values.indexOf(text) < 0) {
values.push(text);
}
}
});
if (!_.isEqual(values, this.state.values)) {
this.setState({
text: '',
values,
});
}
return values;
}
private maybeRemove = (...newValues: string[]) => {
const values = [ ...this.state.values ];
newValues.forEach((text) => {
if (text && text.length > 0) {
const index: number = values.indexOf(text);
if (index >= 0) {
values.splice(index, 1);
}
}
});
if (!_.isEqual(values, this.state.values)) {
this.setState({
text: '',
values,
});
}
return values;
}
private removeByIndex = (index, isNot) => {
const { input } = _.get(this.props, this.props.names[isNot ? 1 : 0]);
const newValues = this.maybeRemove(this.state.values[index]);
input.onChange(newValues);
}
private onInputChange = (e) => {
const { autocomplete, field, filterCode } = this.props;
if (e.target && typeof e.target.value === 'string') {
if (e.target && e.target.value) {
if (e.target.value !== this.state.text) {
if (e.target.value.length >= 3) {
this.setState({text: e.target.value});
autocomplete(field, e.target.value, filterCode)
.then((autocompleteObj) => {
this.setState({...this.state, autocompleteObj});
});
}
}
}
}
}
private onSuggestionSelected = (e, data, autocompleteInput, that) => {
autocompleteInput.onChange.call(that, '');
const { input } = _.get(this.props, this.props.names[0]);
input.onChange(this.maybeAdd(data.suggestion.value));
}
public render() {
const { placeholder, label, t, names, name, terms, classes } = this.props;
const { input } = _.get(this.props, names[0]);
const { input: notInput } = _.get(this.props, names[1]);
const { values, notValues } = this.state;
return (
<div>
<Field
name={ `${name}` }
component={ MaterialAutosuggest }
label={ label }
placeholder={ placeholder }
onChange={ debounce(this.onInputChange, 1000) }
suggestions={ this.state.autocompleteObj }
onSuggestionSelected={ this.onSuggestionSelected }
suggestionLabel="label"
className="full-width"
/>
{ ((values && values.length > 0) || (notValues && notValues.length > 0)) &&
<StringList input={ input } notInput={ notInput } removeByIndex={ this.removeByIndex } />
}
{ terms &&
<Properties>
<h5 className="pl-10 pt-1rem mb-10">{ t('common:f.suggestedFilters') }</h5>
{ terms && Array.from(terms).slice(0, 10).map(([key, value]) => (
<PropertiesItem key={ key } title={ key } onClick={ () => input.onChange(this.maybeAdd(key)) } classes={ {...classes, propertiesRow: 'cursor-pointer'} }>
<span className="float-right">
<Number value={ value }/>
</span>
</PropertiesItem>
)) }
</Properties>
}
</div>
);
}
}
interface IAutocompleteFilter extends React.ClassAttributes<any> {
name: string;
placeholder?: string;
label?: string;
options?: { [key: string]: any; };
terms?: { [key: string]: any; };
byKey?: boolean;
autocomplete: (field: string, term: string, filter: string | AccessionFilter) => Promise<Array<LabelValue<string>>>;
filterCode: string;
classes: any;
t: any;
}
class AutocompleteFilter extends React.Component<IAutocompleteFilter, any> {
public render() {
const { name, label, placeholder, options, terms, byKey, autocomplete, filterCode, classes, t } = this.props;
return (
<div>
<Fields
names={ [`${name}`, `NOT.${name}`] }
field={ name }
component={ AutocompleteFilterInternal }
label={ t(label) }
placeholder={ placeholder }
options={ options }
terms={ terms }
autocomplete={ autocomplete }
filterCode={ filterCode }
byKey={ byKey }
classes={ classes }
t={ t }
/>
</div>
);
}
}
const mapDispatchToProps = (dispatch) => bindActionCreators({
change,
}, dispatch);
export default connect(null, mapDispatchToProps)(translate()(AutocompleteFilter));
......@@ -42,6 +42,8 @@ interface IMaterialAutosuggestProps extends React.ClassAttributes<any> {
// show all suggestions when the input is focused, without further input by the user
emptyquery?: boolean;
onSuggestionSelected?: any;
// styles
classes: any;
......@@ -152,7 +154,7 @@ class MaterialAutosuggest extends React.Component<IMaterialAutosuggestProps, any
}
public render() {
const {suggestions, suggestionLabel, input, classes, ...other} = this.props;
const {suggestions, suggestionLabel, input, onSuggestionSelected, classes, ...other} = this.props;
const getSuggestionValue = (suggestion) => {
input.onChange.call(this, suggestion[suggestionLabel]);
......@@ -183,6 +185,7 @@ class MaterialAutosuggest extends React.Component<IMaterialAutosuggestProps, any
renderSuggestionsContainer={ this.renderSuggestionsContainer }
getSuggestionValue={ getSuggestionValue }
renderSuggestion={ this.renderSuggestion }
onSuggestionSelected={ (e, data) => onSuggestionSelected(e, data, input, this) }
inputProps={ {
classes,
...input,
......
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment