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

Merge branch '158-cms-admin-activity-posts' into 'master'

Resolve "CMS Admin: Activity posts"

Closes #158

See merge request genesys-pgr/genesys-ui!168
parents 96b98dec 3690ee2b
......@@ -84,6 +84,7 @@
"itemEditorWarn": "Don't use the ItemsEditor for more than 100 items!",
"lastModified": "Last modified",
"lastUpdated": "Last updated",
"lastUpdatedBy": "Last updated by {{who}}",
"modified": "Modified",
"name": "Name",
"newVersionAvailable": "New version available",
......
......@@ -254,6 +254,8 @@
"oAuthClients": "OAuth clients",
"requests": "Requests",
"content": "CMS",
"activityPosts": "Activity posts",
"articles": "Articles",
"vocabulary": "Vocabulary",
"fileRepository": "File repository",
"cluster": "Cluster",
......@@ -528,6 +530,10 @@
"slug": "Slug",
"lang": "Language",
"isTemplate": "Is template"
},
"activityPostForm": {
"content": "Content",
"title": "Title"
}
},
"f": {
......@@ -543,8 +549,13 @@
"title": "Browse articles",
"create": "Create new article"
},
"browseActivityPost": {
"title": "Browse activity posts",
"create": "Create new activity post"
},
"edit": {
"articleSaved": "Article saved",
"activityPostSaved": "Activity post saved",
"transifex": {
"fetch": "Fetch translations",
"fetchStarted": "Fetch started",
......@@ -578,11 +589,17 @@
"c": {
"documentationSection": {
"toc": "Table of contents"
},
"activityPostSection": {
"shareArticle": "Share this article",
"tweet": "Tweet!",
"shareLinkedin": "Share on LinkedIn"
}
}
},
"common": {
"articleList": "Article list"
"articleList": "Article list",
"activityPostList": "Activity post list"
}
}
......
......@@ -2,10 +2,12 @@
import { showSnackbar } from 'actions/snackbar';
import navigateTo from 'actions/navigation';
// constants
import { ADMIN_APPEND_ARTICLES, ADMIN_RECEIVE_ARTICLE, ADMIN_RECEIVE_ARTICLES, ADMIN_LOADING_ARTICLE } from 'cms/constants';
import { ADMIN_APPEND_ARTICLES, ADMIN_RECEIVE_ARTICLE, ADMIN_RECEIVE_ARTICLES, ADMIN_LOADING_ARTICLE, ADMIN_APPEND_ACTIVITY_POSTS, ADMIN_RECEIVE_ACTIVITY_POSTS, ADMIN_RECEIVE_ACTIVITY_POST, ADMIN_LOADING_ACTIVITY_POST } from 'cms/constants';
// model
import Article from 'model/cms/Article';
import ArticleFilter from 'model/cms/ArticleFilter';
import ActivityPost from 'model/cms/ActivityPost';
import ActivityPostFilter from 'model/cms/ActivityPostFilter';
import Page from 'model/Page';
import FilteredPage, { IPageRequest } from 'model/FilteredPage';
// service
......@@ -32,6 +34,28 @@ const loadingArticle = (slug: string) => ({
payload: {slug},
});
const receiveActivityPosts = (paged: FilteredPage<ActivityPost>, error = null) => ({
type: ADMIN_RECEIVE_ACTIVITY_POSTS,
payload: { paged, error },
});
const appendActivityPosts = (paged: FilteredPage<ActivityPost>, error = null) => ({
type: ADMIN_APPEND_ACTIVITY_POSTS,
payload: { paged, error },
});
const receiveActivityPost = (id: number, activityPost: ActivityPost) => ({
type: ADMIN_RECEIVE_ACTIVITY_POST,
payload: {id, activityPost},
});
const loadingActivityPost = (id: number) => ({
type: ADMIN_LOADING_ACTIVITY_POST,
payload: {id},
});
export const loadArticle = (locale: string, slug: string, targetId: number, clazz: string) => (dispatch) => {
dispatch(loadingArticle(slug));
if (!targetId || !clazz) {
......@@ -55,6 +79,10 @@ export const updateRoute = (paged: FilteredPage<Article>) => (dispatch) => {
dispatch(navigateTo(paged.filterCode ? `/admin/content/${paged.filterCode}` : '/admin/content'));
};
export const activityPostUpdateRoute = (paged: FilteredPage<ActivityPost>) => (dispatch) => {
dispatch(navigateTo(paged.filterCode ? `/admin/content/activity-post/${paged.filterCode}` : '/admin/content/activity-post'));
};
export const applyFilters = (filters: ArticleFilter, page: IPageRequest = { page: 0 }) => (dispatch) => {
console.log('Applying new filter', filters);
dispatch(showSnackbar('Applying filters...'));
......@@ -70,6 +98,21 @@ 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 CmsService.listActivityPosts(filters, page)
.then((paged) => {
dispatch(receiveActivityPosts(paged));
dispatch(activityPostUpdateRoute(paged));
dispatch(showSnackbar(`Filters applied.`));
}).catch((error) => {
console.log(`API error`, error);
dispatch(receiveArticles(null, error));
dispatch(showSnackbar(`API error: ${error.statusText}`));
});
};
export const loadMoreArticles = (paged?: FilteredPage<Article>) => (dispatch) => {
return CmsService.listArticles(paged && paged.filterCode, Page.nextPage(paged))
.then((paged) => {
......@@ -85,6 +128,43 @@ export const loadMoreArticles = (paged?: FilteredPage<Article>) => (dispatch) =>
});
};
export const loadMoreActivityPosts = (paged?: FilteredPage<ActivityPost>) => (dispatch) => {
return CmsService.listActivityPosts(paged && paged.filterCode, Page.nextPage(paged))
.then((paged) => {
if (paged.number === 0) {
dispatch(receiveActivityPosts(paged));
} else {
dispatch(appendActivityPosts(paged));
}
dispatch(activityPostUpdateRoute(paged));
}).catch((error) => {
console.log(`API error`, error);
dispatch(receiveArticles(null, error));
});
};
export const loadActivityPost = (id: number) => (dispatch) => {
dispatch(loadingActivityPost(id));
return CmsService.getActivityPost(id)
.then((activityPost) => dispatch(receiveActivityPost(id, activityPost)));
};
export const createOrUpdateActivityPost = (activityPost: ActivityPost) => (dispatch) => {
const createOrUpdate = activityPost.id ? CmsService.updateActivityPost : CmsService.createActivityPost;
return createOrUpdate(activityPost)
.then((saved) => {
dispatch(receiveActivityPost(saved.id, saved));
dispatch(showSnackbar('cms.admin.p.edit.activityPostSaved'));
dispatch(navigateTo(`/admin/content/activity-post/${saved.id}/edit`));
}).catch((error) => dispatch(showSnackbar(error.statusText)));
};
export const deleteActivityPost = (id: number) => (dispatch) => {
return CmsService.deleteActivityPost(id)
.then(() => dispatch(navigateTo('/admin/content/activity-post')));
};
export const createGlobalArticle = (article: Article) => (dispatch) => {
article = {
body: '<p></p>',
......
// constants
import { LOADING_SLUG, RECEIVE_ARTICLE, RECEIVE_DOCUMENTATION, RECEIVE_MENU } from 'cms/constants';
import { LOADING_SLUG, RECEIVE_ARTICLE, RECEIVE_DOCUMENTATION, RECEIVE_MENU, RECEIVE_LAST_NEWS, RECEIVE_POST } from 'cms/constants';
// model
import Article from 'model/cms/Article';
import Menu from 'model/cms/Menu';
import ADoc from 'model/cms/ADoc';
import ActivityPost from 'model/cms/ActivityPost';
// service
import CmsService from 'service/genesys/CmsService';
import AsciiDocService from 'service/genesys/AsciiDocService';
......@@ -18,6 +19,16 @@ const receiveArticle = (slug: string, article: Article) => ({
payload: {slug, article},
});
const receiveLastNews = (lastNews: ActivityPost[]) => ({
type: RECEIVE_LAST_NEWS,
payload: lastNews,
});
const receivePost = (post: ActivityPost, id: number) => ({
type: RECEIVE_POST,
payload: {post, id},
});
export const loadArticle = (locale: string, slug: string) => (dispatch) => {
dispatch(loadingSlug(slug));
return CmsService.getArticleBySlugAndLang(locale, slug)
......@@ -48,3 +59,15 @@ export const loadMenu = (menuKey: string) => (dispatch) => {
CmsService.getMenu(menuKey)
.then((menuItem) => dispatch(receiveMenu(menuKey, menuItem)));
};
export const loadLastNews = () => (dispatch) => {
CmsService.getLastNews().then((lastNews) => {
dispatch(receiveLastNews(lastNews));
});
};
export const loadActivityPostById = (id: number) => (dispatch) => {
CmsService.getActivityPost(id).then((post) => {
dispatch(receivePost(post, id));
});
};
......@@ -2,12 +2,22 @@ export const RECEIVE_ARTICLE = 'cms/public/RECEIVE_ARTICLE';
export const RECEIVE_DOCUMENTATION = 'cms/public/RECEIVE_DOCUMENTATION';
export const LOADING_SLUG = 'cms/public/LOADING_SLUG';
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';
// admin
export const ADMIN_RECEIVE_ARTICLE = 'cms/admin/RECEIVE_ARTICLE';
export const ADMIN_RECEIVE_ARTICLES = 'cms/admin/RECEIVE_ARTICLES';
export const ADMIN_APPEND_ARTICLES = 'cms/admin/APPEND_ARTICLES';
export const ADMIN_LOADING_ARTICLE = 'cms/admin/LOADING_ARTICLE';
export const ADMIN_RECEIVE_ACTIVITY_POST = 'cms/admin/RECEIVE_ACTIVITY_POST';
export const ADMIN_RECEIVE_ACTIVITY_POSTS = 'cms/admin/RECEIVE_ACTIVITY_POSTS';
export const ADMIN_APPEND_ACTIVITY_POSTS = 'cms/admin/APPEND_ACTIVITY_POSTS';
export const ADMIN_LOADING_ACTIVITY_POST = 'cms/admin/LOADING_ACTIVITY_POST';
export const ARTICLE_FORM = 'form/cms/admin/article';
export const ACTIVITY_POST_FORM = 'form/cms/admin/activityPost';
export const ARTICLE_FILTERS = 'filter/cms/admin/article';
export const ACTIVITY_POST_FILTERS = 'filter/cms/admin/activityPost';
import { ADMIN_RECEIVE_ARTICLE, ADMIN_RECEIVE_ARTICLES, ADMIN_APPEND_ARTICLES, ADMIN_LOADING_ARTICLE } from 'cms/constants';
import { ADMIN_APPEND_ARTICLES, ADMIN_LOADING_ACTIVITY_POST, ADMIN_LOADING_ARTICLE, ADMIN_RECEIVE_ACTIVITY_POST, ADMIN_RECEIVE_ACTIVITY_POSTS, ADMIN_RECEIVE_ARTICLE, ADMIN_RECEIVE_ARTICLES } from 'cms/constants';
import update from 'immutability-helper';
import Page from 'model/Page';
import Article from 'model/cms/Article';
import ActivityPost from 'model/cms/ActivityPost';
const INITIAL_STATE: {
articles: any,
loading: any,
paged: Page<Article>,
pagedError: any,
activityPost: {
paged: Page<ActivityPost>,
pagedError: any,
posts: any,
loading: any,
},
} = {
articles: {},
loading: null,
paged: null,
pagedError: null,
activityPost: {
paged: null,
pagedError: null,
posts: [],
loading: null,
},
};
export default function cmsAdmin(state = INITIAL_STATE, action: { type?: string, payload?: any } = {type: '', payload: {}}) {
......@@ -59,6 +72,43 @@ export default function cmsAdmin(state = INITIAL_STATE, action: { type?: string,
pagedError: { $set: error },
});
}
case ADMIN_RECEIVE_ACTIVITY_POSTS: {
const { paged, error } = action.payload;
return update(state, {
activityPost: {
paged: { $set: paged },
pagedError: { $set: error },
},
});
}
case ADMIN_RECEIVE_ACTIVITY_POST: {
const {id, activityPost } = action.payload;
return update(state, {
activityPost: {
posts: {
[id]: { $set: activityPost },
},
loading: { $set: null },
},
});
}
case ADMIN_LOADING_ACTIVITY_POST: {
const { id } = action.payload;
return update(state, {
activityPost: {
current: { $set: null },
loading: {
$set: {
id: { $set: id },
},
},
},
});
}
default:
return state;
}
......
import { RECEIVE_ARTICLE, RECEIVE_DOCUMENTATION, RECEIVE_MENU, LOADING_SLUG } from 'cms/constants';
import { RECEIVE_ARTICLE, RECEIVE_DOCUMENTATION, RECEIVE_MENU, LOADING_SLUG, RECEIVE_LAST_NEWS, RECEIVE_POST } from 'cms/constants';
import update from 'immutability-helper';
const INITIAL_STATE: {
......@@ -6,13 +6,19 @@ const INITIAL_STATE: {
documentations: any,
loadingSlug: string,
menus: any,
lastNews: any,
news: any,
} = {
articles: {},
documentations: {},
menus: {},
loadingSlug: null,
lastNews: null,
news: {},
};
/* state will be changes */
export default function listPublic(state = INITIAL_STATE, action: { type?: string, payload?: any } = {type: '', payload: {}}) {
switch (action.type) {
......@@ -47,6 +53,20 @@ export default function listPublic(state = INITIAL_STATE, action: { type?: strin
},
});
}
case RECEIVE_LAST_NEWS: {
const lastNews = action.payload;
return update(state, {
lastNews: {$set: lastNews},
});
}
case RECEIVE_POST: {
const {post, id} = action.payload;
return update(state, {
news: {
[id]: {$set: post},
},
});
}
default:
return state;
}
......
......@@ -3,6 +3,13 @@ import * as Loadable from 'react-loadable';
const publicRoutes = [
{
path: '/content/news/:id/:slug',
component: Loadable({
loader: () => import(/* webpackMode:"lazy", webpackChunkName: "cms" */'cms/ui/ActivityPostDisplayPage'),
loading: Loading,
}),
},
{
path: '/content/:menuKey/:slug',
component: Loadable({
......@@ -27,6 +34,29 @@ const publicRoutes = [
];
const adminRoutes = [
{
path: '/content/activity-post/:id/edit',
component: Loadable({
loader: () => import(/* webpackMode:"lazy", webpackChunkName: "cms" */'cms/ui/admin/ActivityPostEditPage'),
loading: Loading,
}),
exact: true,
},
{
path: '/content/activity-post/edit',
component: Loadable({
loader: () => import(/* webpackMode:"lazy", webpackChunkName: "cms" */'cms/ui/admin/ActivityPostEditPage'),
loading: Loading,
}),
exact: true,
},
{
path: '/content/activity-post/:filterCode(v.+)?',
component: Loadable({
loader: () => import(/* webpackMode:"lazy", webpackChunkName: "cms" */'cms/ui/admin/ActivityPostBrowsePage'),
loading: Loading,
}),
},
{
path: '/content/:slug/edit',
component: Loadable({
......
......@@ -9,6 +9,10 @@
"slug": "Slug",
"lang": "Language",
"isTemplate": "Is template"
},
"activityPostForm": {
"content": "Content",
"title": "Title"
}
},
"f": {
......@@ -24,8 +28,13 @@
"title": "Browse articles",
"create": "Create new article"
},
"browseActivityPost": {
"title": "Browse activity posts",
"create": "Create new activity post"
},
"edit": {
"articleSaved": "Article saved",
"activityPostSaved": "Activity post saved",
"transifex": {
"fetch": "Fetch translations",
"fetchStarted": "Fetch started",
......@@ -59,10 +68,16 @@
"c": {
"documentationSection": {
"toc": "Table of contents"
},
"activityPostSection": {
"shareArticle": "Share this article",
"tweet": "Tweet!",
"shareLinkedin": "Share on LinkedIn"
}
}
},
"common": {
"articleList": "Article list"
"articleList": "Article list",
"activityPostList": "Activity post list"
}
}
import * as React from 'react';
import {connect} from 'react-redux';
import {bindActionCreators} from 'redux';
// Actions
import { loadLastNews, loadActivityPostById } from 'cms/actions/public';
// Models
import ActivityPost from 'model/cms/ActivityPost';
import MenuItem from 'model/cms/MenuItem';
import Menu from 'model/cms/Menu';
// UI
import PageLayout from 'ui/layout/PageLayout';
import ContentHeader from 'ui/common/heading/ContentHeader';
import Loading from 'ui/common/Loading';
import ArticleSection from './c/ArticleSection';
import Grid from '@material-ui/core/Grid';
import MenuStepper from 'cms/ui/c/MenuStepper';
interface IActivityPostDisplayPage {
activityPost: ActivityPost;
lastNews: ActivityPost[];
loadLastNews: () => ActivityPost[];
loadActivityPostById: (id: number) => void;
slug: string;
id: number;
}
class ActivityPostDisplayPage extends React.Component<IActivityPostDisplayPage> {
protected static needs = [
({ params }) => loadActivityPostById(params.id),
({ params }) => params.lastNews ? loadLastNews() : null,
];
public componentWillReceiveProps(nextProps) {
const { loadLastNews, lastNews, activityPost, id, loadActivityPostById } = nextProps;
if (!lastNews) {
loadLastNews();
}
if (!activityPost || activityPost.id !== id) {
loadActivityPostById(id);
}
}
public componentWillMount() {
const { loadLastNews, lastNews, activityPost, id, loadActivityPostById } = this.props;
if (!lastNews) {
loadLastNews();
}
if (!activityPost) {
loadActivityPostById(id);
}
}
public render() {
const { lastNews, activityPost } = this.props;
const title = activityPost && activityPost.title.match(/>(.+)</)[1];
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.url = `/content/news/${newsMenuItem.id}/${
lastNewsItem.title
.match(/>(.+)</)[1]
.toLowerCase()
.replace(/\s/g, '-')
}`;
return newsMenuItem;
});
}
return (
<PageLayout withFooter>
{ !lastNews || !activityPost ? (
<Loading/>
) : (
<Grid container style={ {height: '100%'} }>
<ContentHeader title={ title }/>
<ArticleSection title={ title } body={ activityPost.body }/>
<MenuStepper menu={ lastNewsMenu }/>
</Grid>
) }
</PageLayout>
);
}
}
const mapStateToProps = (state, ownProps) => ({
slug: ownProps.match.params.slug,
id: +ownProps.match.params.id,
lastNews: state.cms.public.lastNews,
activityPost: state.cms.public.news[ownProps.match.params.id],
});
const mapDispatchToProps = (dispatch) => bindActionCreators({
loadLastNews,
loadActivityPostById,
}, dispatch);
export default connect(mapStateToProps, mapDispatchToProps)(ActivityPostDisplayPage);
......@@ -66,9 +66,9 @@ class ContentPage extends React.Component<IContentPageProps> {
return (
<PageLayout withFooter>
{ article &&
<Grid container>
<Grid container style={ {height: '100%'} }>
<ContentHeader title={ article.title } />
<ArticleSection article={ article }/>
<ArticleSection body={ article.body }/>
{ menuKey && <MenuStepper menu={ menu } t={ t }/> }
</Grid>
}
......
import * as React from 'react';
import { Link } from 'react-router-dom';
import { translate } from 'react-i18next';
import { connect } from 'react-redux';
import { bindActionCreators } from 'redux';
import { parse } from 'query-string';
// Actions
import { activityPostApplyFilters, loadMoreActivityPosts, activityPostUpdateRoute } from 'cms/actions/admin';
// Models
import Page from 'model/Page';
import ActivityPost from 'model/cms/ActivityPost';
// 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 ActivityPostFilters from './c/ActivityPostFilters';
import ContentHeaderWithButton from 'ui/common/heading/ContentHeaderWithButton';