From 5b74beabd6955d562a840538ab603acc595e4ff1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bel=C3=A9n=20Curcio?= Date: Fri, 11 Nov 2016 13:14:26 -0300 Subject: [PATCH] Community Section - Search Commenters (#60) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Table * úsers api * Ádding envs and version * Community Table Added * Ádding redux * Adding loading data state * Community Actions * Adding Inmutable * Fetching commenters * Search commenters * Linting and translations * Ádding Sort * sortBuilder * pagr * request per page, pager and mor * package.json * new Page actiokn * éslint * Changes and cleaner comps * removed console.log --- .eslintignore | 2 +- client/coral-admin/src/AppRouter.js | 2 +- client/coral-admin/src/actions/community.js | 42 ++++++++++ client/coral-admin/src/components/Page.js | 12 --- .../coral-admin/src/components/ui/Header.js | 3 + .../coral-admin/src/components/ui/Layout.css | 4 + .../coral-admin/src/components/ui/Layout.js | 5 +- client/coral-admin/src/constants/community.js | 5 ++ .../src/containers/Community/Community.css | 22 +++++ .../src/containers/Community/Community.js | 68 ++++++++++++++++ .../Community/CommunityContainer.js | 80 +++++++++++++++++++ .../src/containers/Community/Loading.js | 7 ++ .../src/containers/Community/NoResults.js | 9 +++ .../src/containers/Community/Pager.css | 10 +++ .../src/containers/Community/Pager.js | 45 +++++++++++ .../src/containers/Community/Table.js | 34 ++++++++ .../src/containers/CommunityContainer.js | 11 --- .../src/containers/LayoutContainer.js | 7 +- client/coral-admin/src/helpers/response.js | 27 +++++++ client/coral-admin/src/index.js | 1 - client/coral-admin/src/reducers/community.js | 47 +++++++++++ client/coral-admin/src/reducers/index.js | 6 +- client/coral-admin/src/translations.js | 8 ++ models/user.js | 21 +++++ routes/api/index.js | 1 + routes/api/user/index.js | 63 +++++++++++++++ webpack.config.dev.js | 6 ++ webpack.config.js | 2 +- 28 files changed, 517 insertions(+), 33 deletions(-) create mode 100644 client/coral-admin/src/actions/community.js delete mode 100644 client/coral-admin/src/components/Page.js create mode 100644 client/coral-admin/src/components/ui/Layout.css create mode 100644 client/coral-admin/src/constants/community.js create mode 100644 client/coral-admin/src/containers/Community/Community.css create mode 100644 client/coral-admin/src/containers/Community/Community.js create mode 100644 client/coral-admin/src/containers/Community/CommunityContainer.js create mode 100644 client/coral-admin/src/containers/Community/Loading.js create mode 100644 client/coral-admin/src/containers/Community/NoResults.js create mode 100644 client/coral-admin/src/containers/Community/Pager.css create mode 100644 client/coral-admin/src/containers/Community/Pager.js create mode 100644 client/coral-admin/src/containers/Community/Table.js delete mode 100644 client/coral-admin/src/containers/CommunityContainer.js create mode 100644 client/coral-admin/src/helpers/response.js create mode 100644 client/coral-admin/src/reducers/community.js create mode 100644 routes/api/user/index.js diff --git a/.eslintignore b/.eslintignore index 1521c8b76..53c37a166 100644 --- a/.eslintignore +++ b/.eslintignore @@ -1 +1 @@ -dist +dist \ No newline at end of file diff --git a/client/coral-admin/src/AppRouter.js b/client/coral-admin/src/AppRouter.js index d1b25d0f1..5d21fc819 100644 --- a/client/coral-admin/src/AppRouter.js +++ b/client/coral-admin/src/AppRouter.js @@ -4,7 +4,7 @@ import {Router, Route, IndexRoute, browserHistory} from 'react-router'; import ModerationQueue from 'containers/ModerationQueue'; import CommentStream from 'containers/CommentStream'; import Configure from 'containers/Configure'; -import CommunityContainer from 'containers/CommunityContainer'; +import CommunityContainer from 'containers/Community/CommunityContainer'; import LayoutContainer from 'containers/LayoutContainer'; const routes = ( diff --git a/client/coral-admin/src/actions/community.js b/client/coral-admin/src/actions/community.js new file mode 100644 index 000000000..06c8ab0f6 --- /dev/null +++ b/client/coral-admin/src/actions/community.js @@ -0,0 +1,42 @@ +import qs from 'qs'; + +import { + FETCH_COMMENTERS_REQUEST, + FETCH_COMMENTERS_SUCCESS, + FETCH_COMMENTERS_FAILURE, + SORT_UPDATE, + COMMENTERS_NEW_PAGE +} from '../constants/community'; + +import {base, getInit, handleResp} from '../helpers/response'; + +export const fetchCommenters = (query = {}) => dispatch => { + dispatch(requestFetchCommenters()); + fetch(`${base}/user?${qs.stringify(query)}`, getInit('GET')) + .then(handleResp) + .then(({result, page, count, limit, totalPages}) => + dispatch({ + type: FETCH_COMMENTERS_SUCCESS, + commenters: result, + page, + count, + limit, + totalPages + }) + ) + .catch(error => dispatch({type: FETCH_COMMENTERS_FAILURE, error})); +}; + +const requestFetchCommenters = () => ({ + type: FETCH_COMMENTERS_REQUEST +}); + +export const updateSorting = sort => ({ + type: SORT_UPDATE, + sort +}); + +export const newPage = () => ({ + type: COMMENTERS_NEW_PAGE +}); + diff --git a/client/coral-admin/src/components/Page.js b/client/coral-admin/src/components/Page.js deleted file mode 100644 index 845c10cb8..000000000 --- a/client/coral-admin/src/components/Page.js +++ /dev/null @@ -1,12 +0,0 @@ -import React from 'react'; -import {Layout} from 'react-mdl'; -import 'material-design-lite'; -import Header from 'components/Header'; - -export default (props) => ( - -
- {props.children} -
-
-); diff --git a/client/coral-admin/src/components/ui/Header.js b/client/coral-admin/src/components/ui/Header.js index b7c654bd4..86e622672 100644 --- a/client/coral-admin/src/components/ui/Header.js +++ b/client/coral-admin/src/components/ui/Header.js @@ -9,6 +9,9 @@ export default () => ( Moderate Community Configure + + {`v${process.env.VERSION}`} + ); diff --git a/client/coral-admin/src/components/ui/Layout.css b/client/coral-admin/src/components/ui/Layout.css new file mode 100644 index 000000000..779dec8b9 --- /dev/null +++ b/client/coral-admin/src/components/ui/Layout.css @@ -0,0 +1,4 @@ +.layout { + max-width: 1170px; + margin: 0 auto; +} \ No newline at end of file diff --git a/client/coral-admin/src/components/ui/Layout.js b/client/coral-admin/src/components/ui/Layout.js index e80d55c30..3e1b9cf2d 100644 --- a/client/coral-admin/src/components/ui/Layout.js +++ b/client/coral-admin/src/components/ui/Layout.js @@ -2,11 +2,14 @@ import React from 'react'; import {Layout as LayoutMDL} from 'react-mdl'; import Header from './Header'; import Drawer from './Drawer'; +import styles from './Layout.css'; export const Layout = ({children}) => (
- {children} +
+ {children} +
); diff --git a/client/coral-admin/src/constants/community.js b/client/coral-admin/src/constants/community.js new file mode 100644 index 000000000..e628a14d6 --- /dev/null +++ b/client/coral-admin/src/constants/community.js @@ -0,0 +1,5 @@ +export const FETCH_COMMENTERS_REQUEST = 'FETCH_COMMENTERS_REQUEST'; +export const FETCH_COMMENTERS_SUCCESS = 'FETCH_COMMENTERS_SUCCESS'; +export const FETCH_COMMENTERS_FAILURE = 'FETCH_COMMENTERS_FAILURE'; +export const SORT_UPDATE = 'SORT_UPDATE'; +export const COMMENTERS_NEW_PAGE = 'COMMENTERS_NEW_PAGE'; diff --git a/client/coral-admin/src/containers/Community/Community.css b/client/coral-admin/src/containers/Community/Community.css new file mode 100644 index 000000000..63148da7e --- /dev/null +++ b/client/coral-admin/src/containers/Community/Community.css @@ -0,0 +1,22 @@ +.dataTable { + width: 100%; +} + +.roleButton { + display: block; +} + +.searchInput { + display: block; + padding-left: 40px; + /*border: none;*/ +} + +.searchBox { + /*border: 1px solid rgba(0,0,0,.12);*/ + background: white; +} + +.email { + display: block; +} \ No newline at end of file diff --git a/client/coral-admin/src/containers/Community/Community.js b/client/coral-admin/src/containers/Community/Community.js new file mode 100644 index 000000000..8e0b955a4 --- /dev/null +++ b/client/coral-admin/src/containers/Community/Community.js @@ -0,0 +1,68 @@ +import React from 'react'; +import I18n from 'coral-framework/i18n/i18n'; +import translations from '../../translations'; +import {Grid, Cell} from 'react-mdl'; + +import styles from './Community.css'; +import Table from './Table'; +import Loading from './Loading'; +import NoResults from './NoResults'; +import Pager from './Pager'; + +const lang = new I18n(translations); + +const tableHeaders = [ + { + title: lang.t('community.username_and_email'), + field: 'displayName' + }, + { + title: lang.t('community.account_creation_date'), + field: 'created_at' + } +]; + +const Community = ({isFetching, commenters, ...props}) => { + const hasResults = !isFetching && !!commenters.length; + return ( + + +
+
+ +
+ +
+
+
+
+ + { isFetching && } + { !hasResults && } + { hasResults && + + } + + + + ); +}; + +export default Community; diff --git a/client/coral-admin/src/containers/Community/CommunityContainer.js b/client/coral-admin/src/containers/Community/CommunityContainer.js new file mode 100644 index 000000000..cf67dba4d --- /dev/null +++ b/client/coral-admin/src/containers/Community/CommunityContainer.js @@ -0,0 +1,80 @@ +import React, {Component} from 'react'; +import {connect} from 'react-redux'; +import { + fetchCommenters, + updateSorting, + newPage, +} from '../../actions/community'; + +import Community from './Community'; + +class CommunityContainer extends Component { + constructor(props) { + super(props); + + this.state = { + searchValue: '' + }; + + this.onKeyDownHandler = this.onKeyDownHandler.bind(this); + this.onChangeHandler = this.onChangeHandler.bind(this); + this.onHeaderClickHandler = this.onHeaderClickHandler.bind(this); + this.onNewPageHandler = this.onNewPageHandler.bind(this); + } + + onKeyDownHandler(e) { + if (e.key === 'Enter') { + e.preventDefault(); + this.search(); + } + } + + onChangeHandler(e) { + this.setState({ + searchValue: e.target.value + }); + } + + search(query = {}) { + const {community} = this.props; + + this.props.dispatch(fetchCommenters({ + value: this.state.searchValue, + field: community.get('field'), + asc: community.get('asc'), + ...query + })); + } + + componentDidMount() { + this.search(); + } + + onHeaderClickHandler(sort) { + this.props.dispatch(updateSorting(sort)); + this.search(); + } + + onNewPageHandler(page) { + this.props.dispatch(newPage(page)); + this.search({page}); + } + + render() { + const {searchValue} = this.state; + const {community} = this.props; + return ( + + ); + } +} + +export default connect(({community}) => ({community}))(CommunityContainer); diff --git a/client/coral-admin/src/containers/Community/Loading.js b/client/coral-admin/src/containers/Community/Loading.js new file mode 100644 index 000000000..beda271eb --- /dev/null +++ b/client/coral-admin/src/containers/Community/Loading.js @@ -0,0 +1,7 @@ +import React from 'react'; + +const Loading = () => ( +

Loading results

+); + +export default Loading; diff --git a/client/coral-admin/src/containers/Community/NoResults.js b/client/coral-admin/src/containers/Community/NoResults.js new file mode 100644 index 000000000..9e0663960 --- /dev/null +++ b/client/coral-admin/src/containers/Community/NoResults.js @@ -0,0 +1,9 @@ +import React from 'react'; + +const NoResults = () => ( +
+ No users found with that user name or email address +
+); + +export default NoResults; diff --git a/client/coral-admin/src/containers/Community/Pager.css b/client/coral-admin/src/containers/Community/Pager.css new file mode 100644 index 000000000..82e96f727 --- /dev/null +++ b/client/coral-admin/src/containers/Community/Pager.css @@ -0,0 +1,10 @@ +.li { + display: inline-block; + margin-right: 5px; + padding: 0; + min-width: 30px; +} + +.current { + background: #e3edf3; +} \ No newline at end of file diff --git a/client/coral-admin/src/containers/Community/Pager.js b/client/coral-admin/src/containers/Community/Pager.js new file mode 100644 index 000000000..3184cfca0 --- /dev/null +++ b/client/coral-admin/src/containers/Community/Pager.js @@ -0,0 +1,45 @@ +import React, {PropTypes} from 'react'; +import styles from './Pager.css'; + +const Rows = (curr, total, onClickHandler) => Array.from(Array(total)).map((e, i) => +
  • onClickHandler(i + 1)}> + {i + 1} +
  • +); + +const Pager = ({totalPages, page, onNewPageHandler}) => ( +
    +
      + { + (totalPages > page) ? +
    • onNewPageHandler(page - 1)}> + Prev +
    • + : + null + } + {Rows(page, totalPages, onNewPageHandler)} + { + (page < totalPages) ? +
    • onNewPageHandler(page + 1)}> + Next +
    • + : + null + } +
    +
    +); + +Pager.propTypes = { + totalPages: PropTypes.number.isRequired, + page: PropTypes.number.isRequired, +}; + +export default Pager; + diff --git a/client/coral-admin/src/containers/Community/Table.js b/client/coral-admin/src/containers/Community/Table.js new file mode 100644 index 000000000..a15a88723 --- /dev/null +++ b/client/coral-admin/src/containers/Community/Table.js @@ -0,0 +1,34 @@ +import React from 'react'; +import styles from './Community.css'; + +const Table = ({headers, data, onHeaderClickHandler}) => ( +
    + + + {headers.map((header, i) =>( + + ))} + + + + {data.map((row, i)=> ( + + + + + ))} + +
    onHeaderClickHandler({field: header.field})}> + {header.title} +
    + {row.displayName} + {row.profiles.map(({id}) => id)} + + {row.created_at} +
    +); + +export default Table; diff --git a/client/coral-admin/src/containers/CommunityContainer.js b/client/coral-admin/src/containers/CommunityContainer.js deleted file mode 100644 index b936edcec..000000000 --- a/client/coral-admin/src/containers/CommunityContainer.js +++ /dev/null @@ -1,11 +0,0 @@ -import React, {Component} from 'react'; - -export default class CommunityContainer extends Component { - render() { - return ( -
    -

    Community

    -
    - ); - } -} diff --git a/client/coral-admin/src/containers/LayoutContainer.js b/client/coral-admin/src/containers/LayoutContainer.js index 50f54a631..921af251d 100644 --- a/client/coral-admin/src/containers/LayoutContainer.js +++ b/client/coral-admin/src/containers/LayoutContainer.js @@ -1,18 +1,19 @@ -import React, {Component}from 'react'; +import React, {Component} from 'react'; import {connect} from 'react-redux'; import {Layout} from '../components/ui/Layout'; class LayoutContainer extends Component { render () { - return ; + return ; } } LayoutContainer.propTypes = {}; -const mapStateToProps = () => ({data: {}}); +const mapStateToProps = () => ({}); const mapDispatchToProps = (dispatch) => ({dispatch}); export default connect(mapStateToProps, mapDispatchToProps)(LayoutContainer); + diff --git a/client/coral-admin/src/helpers/response.js b/client/coral-admin/src/helpers/response.js new file mode 100644 index 000000000..439ba5a8a --- /dev/null +++ b/client/coral-admin/src/helpers/response.js @@ -0,0 +1,27 @@ +export const base = '/api/v1'; + +export const getInit = (method, body) => { + const headers = new Headers({ + 'Content-Type': 'application/json', + 'Accept': 'application/json' + }); + + const init = {method, headers}; + if (method.toLowerCase() !== 'get') { + init.body = JSON.stringify(body); + } + + return init; +}; + +export const handleResp = res => { + if (res.status === 401) { + throw new Error('Not Authorized to make this request'); + } else if (res.status > 399) { + throw new Error('Error! Status ', res.status); + } else if (res.status === 204) { + return res.text(); + } else { + return res.json(); + } +}; diff --git a/client/coral-admin/src/index.js b/client/coral-admin/src/index.js index aed9d5748..84d1b84ad 100644 --- a/client/coral-admin/src/index.js +++ b/client/coral-admin/src/index.js @@ -1,4 +1,3 @@ - import React from 'react'; import ReactDOM from 'react-dom'; import App from './components/App'; diff --git a/client/coral-admin/src/reducers/community.js b/client/coral-admin/src/reducers/community.js new file mode 100644 index 000000000..8d5dfd2c3 --- /dev/null +++ b/client/coral-admin/src/reducers/community.js @@ -0,0 +1,47 @@ +import {Map} from 'immutable'; + +import { + FETCH_COMMENTERS_REQUEST, + FETCH_COMMENTERS_FAILURE, + FETCH_COMMENTERS_SUCCESS, + SORT_UPDATE +} from '../constants/community'; + +const initialState = Map({ + community: Map(), + isFetching: false, + error: '', + commenters: [], + field: 'created_at', + asc: false, + totalPages: 0, + page: 0 +}); + +export default function community (state = initialState, action) { + switch (action.type) { + case FETCH_COMMENTERS_REQUEST : + return state + .set('isFetching', true); + case FETCH_COMMENTERS_FAILURE : + return state + .set('isFetching', false) + .set('error', action.error); + case FETCH_COMMENTERS_SUCCESS : { + const {commenters, type, ...rest} = action; // eslint-disable-line + return state + .merge({ + isFetching: false, + error: '', + ...rest + }) + .set('commenters', commenters); // Sets to normal array + } + case SORT_UPDATE : + return state + .set('field', action.sort.field) + .set('asc', !state.get('asc')); + default : + return state; + } +} diff --git a/client/coral-admin/src/reducers/index.js b/client/coral-admin/src/reducers/index.js index 63176ca3b..1b29ced69 100644 --- a/client/coral-admin/src/reducers/index.js +++ b/client/coral-admin/src/reducers/index.js @@ -1,10 +1,12 @@ - import {combineReducers} from 'redux'; import comments from 'reducers/comments'; import settings from 'reducers/settings'; +import community from 'reducers/community'; // Combine all reducers into a main one export default combineReducers({ settings, - comments + comments, + community }); + diff --git a/client/coral-admin/src/translations.js b/client/coral-admin/src/translations.js index 5302c1221..67a8142fd 100644 --- a/client/coral-admin/src/translations.js +++ b/client/coral-admin/src/translations.js @@ -1,5 +1,9 @@ export default { en: { + 'community': { + username_and_email: 'Username and Email', + account_creation_date: 'Account Creation Date' + }, 'modqueue': { 'pending': 'pending', 'rejected': 'rejected', @@ -24,6 +28,10 @@ export default { } }, es: { + 'community': { + username_and_email: 'Usuario y E-mail', + account_creation_date: 'Fecha de creación de la cuenta' + }, 'modqueue': { 'pending': 'pendiente', 'rejected': 'rechazado', diff --git a/models/user.js b/models/user.js index ac642c910..c0a846f1a 100644 --- a/models/user.js +++ b/models/user.js @@ -25,6 +25,11 @@ const UserSchema = new mongoose.Schema({ } }], roles: [String] +}, { + timestamps: { + createdAt: 'created_at', + updatedAt: 'updated_at' + } }); // Add the indixies on the user profile data. @@ -52,6 +57,22 @@ UserSchema.options.toJSON.transform = (doc, ret, options) => { return ret; }; +/** + * toObject overrides to remove the password field from the toObject + * output. + */ +UserSchema.options.toObject = {}; +UserSchema.options.toObject.hide = 'password'; +UserSchema.options.toObject.transform = (doc, ret, options) => { + if (options.hide) { + options.hide.split(' ').forEach((prop) => { + delete ret[prop]; + }); + } + + return ret; +}; + /** * Finds a user given their email address that we have for them in the system * and ensures that the retuned user matches the password passed in as well. diff --git a/routes/api/index.js b/routes/api/index.js index d161cf622..2fb489b88 100644 --- a/routes/api/index.js +++ b/routes/api/index.js @@ -6,5 +6,6 @@ router.use('/asset', require('./asset')); router.use('/comments', require('./comments')); router.use('/settings', require('./settings')); router.use('/stream', require('./stream')); +router.use('/user', require('./user')); module.exports = router; diff --git a/routes/api/user/index.js b/routes/api/user/index.js new file mode 100644 index 000000000..18872eab7 --- /dev/null +++ b/routes/api/user/index.js @@ -0,0 +1,63 @@ +const express = require('express'); +const router = express.Router(); +const User = require('../../../models/user'); + +router.get('/', (req, res, next) => { + const { + value = '', + field = 'created_at', + page = 1, + asc = 'false', + limit = 50 // Total Per Page + } = req.query; + + let q = { + $or: [ + { + 'displayName': { + $regex: new RegExp(`^${value}`), + $options: 'i' + }, + 'profiles': { + $elemMatch: { + id: { + $regex: new RegExp(`^${value}`), + $options: 'i' + }, + provider: 'local' + } + } + } + ] + }; + + Promise.all([ + User.find(q) + .sort({[field]: (asc === 'true') ? 1 : -1}) + .skip((page - 1) * limit) + .limit(limit), + User.count() + ]) + .then(([data, count]) => { + const users = data.map((user) => { + const {displayName, created_at} = user; + return { + displayName, + created_at, + profiles: user.toObject().profiles + }; + }); + + res.json({ + result: users, + limit: Number(limit), + count, + page: Number(page), + totalPages: Math.ceil(count / limit) + }); + + }) + .catch(next); +}); + +module.exports = router; diff --git a/webpack.config.dev.js b/webpack.config.dev.js index e2c92b396..8e2f253e5 100644 --- a/webpack.config.dev.js +++ b/webpack.config.dev.js @@ -82,6 +82,12 @@ module.exports = { precss, new webpack.ProvidePlugin({ 'fetch': 'imports?this=>global!exports?global.fetch!whatwg-fetch' + }), + new webpack.DefinePlugin({ + 'process.env': { + 'NODE_ENV': `"${'development'}"`, + 'VERSION': `"${require('./package.json').version}"` + } }) ], resolve: { diff --git a/webpack.config.js b/webpack.config.js index 434da160d..bf602733f 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -8,7 +8,7 @@ devConfig.plugins = devConfig.plugins.concat([ new webpack.DefinePlugin({ 'process.env': { 'NODE_ENV': `"${'production'}"`, - 'VERSION': `"${require('./package.json')}"` + 'VERSION': `"${require('./package.json').version}"` } }), new webpack.optimize.UglifyJsPlugin({