[CORL-444] Stories Tab Adjustments (#2404)

* feat: improved stories tab

* feat: swapped date sorting with text sorting
This commit is contained in:
Wyatt Johnson
2019-07-12 20:18:21 +00:00
committed by GitHub
parent 340052cdf0
commit 7fd01e5845
10 changed files with 319 additions and 108 deletions
+27 -13
View File
@@ -4,13 +4,15 @@ import {
FETCH_ASSETS_REQUEST,
FETCH_ASSETS_SUCCESS,
FETCH_ASSETS_FAILURE,
LOAD_MORE_ASSETS_REQUEST,
LOAD_MORE_ASSETS_SUCCESS,
LOAD_MORE_ASSETS_FAILURE,
SET_PAGE,
SET_SEARCH_VALUE,
SET_CRITERIA,
UPDATE_ASSET_STATE_REQUEST,
UPDATE_ASSET_STATE_SUCCESS,
UPDATE_ASSET_STATE_FAILURE,
UPDATE_ASSETS,
} from '../constants/stories';
import t from 'coral-framework/services/i18n';
@@ -21,17 +23,14 @@ import t from 'coral-framework/services/i18n';
// Fetch a page of assets
// Get comments to fill each of the three lists on the mod queue
export const fetchAssets = (query = {}) => (dispatch, _, { rest }) => {
export const fetchAssets = (query = {}) => (dispatch, _, { rest2 }) => {
dispatch({ type: FETCH_ASSETS_REQUEST });
return rest(`/assets?${queryString.stringify(query)}`)
.then(({ result, page, count, limit, totalPages }) =>
return rest2(`/stories?${queryString.stringify(query)}`)
.then(({ edges, pageInfo }) =>
dispatch({
type: FETCH_ASSETS_SUCCESS,
assets: result,
page,
count,
limit,
totalPages,
edges,
pageInfo,
})
)
.catch(error => {
@@ -43,6 +42,25 @@ export const fetchAssets = (query = {}) => (dispatch, _, { rest }) => {
});
};
export const loadMoreAssets = (query = {}) => (dispatch, _l, { rest2 }) => {
dispatch({ type: LOAD_MORE_ASSETS_REQUEST });
return rest2(`/stories?${queryString.stringify(query)}`)
.then(({ edges, pageInfo }) =>
dispatch({
type: LOAD_MORE_ASSETS_SUCCESS,
edges,
pageInfo,
})
)
.catch(error => {
console.error(error);
const errorMessage = error.translation_key
? t(`error.${error.translation_key}`)
: error.toString();
dispatch({ type: LOAD_MORE_ASSETS_FAILURE, error: errorMessage });
});
};
// Update an asset state
// Get comments to fill each of the three lists on the mod queue
export const updateAssetState = (id, closedAt) => (dispatch, _, { rest }) => {
@@ -58,10 +76,6 @@ export const updateAssetState = (id, closedAt) => (dispatch, _, { rest }) => {
});
};
export const updateAssets = assets => dispatch => {
dispatch({ type: UPDATE_ASSETS, assets });
};
export const setPage = page => ({
type: SET_PAGE,
page,
+4 -2
View File
@@ -4,12 +4,14 @@ export const FETCH_ASSETS_REQUEST = `${prefix}_FETCH_ASSETS_REQUEST`;
export const FETCH_ASSETS_SUCCESS = `${prefix}_FETCH_ASSETS_SUCCESS`;
export const FETCH_ASSETS_FAILURE = `${prefix}_FETCH_ASSETS_FAILURE`;
export const LOAD_MORE_ASSETS_REQUEST = `${prefix}_LOAD_MORE_ASSETS_REQUEST`;
export const LOAD_MORE_ASSETS_SUCCESS = `${prefix}_LOAD_MORE_ASSETS_SUCCESS`;
export const LOAD_MORE_ASSETS_FAILURE = `${prefix}_LOAD_MORE_ASSETS_FAILURE`;
export const UPDATE_ASSET_STATE_REQUEST = `${prefix}_UPDATE_ASSET_STATE_REQUEST`;
export const UPDATE_ASSET_STATE_SUCCESS = `${prefix}_UPDATE_ASSET_STATE_SUCCESS`;
export const UPDATE_ASSET_STATE_FAILURE = `${prefix}_UPDATE_ASSET_STATE_FAILURE`;
export const UPDATE_ASSETS = `${prefix}_UPDATE_ASSETS`;
export const SET_PAGE = `${prefix}_SET_PAGE`;
export const SET_SEARCH_VALUE = `${prefix}_SET_SEARCH_VALUE`;
export const SET_CRITERIA = `${prefix}_SET_CRITERIA`;
+40 -27
View File
@@ -3,33 +3,57 @@ import update from 'immutability-helper';
const initialState = {
assets: {
byId: {},
ids: [],
assets: [],
edges: [],
pageInfo: {},
},
searchValue: '',
criteria: {
asc: 'false',
filter: 'all',
limit: 20,
},
loading: true,
loadingMore: false,
};
export default function assets(state = initialState, action) {
switch (action.type) {
case actions.FETCH_ASSETS_SUCCESS: {
const assets = action.assets.reduce((prev, curr) => {
prev[curr.id] = curr;
return prev;
}, {});
case actions.FETCH_ASSETS_REQUEST: {
return update(state, {
loading: { $set: true },
});
}
case actions.FETCH_ASSETS_FAILURE: {
return update(state, {
loading: { $set: false },
});
}
case actions.FETCH_ASSETS_SUCCESS: {
return update(state, {
loading: { $set: false },
assets: {
totalPages: { $set: action.totalPages },
page: { $set: action.page },
byId: { $set: assets },
count: { $set: action.count },
ids: { $set: Object.keys(assets) },
edges: { $set: action.edges },
pageInfo: { $set: action.pageInfo },
},
});
}
case actions.LOAD_MORE_ASSETS_REQUEST: {
return update(state, {
loadingMore: { $set: true },
});
}
case actions.LOAD_MORE_ASSETS_FAILURE: {
return update(state, {
loadingMore: { $set: false },
});
}
case actions.LOAD_MORE_ASSETS_SUCCESS: {
return update(state, {
loadingMore: { $set: false },
assets: {
edges: { $push: action.edges },
pageInfo: {
endCursor: { $set: action.pageInfo.endCursor },
hasNextPage: { $set: action.pageInfo.hasNextPage },
},
},
});
}
@@ -43,17 +67,6 @@ export default function assets(state = initialState, action) {
},
},
});
case actions.UPDATE_ASSETS:
return update(state, {
assets: {
assets: { $set: action.assets },
},
});
case actions.SET_PAGE:
return {
...state,
page: action.page,
};
case actions.SET_SEARCH_VALUE:
return {
...state,
@@ -77,7 +77,6 @@
display: block;
}
.hidden {
display: none;
}
@@ -95,3 +94,11 @@
text-align: left;
}
.loadMore {
margin: 20px auto;
text-align: center;
}
.loadMoreSpinner {
margin: 20px auto 50px;
}
@@ -2,11 +2,14 @@ import React, { Component } from 'react';
import cn from 'classnames';
import { Link } from 'react-router';
import PropTypes from 'prop-types';
import { Dropdown, Option, Paginate, Icon } from 'coral-ui';
import { DataTable, TableHeader, RadioGroup, Radio } from 'react-mdl';
import t from 'coral-framework/services/i18n';
import styles from './Stories.css';
import { Dropdown, Option, Icon, Spinner } from 'coral-ui';
import EmptyCard from 'coral-admin/src/components/EmptyCard';
import LoadMore from 'coral-admin/src/components/LoadMore';
import styles from './Stories.css';
class Stories extends Component {
renderDate = date => {
@@ -39,10 +42,11 @@ class Stories extends Component {
filter,
onSearchChange,
onSettingChange,
onPageChange,
asc,
onLoadMore,
loading,
loadingMore,
} = this.props;
const rows = assets.ids.map(id => assets.byId[id]);
const rows = assets.edges.map(({ node }) => node);
return (
<div className={cn('talk-admin-stories', styles.container)}>
@@ -74,60 +78,58 @@ class Stories extends Component {
<Radio value="open">{t('streams.open')}</Radio>
<Radio value="closed">{t('streams.closed')}</Radio>
</RadioGroup>
<div className={styles.optionHeader}>{t('streams.sort_by')}</div>
<RadioGroup
name="sortBy"
value={asc}
childContainer="div"
onChange={onSettingChange('asc')}
className={styles.radioGroup}
>
<Radio value="false">{t('streams.newest')}</Radio>
<Radio value="true">{t('streams.oldest')}</Radio>
</RadioGroup>
</div>
{rows.length ? (
<div className={styles.mainContent}>
<DataTable className={styles.streamsTable} rows={rows}>
<TableHeader name="title" cellFormatter={this.renderTitle}>
{t('streams.article')}
</TableHeader>
<TableHeader
name="publication_date"
cellFormatter={this.renderDate}
>
{t('streams.pubdate')}
</TableHeader>
<TableHeader
name="closedAt"
cellFormatter={this.renderStatus}
className={styles.status}
>
{t('streams.status')}
</TableHeader>
</DataTable>
<Paginate
pageCount={assets.totalPages}
page={assets.page - 1}
onPageChange={onPageChange}
/>
</div>
) : (
<EmptyCard>{t('streams.empty_result')}</EmptyCard>
)}
<div className={styles.mainContent}>
{loading ? (
<Spinner />
) : rows.length ? (
<div>
<DataTable className={styles.streamsTable} rows={rows}>
<TableHeader name="title" cellFormatter={this.renderTitle}>
{t('streams.article')}
</TableHeader>
<TableHeader
name="publication_date"
cellFormatter={this.renderDate}
>
{t('streams.pubdate')}
</TableHeader>
<TableHeader
name="closedAt"
cellFormatter={this.renderStatus}
className={styles.status}
>
{t('streams.status')}
</TableHeader>
</DataTable>
{loadingMore ? (
<Spinner className={styles.loadMoreSpinner} />
) : (
<LoadMore
showLoadMore={assets.pageInfo.hasNextPage}
loadMore={onLoadMore}
className={styles.loadMore}
/>
)}
</div>
) : (
<EmptyCard>{t('streams.empty_result')}</EmptyCard>
)}
</div>
</div>
);
}
}
Stories.propTypes = {
loading: PropTypes.bool,
loadingMore: PropTypes.bool,
assets: PropTypes.object,
searchValue: PropTypes.string,
asc: PropTypes.string,
filter: PropTypes.string,
onLoadMore: PropTypes.func.isRequired,
onStatusChange: PropTypes.func.isRequired,
onSearchChange: PropTypes.func.isRequired,
onPageChange: PropTypes.func.isRequired,
onSettingChange: PropTypes.func.isRequired,
};
@@ -5,9 +5,9 @@ import { bindActionCreators } from 'redux';
import {
fetchAssets,
updateAssetState,
setPage,
setSearchValue,
setCriteria,
loadMoreAssets,
} from 'coral-admin/src/actions/stories';
import Stories from '../components/Stories';
@@ -35,13 +35,11 @@ class StoriesContainer extends Component {
};
fetchAssets = query => {
const { searchValue, asc, filter, limit } = this.props;
const { searchValue: value, filter } = this.props;
this.props.fetchAssets({
value: searchValue,
asc,
value,
filter,
limit,
...query,
});
};
@@ -57,24 +55,34 @@ class StoriesContainer extends Component {
}
};
onPageChange = ({ selected }) => {
const page = selected + 1;
this.props.setPage(page);
this.fetchAssets({ page });
onLoadMore = async () => {
const {
searchValue: value,
filter,
assets: {
pageInfo: { endCursor: cursor },
},
loadMoreAssets,
} = this.props;
try {
await loadMoreAssets({ cursor, value, filter });
} catch (err) {
console.error(err);
}
};
render() {
return (
<Stories
loading={this.props.loading}
loadingMore={this.props.loadingMore}
assets={this.props.assets}
searchValue={this.props.searchValue}
asc={this.props.asc}
filter={this.props.filter}
limit={this.props.limit}
onPageChange={this.onPageChange}
onStatusChange={this.onStatusChange}
onSettingChange={this.onSettingChange}
onSearchChange={this.onSearchChange}
onLoadMore={this.onLoadMore}
/>
);
}
@@ -82,34 +90,34 @@ class StoriesContainer extends Component {
const mapStateToProps = ({ stories }) => ({
assets: stories.assets,
loading: stories.loading,
loadingMore: stories.loadingMore,
searchValue: stories.searchValue,
asc: stories.criteria.asc,
filter: stories.criteria.filter,
limit: stories.criteria.limit,
});
const mapDispatchToProps = dispatch =>
bindActionCreators(
{
setPage,
setCriteria,
setSearchValue,
fetchAssets,
updateAssetState,
loadMoreAssets,
},
dispatch
);
StoriesContainer.propTypes = {
loading: PropTypes.bool,
loadingMore: PropTypes.bool,
assets: PropTypes.object,
searchValue: PropTypes.string,
asc: PropTypes.string,
filter: PropTypes.string,
limit: PropTypes.number,
setPage: PropTypes.func.isRequired,
setCriteria: PropTypes.func.isRequired,
setSearchValue: PropTypes.func.isRequired,
fetchAssets: PropTypes.func.isRequired,
loadMoreAssets: PropTypes.func.isRequired,
updateAssetState: PropTypes.func.isRequired,
};
+6
View File
@@ -159,6 +159,11 @@ export async function createContext({
token,
});
const rest2 = createRestClient({
uri: `${BASE_PATH}api/v2`,
token,
});
const staticConfig = getStaticConfiguration();
let { LIVE_URI: liveUri, BASE_ORIGIN: origin } = staticConfig;
if (liveUri == null) {
@@ -193,6 +198,7 @@ export async function createContext({
plugins,
eventEmitter,
rest,
rest2,
graphql,
notification,
localStorage,
+1
View File
@@ -3,5 +3,6 @@ const router = express.Router();
// Return the current version.
router.use('/v1', require('./v1'));
router.use('/v2', require('./v2'));
module.exports = router;
+13
View File
@@ -0,0 +1,13 @@
const express = require('express');
const { version } = require('../../../package.json');
const { REVISION_HASH } = require('../../../config');
const router = express.Router();
// Return the current version.
router.get('/', (req, res) => {
res.json({ version, revision: REVISION_HASH });
});
router.use('/stories', require('./stories'));
module.exports = router;
+145
View File
@@ -0,0 +1,145 @@
const express = require('express');
const Joi = require('joi');
const { get, first, last } = require('lodash');
const authorization = require('../../../middleware/authorization');
const AssetModel = require('../../../models/asset');
const router = express.Router();
const ListStorySchema = Joi.object({
value: Joi.string()
.empty('')
.default(''),
filter: Joi.string()
.empty('')
.valid(['all', 'open', 'closed'])
.default('all'),
limit: Joi.number()
.empty('')
.default(20)
.max(500)
.min(0),
cursor: Joi.string()
.empty('')
.default(''),
});
function validate(query) {
const { value, error } = Joi.validate(query, ListStorySchema, {
presence: 'optional',
});
if (error) {
throw error;
}
return value;
}
router.get(
'/',
authorization.needed('ADMIN', 'MODERATOR'),
async (req, res, next) => {
try {
// Validate and extract the query arguments.
let { cursor, value, filter, limit } = validate(req.query);
const isTextBasedSearch = value.length > 0;
// The cursor can be a date or a number based on the style of search being
// performed.
cursor = Joi.attempt(
cursor,
isTextBasedSearch
? Joi.number()
.empty('')
.min(0)
.default(0)
: Joi.date()
.empty('')
.default(null)
);
// Create a new query to begin adding conditions.
let query = AssetModel.find(
{},
isTextBasedSearch ? { score: { $meta: 'textScore' } } : {}
);
if (filter === 'open') {
// Filter by open stories.
query.merge({
$or: [{ closedAt: null }, { closedAt: { $gt: Date.now() } }],
});
} else if (filter === 'closed') {
// Filter by closed stories.
query.merge({
closedAt: {
$lt: Date.now(),
},
});
}
if (isTextBasedSearch) {
// This is a text based search, so search by the value.
query.merge({
$text: {
$search: value,
},
});
// Sort by text search score.
query.sort({ score: { $meta: 'textScore' } });
if (cursor) {
// We are paginating, so we should skip stories based on the cursor.
query.skip(cursor);
}
} else {
// This is not a text based search, so sort by the created timestamp.
query.sort({ created_at: -1 });
if (cursor) {
// We are paginating, so we should sort based on the created
// timestamp.
query.merge({
created_at: {
$lt: cursor,
},
});
}
}
// Execute the query.
const results = await query.limit(limit + 1);
const textTransformer = (node, idx) => ({
node,
cursor: idx + cursor + 1,
});
const dateTransformer = node => ({
node,
cursor: node.created_at,
});
// Slice the nodes to get only the requested number of elements.
const edges = results
.slice(0, limit)
.map(isTextBasedSearch ? textTransformer : dateTransformer);
// Generate the pageInfo.
const pageInfo = {
startCursor: get(first(edges), 'cursor', null),
endCursor: get(last(edges), 'cursor', null),
hasNextPage: results.length > limit,
};
// Send back the asset data.
return res.json({ edges, pageInfo });
} catch (err) {
return next(err);
}
}
);
module.exports = router;