Commit 37738c36 authored by Viacheslav Pavlov's avatar Viacheslav Pavlov Committed by Matija Obreza
Browse files

Institutes: Public pages

parent 2a1fae92
......@@ -21,7 +21,9 @@
"subset": "Subset",
"subset_plural": "Subsets",
"accession": "Accession",
"accession_plural": "Accessions"
"accession_plural": "Accessions",
"institute": "Institute",
"institute_plural": "Institutes"
},
"m": {
"crop": {
......@@ -39,7 +41,8 @@
"My Dashboard": "My dashboard",
"My profile": "My profile",
"Partners": "Partners",
"Subsets": "Subsets"
"Subsets": "Subsets",
"Institutes": "Institutes"
},
"p": {
"crop": {
......@@ -129,6 +132,12 @@
},
"subsets": {
"title": "Title"
},
"wiews": {
"code": "Institute code",
"country": {
"iso3": "Country code"
}
}
},
"accession": {
......
import InstituteService from 'service/genesys/InstituteService';
import FilteredPage, { IPageRequest } from 'model/FilteredPage';
import FaoInstitute from 'model/FaoInstitute';
import FaoInstituteFilter from 'model/FaoInstituteFilter';
/**
* Action list
*
* @param filter filter
* @param d d
* @param l l
* @param p p
* @param s s
*/
export const list = (filter: string | FaoInstituteFilter, page: IPageRequest) => (dispatch, getState): Promise<FilteredPage<FaoInstitute>> => {
const authorization = getState().login.access_token;
return InstituteService.list(authorization, filter, page);
};
/**
* Action get
*
* @param code wiewsCode
*/
export const get = (code: string) => (dispatch, getState): Promise<FaoInstitute> => {
const authorization = getState().login.access_token;
return InstituteService.get(authorization, code);
};
import { list as listInstitutes, get } from 'actions/genesys/instituteService';
import {RECEIVE_INSTITUTE, RECEIVE_INSTITUTES} from 'constants/institutes';
import FaoInstitute from 'model/FaoInstitute';
import FaoInstituteFilter from 'model/FaoInstituteFilter';
import FilteredPage, {IPageRequest} from 'model/FilteredPage';
import navigateTo from './navigation';
const receiveInstitutes = (paged: FilteredPage<FaoInstitute>, error = null) => ({
type: RECEIVE_INSTITUTES,
payload: { paged, error },
});
const receiveInstitute = (institute: FaoInstitute, error = null) => ({
type: RECEIVE_INSTITUTE,
payload: { institute, error },
});
export const updateRoute = (paged: FilteredPage<FaoInstitute>) => (dispatch) => {
const qs = {
s: paged.sort[0].property === FaoInstitute.DEFAULT_SORT.property ? undefined : paged.sort[0].property,
d: paged.sort[0].direction === FaoInstitute.DEFAULT_SORT.direction ? undefined : paged.sort[0].direction,
};
dispatch(navigateTo(paged.filterCode ? `/wiews/${paged.filterCode}` : '/wiews', qs));
};
export { listInstitutes as listInstitutesPromise };
export const applyFilters = (filters: string | FaoInstituteFilter, page: IPageRequest = { page: 0 }) => (dispatch) => {
console.log('Applying new filter', filters);
return dispatch(listInstitutes(filters, page))
.then((paged) => {
dispatch(receiveInstitutes(paged));
dispatch(updateRoute(paged));
}).catch((error) => {
console.log(`API error`, error);
dispatch(receiveInstitutes(null, error.response));
});
};
export const loadInstitutesPage = (page: IPageRequest) => (dispatch, getState) => {
const filterCode = getState().institutes.paged.filterCode;
return dispatch(listInstitutes(filterCode, page))
.then((paged) => {
dispatch(receiveInstitutes(paged));
dispatch(updateRoute(paged));
}).catch((error) => {
console.log(`API error`, error);
dispatch(receiveInstitutes(null, error.response));
});
};
export const loadInstitute = (code: string) => (dispatch) => {
return dispatch(get(code))
.then((institute) => {
dispatch(receiveInstitute(institute));
}).catch((error) => {
console.log(`API error`, error);
dispatch(receiveInstitute(null, error.response));
});
};
export const RECEIVE_INSTITUTES = 'institutes/RECEIVE_INSTITUTES';
export const RECEIVE_INSTITUTE = 'institutes/RECEIVE_INSTITUTE';
export const INSTITUTE_FILTERFORM = 'Form/institutes/INSTITUTE_FILTERFORM';
......@@ -26,6 +26,13 @@ class FaoInstitute {
public vCode: string;
public country: Country;
public static DEFAULT_SORT = {
property: 'code',
direction: 'ASC',
};
public static SORT_OPTIONS = {
code: {label: 'Institute code', dir: 'ASC'},
};
}
export default FaoInstitute;
/*
* Defined in OpenAPI as '#/definitions/FaoInstituteFilter'
*/
import StringFilter from 'model/StringFilter';
import CountryFilter from './CountryFilter';
class FaoInstituteFilter {
public code: string[];
public name: StringFilter;
public country: CountryFilter;
}
export default FaoInstituteFilter;
......@@ -10,6 +10,7 @@ import snackbar from './snackbar';
import filterCode from './filterCode';
import subsets from './subsets';
import accessions from './accessions';
import institutes from './institute';
import applicationConfig from './applicationConfig';
import crop from './crop';
import user from './user';
......@@ -26,6 +27,7 @@ const rootReducer = combineReducers({
form: formReducer,
subsets,
accessions,
institutes,
applicationConfig,
crop,
user,
......
import update from 'immutability-helper';
import { IReducerAction } from 'model/common.model';
import FilteredPage from 'model/FilteredPage';
import {RECEIVE_INSTITUTE, RECEIVE_INSTITUTES} from 'constants/institutes';
import FaoInstitute from 'model/FaoInstitute';
const INITIAL_STATE: {
institute: FaoInstitute;
instituteError: any;
paged: FilteredPage<FaoInstitute>;
pagedError: any;
} = {
institute: null,
instituteError: null,
paged: null,
pagedError: null,
};
function institutes(state = INITIAL_STATE, action: IReducerAction) {
switch (action.type) {
case RECEIVE_INSTITUTE: {
const { institute, error } = action.payload;
return update(state, {
institute: { $set: institute },
instituteError: { $set: error },
});
}
case RECEIVE_INSTITUTES: {
const { paged, error } = action.payload;
return update(state, {
paged: { $set: paged },
pagedError: { $set: error },
});
}
default:
return state;
}
}
export default institutes;
import * as UrlTemplate from 'url-template';
import * as QueryString from 'query-string';
import authenticatedRequest from 'utilities/requestUtils';
import { API_ROOT } from 'constants/apiURLS';
import FilteredPage, { IPageRequest } from 'model/FilteredPage';
import FaoInstitute from 'model/FaoInstitute';
import FaoInstituteFilter from 'model/FaoInstituteFilter';
const URL_LIST = `${API_ROOT}/api/v1/wiews/list`;
const URL_GET = UrlTemplate.parse(`${API_ROOT}/api/v1/wiews/{code}`);
/*
* Defined in OpenAPI as 'wiews'
*/
class InstituteService {
/**
* list at /api/v1/wiews/list
*
* @param authToken Authorization token
* @param filter filter
* @param d d
* @param l l
* @param p p
* @param s s
*/
public static list(authToken: string, filter: string | FaoInstituteFilter, page: IPageRequest): Promise<FilteredPage<FaoInstitute>> {
const qs = QueryString.stringify({
f: typeof filter === 'string' ? filter : undefined,
p: page.page || undefined,
l: page.size || undefined,
d: page.direction && page.direction.length && page.direction || FaoInstitute.DEFAULT_SORT.direction,
s: page.properties || FaoInstitute.DEFAULT_SORT.property,
}, {});
const apiUrl = URL_LIST + (qs ? `?${qs}` : '');
// console.log(`Fetching from ${apiUrl}`);
const content = { data: typeof filter === 'string' ? null : { ...filter } };
return authenticatedRequest(authToken, {
url: apiUrl,
method: 'POST',
headers: {'Content-Type': 'application/json'},
...content,
}).then(({ data }) => data as FilteredPage<FaoInstitute>);
}
/**
* get at /api/v1/wiews/{code}
*
* @param authToken Authorization token
* @param code code
*/
public static get(authToken: string, code: string): Promise<FaoInstitute> {
const apiUrl = URL_GET.expand(code);
// console.log(`Fetching from ${apiUrl}`);
const content = { /* No content in request body */ };
return authenticatedRequest(authToken, {
url: apiUrl,
method: 'GET',
...content,
}).then(({ data }) => data as FaoInstitute);
}
}
export default InstituteService;
......@@ -4,6 +4,7 @@ import Markdown from 'ui/common/markdown';
import Subset from 'model/Subset';
import Accession from 'model/Accession';
import FaoInstitute from '../../model/FaoInstitute';
function SubsetLink({ to: subset, edit = false, children = null }
: { to: Subset, edit?: boolean, children?: any }) {
......@@ -41,4 +42,17 @@ function AccessionLink({ to: accession, edit = false, children = null }
}
}
export { SubsetLink, AccessionLink };
function InstituteLink({ to: institute, edit = false, children = null }
: { to: FaoInstitute, edit?: boolean, children?: any }) {
if (institute) {
return (
<Link to={ `/wiews/${institute.code}` }>
{ children || institute.code }
</Link>
);
} else {
return null;
}
}
export { SubsetLink, AccessionLink, InstituteLink };
import * as React from 'react';
import { withStyles } from '@material-ui/core/styles';
import FaoInstitute from 'model/FaoInstitute';
import {InstituteLink} from 'ui/genesys/Links';
import Card, {CardContent, CardActions} from 'ui/common/Card';
import SciName from 'ui/genesys/SciName';
const styles = (theme) => ({
firstRow: {
marginBottom: '1em',
},
});
const InstituteCard = ({ institute, classes, index, ...other }: { institute: FaoInstitute, classes: any, index?: number } & React.ClassAttributes<any>) => {
return (
<Card>
<CardContent>
<div className={ classes.firstRow }>
<b>
{ index !== undefined && `${index + 1}. ` }
<InstituteLink to={ institute }>
{ institute.code }
{ institute.acronym && ` • ` }
{ institute.acronym &&
< SciName taxa={ institute.acronym } />
}
</InstituteLink>
</b>
{ institute.country && ` • ${institute.country.name}` }
<span className="float-right">
{ `Accessions in Genesys: ${institute.accessionCount}` }
</span>
</div>
<div>
<em>
{ institute.fullName }
</em>
</div>
</CardContent>
{ false &&
<CardActions>
Actions
</CardActions>
}
</Card>
);
};
export default withStyles(styles)(InstituteCard);
......@@ -164,6 +164,7 @@ class Header extends React.Component<IHeaderProps | any, any> {
<div className="navigation-block">
<NavLink activeClassName="active" to="/a">{ t('menu.Accessions') }</NavLink>
<NavLink activeClassName="active" to="/subsets">{ t('menu.Subsets') }</NavLink>
<NavLink activeClassName="active" to="/wiews">{ t('menu.Institutes') }</NavLink>
</div>
</div>
<div>{ this.renderLogin([ROLE_USER, ROLE_ADMINISTRATOR]) }</div>
......
import * as React from 'react';
import {connect} from 'react-redux';
import {bindActionCreators} from 'redux';
import { parse } from 'query-string';
// Actions
import { applyFilters, loadInstitutesPage, listInstitutesPromise, updateRoute } from 'actions/institutes';
// Models
import FaoInstitute from 'model/FaoInstitute';
// UI
import BrowsePageTemplate, { IBrowsePageProps } from 'ui/pages/_base/BrowsePage';
import PageLayout, { PageContents } from 'ui/layout/PageLayout';
import ContentHeader from 'ui/common/heading/ContentHeader';
import Loading from 'ui/common/Loading';
import PagedLoader from 'ui/common/PagedLoader';
import PrettyFilters from 'ui/common/filter/PrettyFilters';
import PaginationComponent from 'ui/common/pagination';
import InstituteCard from 'ui/genesys/institute/InstituteCard';
import InstituteFilters from './с/Filters';
class BrowsePage extends BrowsePageTemplate<FaoInstitute> {
protected static needs = [
({ search, params: { filterCode } }) => {
const qs = parse(search || '');
const page = { direction: qs.d, properties: null };
if (qs.s) {
page.properties = [ ...qs.s ];
}
return applyFilters(filterCode || '', page);
},
];
constructor(props: IBrowsePageProps<FaoInstitute>, context: any) {
super(props, context);
}
public render() {
const { paged } = this.props;
const renderInstitute = (s: FaoInstitute, index: number) => {
return <InstituteCard key={ s.code } index={ index } institute={ s } />;
};
return (
<PageLayout sidebar={
<InstituteFilters initialValues={ paged && paged.filter || {} } onSubmit={ this.myApplyFilters } />
}>
<ContentHeader title="WIEWS Institutes" subTitle="Explore wiews institutes" />
<PaginationComponent
pageObj={ paged }
onChange={ this.onPaginationChange }
displayName="label.institute"
sortOptions={ FaoInstitute.SORT_OPTIONS }
infinite
/>
<PrettyFilters
prefix="wiews"
filterObj={ paged && paged.filter || {} }
onSubmit={ this.myApplyFilters }
/>
<PageContents>
{ ! paged ? <Loading /> :
<PagedLoader
paged={ paged }
loadPage={ this.loadNextPage }
roughItemHeight={ 45 }
itemRenderer={ renderInstitute } />
}
</PageContents>
</PageLayout>
);
}
}
const mapStateToProps = (state, ownProps) => ({
paged: state.institutes.paged || undefined,
filterCode: ownProps.match.params.filterCode,
});
const mapDispatchToProps = (dispatch) => bindActionCreators({
applyFilters,
loadDataPage: loadInstitutesPage,
loadDataPromise: listInstitutesPromise,
updateRoute,
}, dispatch);
export default connect(mapStateToProps, mapDispatchToProps)(BrowsePage);
import * as React from 'react';
import {connect} from 'react-redux';
import {bindActionCreators} from 'redux';
import { translate } from 'react-i18next';
// Actions
import {loadInstitute} from 'actions/institutes';
// Models
import FaoInstitute from 'model/FaoInstitute';
// UI
import PageLayout, {MainSection, PageContents, PageSection} from 'ui/layout/PageLayout';
import ContentHeader from 'ui/common/heading/ContentHeader';
import Loading from 'ui/common/Loading';
import { ScrollToTopOnMount } from 'ui/common/page/scrollers';
import {Properties, PropertiesItem} from 'ui/common/Properties';
import LocationMap from './с/LocationMap';
interface IBrowsePageProps extends React.ClassAttributes<any> {
t: any;
code: string;
doi: string; // DOI comes from the route without the '10.'
institute: FaoInstitute;
error: any;
loadInstitute: any;
}
class BrowsePage extends React.Component<IBrowsePageProps, any> {
constructor(props: IBrowsePageProps, context: any) {
super(props, context);
}
public componentWillMount() {
const { institute, code, loadInstitute } = this.props;
if (code && (! institute || code !== institute.code)) {
loadInstitute({ code });
}
}
public render() {
const { error, institute, code } = this.props;
const stillLoading: boolean = ! error && (! institute
|| (code && institute && institute.code !== code));
return (
<PageLayout>
<ScrollToTopOnMount />
<ContentHeader title="Institute details" subtitle="smt" />
{ stillLoading && <Loading /> }
{ error && <div>{ JSON.stringify(error) }</div> }
{ institute &&
<PageContents>
<MainSection title={ `${institute.fullName}` }>
<Properties>
<PropertiesItem title="Institute code">{ institute.code }</PropertiesItem>
<PropertiesItem title="Type">{ institute.type }</PropertiesItem>
<PropertiesItem title="Country">{ institute.country.name }</PropertiesItem>
{ institute.url &&
<PropertiesItem title="Web link">
<a href={ institute.url }> { institute.url }</a>
</PropertiesItem>
}
<PropertiesItem title="Accessions in Genesys">{ institute.accessionCount }</PropertiesItem>
</Properties>
</MainSection>
{ institute.latitude !== null && institute.longitude !== null && <PageSection title="Location">
<Properties>
<PropertiesItem title="latitude">{ institute.latitude }</PropertiesItem>
<PropertiesItem title="longitude">{ institute.longitude }</PropertiesItem>
</Properties>
<div className="p-20">
<LocationMap institute={ institute } classes={ {} }/>
</div>
</PageSection> }
</PageContents>
}
</PageLayout>
);
}
}
const mapStateToProps = (state, ownProps) => ({
institute: state.institutes.institute,
error: state.institutes.instituteError,
code: ownProps.match.params.wiewsCode,
});
const mapDispatchToProps = (dispatch) => bindActionCreators({
loadInstitute,
}, dispatch);
export default connect(mapStateToProps, mapDispatchToProps)(translate()(BrowsePage));
import * as React from 'react';
import { reduxForm } from 'redux-form';
import { INSTITUTE_FILTERFORM } from 'constants/institutes';
import FiltersBlock from 'ui/common/filter/FiltersBlock';