Commit 204b5281 authored by Matija Obreza's avatar Matija Obreza
Browse files

Datasets: Facilitated search with search suggestions

parent 2b7b92ac
variables:
IMAGE_VERSION: "1.8"
IMAGE_VERSION: "1.9-SNAPSHOT"
DOCKER_HOST: "genesys1.swarm.genesys-pgr.org"
ARTIFACTS: "target/app"
......
......@@ -127,5 +127,14 @@
"CURATOR": "Organizes and validates data and metadata in correct format, ensures quality of both."
}
}
},
"search": {
"group": {
"descriptor": "Descriptors",
"accession": "Accessions",
"crop": "Crops",
"partner": "Data providers",
"dataset": "Datasets"
}
}
}
......@@ -37,6 +37,10 @@ function editDataset(uuid: string) {
};
}
export const applyDatasetFilters = (filters?: any) => (dispatch) => {
dispatch(listDatasetsRequest(undefined, undefined, undefined, filters));
};
function listMyDatasets(page?, results?, sortBy?, filter?, order?) {
return (dispatch, getState) => {
const token = getState().login.access_token;
......
import {push} from 'react-router-redux';
import {ADD_FILTER_CODE} from 'constants/filterCode';
import {cleanFilters} from 'utilities';
import * as _ from 'lodash';
import detectLocaleFromPath from '../../server/middleware/detectLocaleFromPath';
import { navigateTo } from 'actions/navigation';
import * as QS from 'query-string';
import { ADD_FILTER_CODE } from 'constants/filterCode';
import { cleanFilters } from 'utilities';
const addFilter = (code: string, filterJson: string) => ({
type: ADD_FILTER_CODE,
payload: { code, filterJson },
type: ADD_FILTER_CODE,
payload: { code, filterJson },
});
export const addFilterCode = (code: string, receivedFilter: object) => (dispatch, getState) => {
if (!code) {
return;
}
if (!code) {
return;
}
const receivedFilterJson = cleanFilters(receivedFilter, ['published']);
const filtersMap = _.isEmpty(getState().filterCode.filters) ? new Map<string, object>() : new Map<string, object>(getState().filterCode.filters);
const filterJson = filtersMap.get(code);
const receivedFilterJson = cleanFilters(receivedFilter, ['published']);
dispatch(addFilter(code, receivedFilterJson));
if (!filterJson) {
dispatch(addFilter(code, receivedFilterJson));
}
const params = new URLSearchParams(window.location.search);
_.isEmpty(receivedFilterJson) ? params.delete('filter') : params.set('filter', code);
let currentSearch = window.location.search;
currentSearch = currentSearch ? currentSearch.substring(1, currentSearch.length) : currentSearch;
if (currentSearch !== params.toString()) {
const lang = detectLocaleFromPath(window.location.pathname, 0);
const originalPath = window.location.pathname;
const pathname = lang !== 'en' ? originalPath.substring(3, originalPath.length) : originalPath;
dispatch(push(params.toString() ? `${pathname}?${params}` : `${pathname}`));
}
const qs = window && QS.parse(window.location.search) || {};
qs.filter = code;
dispatch(navigateTo('', qs));
};
import { SearchService } from 'service/SearchService';
import {DescriptorService} from 'service/DescriptorService';
import {DatasetService} from 'service/DatasetService';
import {DescriptorListService} from 'service/DescriptorListService';
......@@ -9,6 +10,7 @@ import {
SEARCH_DESCRIPTOR_PAGE,
SEARCH_DESCRIPTOR_LIST_PAGE,
NEW_SEARCH,
SEARCH_DATASET_SUGGESTIONS,
} from 'constants/search';
import {log} from 'utilities/debug';
......@@ -32,6 +34,29 @@ const searchDescriptorListPage = (paged: Page<DescriptorList>) => ({
payload: { paged },
});
const searchDatasetsSuggestions = (suggestions: any) => ({
type: SEARCH_DATASET_SUGGESTIONS,
payload: suggestions,
});
export function datasetSuggestions(searchQuery, filter?) {
return (dispatch, getState) => {
const token = getState().login.access_token;
if (searchQuery && searchQuery.trim() !== '') {
return SearchService.datasetSuggestions(token, searchQuery, filter)
.then((data) => {
return dispatch(searchDatasetsSuggestions(data));
})
.catch((error) => {
log('Error', error);
});
} else {
log('No search query for datasets');
return null;
}
};
}
export function searchDatasets(page?, results?, sortBy?, filter?, order?) {
return (dispatch, getState) => {
const token = getState().login.access_token;
......
......@@ -2,3 +2,5 @@ export const NEW_SEARCH = 'App/NEW_SEARCH';
export const SEARCH_DATASET_PAGE = 'App/SEARCH_DATASET_PAGE';
export const SEARCH_DESCRIPTOR_PAGE = 'App/SEARCH_DESCRIPTOR_PAGE';
export const SEARCH_DESCRIPTOR_LIST_PAGE = 'App/SEARCH_DESCRIPTOR_LIST_PAGE';
export const SEARCH_DATASET_SUGGESTIONS = 'App/SEARCH_DATASET_SUGGESTIONS';
export const SEARCHSUGGESTIONS_FORM = 'Form/SEARCH_SUGGESTIONS';
......@@ -70,4 +70,5 @@ export interface IDatasetFilter extends IAuditedVersionedModelFilter {
description?: IStringFilter;
language?: string[];
lastModifiedBy?: number[];
crop?: string[];
}
......@@ -5,6 +5,7 @@ import {
SEARCH_DESCRIPTOR_PAGE,
SEARCH_DESCRIPTOR_LIST_PAGE,
NEW_SEARCH,
SEARCH_DATASET_SUGGESTIONS,
} from 'constants/search';
import {Dataset} from 'model/dataset.model';
......@@ -17,12 +18,14 @@ const INITIAL_STATE: {
pagedDatasets: Page<Dataset>;
pagedDescriptors: Page<Descriptor>;
pagedDescriptorLists: Page<DescriptorList>;
datasetSuggestions: any;
} = {
search: null,
loading: false,
pagedDatasets: null,
pagedDescriptors: null,
pagedDescriptorLists: null,
datasetSuggestions: null,
};
function search(state = INITIAL_STATE, action: { type?: string, payload?: any } = {type: '', payload: {}}) {
......@@ -53,7 +56,12 @@ function search(state = INITIAL_STATE, action: { type?: string, payload?: any }
loading: {$set: false},
});
}
case SEARCH_DATASET_SUGGESTIONS: {
return update(state, {
datasetSuggestions: {$set: action.payload},
loading: {$set: false},
});
}
default:
return state;
}
......
import authenticatedRequest from 'utilities/requestUtils';
// import { dereferenceReferences } from 'utilities';
// import {log} from 'utilities/debug';
import { API_BASE_URL } from 'constants/apiURLS';
import { IDatasetFilter } from 'model/dataset.model';
// import { Partner } from 'model/partner.model';
export class SearchService {
// List my datasets
public static datasetSuggestions(token: string, searchQuery: string, filter: IDatasetFilter = {}): Promise<any> {
return authenticatedRequest(token, {
url: `${API_BASE_URL}/search/dataset/suggest?q=${searchQuery}`,
method: 'POST',
data: filter,
})
.then(({ data }) => {
return data;
});
}
}
......@@ -17,6 +17,7 @@ import { loadVocabulary } from 'actions/vocabulary';
import { Crop } from 'model/crop.model';
import { Link } from 'react-router-dom';
import Markdown from 'ui/common/markdown';
interface IDatasetLinkProps extends React.Props<any> {
to: Dataset;
......@@ -35,13 +36,13 @@ function DatasetLink({
if (edit) {
return (
<Link to={ `/datasets/${dataset.uuid}/edit` }>
{ children || dataset.title }
{ children || <Markdown basic source={ dataset.title } /> }
</Link>
);
} else {
return (
<Link to={ `/datasets/${dataset.uuid}` }>
{ children || dataset.title }
{ children || <Markdown basic source={ dataset.title } /> }
</Link>
);
}
......@@ -92,11 +93,11 @@ const DescriptorListLink_ = ({ to: descriptorList, edit = false, children, loadD
if (edit) {
return (
<Link to={ `/descriptorlists/${descriptorList.uuid}/edit` } onClick={ clickMe }>{ children || descriptorList.title }</Link>
<Link to={ `/descriptorlists/${descriptorList.uuid}/edit` } onClick={ clickMe }>{ children || <Markdown basic source={ descriptorList.title } /> }</Link>
);
} else {
return (
<Link to={ `/descriptorlists/${descriptorList.uuid}` } onClick={ clickMe }>{ children || descriptorList.title }</Link>
<Link to={ `/descriptorlists/${descriptorList.uuid}` } onClick={ clickMe }>{ children || <Markdown basic source={ descriptorList.title } /> }</Link>
);
}
};
......
......@@ -14,9 +14,9 @@ function Markdown({source, rows, style, className, basic}: IMarkdownTextProps) {
const mdStyle: object = rows ? { maxHeight: `${rows + 1.5}rem`, overflow: 'hidden', marginBottom: '1.429rem' } : {};
if (basic) {
// the 'markdown-basic' className is used to render the div as inline-block
// the 'markdown-basic' className is used to render the div as inline
return (
<div style={ { ...style, ...mdStyle } } className={ `${className ? className : ''}` }>
<div style={ { ...style, ...mdStyle } } className={ `markdown-basic ${className ? className : ''}` }>
<MarkdownComponent skipHtml unwrapDisallowed allowedTypes={ [ 'strong', 'emphasis'] }
className="markdown-basic" source={ source } />
</div>
......
......@@ -104,11 +104,9 @@ class BrowsePage extends React.Component<IDatasetsProps, any> {
protected loadNextPage = (page: number, pageSize: number) => {
const {promiselistDatasets, pagination} = this.props;
return promiselistDatasets(page, pageSize, pagination.sort, pagination.filter, pagination.dir);
}
}
protected renderDataset = (d: Dataset) => (
<DatasetCard className="m-20" dataset={ d } key={ d.uuid } />
)
protected renderDataset = (d: Dataset) => <DatasetCard className="m-20" dataset={ d } key={ d.uuid } />;
public render() {
const {classes, paged, pagination} = this.props;
......
import * as React from 'react';
import {bindActionCreators} from 'redux';
import {connect} from 'react-redux';
import {withStyles} from 'material-ui/styles';
import Grid from 'material-ui/Grid';
import TextField from 'material-ui/TextField';
import Tabs, {Tab} from 'material-ui/Tabs';
import Paper from 'material-ui/Paper';
import Loading from 'ui/common/Loading';
import {searchDatasets, searchDescriptorLists, searchDescriptors} from 'actions/search';
import {Dataset, IDatasetFilter} from 'model/dataset.model';
import {Page} from 'model/common.model';
import {Descriptor, DescriptorList, IDescriptorFilter, IDescriptorListFilter} from 'model/descriptor.model';
import ContentHeader from 'ui/common/heading/ContentHeader';
interface ISearchPageProps extends React.ClassAttributes<any> {
classes: any;
search: string;
loading: boolean;
pagedDatasets: Page<Dataset>;
pagedDescriptors: Page<Descriptor>;
pagedDescriptorLists: Page<DescriptorList>;
searchDatasets: (page?: number, results?: number, sortBy?: string, filter?: IDatasetFilter) => any;
searchDescriptors: (page?: number, results?: number, sortBy?: string, filter?: IDescriptorFilter) => any;
searchDescriptorLists: (page?: number, results?: number, sortBy?: string, filter?: IDescriptorListFilter) => any;
}
const styles = (theme) => ({
alignCenter: {
textAlign: 'center',
},
paper: {
margin: 20,
height: 500,
overflow: 'auto' as 'auto',
},
});
class SearchPage extends React.Component<ISearchPageProps, any> {
constructor(props: any, context: any) {
super(props, context);
this.state = {
search: props.search || '',
value: 0,
};
}
protected search = (e) => {
e.preventDefault();
const {search, value} = this.state;
const {searchDatasets, searchDescriptors, searchDescriptorLists} = this.props;
if (!search) {
return;
}
switch (value) {
case 0:
searchDatasets(0, 20, null, {_text: search});
break;
case 1:
searchDescriptors(0, 20, null, {_text: search});
break;
case 2:
searchDescriptorLists(0, 20, null, {_text: search});
break;
}
}
protected onSearchChange = (e) => {
const search = e.target.value;
this.setState((state) => ({...state, search}));
}
protected handleChange = (event, value) => {
this.setState((state) => ({...state, value}));
}
protected getContent = (value) => {
const {pagedDatasets, pagedDescriptors, pagedDescriptorLists} = this.props;
switch (value) {
case 0:
return pagedDatasets && JSON.stringify(pagedDatasets.content);
case 1:
return pagedDescriptors && JSON.stringify(pagedDescriptors.content);
case 2:
return pagedDescriptorLists && JSON.stringify(pagedDescriptorLists.content);
}
}
public render() {
const {classes, loading} = this.props;
const {search, value} = this.state;
return (
<div>
<ContentHeader title="Search the Genesys Catalog"/>
<Grid container spacing={ 0 }>
<Grid item xs={ 12 } className={ classes.alignCenter }>
<form onSubmit={ this.search }>
<TextField
label="Search"
value={ search }
onChange={ this.onSearchChange }
margin="normal"
/>
</form>
</Grid>
<Grid item xs={ 12 }>
<Tabs value={ value } onChange={ this.handleChange } centered>
<Tab label="Datasets"/>
<Tab label="Descriptors"/>
<Tab label="Descriptor lists"/>
</Tabs>
<Paper className={ classes.paper }>
{ loading ? <Loading/> : this.getContent(value) }
</Paper>
</Grid>
</Grid>
</div>
);
}
}
const styled = withStyles(styles)(SearchPage);
const mapStateToProps = (state, ownProps) => ({
search: state.search.search,
loading: state.search.loading,
pagedDatasets: state.search.pagedDatasets,
pagedDescriptors: state.search.pagedDescriptors,
pagedDescriptorLists: state.search.pagedDescriptorLists,
});
const mapDispatchToProps = (dispatch) => bindActionCreators({
searchDatasets,
searchDescriptors,
searchDescriptorLists,
}, dispatch);
export default connect(
mapStateToProps,
mapDispatchToProps,
)(styled);
import * as React from 'react';
import { bindActionCreators } from 'redux';
import { translate } from 'react-i18next';
import { connect } from 'react-redux';
import { withStyles } from 'material-ui/styles';
import { parse } from 'query-string';
import { navigateTo } from 'actions/navigation';
import { datasetSuggestions } from 'actions/search';
import { applyDatasetFilters } from 'actions/dataset';
import { IDatasetFilter } from 'model/dataset.model';
// import {Page} from 'model/common.model';
// import {Descriptor, DescriptorList, IDescriptorFilter, IDescriptorListFilter} from 'model/descriptor.model';
import Grid from 'material-ui/Grid';
import TextField from 'material-ui/TextField';
import Loading from 'ui/common/Loading';
import SuggestionsForm from './c/SuggestionsForm';
interface ISuggestionsPageProps extends React.ClassAttributes<any> {
t: any;
classes: any;
search: string;
filters: any;
filterCode: string;
loading: boolean;
navigateTo: (page, query?) => any;
datasetSuggestions: (searchQuery, filter?: IDatasetFilter) => any;
applyDatasetFilters: any;
suggestions: any;
}
const styles = (theme) => ({
alignCenter: {
textAlign: 'center',
},
paper: {
margin: 20,
height: 500,
overflow: 'auto' as 'auto',
},
});
class SuggestionsPage extends React.Component<ISuggestionsPageProps, any> {
constructor(props: any, context: any) {
super(props, context);
this.state = {
search: props.search || '',
filterCode: props.filter,
};
}
public componentWillMount() {
const { datasetSuggestions, search } = this.props;
console.log(`On mount query for ${search}`);
datasetSuggestions(search);
}
public componentWillReceiveProps(nextProps) {
const { datasetSuggestions, navigateTo, filterCode, filters } = this.props;
console.log(`Should I be querying for ${nextProps.search} when I have ${this.props.search}?`);
console.log(nextProps, this.props);
if (nextProps.search !== this.props.search) {
console.log(`Querying for ${nextProps.search}`, filters);
datasetSuggestions(nextProps.search, filters[filterCode]);
}
if (nextProps.filterCode !== this.props.filterCode) {
navigateTo(`/datasets`, { filter: nextProps.filterCode });
}
}
protected search = (e) => {
e.preventDefault();
const { navigateTo } = this.props;
const { search } = this.state;
console.log(`Searching for ${search}`);
if (!search) {
console.log('Search is blank... No go.');
return;
}
navigateTo(``, { q: search });
}
protected onSearchChange = (e) => {
const search = e.target.value;
this.setState({ ...this.state, search });
}
protected applyFilters = (newFilters) => {
console.log(`Applying filters`, newFilters);
const { applyDatasetFilters } = this.props;
applyDatasetFilters(newFilters);
}
public render() {
const { classes, loading, suggestions, t } = this.props;
const { search } = this.state;
return (
<Grid container spacing={ 0 }>
<Grid item xs={ 12 } className={ classes.alignCenter }>
<form onSubmit={ this.search }>
<TextField
label={ t('common:action.search') }
value={ search }
onChange={ this.onSearchChange }
margin="normal"
/>
</form>
</Grid>
<Grid item xs={ 12 }>
{ loading && <Loading /> }
{ suggestions && <SuggestionsForm suggestions={ suggestions } onSubmit={ this.applyFilters } /> }
</Grid>
</Grid>
);
}
}
const styled = translate()(withStyles(styles)(SuggestionsPage));
const mapStateToProps = (state, ownProps) => ({
search: parse(ownProps.location.search).q || '',
filterCode: parse(ownProps.location.search).filter || null,
filters: state.filterCode.filters,
loading: state.search.loading,
suggestions: state.search.datasetSuggestions,
});
const mapDispatchToProps = (dispatch) => bindActionCreators({
navigateTo,
datasetSuggestions,
applyDatasetFilters,
}, dispatch);
export default connect(
mapStateToProps,
mapDispatchToProps,
)(styled);
import * as React from 'react';
import Generic from './hits/_Generic';
import Descriptor from './hits/Descriptor';
import Crop from './hits/Crop';
import Partner from './hits/Partner';
import Dataset from './hits/Dataset';
import { Field } from 'redux-form';
import Grid from 'material-ui/Grid';
import Checkbox from 'material-ui/Checkbox';
const HitCheckbox = ({hitId, input, ...other}) => {
const handleChange = (e) => {
// console.log(`handling tick val=${hitId}`, e.target.checked, input, other);
const values = (input.value || []).filter((x) => x !== hitId);
if (e.target.checked) {
input.onChange([ ...values, hitId ]);
} else {
input.onChange(values);
}
};