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

Network routes

added map popup, blurb, ssr fixes

Changes related backend changes
parent 109ac88d
......@@ -111,4 +111,5 @@ export interface IGeoPoint {
id?: any;
lat: number;
lng: number;
popup?: any;
}
/*
* Defined in Swagger as '#/definitions/Organization'
*/
class Organization {
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 Organization;
/*
* Defined in Swagger as '#/definitions/OrganizationBlurbJson'
*/
class OrganizationBlurbJson {
public blurp: string;
public locale: string;
public summary: string;
}
export default OrganizationBlurbJson;
import Article from 'model/cms/Article';
import Organization from 'model/network/Organization';
import Page from 'model/Page';
import FaoInstitute from 'model/genesys/FaoInstitute';
/*
* Defined in Swagger as '#/definitions/OrganizationDetails'
*/
class OrganizationDetails {
public blurb: Article;
public institutes: Page<FaoInstitute>;
public organization: Organization;
}
export default OrganizationDetails;
// 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 Organization from 'model/network/Organization';
import OrganizationDetails from 'model/network/OrganizationDetails';
// constants
import {APPEND_NETWORK_PAGE, RECEIVE_NETWORK_DETAILS, RECEIVE_NETWORK_INSTITUTES} from 'networks/constants';
const INITIAL_STATE: {
networks: ApiCall<Organization[]>,
network: ApiCall<OrganizationDetails>,
} = {
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":{
"title": "Network details"
},
"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 Organization from 'model/network/Organization';
// 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<Organization[]>;
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: Organization) => (
<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';
// model
import ApiCall from 'model/ApiCall';
import MapLayer from 'model/genesys/MapTileLayer';
import Page from 'model/Page';
import FaoInstitute from 'model/genesys/FaoInstitute';
import OrganizationDetails from 'model/network/OrganizationDetails';
// ui
import NetworkCard from 'networks/ui/c/NetworkCard';
import InstituteCard from 'institutes/ui/с/InstituteCard';
import PageLayout, {PageContents, PageSection} 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';
interface IDisplayPageProps extends React.ClassAttributes<any> {
apiCall: ApiCall<OrganizationDetails>;
mapLayers: MapLayer[];
shortName: string;
loadNetwork: (shortName: string, lang: string) => void;
loadMoreNetworkInstitutes: (shortName: string, page: number) => void;
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 {apiCall, shortName, loadNetwork, i18n} = this.props;
if (!apiCall) {
return loadNetwork(shortName, i18n.language);
}
const {loading, data: network} = apiCall;
if (!loading && (!network || !network.organization)) {
return loadNetwork(shortName, i18n.language);
}
if (network.organization.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 }/>;
}
public render(): React.ReactNode {
const {apiCall, mapLayers, t} = this.props;
const {data: networkDetails, loading, error} = apiCall || {data: undefined, loading: true, error: undefined};
const {organization: network, blurb, institutes } = networkDetails || {organization: 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">
<NetworkCard key={ network.slug } network={ network } blurbBody={ blurb.body }/>
{ 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 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) => ({
apiCall: 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,
}, 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 Organization from 'model/network/Organization';
// ui
import PrettyDate from 'ui/common/time/PrettyDate';
import {Properties, PropertiesItem} from 'ui/common/Properties';
import Card, {CardHeader} from 'ui/common/Card';
import {NetworkLink} from 'ui/catalog/Links';
import {MainSection} from 'ui/layout/PageLayout';
import BlurbText from 'cms/ui/c/BlurbText';
// import { Properties, PropertiesItem } from 'ui/catalog/Properties';
interface INetworkCardProps extends React.ClassAttributes<any> {
network: Organization;
blurbBody: string;
compact: boolean;
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, blurbBody, compact = false, classes, t} = this.props;
if (!network) {
return null;
}
return compact ? (
<Card square>
<CardHeader
classes={ {
title: `${classes.titleCard}`,
subheader: classes.italic,
} }
title={ <NetworkLink to={ network }/> as any }
subheader={ network.createdDate && <span>{ t('common:label.registered') } <PrettyDate value={ network.createdDate }/></span> }
/>
</Card>
) : (
<MainSection title={ t('networks.public.p.display.title') }>
{ blurbBody &&
<div className="mb-20">
<BlurbText body={ blurbBody } />
</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>
</MainSection>
);
}
}
export default translate()((withStyles as any)(styles)(NetworkCard));
......@@ -19,6 +19,7 @@ import crop from 'crop/reducers';
import user from 'user/reducers';
import cms from 'cms/reducers';
import list from 'list/reducers';
import networks from 'networks/reducers';
import requests from 'requests/reducers';
import repository from 'repository/reducers';
import geo from 'geo/reducers';
......@@ -59,6 +60,7 @@ const rootReducer = combineReducers({
descriptors,
descriptorList,
partner,
networks,
subsets,
accessions,
......
import * as UrlTemplate from 'url-template';
import * as QueryString from 'query-string';
import { axiosBackend } from 'utilities/requestUtils';
import Article from 'model/cms/Article';
import FaoInstitute from 'model/genesys/FaoInstitute';
import Organization from 'model/network/Organization';
import OrganizationBlurbJson from 'model/network/OrganizationBlurbJson';
import OrganizationDetails from 'model/network/OrganizationDetails';
import Page from 'model/Page';
const URL_LIST_NETWORKS = `/api/v1/network`;
const URL_UPDATE_NETWORK = `/api/v1/network`;
const URL_GET_NETWORK = UrlTemplate.parse(`/api/v1/network/{shortName}`);
const URL_DELETE_NETWORK = UrlTemplate.parse(`/api/v1/network/{shortName}`);
const URL_UPDATE_BLURB = UrlTemplate.parse(`/api/v1/network/{shortName}/blurb`);
const URL_GET_BLURB = UrlTemplate.parse(`/api/v1/network/{shortName}/blurb/{language}`);
const URL_GET_NETWORK_DETAILS = UrlTemplate.parse(`/api/v1/network/{shortName}/details`);
const URL_GET_NETWORK_INSTITUTES = UrlTemplate.parse(`/api/v1/network/{shortName}/institutes`);
const URL_GET_NETWORK_INSTITUTES_CODES = UrlTemplate.parse(`/api/v1/network/{shortName}/institutes/codes`);
const URL_ADD_NETWORK_INSTITUTES = UrlTemplate.parse(`/api/v1/network/{slug}/add-institutes`);
const URL_SET_NETWORK_INSTITUTES = UrlTemplate.parse(`/api/v1/network/{slug}/set-institutes`);
/*
* Defined in Swagger as 'network'
*/
class NetworkService {
/**
* listNetworks at /api/v1/network
*
* @param page page
* @param xhrConfig additional xhr config
*/
public static listNetworks(page?: number, xhrConfig?: any): Promise<Organization[]> {
const qs = QueryString.stringify({
page: page || undefined,
}, {});
const apiUrl = URL_LIST_NETWORKS + (qs ? `?${qs}` : '');
// console.log(`Fetching from ${apiUrl}`);
const content = { /* No content in request body */ };
return axiosBackend.request({
...xhrConfig,
url: apiUrl,
method: 'GET',
...content,
}).then(({ data }) => data as Organization[]);
}
/**
* updateNetwork at /api/v1/network
*
* @param organizationJson organizationJson
* @param xhrConfig additional xhr config
*/
public static updateNetwork(organizationJson: Organization, xhrConfig?: any): Promise<Organization> {
const apiUrl = URL_UPDATE_NETWORK;
// console.log(`Fetching from ${apiUrl}`);
const content = { data: organizationJson };
return axiosBackend.request({
...xhrConfig,
url: apiUrl,
method: 'POST',
...content,
}).then(({ data }) => data as Organization);
}
/**
* getNetwork at /api/v1/network/{shortName}
*
* @param shortName shortName
* @param xhrConfig additional xhr config
*/
public static getNetwork(shortName: string, xhrConfig?: any): Promise<Organization> {