Commit 8c2204de authored by Viacheslav Pavlov's avatar Viacheslav Pavlov
Browse files

Merge branch '238-news-list' into 'master'

News list

Closes #238

See merge request genesys-pgr/genesys-ui!244
parents 7ed6e0b2 0509df8d
......@@ -7,6 +7,7 @@
"Bookmark this page": "Bookmark this page",
"Contribute to Genesys": "Contribute to Genesys",
"Subscribe to Newsletter": "Subscribe to Newsletter",
"newsList": "News list",
"search": {
"placeholder": "Search Genesys...",
"suggestion0": "Try 'maize' or 'wild triticum'",
......@@ -837,8 +838,7 @@
"article": "Article"
},
"browseActivityPost": {
"title": "Browse activity posts",
"activityPost": "Activity post"
"title": "Browse activity posts"
},
"edit": {
"articleSaved": "Article saved",
......@@ -887,7 +887,15 @@
},
"common": {
"articleList": "Article list",
"activityPostList": "Activity post list"
"activityPostList": "Activity post list",
"activityPost": "Activity post",
"activityPost_plural": "Activity posts"
},
"sort": {
"createdDateAsc": "Created date (old to new)",
"createdDateDesc": "Created date (new to old)",
"lastModifiedDateAsc": "Last modified date (old to new)",
"lastModifiedDateDesc": "Last modified date (new to old)"
}
}
......
......@@ -72,7 +72,11 @@ export const applyFilters = (filters: ArticleFilter, page: IPageRequest = { page
export const activityPostApplyFilters = (filters: ActivityPostFilter, page: IPageRequest = { page: 0 }) => (dispatch) => {
console.log('Applying new filter', filters);
dispatch(showSnackbar('Applying filters...'));
return dispatch(apiListActivityPosts(filters, page))
return dispatch(apiListActivityPosts(filters, {
direction: ActivityPost.DEFAULT_SORT.direction,
properties: ActivityPost.DEFAULT_SORT.property,
...page,
}))
.then((paged) => {
dispatch(activityPostUpdateRoute(paged));
dispatch(showSnackbar(`Filters applied.`));
......
// actions
import {createApiCaller, createPureApiCaller} from 'actions/ApiCall';
import { showSnackbar } from 'actions/snackbar';
import navigateTo from 'actions/navigation';
// constants
import { RECEIVE_ARTICLE, RECEIVE_DOCUMENTATION, RECEIVE_MENU, RECEIVE_LAST_NEWS, RECEIVE_POST } from 'cms/constants';
import {
RECEIVE_ARTICLE,
RECEIVE_DOCUMENTATION,
RECEIVE_MENU,
RECEIVE_LAST_NEWS,
RECEIVE_POST,
APPEND_ACTIVITY_POSTS,
} from 'cms/constants';
// model
import ADoc from 'model/cms/ADoc';
import Page from 'model/Page';
import ActivityPost from 'model/cms/ActivityPost';
import FilteredPage, { IPageRequest } from 'model/FilteredPage';
import ActivityPostFilter from 'model/cms/ActivityPostFilter';
// service
import CmsService from 'service/genesys/CmsService';
import AsciiDocService from 'service/genesys/AsciiDocService';
......@@ -16,6 +29,7 @@ const apiLoadMenu = createApiCaller(CmsService.getMenu, RECEIVE_MENU);
const apiGetLastNews = createApiCaller(CmsService.getLastNews, RECEIVE_LAST_NEWS);
const apiGetActivityPost = createApiCaller(CmsService.getActivityPost, RECEIVE_POST);
const apiViewDoc = createPureApiCaller(AsciiDocService.viewDoc);
const apiListActivityPosts = createApiCaller(CmsService.listActivityPosts, APPEND_ACTIVITY_POSTS);
export const loadArticle = (locale: string, slug: string) => (dispatch) => {
return dispatch(apiGetArticleBySlugAndLang(locale, slug));
......@@ -46,3 +60,31 @@ export const loadLastNews = () => (dispatch) => {
export const loadActivityPostById = (id: number) => (dispatch) => {
return dispatch(apiGetActivityPost(id));
};
export const activityPostUpdateRoute = (paged: FilteredPage<ActivityPost>) => (dispatch) => {
const qs = {
s: paged.sort[0].property === ActivityPost.DEFAULT_SORT.property ? undefined : paged.sort[0].property,
d: paged.sort[0].direction === ActivityPost.DEFAULT_SORT.direction ? undefined : paged.sort[0].direction,
};
dispatch(navigateTo(paged.filterCode ? `/content/news/${paged.filterCode}` : '/content/news', qs));
};
export const activityPostApplyFilters = (filters: ActivityPostFilter, page: IPageRequest = { page: 0 }) => (dispatch) => {
dispatch(showSnackbar('Applying filters...'));
return dispatch(apiListActivityPosts(filters, {
direction: ActivityPost.DEFAULT_SORT.direction,
properties: ActivityPost.DEFAULT_SORT.property,
...page,
}))
.then((paged) => {
dispatch(activityPostUpdateRoute(paged));
dispatch(showSnackbar(`Filters applied.`));
});
};
export const loadMoreActivityPosts = (paged?: FilteredPage<ActivityPost>) => (dispatch) => {
return dispatch(apiListActivityPosts(paged && paged.filterCode, Page.nextPage(paged)))
.then((paged) => {
return dispatch(activityPostUpdateRoute(paged));
});
};
......@@ -3,6 +3,7 @@ export const RECEIVE_DOCUMENTATION = 'cms/public/RECEIVE_DOCUMENTATION';
export const RECEIVE_MENU = 'cms/public/RECEIVE_MENU';
export const RECEIVE_LAST_NEWS = 'cms/public/RECEIVE_LAST_NEWS';
export const RECEIVE_POST = 'cms/public/RECEIVE_POST';
export const APPEND_ACTIVITY_POSTS = 'cms/public/APPEND_ACTIVITY_POSTS';
// admin
export const ADMIN_RECEIVE_ARTICLE = 'cms/admin/RECEIVE_ARTICLE';
......
import { RECEIVE_ARTICLE, RECEIVE_DOCUMENTATION, RECEIVE_MENU, RECEIVE_LAST_NEWS, RECEIVE_POST } from 'cms/constants';
import {
RECEIVE_ARTICLE,
RECEIVE_DOCUMENTATION,
RECEIVE_MENU,
RECEIVE_LAST_NEWS,
RECEIVE_POST,
APPEND_ACTIVITY_POSTS,
} from 'cms/constants';
import update from 'immutability-helper';
import ApiCall from 'model/ApiCall';
import FilteredPage from 'model/FilteredPage';
import ActivityPost from 'model/cms/ActivityPost';
const INITIAL_STATE: {
articles: any,
......@@ -8,20 +17,24 @@ const INITIAL_STATE: {
loadingSlug: string,
apiError: any,
menus: any,
lastNews: ApiCall<any>,
news: any,
activityPost: {
paged: ApiCall<FilteredPage<ActivityPost>>,
posts: any,
lastNews: ApiCall<any>,
},
} = {
articles: {},
documentations: {},
menus: {},
loadingSlug: null,
apiError: null,
lastNews: null,
news: {},
activityPost: {
paged: null,
posts: [],
lastNews: null,
},
};
/* state will be changes */
export default function listPublic(state = INITIAL_STATE, action: { type?: string, payload?: any } = {type: '', payload: {}}) {
switch (action.type) {
......@@ -68,19 +81,38 @@ export default function listPublic(state = INITIAL_STATE, action: { type?: strin
case RECEIVE_LAST_NEWS: {
const {apiCall} = action.payload;
return update(state, {
lastNews: {$set: apiCall},
activityPost: {
lastNews: {$set: apiCall},
},
});
}
case RECEIVE_POST: {
const {apiCall} = action.payload;
return apiCall.data ? update(state, {
news: {
[apiCall.data.id]: {$set: apiCall.data},
activityPost: {
posts: {
[apiCall.data.id]: {$set: apiCall.data},
},
},
loadingSlug: {$set: apiCall.loading},
apiError: {$set: apiCall.error},
}) : state;
}
case APPEND_ACTIVITY_POSTS: {
const {apiCall: {loading, error, timestamp, data}} = action.payload;
return update(state, {
activityPost: {
paged: {
$set: {
loading,
error,
timestamp,
data: FilteredPage.merge(state.activityPost.paged && state.activityPost.paged.data, data),
},
},
},
});
}
default:
return state;
}
......
......@@ -6,6 +6,10 @@ const publicRoutes = [
path: '/content/news/:id/:slug',
component: Loadable({ loader: () => import(/* webpackMode:"lazy", webpackChunkName: "cms" */'cms/ui/ActivityPostDisplayPage') }),
},
{
path: '/content/news/:filterCode(v.+)?',
component: Loadable({ loader: () => import(/* webpackMode:"lazy", webpackChunkName: "cms" */'cms/ui/NewsBrowsePage') }),
},
{
path: '/content/:menuKey/:slug',
component: Loadable({ loader: () => import(/* webpackMode:"lazy", webpackChunkName: "cms" */'cms/ui/ContentPage') }),
......
......@@ -29,8 +29,7 @@
"article": "Article"
},
"browseActivityPost": {
"title": "Browse activity posts",
"activityPost": "Activity post"
"title": "Browse activity posts"
},
"edit": {
"articleSaved": "Article saved",
......@@ -79,6 +78,14 @@
},
"common": {
"articleList": "Article list",
"activityPostList": "Activity post list"
"activityPostList": "Activity post list",
"activityPost": "Activity post",
"activityPost_plural": "Activity posts"
},
"sort": {
"createdDateAsc": "Created date (old to new)",
"createdDateDesc": "Created date (new to old)",
"lastModifiedDateAsc": "Last modified date (old to new)",
"lastModifiedDateDesc": "Last modified date (new to old)"
}
}
......@@ -58,18 +58,16 @@ class ActivityPostDisplayPage extends React.Component<IActivityPostDisplayPage>
public render() {
const { lastNews, activityPost } = this.props;
const title = activityPost && activityPost.title.match(/>(.+)</)[1];
const title = activityPost &&
activityPost.title.split(/<.*?>/).filter((el) => !!el).join('');
const lastNewsMenu = new Menu();
if (lastNews) {
lastNewsMenu.items = lastNews.map((lastNewsItem) => {
const newsMenuItem = new MenuItem();
newsMenuItem.id = lastNewsItem.id;
newsMenuItem.text = lastNewsItem.title.match(/>(.+)</)[1];
newsMenuItem.text = lastNewsItem.title.split(/<.*?>/).filter((el) => !!el).join('');
newsMenuItem.url = `/content/news/${newsMenuItem.id}/${
lastNewsItem.title
.match(/>(.+)</)[1]
.toLowerCase()
.replace(/\s/g, '-')
newsMenuItem.text.toLowerCase().replace(/\s/g, '-')
}`;
return newsMenuItem;
});
......@@ -94,8 +92,8 @@ class ActivityPostDisplayPage extends React.Component<IActivityPostDisplayPage>
const mapStateToProps = (state, ownProps) => ({
slug: ownProps.match.params.slug,
id: +ownProps.match.params.id,
lastNews: state.cms.public.lastNews ? state.cms.public.lastNews.data : undefined,
activityPost: state.cms.public.news[ownProps.match.params.id],
lastNews: state.cms.public.activityPost.lastNews ? state.cms.public.activityPost.lastNews.data : undefined,
activityPost: state.cms.public.activityPost.posts ? state.cms.public.activityPost.posts[ownProps.match.params.id] : undefined,
});
const mapDispatchToProps = (dispatch) => bindActionCreators({
......
import * as React from 'react';
import { translate } from 'react-i18next';
import { connect } from 'react-redux';
import { bindActionCreators } from 'redux';
// Actions
import { activityPostApplyFilters, loadMoreActivityPosts, activityPostUpdateRoute } from 'cms/actions/public';
// Models
import ActivityPost from 'model/cms/ActivityPost';
// UI
import BrowsePageTemplate, { IBrowsePageProps } from 'ui/pages/_base/BrowsePage';
import PageLayout, { PageContents } from 'ui/layout/PageLayout';
import Loading from 'ui/common/Loading';
import PagedLoader from 'ui/common/PagedLoader';
import PageTitle from 'ui/common/PageTitle';
import ContentHeader from 'ui/common/heading/ContentHeader';
import ActivityPostCard from './c/ActivityPostCard';
import PrettyFilters from 'ui/common/filter/PrettyFilters';
import PaginationComponent from 'ui/common/pagination';
class ActivityPostBrowsePage extends BrowsePageTemplate<ActivityPost> {
constructor(props: IBrowsePageProps<ActivityPost>, context: any) {
super(props, context);
}
public render() {
const { paged, loadMoreData, error, loading, t } = this.props;
const renderActivityPost = (post: ActivityPost, index: number) => {
return <div key={ index }><ActivityPostCard key={ index } index={ index } post={ post } compact/></div>;
};
return (
<PageLayout>
<PageTitle title={ t('cms.admin.p.browseActivityPost.title') }/>
<ContentHeader
title={ t('cms.admin.p.browseActivityPost.title') }
/>
<PaginationComponent
pageObj={ paged }
onSortChange={ this.onSortChange }
displayName="cms.common.activityPost"
sortOptions={ ActivityPost.SORT_OPTIONS }
sortBy="createdDate"
/>
<PrettyFilters
prefix="cms.activityPosts"
filterObj={ paged && paged.filter || {} }
onSubmit={ this.myApplyFilters }
/>
<PageContents className="pt-1rem container-spacing-horizontal">
{ loading && <Loading/> }
{ error && <div>{ JSON.stringify(error) }</div> }
<PagedLoader
paged={ paged }
loadMore={ loadMoreData }
roughItemHeight={ 70 }
itemRenderer={ renderActivityPost }
/>
</PageContents>
</PageLayout>
);
}
}
const mapStateToProps = (state, ownProps) => ({
paged: state.cms.public.activityPost.paged ? state.cms.public.activityPost.paged.data : undefined,
loading: state.cms.public.activityPost.paged ? state.cms.public.activityPost.paged.loading : false,
error: state.cms.public.activityPost.paged ? state.cms.public.activityPost.paged.error : undefined,
filterCode: ownProps.match.params.filterCode,
});
const mapDispatchToProps = (dispatch) => bindActionCreators({
applyFilters: activityPostApplyFilters,
loadMoreData: loadMoreActivityPosts,
updateRoute: activityPostUpdateRoute,
}, dispatch);
export default translate()(connect(mapStateToProps, mapDispatchToProps)(ActivityPostBrowsePage));
......@@ -17,7 +17,7 @@ import PagedLoader from 'ui/common/PagedLoader';
import PageTitle from 'ui/common/PageTitle';
import ActivityPostFilters from './c/ActivityPostFilters';
import ContentHeaderWithButton from 'ui/common/heading/ContentHeaderWithButton';
import ActivityPostCard from './c/ActivityPostCard';
import ActivityPostCard from 'cms/ui/c/ActivityPostCard';
import CreateNewButton from 'ui/common/buttons/CreateNewButton';
import PrettyFilters from 'ui/common/filter/PrettyFilters';
......@@ -31,7 +31,7 @@ class ActivityPostBrowsePage extends BrowsePageTemplate<ActivityPost> {
const { paged, loadMoreData, error, loading, t } = this.props;
const renderActivityPost = (aP: ActivityPost, index: number) => {
return <div><ActivityPostCard key={ index } index={ index } post={ aP } edit compact/></div>;
return <div key={ index }><ActivityPostCard key={ index } index={ index } post={ aP } edit compact/></div>;
};
return (
......@@ -58,7 +58,7 @@ class ActivityPostBrowsePage extends BrowsePageTemplate<ActivityPost> {
/>
</PageContents>
<CreateNewButton
title="cms.admin.p.browse.activityPost"
title="cms.common.activityPost"
path="/admin/content/activity-post/edit"
/>
</PageLayout>
......
import * as React from 'react';
import { translate } from 'react-i18next';
// Models
import ActivityPost from 'model/cms/ActivityPost';
// UI
import Card, { CardHeader, CardContent } from 'ui/common/Card';
import Grid from '@material-ui/core/Grid';
import withStyles from '@material-ui/core/styles/withStyles';
import { ActivityPostLink } from 'ui/genesys/Links';
const style = (theme) => ({
root: {
},
header: {
paddingBottom: '0 !important',
borderBottom: '0 !important',
},
/*tslint:disable*/
titleText: {
color: 'black',
fontSize: '20px',
fontWeight: 500,
'& > p': {
marginBottom: 0,
},
},
/*tslint:enable*/
});
const ActivityPostCard = ({classes, post, edit, compact}: { post: ActivityPost, edit?: false, compact?: false, classes?: any }) => {
return compact ? (
<Grid item xs={ 12 }>
<Card className="p-20">
<ActivityPostLink to={ post } edit={ edit }>
{ <div dangerouslySetInnerHTML={ {__html: post.title} } /> }
</ActivityPostLink>
<div dangerouslySetInnerHTML={ {__html: post.body} } />
</Card>
</Grid>
)
:
(
<Grid item xs={ 12 }>
<Card className={ classes.root }>
<CardHeader
className={ classes.header }
title={
<ActivityPostLink to={ post } edit={ edit }>
{ <div className={ classes.titleText } dangerouslySetInnerHTML={ {__html: post.title} } /> }
</ActivityPostLink>
}
subheader={ `Last updated ${new Date(post.lastModifiedDate).toLocaleDateString()}`/*will be changes*/ }
/>
<CardContent className="pb-10">
<div dangerouslySetInnerHTML={ {__html: post.body} } />
</CardContent>
</Card>
</Grid>
);
};
export default translate()(withStyles(style)(ActivityPostCard));
......@@ -11,70 +11,86 @@ import Card, { CardHeader, CardContent } from 'ui/common/Card';
import Grid from '@material-ui/core/Grid';
import withStyles from '@material-ui/core/styles/withStyles';
import Avatar from '@material-ui/core/Avatar';
import { ActivityPostLink } from 'ui/genesys/Links';
const style = (theme) => ({
root: {
},
header: {
paddingBottom: '0 !important',
borderBottom: '0 !important',
},
headerWithContent: {
paddingBottom: '0 !important',
},
/*tslint:disable*/
headerWrapper: {
display: 'flex' as 'flex',
flexDirection: 'row' as 'row',
},
titleText: {
color: 'black',
fontSize: '20px',
fontWeight: 500,
'& > p': {
marginBottom: 0,
},
},
compactText: {
fontSize: '16px',
},
text: {
color: 'black',
fontSize: '20px',
},
avatar: {
margin: '1.143rem 0 0 1.429rem',
}
/*tslint:enable*/
});
const ActivityPostCard = ({classes, post, compact, t}: { post: ActivityPost, compact?: false, classes?: any, t: any }) => {
const urlTitle = post.title.match(/>(.+)</)[1].toLowerCase().replace(/\s/g, '-');
const ActivityPostCard = ({classes, post, compact, edit, t}: { post: ActivityPost, compact?: false, edit?: false, classes?: any, t: any }) => {
const urlTitle = post.title && post.title
.split(/<.*?>/)
.filter((el) => !!el)
.join('')
.toLowerCase()
.replace(/\s/g, '-');
return compact ? (
<Grid item xs={ 12 }>
<Card className="p-20">
<Link to={ `/content/news/${post.id}/${urlTitle}` }>
{ <div dangerouslySetInnerHTML={ {__html: post.title} } /> }
</Link>
<div dangerouslySetInnerHTML={ {__html: post.body} } />
</Card>
</Grid>
)
:
(
return (
<Grid item xs={ 12 }>
<Card className={ classes.root }>
<Card>
<div className={ classes.headerWrapper }>
<Avatar className={ classes.avatar }>{ post.lastModifiedBy[0].toUpperCase() }</Avatar>
{ !compact &&
<Avatar className={ classes.avatar }>{ post.lastModifiedBy[0].toUpperCase() }</Avatar>
}
<CardHeader
className={ classes.header }
className={ `${classes.header} ${!compact && classes.headerWithContent}` }
title={
<Link to={ `/content/news/${post.id}/${urlTitle}` }>
{ <div className={ classes.titleText } dangerouslySetInnerHTML={ {__html: post.title} } /> }
</Link>
edit ? (
<ActivityPostLink to={ post } edit>
<div
className={ `${classes.titleText} ${compact ? classes.compactText : classes.text}` }
dangerouslySetInnerHTML={ {__html: post.title} }
/>
</ActivityPostLink>
) : (
<Link to={ `/content/news/${post.id}/${urlTitle}` }>
<div
className={ `${classes.titleText} ${compact ? classes.compactText : classes.text}` }
dangerouslySetInnerHTML={ {__html: post.title} }
/>
</Link>
)
}
subheader={
<span>
{ `${t('common:label.lastUpdatedBy', {who: post.lastModifiedBy})} ` }
{ `${t('common:label.lastUpdatedBy', {who: post.lastModifiedBy})} ` }
<PrettyDate value={ new Date(post.lastModifiedDate) }/>
</span>
</span>
}
/>
</div>
<CardContent className="pb-10">
<div dangerouslySetInnerHTML={ {__html: post.body} } />
</CardContent>
{ !compact &&
<CardContent className="pb-10">
<div dangerouslySetInnerHTML={ {__html: post.body} } />
</CardContent>
}
</Card>
</Grid>
);
......
......@@ -2,6 +2,8 @@
/*
* Defined in Swagger as '#/definitions/ActivityPost'
*/
import { SortDirection } from 'model/Page';
class ActivityPost {
public active: boolean;
public body: string;
......@@ -14,7 +16,33 @@ class ActivityPost {
public title: string;
public version: number;
public static DEFAULT_SORT = {