Commit 7568e4ad authored by Matija Obreza's avatar Matija Obreza
Browse files

Merge branch '244-network-routes' into 'master'

Resolve "Network routes"

Closes #244

See merge request genesys-pgr/genesys-ui!253
parents 109ac88d 57367762
...@@ -54,6 +54,7 @@ ...@@ -54,6 +54,7 @@
}, },
"holder": { "holder": {
"code": "Holder", "code": "Holder",
"networks": "Network",
"owner": { "owner": {
"uuid": "Data provider" "uuid": "Data provider"
}, },
...@@ -1751,6 +1752,32 @@ ...@@ -1751,6 +1752,32 @@
"menu": "My List" "menu": "My List"
} }
}, },
"networks": {
"public": {
"c": {
"card": {
"title": "Title",
"slug": "Short name",
"createdDate": "Date of creation"
}
},
"p": {
"display": {
"actions": "Browse or download data",
"title": "Network details",
"browseAccessions": "Browse accessions",
"overview": "Accession overview",
"deleteNetwork": "Delete network {{slug}}?"
},
"networkBrowse": {
"title": "Browse networks"
}
}
},
"common": {
"locations": "Members locations"
}
},
"partners": { "partners": {
"admin": { "admin": {
"c": { "c": {
......
...@@ -20,7 +20,7 @@ const ArticleFilters = ({ handleSubmit, initialValues, initialize, t, ...other } ...@@ -20,7 +20,7 @@ const ArticleFilters = ({ handleSubmit, initialValues, initialize, t, ...other }
<StringFilter name="title" label={ t('cms.admin.f.title') } searchType="contains"/> <StringFilter name="title" label={ t('cms.admin.f.title') } searchType="contains"/>
</CollapsibleComponentSearch> </CollapsibleComponentSearch>
<CollapsibleComponentSearch title={ t('cms.admin.f.meta') }> <CollapsibleComponentSearch title={ t('cms.admin.f.meta') }>
<StringArrFilter name="slug" label="cms.admin.f.slug" placeholder="blurp"/> <StringArrFilter name="slug" label="cms.admin.f.slug" placeholder="blurb"/>
<StringArrFilter name="lang" label="cms.admin.f.lang" placeholder="en"/> <StringArrFilter name="lang" label="cms.admin.f.lang" placeholder="en"/>
</CollapsibleComponentSearch> </CollapsibleComponentSearch>
</FiltersBlock> </FiltersBlock>
......
...@@ -111,4 +111,5 @@ export interface IGeoPoint { ...@@ -111,4 +111,5 @@ export interface IGeoPoint {
id?: any; id?: any;
lat: number; lat: number;
lng: number; lng: number;
popup?: any;
} }
import { Permissions, IUserPermissions } from 'model/acl/ACL';
/*
* Defined in Swagger as '#/definitions/PGRFANetwork'
*/
class PGRFANetwork implements IUserPermissions {
public static clazz: string = 'org.genesys2.server.model.impl.PGRFANetwork';
public _permissions: Permissions;
public active: boolean;
public createdBy: number;
public createdDate: Date;
public id: number;
public lastModifiedBy: number;
public lastModifiedDate: Date;
public slug: string;
public title: string;
public version: number;
}
export default PGRFANetwork;
import Article from 'model/cms/Article';
import PGRFANetwork from 'model/network/PGRFANetwork';
import Page from 'model/Page';
import FaoInstitute from 'model/genesys/FaoInstitute';
/*
* Defined in Swagger as '#/definitions/PGRFANetworkDetails'
*/
class PGRFANetworkDetails {
public blurb: Article;
public institutes: Page<FaoInstitute>;
public network: PGRFANetwork;
}
export default PGRFANetworkDetails;
// actions
// constants
// service
import NetworkService from 'service/genesys/NetworkService';
import PGRFANetwork from 'model/network/PGRFANetwork';
export const deleteNetwork = (network: PGRFANetwork) => (dispatch, getState) => {
return NetworkService.deleteNetwork(network.slug, network.version);
};
// actions
import {createApiCaller} from 'actions/ApiCall';
// constants
import {APPEND_NETWORK_PAGE, RECEIVE_NETWORK_DETAILS, RECEIVE_NETWORK_INSTITUTES} from 'networks/constants';
// service
import NetworkService from 'service/genesys/NetworkService';
// Wrapped API Calls
const apiListNetworks = createApiCaller(NetworkService.listNetworks, APPEND_NETWORK_PAGE);
const apiLoadNetworkDetails = createApiCaller(NetworkService.getNetworkDetails, RECEIVE_NETWORK_DETAILS);
const apiLoadMoreNetworkInstitutes = createApiCaller(NetworkService.getNetworkInstitutes, RECEIVE_NETWORK_INSTITUTES);
export const listNetworks = (page: number) => (dispatch, getState) => {
return dispatch(apiListNetworks(page));
};
export const loadNetwork = (shortName: string, lang) => (dispatch) => {
return dispatch(apiLoadNetworkDetails(shortName, lang));
};
export const loadMoreNetworkInstitutes = (shortName: string, page: number = 1) => (dispatch) => {
return dispatch(apiLoadMoreNetworkInstitutes(shortName, page));
};
export const APPEND_NETWORK_PAGE = 'networks/APPEND_NETWORK_PAGE';
export const RECEIVE_NETWORK_DETAILS = 'networks/RECEIVE_NETWORK_DETAILS';
export const RECEIVE_NETWORK_INSTITUTES = 'networks/RECEIVE_NETWORK_INSTITUTES';
import { combineReducers } from 'redux';
import publicNetworks from './public';
const rootReducer = combineReducers({
public: publicNetworks,
});
export default rootReducer;
import update from 'immutability-helper';
// model
import { IReducerAction } from 'model/common.model';
import Page from 'model/Page';
import ApiCall from 'model/ApiCall';
import PGRFANetwork from 'model/network/PGRFANetwork';
import PGRFANetworkDetails from 'model/network/PGRFANetworkDetails';
// constants
import {APPEND_NETWORK_PAGE, RECEIVE_NETWORK_DETAILS, RECEIVE_NETWORK_INSTITUTES} from 'networks/constants';
const INITIAL_STATE: {
networks: ApiCall<PGRFANetwork[]>,
network: ApiCall<PGRFANetworkDetails>,
} = {
networks: null,
network: null,
};
export default function network(state = INITIAL_STATE, action: IReducerAction = { type: '' }) {
switch (action.type) {
// set the currentPartner to whatever came in
case RECEIVE_NETWORK_DETAILS: {
const {apiCall} = action.payload;
return update(state, {
network: {$set: apiCall},
});
}
// set the paged to whatever came in
case APPEND_NETWORK_PAGE: {
const {apiCall} = action.payload;
return update(state, {
networks: {$set: apiCall},
});
}
case RECEIVE_NETWORK_INSTITUTES: {
const {apiCall} = action.payload;
return update(state, {
network: {
data: {
institutes: { $set: Page.merge(state.network && state.network.data && state.network.data.institutes, apiCall.data) },
},
},
});
}
default:
return state;
}
}
import Loadable from 'utilities/CustomReactLoadable';
const publicRoutes = [
// Root
{
path: '/network',
component: Loadable({
loader: () => import(/* webpackMode:"lazy", webpackChunkName: "networks" */'networks/ui/BrowsePage'),
}),
exact: true,
},
{
path: '/network/:shortName',
component: Loadable({
loader: () => import(/* webpackMode:"lazy", webpackChunkName: "networks" */'networks/ui/DisplayPage'),
}),
exact: true,
},
];
export { publicRoutes as networksPublicRoutes };
{
"public": {
"c": {
"card": {
"title": "Title",
"slug": "Short name",
"createdDate": "Date of creation"
}
},
"p": {
"display": {
"actions": "Browse or download data",
"title": "Network details",
"browseAccessions": "Browse accessions",
"overview": "Accession overview",
"deleteNetwork": "Delete network {{slug}}?"
},
"networkBrowse": {
"title": "Browse networks"
}
}
},
"common": {
"locations": "Members locations"
}
}
import * as React from 'react';
import { connect } from 'react-redux';
import { bindActionCreators } from 'redux';
import { translate } from 'react-i18next';
// actions
import {listNetworks} from 'networks/actions/public';
// model
import ApiCall from 'model/ApiCall';
import PGRFANetwork from 'model/network/PGRFANetwork';
// ui
import PageLayout, {PageContents} from 'ui/layout/PageLayout';
import ContentHeader from 'ui/common/heading/ContentHeader';
import PageTitle from 'ui/common/PageTitle';
import Loading from 'ui/common/Loading';
import NetworkCard from 'networks/ui/c/NetworkCard';
interface IBrowsePageProps extends React.ClassAttributes<any> {
apiCall: ApiCall<PGRFANetwork[]>;
listNetworks: () => void;
t: any;
}
class BrowsePage extends React.Component<IBrowsePageProps> {
public componentWillMount() {
const { listNetworks, apiCall } = this.props;
const {data: networks, loading} = apiCall || {data: undefined, loading: false};
if (typeof window !== 'undefined' && !networks && !loading) {
listNetworks();
}
}
public render() {
const {apiCall, t} = this.props;
const {data: networks, loading} = apiCall || {data: undefined, loading: true};
return (
<PageLayout>
<PageTitle title={ t(`networks.public.p.networkBrowse.title`) }/>
<ContentHeader
title={ t(`networks.public.p.networkBrowse.title`) }
/>
<PageContents className="pt-1rem">
{ loading ? <Loading /> :
<div className="full-width container-spacing-horizontal">
{
networks && networks.sort((a, b) => a.title.localeCompare(b.title)).map((network: PGRFANetwork) => (
<NetworkCard compact key={ network.slug } network={ network }/>
))
}
</div>
}
</PageContents>
</PageLayout>
);
}
}
const mapStateToProps = (state, ownProps) => ({
apiCall: state.networks.public.networks,
});
const mapDispatchToProps = (dispatch) => bindActionCreators({
listNetworks,
}, dispatch);
export default translate()((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 {loadMoreNetworkInstitutes, loadNetwork} from 'networks/actions/public';
import { deleteNetwork } from 'networks/actions/admin';
import { applyFilters, applyOverviewFilters } from 'accessions/actions/public';
import { navigateTo } from 'actions/navigation';
// model
import ApiCall from 'model/ApiCall';
import MapLayer from 'model/genesys/MapTileLayer';
import Page from 'model/Page';
import FaoInstitute from 'model/genesys/FaoInstitute';
import PGRFANetwork from 'model/network/PGRFANetwork';
import PGRFANetworkDetails from 'model/network/PGRFANetworkDetails';
// ui
import InstituteCard from 'institutes/ui/с/InstituteCard';
import PageLayout, {PageContents, PageSection, MainSection} from 'ui/layout/PageLayout';
import Loading from 'ui/common/Loading';
import {ScrollToTopOnMount} from 'ui/common/page/scrollers';
import ContentHeader from 'ui/common/heading/ContentHeader';
import PageTitle from 'ui/common/PageTitle';
import LocationMap from 'ui/common/LocationMap';
import PagedLoader from 'ui/common/PagedLoader';
import {InstituteLink} from 'ui/genesys/Links';
import { PropertiesItem, Properties } from 'ui/common/Properties';
import BlurbText from 'cms/ui/c/BlurbText';
import PrettyDate from 'ui/common/time/PrettyDate';
import { CardActions } from 'ui/common/Card';
import ButtonBar from 'ui/common/buttons/ButtonBar';
import { Button } from '@material-ui/core';
import confirmAlert from 'utilities/confirmAlert';
import Authorize from 'ui/common/authorized/Authorize';
interface IDisplayPageProps extends React.ClassAttributes<any> {
networkCall: ApiCall<PGRFANetworkDetails>;
mapLayers: MapLayer[];
shortName: string;
loadNetwork: (shortName: string, lang: string) => void;
loadMoreNetworkInstitutes: (shortName: string, page: number) => void;
applyFilters: any;
applyOverviewFilters: any;
deleteNetwork: (network: PGRFANetwork) => any;
navigateTo: any;
t: any;
i18n: any;
}
class DisplayPage extends React.Component<IDisplayPageProps> {
protected static needs = [
({ params: { shortName }, state }) => loadNetwork(shortName, state.applicationConfig.lang),
];
public componentWillMount(): void {
const {networkCall, shortName, loadNetwork, i18n} = this.props;
if (!networkCall) {
return loadNetwork(shortName, i18n.language);
}
const {loading, data: network} = networkCall;
if (!loading && (!network || !network.network)) {
return loadNetwork(shortName, i18n.language);
}
if (network.network.slug !== shortName) {
return loadNetwork(shortName, i18n.language);
}
}
private loadMoreNetworkMembers = (page: Page<FaoInstitute>) => {
const {shortName, loadMoreNetworkInstitutes} = this.props;
return loadMoreNetworkInstitutes(shortName, page.number + 1);
}
private renderInstitute = (s: FaoInstitute, index: number) => {
return <InstituteCard key={ s.code } index={ index } institute={ s }/>;
}
private applyNetworkFilter = () => {
const { networkCall: { data: networkDetails }, applyFilters} = this.props;
const filter = {holder: {networks: [ networkDetails.network.slug ]}};
applyFilters(filter);
}
private applyNetworkOverviewFilter = () => {
const { networkCall: { data: networkDetails }, applyOverviewFilters} = this.props;
const filter = {holder: {networks: [ networkDetails.network.slug ]}};
applyOverviewFilters(filter);
}
private onDelete = () => {
const { networkCall: { data: networkDetails }, deleteNetwork, navigateTo, t } = this.props;
confirmAlert(t('networks.public.p.display.deleteNetwork', { slug: networkDetails.network.slug }))
.then(() => {
deleteNetwork(networkDetails.network).then(() => {
navigateTo('/network');
});
}).catch((error) => {
console.log(`Couldn't delete network`, error);
});
}
public render(): React.ReactNode {
const {networkCall, mapLayers, t} = this.props;
const {data: networkDetails, loading, error} = networkCall || {data: undefined, loading: true, error: undefined};
const {network, blurb, institutes } = networkDetails || {network: undefined, blurb: undefined, institutes: undefined};
return (
<PageLayout withFooter>
<ScrollToTopOnMount/>
<PageTitle title={ !loading && network ? network.title || network.slug : t('common:label.loading', {what: t('networks.public.p.display.title')}) }/>
<ContentHeader title={ !loading && network ? network.title || network.slug : t('common:label.loading', {what: t('networks.public.p.display.title')}) }/>
<div>
{ loading && <Loading/> }
{ error && <div>{ JSON.stringify(error) }</div> }
{ network &&
<PageContents className="pt-1rem">
<MainSection title={ t('networks.public.p.display.title') }>
{ blurb &&
<div className="mb-20">
<BlurbText body={ blurb.body } />
</div>
}
<Properties>
<PropertiesItem title={ t('networks.public.c.card.title') }>{ network.title }</PropertiesItem>
<PropertiesItem title={ t('networks.public.c.card.slug') }>{ network.slug }</PropertiesItem>
<PropertiesItem title={ t('networks.public.c.card.createdDate') }><PrettyDate value={ network.createdDate }/></PropertiesItem>
</Properties>
<CardActions className="container-spacing-vertical mt-15">
<ButtonBar barLabelText={ t('networks.public.p.display.actions') }>
<Button onClick={ this.applyNetworkFilter }>{ t('networks.public.p.display.browseAccessions') }</Button>
<Button onClick={ this.applyNetworkOverviewFilter }>{ t('networks.public.p.display.overview') }</Button>
<Authorize role="ROLE_ADMINISTRATOR">
<Button onClick={ this.onDelete }>{ t('common:action.delete') }</Button>
</Authorize>
</ButtonBar>
</CardActions>
</MainSection>
{ institutes && institutes.content && institutes.content.length > 0 &&
<PageSection title={ t('networks.common.locations') }>
<LocationMap
locations={ institutes.content.map(
(institute) => ({id: institute.id, lat: institute.latitude, lng: institute.longitude, popup: <InstituteLink key={ institute.code } to={ institute }>{ institute.fullName }</InstituteLink>}))
}
mapLayers={ mapLayers }
/>
</PageSection>
}
<div className="container-spacing-horizontal pt-1rem">
<PagedLoader paged={ institutes } itemRenderer={ this.renderInstitute } loadMore={ this.loadMoreNetworkMembers }/>
</div>
</PageContents>
}
</div>
</PageLayout>
);
}
}
const mapStateToProps = (state, ownProps) => ({
networkCall: state.networks.public.network,
memberCodes: state.networks.public.memberCodes,
blurb: state.networks.public.blurb,
members: state.networks.public.members,
shortName: ownProps.match.params.shortName,
mapLayers: state.accessions.public.mapLayers,
});
const mapDispatchToProps = (dispatch) => bindActionCreators({
loadNetwork,
loadMoreNetworkInstitutes,
deleteNetwork,
applyOverviewFilters,
applyFilters,
navigateTo,
}, dispatch);
export default translate()((connect(mapStateToProps, mapDispatchToProps)(DisplayPage)));
import * as React from 'react';
import { translate } from 'react-i18next';
import { withStyles } from '@material-ui/core/styles';
// model
import PGRFANetwork from 'model/network/PGRFANetwork';
// ui
import PrettyDate from 'ui/common/time/PrettyDate';
import Card, { CardHeader } from 'ui/common/Card';
import { NetworkLink } from 'ui/catalog/Links';
// import { Properties, PropertiesItem } from 'ui/catalog/Properties';
interface INetworkCardProps extends React.ClassAttributes<any> {
network: PGRFANetwork;
classes: any;
t: any;
}
const styles = (theme) => ({
/* tslint:disable */
leftBorder: {
borderLeft: 'solid 1px #eee',
padding: '10px 5px 20px 20px',
'& > h1': {
fontSize: '30px',
marginTop: '3px',
color: '#006CB4',
},
[theme.breakpoints.down('sm')]: {
padding: '7px 5px 7px 10px',
},
},
/* tslint:enable */
italic: {
fontSize: '1rem',
fontStyle: 'italic',
color: '#4d4c46',
},
margin: {
marginLeft: '2px',
},
grayBackgroundWrapper: {
flexWrap: 'nowrap',
marginBottom: '20px',
[theme.breakpoints.down('sm')]: {
flexWrap: 'wrap',
},
},
titleCard: {
marginBottom: '20px',
[theme.breakpoints.down('sm')]: {
fontSize: '20px',
lineHeight: '25px',
},
},
});
class NetworkCard extends React.Component<INetworkCardProps, any> {
public render() {
const {network, classes, t} = this.props;
if (!network) {
return null;
}
return (
<Card square>
<CardHeader
classes={ {