Commit b2281a97 authored by Matija Obreza's avatar Matija Obreza
Browse files

Merge branch '311-new-search-demo' into 'master'

Resolve "New search demo"

Closes #311

See merge request !241
parents 301a678a 204b5281
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 {push} from 'react-router-redux';
import { stringify } from 'query-string';
export function navigateTo(path: string) {
export function navigateTo(path: string, query?: object) {
return (dispatch) => {
dispatch(push(path));
if (! query) {
dispatch(push(path));
} else {
dispatch(push(`${path}?` + stringify(query)));
}
};
}
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>
);
}
};
......
......@@ -4,8 +4,6 @@ import { withStyles } from 'material-ui/styles';
import {Crop} from 'model/crop.model';
import Grid from 'material-ui/Grid';
interface ICropChipsProps extends React.ClassAttributes<any> {
crops: string[];
availableCrops: Crop[];
......@@ -13,28 +11,37 @@ interface ICropChipsProps extends React.ClassAttributes<any> {
}
const styles = (theme) => ({
wrap: {
display: 'flex' as 'flex',
flexWrap: 'wrap' as 'wrap',
padding: theme.spacing.unit / 2,
chipHolder: {
display: 'inline-block',
position: 'relative' as 'relative',
height: '1em',
},
chip: {
display: 'inline-block',
marginRight: theme.spacing.unit,
padding: '.2em .5em',
position: 'relative' as 'relative',
top: '-.2em',
border: 'solid 1px Gray',
borderRadius: '.2em',
backgroundColor: '#e2e2e2',
color: 'Black',
},
});
const CropChips = ({crops, availableCrops, classes}: ICropChipsProps) => crops && crops.length > 0 && (
<Grid container>
<Grid item xs={ 12 } className={ classes.wrap }>
{ crops.map((c) => availableCrops ? availableCrops.find((crop) => crop.code === c) || c : c).map((crop) => {
if (typeof crop === 'string') {
return <div key={ crop } className={ classes.chip }>{ crop }</div>;
} else {
return <div key={ crop.code } className={ classes.chip }>{ crop.title }</div>;
}
}) }
</Grid>
</Grid>
const CropChips = ({crops, availableCrops, classes}: ICropChipsProps) => crops && crops.length > 0 && crops.filter((c) => c !== null).length > 0 && (
<div className={ classes.chipHolder }>
{ crops.map((c) => availableCrops ? availableCrops.find((crop) => crop.code === c) || c : c).map((crop) => {
if (! crop) {
console.log('Null crop in', crops);
return null;
} else if (typeof crop === 'string') {
return <div key={ crop } className={ classes.chip }>{ crop }</div>;
} else {
return <div key={ crop.code } className={ classes.chip }>{ crop.title }</div>;
}
}) }
</div>
);
const mapStateToProps = (state) => ({
......
......@@ -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(``, <