From b01db3297d02646b2d714a45f37cbdb7ba743f7a Mon Sep 17 00:00:00 2001 From: gaba Date: Thu, 23 Feb 2017 16:37:16 -0800 Subject: [PATCH 01/20] Adds menu for Community. Rename community to people. Adds account flagged tabs. --- client/coral-admin/src/AppRouter.js | 17 +- .../Community/CommunityContainer.js | 49 ++- .../containers/Community/CommunityLayout.js | 9 + .../containers/Community/FlaggedAccounts.js | 19 + .../Community/{Community.js => People.js} | 4 +- .../Community/components/CommunityMenu.js | 31 ++ .../Community/components/styles.css | 336 ++++++++++++++++++ .../ModerationQueue/ModerationContainer.js | 1 - client/coral-admin/src/translations.json | 8 +- 9 files changed, 456 insertions(+), 18 deletions(-) create mode 100644 client/coral-admin/src/containers/Community/CommunityLayout.js create mode 100644 client/coral-admin/src/containers/Community/FlaggedAccounts.js rename client/coral-admin/src/containers/Community/{Community.js => People.js} (95%) create mode 100644 client/coral-admin/src/containers/Community/components/CommunityMenu.js create mode 100644 client/coral-admin/src/containers/Community/components/styles.css diff --git a/client/coral-admin/src/AppRouter.js b/client/coral-admin/src/AppRouter.js index d96bacfe0..194c2351f 100644 --- a/client/coral-admin/src/AppRouter.js +++ b/client/coral-admin/src/AppRouter.js @@ -5,10 +5,13 @@ import Streams from 'containers/Streams/Streams'; import Configure from 'containers/Configure/Configure'; import LayoutContainer from 'containers/LayoutContainer'; import InstallContainer from 'containers/Install/InstallContainer'; + +import CommunityLayout from 'containers/Community/CommunityLayout'; import CommunityContainer from 'containers/Community/CommunityContainer'; import ModerationLayout from 'containers/ModerationQueue/ModerationLayout'; import ModerationContainer from 'containers/ModerationQueue/ModerationContainer'; + import Dashboard from 'containers/Dashboard/Dashboard'; const routes = ( @@ -16,11 +19,23 @@ const routes = ( - + + {/* Community Routes */} + + + + + + + + + + + {/* Moderation Routes */} diff --git a/client/coral-admin/src/containers/Community/CommunityContainer.js b/client/coral-admin/src/containers/Community/CommunityContainer.js index e4263cc06..56e4e7762 100644 --- a/client/coral-admin/src/containers/Community/CommunityContainer.js +++ b/client/coral-admin/src/containers/Community/CommunityContainer.js @@ -6,7 +6,10 @@ import { newPage, } from '../../actions/community'; -import Community from './Community'; +import CommunityMenu from './components/CommunityMenu'; + +import People from './People'; +import FlaggedAccounts from './FlaggedAccounts'; class CommunityContainer extends Component { constructor(props) { @@ -60,19 +63,41 @@ class CommunityContainer extends Component { this.search({page}); } - render() { - const {searchValue} = this.state; + getTabContent(searchValue) { const {community} = this.props; + const activeTab = this.props.route.path === ':id' ? 'flagged' : this.props.route.path; + + if (activeTab === 'people') { + return ( + + ); + } + return ( - + + ); + } + + render() { + const {searchValue, activeTab} = this.state; + + const tab = this.getTabContent(activeTab, searchValue, this.props); + + return ( +
+ +
+ { tab } +
+
); } } diff --git a/client/coral-admin/src/containers/Community/CommunityLayout.js b/client/coral-admin/src/containers/Community/CommunityLayout.js new file mode 100644 index 000000000..d6022fed9 --- /dev/null +++ b/client/coral-admin/src/containers/Community/CommunityLayout.js @@ -0,0 +1,9 @@ +import React from 'react'; + +const CommunityLayout = props => ( +
+ {props.children} +
+); + +export default CommunityLayout; diff --git a/client/coral-admin/src/containers/Community/FlaggedAccounts.js b/client/coral-admin/src/containers/Community/FlaggedAccounts.js new file mode 100644 index 000000000..4f4df8edc --- /dev/null +++ b/client/coral-admin/src/containers/Community/FlaggedAccounts.js @@ -0,0 +1,19 @@ +import React from 'react'; + +// import I18n from 'coral-framework/modules/i18n/i18n'; +// import translations from 'coral-admin/src/translations.json'; + +import styles from './Community.css'; + +// import EmptyCard from '../../components/EmptyCard'; +// const lang = new I18n(translations); + +const FlaggedAccounts = () => { + return ( +
+ Flagged Accounts +
+ ); +}; + +export default FlaggedAccounts; diff --git a/client/coral-admin/src/containers/Community/Community.js b/client/coral-admin/src/containers/Community/People.js similarity index 95% rename from client/coral-admin/src/containers/Community/Community.js rename to client/coral-admin/src/containers/Community/People.js index 08a962c34..b91da0fee 100644 --- a/client/coral-admin/src/containers/Community/Community.js +++ b/client/coral-admin/src/containers/Community/People.js @@ -29,7 +29,7 @@ const tableHeaders = [ } ]; -const Community = ({isFetching, commenters, ...props}) => { +const People = ({isFetching, commenters, ...props}) => { const hasResults = !isFetching && !!commenters.length; return (
@@ -73,4 +73,4 @@ const Community = ({isFetching, commenters, ...props}) => { ); }; -export default Community; +export default People; diff --git a/client/coral-admin/src/containers/Community/components/CommunityMenu.js b/client/coral-admin/src/containers/Community/components/CommunityMenu.js new file mode 100644 index 000000000..9a51798ec --- /dev/null +++ b/client/coral-admin/src/containers/Community/components/CommunityMenu.js @@ -0,0 +1,31 @@ +import React from 'react'; + +import styles from './styles.css'; + +import I18n from 'coral-framework/modules/i18n/i18n'; +import translations from 'coral-admin/src/translations.json'; + +import {Link} from 'react-router'; + +const lang = new I18n(translations); + +const CommunityMenu = () => { + const flaggedPath = '/admin/community/flagged'; + const peoplePath = '/admin/community/people'; + return ( +
+
+
+ + {lang.t('community.flaggedaccounts')} + + + {lang.t('community.people')} + +
+
+
+ ); +}; + +export default CommunityMenu; diff --git a/client/coral-admin/src/containers/Community/components/styles.css b/client/coral-admin/src/containers/Community/components/styles.css new file mode 100644 index 000000000..e2fd31911 --- /dev/null +++ b/client/coral-admin/src/containers/Community/components/styles.css @@ -0,0 +1,336 @@ +@custom-media --big-viewport (min-width: 780px); + +.listContainer { + max-width: 860px; + margin: 0 auto; +} + +.tabBar { + background-color: rgba(44, 44, 44, 0.89); + z-index: 5; +} + +.tab { + flex: 1; + color: white; + text-transform: capitalize; + font-weight: 500; + font-size: 15px; + letter-spacing: 1px; + transition: border-bottom 200ms; +} + +.active { + color: white; + box-sizing: border-box; + border-bottom: solid 5px #F36451; +} + +.active > span { + color: white; +} + +.active:after { + background: transparent !important; +} + +.showShortcuts { + position: absolute; + right: 130px; + display: flex; + align-items: center; + font-size: 13px; + +span { + margin-left: 7px; +} +} + +@media (--big-viewport) { + .tab { + flex: none; + } +} + +.notFound { + position: relative; + margin: 20px auto; + text-align: center; + padding: 68px 45px; + vertical-align: middle; + min-width: 500px; + + a { + color: rgb(244, 126, 107); + font-weight: 500; + + &.goToStreams { + position: absolute; + right: 10px; + bottom: 10px; + } + } +} + +.header { + background-color: #2c2c2c; + color: white; + margin-bottom: -1px; + + .settingsButton { + i { + vertical-align: middle; + margin-left: 10px; + margin-top: -4px; + } + } + + .moderateAsset { + a { + -webkit-box-flex: 1; + -ms-flex: 1; + flex: 1; + color: white; + text-transform: capitalize; + font-weight: 500; + font-size: 15px; + letter-spacing: 1px; + transition: opacity 200ms; + opacity: 1; + + &:hover { + opacity: .8; + cursor: pointer; + } + + &:first-child { + text-align: left; + } + + &:nth-child(2) { + text-align: center; + } + + &:last-child { + text-align: right; + } + } + } +} + + +@custom-media --big-viewport (min-width: 780px); + +.list { + padding: 8px 0; + list-style: none; + display: block; + + &.singleView .listItem { + display: none; + } + + &.singleView .listItem.activeItem { + display: block; + height: 100%; + font-size: 1.5em; + line-height: 1.5em; + border: none; + + .actions { + position: fixed; + bottom: 60px; + left: 25%; + margin: 0 auto; + display: flex; + justify-content: space-around; + width: 50%; + margin: 0; + } + + .actionButton { + transform: scale(1.4); + } + } +} + +.listItem { + border-bottom: 1px solid #e0e0e0; + font-size: 16px; + width: 100%; + max-width: 660px; + min-width: 400px; + margin: 0 auto; + padding: 16px 14px; + position: relative; + transition: box-shadow 200ms; + margin-top: 0; + + + &:hover { + box-shadow: 0 3px 6px rgba(0,0,0,0.16), 0 3px 6px rgba(0,0,0,0.23); + } + + &:last-child { + border-bottom: none; + } + + .context { + a { + color: #f36451; + text-decoration: underline; + float: right; + } + } + + .sideActions { + position: absolute; + right: 0; + height: 100%; + top: 0; + padding: 40px 18px; + box-sizing: border-box; + } + + .itemHeader { + display: flex; + align-items: center; + justify-content: space-between; + + .author { + min-width: 230px; + display: flex; + align-items: center; + } + } + + .itemBody { + display: flex; + justify-content: space-between; + } + + .avatar { + margin-right: 16px; + height: 40px; + width: 40px; + border-radius: 50%; + background-color: #757575; + font-size: 40px; + color: #fff; + } + + .created { + color: #666; + font-size: 13px; + margin-left: 40px; + } + + .actionButton { + transform: scale(.8); + margin: 0; + } + + .body { + margin-top: 0px; + flex: 1; + font-size: 0.88em; + color: black; + max-width: 500px; + word-wrap: break-word; + } + + .flagged { + color: rgba(255, 0, 0, .5); + padding-top: 15px; + padding-left: 10px; + } + + .flagCount{ + font-size: 12px; + color: #d32f2f; + } + +} + +.empty { + color: #444; + margin-top: 50px; + text-align: center; +} + + +@media (--big-viewport) { + .listItem { + border: 1px solid #e0e0e0; + margin-bottom: 30px; + + &:last-child { + border-bottom: 1px solid #e0e0e0; + } + + &.activeItem { + border: 2px solid #333; + } + } + +} + +.hasLinks { + color: #f00; + text-align: right; + display: flex; + align-items: center; + + i { + margin-right: 5px; + } +} + +.banned { + color: #f00; + text-align: left; + display: flex; + align-items: center; + + i { + margin-right: 5px; + } +} + +.ban { + display: block; + text-align: center; + margin-top: 5px; +} + +.Comment { + .moderateArticle { + font-size: 12px; + a { + display: inline-block; + color: #679af3; + text-decoration: none; + font-size: 1em; + font-weight: 400; + letter-spacing: .5px; + font-size: 12px; + margin-left: 10px; + + &:hover { + text-decoration: underline; + opacity: .9; + cursor: pointer; + } + } + } +} + +.flagBox { + max-width: 480px; + border-top: 1px solid rgba(66, 66, 66, 0.12); + h3 { + font-size: 14px; + margin: 0; + font-weight: 500; + } +} diff --git a/client/coral-admin/src/containers/ModerationQueue/ModerationContainer.js b/client/coral-admin/src/containers/ModerationQueue/ModerationContainer.js index f685ac094..b1a44533e 100644 --- a/client/coral-admin/src/containers/ModerationQueue/ModerationContainer.js +++ b/client/coral-admin/src/containers/ModerationQueue/ModerationContainer.js @@ -54,7 +54,6 @@ class ModerationContainer extends Component { } if (data.error) { - console.log(data); return
Error
; } diff --git a/client/coral-admin/src/translations.json b/client/coral-admin/src/translations.json index 7219a22ca..e99a08a45 100644 --- a/client/coral-admin/src/translations.json +++ b/client/coral-admin/src/translations.json @@ -13,7 +13,9 @@ "active": "Active", "banned": "Banned", "banned-user": "Banned User", - "loading": "Loading results" + "loading": "Loading results", + "flaggedaccounts": "Account Flags", + "people": "People" }, "modqueue": { "likes": "likes", @@ -145,7 +147,9 @@ "active": "Activa", "banned": "Suspendido", "banned-user": "Usuario Suspendido", - "loading": "Cargando resultados" + "loading": "Cargando resultados", + "flaggedaccounts": "Cuentas Reportadas", + "people": "Gente" }, "modqueue": { "likes": "gustos", From ad1e640b8775d6b8bccdc301081b1e9b409df382 Mon Sep 17 00:00:00 2001 From: gaba Date: Thu, 23 Feb 2017 16:44:30 -0800 Subject: [PATCH 02/20] Adds emptystate. --- .../containers/Community/FlaggedAccounts.js | 22 ++++++++++++++----- client/coral-admin/src/translations.json | 6 +++-- 2 files changed, 20 insertions(+), 8 deletions(-) diff --git a/client/coral-admin/src/containers/Community/FlaggedAccounts.js b/client/coral-admin/src/containers/Community/FlaggedAccounts.js index 4f4df8edc..b84de197f 100644 --- a/client/coral-admin/src/containers/Community/FlaggedAccounts.js +++ b/client/coral-admin/src/containers/Community/FlaggedAccounts.js @@ -1,17 +1,27 @@ import React from 'react'; -// import I18n from 'coral-framework/modules/i18n/i18n'; -// import translations from 'coral-admin/src/translations.json'; +import I18n from 'coral-framework/modules/i18n/i18n'; +import translations from 'coral-admin/src/translations.json'; +const lang = new I18n(translations); import styles from './Community.css'; -// import EmptyCard from '../../components/EmptyCard'; -// const lang = new I18n(translations); +import Loading from './Loading'; +import EmptyCard from '../../components/EmptyCard'; + +const FlaggedAccounts = ({isFetching}) => { + const hasResults = false; // !isFetching && !!commenters.length; -const FlaggedAccounts = () => { return (
- Flagged Accounts +
+ { isFetching && } + { + hasResults + ?
+ : {lang.t('community.no-flagged-accounts')} + } +
); }; diff --git a/client/coral-admin/src/translations.json b/client/coral-admin/src/translations.json index e99a08a45..f83d64116 100644 --- a/client/coral-admin/src/translations.json +++ b/client/coral-admin/src/translations.json @@ -15,7 +15,8 @@ "banned-user": "Banned User", "loading": "Loading results", "flaggedaccounts": "Account Flags", - "people": "People" + "people": "People", + "no-flagged-accounts": "No flagged accounts in the queue." }, "modqueue": { "likes": "likes", @@ -149,7 +150,8 @@ "banned-user": "Usuario Suspendido", "loading": "Cargando resultados", "flaggedaccounts": "Cuentas Reportadas", - "people": "Gente" + "people": "Gente", + "no-flagged-accounts": "No hay ninguna cuenta reportada." }, "modqueue": { "likes": "gustos", From 7557592dc4bb10b78685f13d33fbe6de1fc86d21 Mon Sep 17 00:00:00 2001 From: gaba Date: Fri, 24 Feb 2017 16:06:18 -0800 Subject: [PATCH 03/20] Get more data into the store. --- client/coral-admin/src/actions/community.js | 35 +++++++-- client/coral-admin/src/constants/comments.js | 1 - client/coral-admin/src/constants/community.js | 4 ++ client/coral-admin/src/constants/users.js | 1 - .../src/containers/Community/Community.css | 6 ++ .../Community/CommunityContainer.js | 29 +++++--- .../containers/Community/FlaggedAccounts.js | 29 +++++--- .../src/containers/Community/People.js | 2 +- .../src/containers/Community/Table.js | 2 +- client/coral-admin/src/reducers/community.js | 72 +++++++++++++------ client/coral-admin/src/translations.json | 2 +- 11 files changed, 131 insertions(+), 52 deletions(-) diff --git a/client/coral-admin/src/actions/community.js b/client/coral-admin/src/actions/community.js index c2d738460..030ae1c0c 100644 --- a/client/coral-admin/src/actions/community.js +++ b/client/coral-admin/src/actions/community.js @@ -7,18 +7,21 @@ import { SORT_UPDATE, COMMENTERS_NEW_PAGE, SET_ROLE, - SET_COMMENTER_STATUS + SET_COMMENTER_STATUS, + FETCH_FLAGGED_COMMENTERS_REQUEST, + FETCH_FLAGGED_COMMENTERS_SUCCESS, + FETCH_FLAGGED_COMMENTERS_FAILURE } from '../constants/community'; import coralApi from '../../../coral-framework/helpers/response'; -export const fetchCommenters = (query = {}) => dispatch => { - dispatch(requestFetchCommenters()); +export const fetchAccounts = (query = {}) => dispatch => { + dispatch(requestFetchAccounts()); coralApi(`/users?${qs.stringify(query)}`) .then(({result, page, count, limit, totalPages}) => dispatch({ type: FETCH_COMMENTERS_SUCCESS, - commenters: result, + accounts: result, page, count, limit, @@ -28,7 +31,7 @@ export const fetchCommenters = (query = {}) => dispatch => { .catch(error => dispatch({type: FETCH_COMMENTERS_FAILURE, error})); }; -const requestFetchCommenters = () => ({ +const requestFetchAccounts = () => ({ type: FETCH_COMMENTERS_REQUEST }); @@ -55,3 +58,25 @@ export const setCommenterStatus = (id, status) => (dispatch) => { return dispatch({type: SET_COMMENTER_STATUS, id, status}); }); }; + +// Fetch flagged accounts to display in the moderation queue of the community. + +export const fetchFlaggedAccounts = (query = {}) => dispatch => { + dispatch(requestFetchFlaggedAccounts()); + coralApi(`/users?${qs.stringify(query)}`) + .then(({result, page, count, limit, totalPages}) => + dispatch({ + type: FETCH_FLAGGED_COMMENTERS_SUCCESS, + flaggedAccounts: result, + page, + count, + limit, + totalPages + }) + ) + .catch(error => dispatch({type: FETCH_FLAGGED_COMMENTERS_FAILURE, error})); +}; + +const requestFetchFlaggedAccounts = () => ({ + type: FETCH_FLAGGED_COMMENTERS_REQUEST +}); diff --git a/client/coral-admin/src/constants/comments.js b/client/coral-admin/src/constants/comments.js index 35a915a1b..603a3ce00 100644 --- a/client/coral-admin/src/constants/comments.js +++ b/client/coral-admin/src/constants/comments.js @@ -1,6 +1,5 @@ export const SHOW_BANUSER_DIALOG = 'SHOW_BANUSER_DIALOG'; export const HIDE_BANUSER_DIALOG = 'HIDE_BANUSER_DIALOG'; -export const USERS_MODERATION_QUEUE_FETCH_SUCCESS = 'USERS_MODERATION_QUEUE_FETCH_SUCCESS'; export const COMMENTS_MODERATION_QUEUE_FETCH_REQUEST = 'COMMENTS_MODERATION_QUEUE_FETCH_REQUEST'; export const COMMENTS_MODERATION_QUEUE_FETCH_SUCCESS = 'COMMENTS_MODERATION_QUEUE_FETCH_SUCCESS'; export const COMMENT_CREATE_SUCCESS = 'COMMENT_CREATE_SUCCESS'; diff --git a/client/coral-admin/src/constants/community.js b/client/coral-admin/src/constants/community.js index e3fd88a71..5d9fb4ab8 100644 --- a/client/coral-admin/src/constants/community.js +++ b/client/coral-admin/src/constants/community.js @@ -5,3 +5,7 @@ export const SORT_UPDATE = 'SORT_UPDATE'; export const COMMENTERS_NEW_PAGE = 'COMMENTERS_NEW_PAGE'; export const SET_ROLE = 'SET_ROLE'; export const SET_COMMENTER_STATUS = 'SET_COMMENTER_STATUS'; + +export const FETCH_FLAGGED_COMMENTERS_REQUEST = 'FETCH_FLAGGED_COMMENTERS_REQUEST'; +export const FETCH_FLAGGED_COMMENTERS_SUCCESS = 'FETCH_FLAGGED_COMMENTERS_SUCCESS'; +export const FETCH_FLAGGED_COMMENTERS_FAILURE = 'FETCH_FLAGGED_COMMENTERS_FAILURE'; diff --git a/client/coral-admin/src/constants/users.js b/client/coral-admin/src/constants/users.js index 9cd7e2093..6e8d173c0 100644 --- a/client/coral-admin/src/constants/users.js +++ b/client/coral-admin/src/constants/users.js @@ -3,4 +3,3 @@ export const UPDATE_STATUS_SUCCESS = 'UPDATE_STATUS_SUCCESS'; export const UPDATE_STATUS_FAILURE = 'UPDATE_STATUS_FAILURE'; export const USER_EMAIL_FAILURE = 'USER_EMAIL_FAILURE'; export const USERNAME_ENABLE_FAILURE = 'USERNAME_ENABLE_FAILURE'; -export const USERS_MODERATION_QUEUE_FETCH_SUCCESS = 'USERS_MODERATION_QUEUE_FETCH_SUCCESS'; diff --git a/client/coral-admin/src/containers/Community/Community.css b/client/coral-admin/src/containers/Community/Community.css index 6c101fcaf..05a268d5a 100644 --- a/client/coral-admin/src/containers/Community/Community.css +++ b/client/coral-admin/src/containers/Community/Community.css @@ -15,6 +15,12 @@ box-sizing: border-box; } +.mainFlaggedContent { + width: 100%; + padding: 34px 14px; + box-sizing: border-box; +} + .roleButton { display: block; } diff --git a/client/coral-admin/src/containers/Community/CommunityContainer.js b/client/coral-admin/src/containers/Community/CommunityContainer.js index 56e4e7762..abe481677 100644 --- a/client/coral-admin/src/containers/Community/CommunityContainer.js +++ b/client/coral-admin/src/containers/Community/CommunityContainer.js @@ -1,7 +1,7 @@ import React, {Component} from 'react'; import {connect} from 'react-redux'; import { - fetchCommenters, + fetchAccounts, updateSorting, newPage, } from '../../actions/community'; @@ -15,6 +15,8 @@ class CommunityContainer extends Component { constructor(props) { super(props); + console.log('DEBUG CONSTRUCTOR CommunityContainer ', props); + this.state = { searchValue: '' }; @@ -41,10 +43,10 @@ class CommunityContainer extends Component { search(query = {}) { const {community} = this.props; - this.props.dispatch(fetchCommenters({ + this.props.dispatch(fetchAccounts({ value: this.state.searchValue, - field: community.field, - asc: community.asc, + field: community.fieldPeople, + asc: community.ascPeople, ...query })); } @@ -70,19 +72,26 @@ class CommunityContainer extends Component { if (activeTab === 'people') { return ( ); } return ( - + ); } diff --git a/client/coral-admin/src/containers/Community/FlaggedAccounts.js b/client/coral-admin/src/containers/Community/FlaggedAccounts.js index b84de197f..9b218de23 100644 --- a/client/coral-admin/src/containers/Community/FlaggedAccounts.js +++ b/client/coral-admin/src/containers/Community/FlaggedAccounts.js @@ -6,22 +6,29 @@ const lang = new I18n(translations); import styles from './Community.css'; -import Loading from './Loading'; +// import Loading from './Loading'; import EmptyCard from '../../components/EmptyCard'; -const FlaggedAccounts = ({isFetching}) => { - const hasResults = false; // !isFetching && !!commenters.length; +const FlaggedAccounts = ({...props}) => { + const {commenters, isFetching, error, totalPages, page} = props; + const hasResults = !isFetching && !!commenters.length; + + console.log('debug props', props); + console.log('debug commenters', commenters); + console.log('debug error', error); + console.log('debug totalPages', totalPages); + console.log('debug page', page); return (
-
- { isFetching && } - { - hasResults - ?
- : {lang.t('community.no-flagged-accounts')} - } -
+
+ { isFetching && } + { + hasResults + ?
+ : {lang.t('community.no-flagged-accounts')} + } +
); }; diff --git a/client/coral-admin/src/containers/Community/People.js b/client/coral-admin/src/containers/Community/People.js index b91da0fee..b5701b5a4 100644 --- a/client/coral-admin/src/containers/Community/People.js +++ b/client/coral-admin/src/containers/Community/People.js @@ -58,7 +58,7 @@ const People = ({isFetching, commenters, ...props}) => { hasResults ? : {lang.t('community.no-results')} diff --git a/client/coral-admin/src/containers/Community/Table.js b/client/coral-admin/src/containers/Community/Table.js index 480ce72bf..aacc9c82f 100644 --- a/client/coral-admin/src/containers/Community/Table.js +++ b/client/coral-admin/src/containers/Community/Table.js @@ -77,4 +77,4 @@ class Table extends Component { } } -export default connect(state => ({commenters: state.community.get('commenters')}))(Table); +export default connect(state => ({commenters: state.community.get('accounts')}))(Table); diff --git a/client/coral-admin/src/reducers/community.js b/client/coral-admin/src/reducers/community.js index 367e67b2a..9701946f8 100644 --- a/client/coral-admin/src/reducers/community.js +++ b/client/coral-admin/src/reducers/community.js @@ -6,58 +6,88 @@ import { FETCH_COMMENTERS_SUCCESS, SORT_UPDATE, SET_ROLE, - SET_COMMENTER_STATUS + SET_COMMENTER_STATUS, + FETCH_FLAGGED_COMMENTERS_REQUEST, + FETCH_FLAGGED_COMMENTERS_SUCCESS, + FETCH_FLAGGED_COMMENTERS_FAILURE } from '../constants/community'; const initialState = Map({ community: Map(), - isFetching: false, - error: '', - commenters: [], - field: 'created_at', - asc: false, - totalPages: 0, - page: 0 + isFetchingPeople: false, + errorPeople: '', + accounts: [], + fieldPeople: 'created_at', + ascPeople: false, + totalPagesPeople: 0, + pagePeople: 0, + isFetchingFlagged: false, + errorFlagged: '', + flaggedAccounts: [], }); export default function community (state = initialState, action) { switch (action.type) { case FETCH_COMMENTERS_REQUEST : return state - .set('isFetching', true); + .set('isFetchingPeople', true); case FETCH_COMMENTERS_FAILURE : return state - .set('isFetching', false) - .set('error', action.error); + .set('isFetchingPeople', false) + .set('errorPeople', action.error); + case FETCH_COMMENTERS_SUCCESS : { - const {commenters, type, ...rest} = action; // eslint-disable-line + const {accounts, type, page, count, limit, totalPages, ...rest} = action; // eslint-disable-line return state .merge({ - isFetching: false, - error: '', + isFetchingPeople: false, + errorPeople: '', + pagePeople: page, + countPeople: count, + limitPeople: limit, + totalPagesPeople: totalPages, ...rest }) - .set('commenters', commenters); // Sets to normal array + .set('accounts', accounts); // Sets to normal array } case SET_ROLE : { - const commenters = state.get('commenters'); + const commenters = state.get('accounts'); const idx = commenters.findIndex(el => el.id === action.id); commenters[idx].roles[0] = action.role; - return state.set('commenters', commenters.map(id => id)); + return state.set('accounts', commenters.map(id => id)); } case SET_COMMENTER_STATUS: { - const commenters = state.get('commenters'); + const commenters = state.get('accounts'); const idx = commenters.findIndex(el => el.id === action.id); commenters[idx].status = action.status; - return state.set('commenters', commenters.map(id => id)); + return state.set('accounts', commenters.map(id => id)); } case SORT_UPDATE : return state - .set('field', action.sort.field) - .set('asc', !state.get('asc')); + .set('fieldPeople', action.sort.field) + .set('ascPeople', !state.get('ascPeople')); + case FETCH_FLAGGED_COMMENTERS_REQUEST : { + return state + .set('isFetchingFlagged', true); + } + case FETCH_FLAGGED_COMMENTERS_SUCCESS : { + const {flaggedAccounts, type, ...rest} = action; // eslint-disable-line + return state + .merge({ + isFetchingFlagged: false, + errorFlagged: '', + ...rest + }) + .set('flaggedAccounts', flaggedAccounts); // Sets to normal array + } + case FETCH_FLAGGED_COMMENTERS_FAILURE : { + return state + .set('isFetchingFlagged', false) + .set('errorFlagged', action.error); + } default : return state; } diff --git a/client/coral-admin/src/translations.json b/client/coral-admin/src/translations.json index f83d64116..409c47a00 100644 --- a/client/coral-admin/src/translations.json +++ b/client/coral-admin/src/translations.json @@ -16,7 +16,7 @@ "loading": "Loading results", "flaggedaccounts": "Account Flags", "people": "People", - "no-flagged-accounts": "No flagged accounts in the queue." + "no-flagged-accounts": "The Account Flags queue is currently empty." }, "modqueue": { "likes": "likes", From da3c812dc132f7161b4bb246c2fd6fc52bf264b6 Mon Sep 17 00:00:00 2001 From: gaba Date: Tue, 28 Feb 2017 21:33:01 -0800 Subject: [PATCH 04/20] Adds graphql for users flagged in the server and client. --- client/coral-admin/src/actions/community.js | 33 +--- .../Community/CommunityContainer.js | 17 +- .../containers/Community/FlaggedAccounts.js | 33 +++- .../Community/UserModerationList.css | 187 ++++++++++++++++++ .../Community}/components/User.js | 53 +++-- .../coral-admin/src/graphql/queries/index.js | 3 + .../queries/modUserFlaggedQuery.graphql | 19 ++ client/coral-admin/src/translations.json | 16 +- graph/loaders/users.js | 48 +++++ graph/resolvers/root_query.js | 16 ++ graph/typeDefs.graphql | 20 +- 11 files changed, 378 insertions(+), 67 deletions(-) create mode 100644 client/coral-admin/src/containers/Community/UserModerationList.css rename client/coral-admin/src/{ => containers/Community}/components/User.js (51%) create mode 100644 client/coral-admin/src/graphql/queries/modUserFlaggedQuery.graphql diff --git a/client/coral-admin/src/actions/community.js b/client/coral-admin/src/actions/community.js index 030ae1c0c..92d10fac9 100644 --- a/client/coral-admin/src/actions/community.js +++ b/client/coral-admin/src/actions/community.js @@ -7,10 +7,7 @@ import { SORT_UPDATE, COMMENTERS_NEW_PAGE, SET_ROLE, - SET_COMMENTER_STATUS, - FETCH_FLAGGED_COMMENTERS_REQUEST, - FETCH_FLAGGED_COMMENTERS_SUCCESS, - FETCH_FLAGGED_COMMENTERS_FAILURE + SET_COMMENTER_STATUS } from '../constants/community'; import coralApi from '../../../coral-framework/helpers/response'; @@ -18,7 +15,7 @@ import coralApi from '../../../coral-framework/helpers/response'; export const fetchAccounts = (query = {}) => dispatch => { dispatch(requestFetchAccounts()); coralApi(`/users?${qs.stringify(query)}`) - .then(({result, page, count, limit, totalPages}) => + .then(({result, page, count, limit, totalPages}) =>{ dispatch({ type: FETCH_COMMENTERS_SUCCESS, accounts: result, @@ -26,8 +23,8 @@ export const fetchAccounts = (query = {}) => dispatch => { count, limit, totalPages - }) - ) + }); + }) .catch(error => dispatch({type: FETCH_COMMENTERS_FAILURE, error})); }; @@ -58,25 +55,3 @@ export const setCommenterStatus = (id, status) => (dispatch) => { return dispatch({type: SET_COMMENTER_STATUS, id, status}); }); }; - -// Fetch flagged accounts to display in the moderation queue of the community. - -export const fetchFlaggedAccounts = (query = {}) => dispatch => { - dispatch(requestFetchFlaggedAccounts()); - coralApi(`/users?${qs.stringify(query)}`) - .then(({result, page, count, limit, totalPages}) => - dispatch({ - type: FETCH_FLAGGED_COMMENTERS_SUCCESS, - flaggedAccounts: result, - page, - count, - limit, - totalPages - }) - ) - .catch(error => dispatch({type: FETCH_FLAGGED_COMMENTERS_FAILURE, error})); -}; - -const requestFetchFlaggedAccounts = () => ({ - type: FETCH_FLAGGED_COMMENTERS_REQUEST -}); diff --git a/client/coral-admin/src/containers/Community/CommunityContainer.js b/client/coral-admin/src/containers/Community/CommunityContainer.js index abe481677..79bfd2123 100644 --- a/client/coral-admin/src/containers/Community/CommunityContainer.js +++ b/client/coral-admin/src/containers/Community/CommunityContainer.js @@ -4,6 +4,7 @@ import { fetchAccounts, updateSorting, newPage, + fetchFlaggedAccounts, } from '../../actions/community'; import CommunityMenu from './components/CommunityMenu'; @@ -12,11 +13,16 @@ import People from './People'; import FlaggedAccounts from './FlaggedAccounts'; class CommunityContainer extends Component { + + // static propTypes = { + // + // // list of actions (approve, reject, ban) associated with the users + // modActions: PropTypes.arrayOf(PropTypes.string).isRequired, + // } + constructor(props) { super(props); - console.log('DEBUG CONSTRUCTOR CommunityContainer ', props); - this.state = { searchValue: '' }; @@ -49,6 +55,11 @@ class CommunityContainer extends Component { asc: community.ascPeople, ...query })); + + } + + componentWillMount() { + this.props.dispatch(fetchFlaggedAccounts()); } componentDidMount() { @@ -88,8 +99,6 @@ class CommunityContainer extends Component { commenters={community.flaggedAccounts} isFetching={community.isFetchingFlagged} error={community.errorFlagged} - totalPages={community.totalPagesFlagged} - page={community.pageFlagged} {...this} /> ); diff --git a/client/coral-admin/src/containers/Community/FlaggedAccounts.js b/client/coral-admin/src/containers/Community/FlaggedAccounts.js index 9b218de23..28fc0e1f9 100644 --- a/client/coral-admin/src/containers/Community/FlaggedAccounts.js +++ b/client/coral-admin/src/containers/Community/FlaggedAccounts.js @@ -6,18 +6,28 @@ const lang = new I18n(translations); import styles from './Community.css'; -// import Loading from './Loading'; +import Loading from './Loading'; import EmptyCard from '../../components/EmptyCard'; +import User from './components/User'; + +// actions={commenter.actions} const FlaggedAccounts = ({...props}) => { - const {commenters, isFetching, error, totalPages, page} = props; - const hasResults = !isFetching && !!commenters.length; + const {commenters, isFetching} = props; + const hasResults = !isFetching && commenters && !!commenters.length; - console.log('debug props', props); - console.log('debug commenters', commenters); - console.log('debug error', error); - console.log('debug totalPages', totalPages); - console.log('debug page', page); + // const menuOptions = { + // 'reject': {status: 'REJECTED', icon: 'close', key: 'r'}, + // 'approve': {status: 'ACCEPTED', icon: 'done', key: 't'}, + // 'ban': {status: 'BANNED', icon: 'not interested'} + // }; + // + // + // onClickAction={this.onClickAction} + // onClickShowBanDialog={this.onClickShowBanDialog} + // acceptCommenter={props.acceptCommenter} + // rejectCommenter={props.rejectCommenter} + // menuOptions={menuOptions} return (
@@ -25,7 +35,12 @@ const FlaggedAccounts = ({...props}) => { { isFetching && } { hasResults - ?
+ ? commenters.map((commenter, index) => { + return ; + }) : {lang.t('community.no-flagged-accounts')} }
diff --git a/client/coral-admin/src/containers/Community/UserModerationList.css b/client/coral-admin/src/containers/Community/UserModerationList.css new file mode 100644 index 000000000..dc0e6b57b --- /dev/null +++ b/client/coral-admin/src/containers/Community/UserModerationList.css @@ -0,0 +1,187 @@ + +@custom-media --big-viewport (min-width: 780px); + +.list { + padding: 8px 0; + list-style: none; + display: block; + + &.singleView .listItem { + display: none; + } + + &.singleView .listItem.activeItem { + display: block; + height: 100%; + font-size: 1.5em; + line-height: 1.5em; + border: none; + + .actions { + position: fixed; + bottom: 60px; + left: 25%; + margin: 0 auto; + display: flex; + justify-content: space-around; + width: 50%; + margin: 0; + } + + .actionButton { + transform: scale(1.4); + } + } +} + +.listItem { + border-bottom: 1px solid #e0e0e0; + font-size: 16px; + width: 100%; + max-width: 660px; + min-width: 400px; + margin: 0 auto; + padding: 16px 14px; + position: relative; + transition: box-shadow 200ms; + + + &:hover { + box-shadow: 0 3px 6px rgba(0,0,0,0.16), 0 3px 6px rgba(0,0,0,0.23); + } + + &:last-child { + border-bottom: none; + } + + .sideActions { + position: absolute; + right: 0; + height: 100%; + top: 0; + padding: 40px 18px; + box-sizing: border-box; + } + + .itemHeader { + display: block; + + .author { + font-size: 24px; + min-width: 50px; + align-items: left; + margin-bottom: 15px; + } + } + + .itemBody { + display: block; + } + + .created { + color: #666; + font-size: 13px; + margin-left: 40px; + } + + .body { + margin-top: 20px; + flex: 1; + font-size: 0.88em; + color: black; + } + + .flagged { + color: rgba(255, 0, 0, .5); + padding-top: 15px; + padding-left: 10px; + } + + .flagCount{ + font-size: 12px; + color: #d32f2f; + } + +} + +.empty { + color: #444; + margin-top: 50px; + text-align: center; +} + + +@media (--big-viewport) { + .listItem { + border: 1px solid #e0e0e0; + margin-bottom: 30px; + + &:last-child { + border-bottom: 1px solid #e0e0e0; + } + + &.activeItem { + border: 2px solid #333; + } + } + +} + +.hasLinks { + color: #f00; + text-align: right; + display: flex; + align-items: center; + + i { + margin-right: 5px; + } +} + +.banned { + color: #f00; + text-align: left; + display: flex; + align-items: center; + + i { + margin-right: 5px; + } +} + +.ban { + display: block; + text-align: center; + margin-top: 5px; +} + +.banButton { + width: 114px; + letter-spacing: 1px; + + i { + vertical-align: middle; + margin-right: 10px; + font-size: 14px; + } +} + + +.actionButton { + transform: scale(.8); + margin: 0; +} + +.flaggedByCount { + display: block; + text-align: left; +} + +.flaggedBy { + display: inline; + padding: 3px; +} + +.flaggedByLabel { + font-weight: bold; +} diff --git a/client/coral-admin/src/components/User.js b/client/coral-admin/src/containers/Community/components/User.js similarity index 51% rename from client/coral-admin/src/components/User.js rename to client/coral-admin/src/containers/Community/components/User.js index 129524993..1f18a186f 100644 --- a/client/coral-admin/src/components/User.js +++ b/client/coral-admin/src/containers/Community/components/User.js @@ -1,15 +1,17 @@ import React from 'react'; -import styles from './ModerationList.css'; +import styles from '../UserModerationList.css'; import I18n from 'coral-framework/modules/i18n/i18n'; -import translations from '../translations.json'; +import translations from '../../../translations.json'; -import {Icon} from 'react-mdl'; -import ActionButton from './ActionButton'; +const lang = new I18n(translations); -// Render a single comment for the list +// import {Icon} from 'react-mdl'; +// import ActionButton from './ActionButton'; + +// Render a single user for the list const User = props => { - const {action, user} = props; + const {user} = props; let userStatus = user.status; // Do not display unless the user status is 'pending' or 'banned'. @@ -19,32 +21,45 @@ const User = props => {
{user.username} -
+
+ +
+
+ flagFlags({ user.actions.length }): + { user.action_summaries.map( + (action, i ) => { + return + {lang.t(`community.${action.reason}`)} ({action.count}) + ; + } + )} +
+
+ {user.actions.map( + (action, i) => { + return + {action.reason} + {/* action.user.username */} + ; + } + )} +
- {props.modActions.map( + {/* props.modActions.map( (action, i) => - - )} + )*/}
-
- {userStatus === 'banned' ? - {lang.t('comment.banned_user')} : null} -
-
-
- {`${action.count} ${action.action_type === 'flag_bio' ? lang.t('user.bio_flags') : lang.t('user.username_flags')}`}
; }; export default User; - -const lang = new I18n(translations); diff --git a/client/coral-admin/src/graphql/queries/index.js b/client/coral-admin/src/graphql/queries/index.js index 3325249e1..81aa24410 100644 --- a/client/coral-admin/src/graphql/queries/index.js +++ b/client/coral-admin/src/graphql/queries/index.js @@ -2,6 +2,7 @@ import {graphql} from 'react-apollo'; import MOST_FLAGS from './mostFlags.graphql'; import MOD_QUEUE_QUERY from './modQueueQuery.graphql'; +import USER_FLAGGED_QUERY from './modUserFlaggedQuery.graphql'; export const mostFlags = graphql(MOST_FLAGS, { options: () => { @@ -28,3 +29,5 @@ export const modQueueQuery = graphql(MOD_QUEUE_QUERY, { }; } }); + +export const modUserFlaggedQuery = graphql(USER_FLAGGED_QUERY); diff --git a/client/coral-admin/src/graphql/queries/modUserFlaggedQuery.graphql b/client/coral-admin/src/graphql/queries/modUserFlaggedQuery.graphql new file mode 100644 index 000000000..55090d8e4 --- /dev/null +++ b/client/coral-admin/src/graphql/queries/modUserFlaggedQuery.graphql @@ -0,0 +1,19 @@ +query Users { + usersFlagged { + id + username + status + roles + actions{ + id + reason + user { + username + } + } + action_summaries { + count + reason + } + } +} diff --git a/client/coral-admin/src/translations.json b/client/coral-admin/src/translations.json index 409c47a00..eae4543ee 100644 --- a/client/coral-admin/src/translations.json +++ b/client/coral-admin/src/translations.json @@ -14,9 +14,13 @@ "banned": "Banned", "banned-user": "Banned User", "loading": "Loading results", - "flaggedaccounts": "Account Flags", + "flaggedaccounts": "Flagged Usernames", "people": "People", - "no-flagged-accounts": "The Account Flags queue is currently empty." + "no-flagged-accounts": "The Account Flags queue is currently empty.", + "This user is impersonating": "Impersonation", + "This looks like an ad/marketing": "Spam/Ads", + "This username is offensive": "Offensive", + "Other": "Other" }, "modqueue": { "likes": "likes", @@ -149,9 +153,13 @@ "banned": "Suspendido", "banned-user": "Usuario Suspendido", "loading": "Cargando resultados", - "flaggedaccounts": "Cuentas Reportadas", + "flaggedaccounts": "Nombres de Usuario Reportados", "people": "Gente", - "no-flagged-accounts": "No hay ninguna cuenta reportada." + "no-flagged-accounts": "No hay ninguna cuenta reportada.", + "This user is impersonating": "Suplantación", + "This looks like an ad/marketing": "Spam/Propaganda", + "This username is offensive": "Ofensivo", + "Other": "Otros" }, "modqueue": { "likes": "gustos", diff --git a/graph/loaders/users.js b/graph/loaders/users.js index 90c661f71..2928e41f3 100644 --- a/graph/loaders/users.js +++ b/graph/loaders/users.js @@ -3,11 +3,58 @@ const DataLoader = require('dataloader'); const util = require('./util'); const UsersService = require('../../services/users'); +const UserModel = require('../../models/user'); const genUserByIDs = (context, ids) => UsersService .findByIdArray(ids) .then(util.singleJoinBy(ids, 'id')); +/** + * Retrieves users based on the passed in query that is filtered by the + * current used passed in via the context. + * @param {Object} context graph context + * @param {Object} query query terms to apply to the users query + */ +const getUsersByQuery = ({user}, {ids, limit, cursor, sort}) => { + + let users = UserModel.find(); + + // Only administrators can search for users + if (user == null || !user.hasRoles('ADMIN')) { + return null; + } + + users = users.find(); + + if (ids) { + users = users.find({ + id: { + $in: ids + } + }); + } + + if (cursor) { + if (sort === 'REVERSE_CHRONOLOGICAL') { + users = users.where({ + created_at: { + $lt: cursor + } + }); + } else { + users = users.where({ + created_at: { + $gt: cursor + } + }); + } + } + + return users + .sort({created_at: sort === 'REVERSE_CHRONOLOGICAL' ? -1 : 1}) + .limit(limit); +}; + /** * Creates a set of loaders based on a GraphQL context. * @param {Object} context the context of the GraphQL request @@ -15,6 +62,7 @@ const genUserByIDs = (context, ids) => UsersService */ module.exports = (context) => ({ Users: { + getByQuery: (query) => getUsersByQuery(context, query), getByID: new DataLoader((ids) => genUserByIDs(context, ids)) } }); diff --git a/graph/resolvers/root_query.js b/graph/resolvers/root_query.js index ced4e65cf..852bc41ad 100644 --- a/graph/resolvers/root_query.js +++ b/graph/resolvers/root_query.js @@ -72,6 +72,22 @@ const RootQuery = { } return user; + }, + + // This endpoint is used for loading the user moderation queues (users whose username has been flagged), + // so hide it in the event that we aren't an admin. + usersFlagged(_, args, {user, loaders: {Users, Actions}}) { + + if (user == null || !user.hasRoles('ADMIN')) { + return null; + } + + return Actions.getByTypes({action_type: 'FLAG', item_type: 'USERS'}) + .then((ids) => { + + // Perform the query using the available resolver. + return Users.getByQuery({ids}); + }); } }; diff --git a/graph/typeDefs.graphql b/graph/typeDefs.graphql index fbdeb4279..bd032196b 100644 --- a/graph/typeDefs.graphql +++ b/graph/typeDefs.graphql @@ -31,10 +31,10 @@ type User { username: String! # Action summaries against the user. - action_summaries: [ActionSummary] + action_summaries: [FlagActionSummary] # Actions completed on the parent. - actions: [Action] + actions: [FlagAction] # the current roles of the user. roles: [USER_ROLES] @@ -60,6 +60,19 @@ type Tag { created_at: Date! } +# UsersQuery allows the ability to query users by a specific methods. +input UsersQuery { + + # Limit the number of results to be returned. + limit: Int = 10 + + # Skip results from the last created_at timestamp. + cursor: Date + + # Sort the results by created_at. + sort: SORT_ORDER = REVERSE_CHRONOLOGICAL +} + ################################################################################ ## Comments ################################################################################ @@ -497,6 +510,9 @@ type RootQuery { # Metrics related to user actions are saturated into the assets returned. The # sort will affect if it will allow metrics(from: Date!, to: Date!, sort: ACTION_TYPE!, limit: Int = 10): [Asset] + + # Users returned based on a query. + usersFlagged(query: UsersQuery): [User] } ################################################################################ From 6dfd3fc4ef9fadfc4afedc3dc6af547b9cc0daa9 Mon Sep 17 00:00:00 2001 From: gaba Date: Tue, 28 Feb 2017 22:10:14 -0800 Subject: [PATCH 05/20] Fix the people tag. --- client/coral-admin/src/actions/community.js | 1 + .../Community/CommunityContainer.js | 46 ++++++++++--------- client/coral-admin/src/reducers/community.js | 29 +----------- 3 files changed, 28 insertions(+), 48 deletions(-) diff --git a/client/coral-admin/src/actions/community.js b/client/coral-admin/src/actions/community.js index 92d10fac9..0674926db 100644 --- a/client/coral-admin/src/actions/community.js +++ b/client/coral-admin/src/actions/community.js @@ -13,6 +13,7 @@ import { import coralApi from '../../../coral-framework/helpers/response'; export const fetchAccounts = (query = {}) => dispatch => { + dispatch(requestFetchAccounts()); coralApi(`/users?${qs.stringify(query)}`) .then(({result, page, count, limit, totalPages}) =>{ diff --git a/client/coral-admin/src/containers/Community/CommunityContainer.js b/client/coral-admin/src/containers/Community/CommunityContainer.js index 79bfd2123..0effb551c 100644 --- a/client/coral-admin/src/containers/Community/CommunityContainer.js +++ b/client/coral-admin/src/containers/Community/CommunityContainer.js @@ -1,10 +1,11 @@ import React, {Component} from 'react'; import {connect} from 'react-redux'; +import {modUserFlaggedQuery} from 'coral-admin/src/graphql/queries'; +import {compose} from 'react-apollo'; import { fetchAccounts, updateSorting, - newPage, - fetchFlaggedAccounts, + newPage } from '../../actions/community'; import CommunityMenu from './components/CommunityMenu'; @@ -33,6 +34,10 @@ class CommunityContainer extends Component { this.onNewPageHandler = this.onNewPageHandler.bind(this); } + componentWillMount() { + this.props.fetchAccounts({}); + } + onKeyDownHandler(e) { if (e.key === 'Enter') { e.preventDefault(); @@ -49,23 +54,15 @@ class CommunityContainer extends Component { search(query = {}) { const {community} = this.props; - this.props.dispatch(fetchAccounts({ + this.props.fetchAccounts({ value: this.state.searchValue, field: community.fieldPeople, asc: community.ascPeople, ...query - })); + }); } - componentWillMount() { - this.props.dispatch(fetchFlaggedAccounts()); - } - - componentDidMount() { - this.search(); - } - onHeaderClickHandler(sort) { this.props.dispatch(updateSorting(sort)); this.search(); @@ -76,9 +73,9 @@ class CommunityContainer extends Component { this.search({page}); } - getTabContent(searchValue) { - const {community} = this.props; - const activeTab = this.props.route.path === ':id' ? 'flagged' : this.props.route.path; + getTabContent(searchValue, props) { + const {community, data} = props; + const activeTab = props.route.path === ':id' ? 'flagged' : props.route.path; if (activeTab === 'people') { return ( @@ -96,18 +93,18 @@ class CommunityContainer extends Component { return ( ); } render() { - const {searchValue, activeTab} = this.state; + const {searchValue} = this.state; - const tab = this.getTabContent(activeTab, searchValue, this.props); + const tab = this.getTabContent(searchValue, this.props); return (
@@ -124,4 +121,11 @@ const mapStateToProps = state => ({ community: state.community.toJS() }); -export default connect(mapStateToProps)(CommunityContainer); +const mapDispatchToProps = dispatch => ({ + fetchAccounts: query => dispatch(fetchAccounts(query)) +}); + +export default compose( + connect(mapStateToProps, mapDispatchToProps), + modUserFlaggedQuery +)(CommunityContainer); diff --git a/client/coral-admin/src/reducers/community.js b/client/coral-admin/src/reducers/community.js index 9701946f8..50641118c 100644 --- a/client/coral-admin/src/reducers/community.js +++ b/client/coral-admin/src/reducers/community.js @@ -6,10 +6,7 @@ import { FETCH_COMMENTERS_SUCCESS, SORT_UPDATE, SET_ROLE, - SET_COMMENTER_STATUS, - FETCH_FLAGGED_COMMENTERS_REQUEST, - FETCH_FLAGGED_COMMENTERS_SUCCESS, - FETCH_FLAGGED_COMMENTERS_FAILURE + SET_COMMENTER_STATUS } from '../constants/community'; const initialState = Map({ @@ -20,10 +17,7 @@ const initialState = Map({ fieldPeople: 'created_at', ascPeople: false, totalPagesPeople: 0, - pagePeople: 0, - isFetchingFlagged: false, - errorFlagged: '', - flaggedAccounts: [], + pagePeople: 0 }); export default function community (state = initialState, action) { @@ -69,25 +63,6 @@ export default function community (state = initialState, action) { return state .set('fieldPeople', action.sort.field) .set('ascPeople', !state.get('ascPeople')); - case FETCH_FLAGGED_COMMENTERS_REQUEST : { - return state - .set('isFetchingFlagged', true); - } - case FETCH_FLAGGED_COMMENTERS_SUCCESS : { - const {flaggedAccounts, type, ...rest} = action; // eslint-disable-line - return state - .merge({ - isFetchingFlagged: false, - errorFlagged: '', - ...rest - }) - .set('flaggedAccounts', flaggedAccounts); // Sets to normal array - } - case FETCH_FLAGGED_COMMENTERS_FAILURE : { - return state - .set('isFetchingFlagged', false) - .set('errorFlagged', action.error); - } default : return state; } From f5ef33d24e734773f2fb2502ec939d0d2f92da84 Mon Sep 17 00:00:00 2001 From: gaba Date: Wed, 1 Mar 2017 12:57:12 -0800 Subject: [PATCH 06/20] Ban Button with graphql mutation. --- .gitignore | 2 + client/coral-admin/src/actions/community.js | 8 +- client/coral-admin/src/constants/community.js | 3 + .../src/containers/Community/Community.css | 187 ++++++++++++++++++ .../Community/CommunityContainer.js | 47 +++-- .../containers/Community/FlaggedAccounts.js | 3 +- .../Community/UserModerationList.css | 187 ------------------ .../Community/components/ActionButton.js | 22 +++ .../Community/components/BanUserDialog.css | 164 +++++++++++++++ .../Community/components/BanUserDialog.js | 53 +++++ .../components/SuspendUserModal.css | 0 .../Community}/components/SuspendUserModal.js | 0 .../containers/Community/components/User.js | 14 +- .../src/graphql/mutations/index.js | 26 ++- client/coral-admin/src/reducers/community.js | 17 +- client/coral-admin/src/translations.json | 16 +- 16 files changed, 534 insertions(+), 215 deletions(-) delete mode 100644 client/coral-admin/src/containers/Community/UserModerationList.css create mode 100644 client/coral-admin/src/containers/Community/components/ActionButton.js create mode 100644 client/coral-admin/src/containers/Community/components/BanUserDialog.css create mode 100644 client/coral-admin/src/containers/Community/components/BanUserDialog.js rename client/coral-admin/src/{ => containers/Community}/components/SuspendUserModal.css (100%) rename client/coral-admin/src/{ => containers/Community}/components/SuspendUserModal.js (100%) diff --git a/.gitignore b/.gitignore index 29f5961b2..fbc3c54d3 100644 --- a/.gitignore +++ b/.gitignore @@ -12,3 +12,5 @@ dump.rdb *.cfg .idea/ coverage/ +.tags +.tags1 diff --git a/client/coral-admin/src/actions/community.js b/client/coral-admin/src/actions/community.js index 0674926db..82cee32f4 100644 --- a/client/coral-admin/src/actions/community.js +++ b/client/coral-admin/src/actions/community.js @@ -7,7 +7,9 @@ import { SORT_UPDATE, COMMENTERS_NEW_PAGE, SET_ROLE, - SET_COMMENTER_STATUS + SET_COMMENTER_STATUS, + SHOW_BANUSER_DIALOG, + HIDE_BANUSER_DIALOG } from '../constants/community'; import coralApi from '../../../coral-framework/helpers/response'; @@ -56,3 +58,7 @@ export const setCommenterStatus = (id, status) => (dispatch) => { return dispatch({type: SET_COMMENTER_STATUS, id, status}); }); }; + +// Ban User Dialog +export const showBanUserDialog = (user) => ({type: SHOW_BANUSER_DIALOG, user}); +export const hideBanUserDialog = (showDialog) => ({type: HIDE_BANUSER_DIALOG, showDialog}); diff --git a/client/coral-admin/src/constants/community.js b/client/coral-admin/src/constants/community.js index 5d9fb4ab8..9ea4568ff 100644 --- a/client/coral-admin/src/constants/community.js +++ b/client/coral-admin/src/constants/community.js @@ -9,3 +9,6 @@ export const SET_COMMENTER_STATUS = 'SET_COMMENTER_STATUS'; export const FETCH_FLAGGED_COMMENTERS_REQUEST = 'FETCH_FLAGGED_COMMENTERS_REQUEST'; export const FETCH_FLAGGED_COMMENTERS_SUCCESS = 'FETCH_FLAGGED_COMMENTERS_SUCCESS'; export const FETCH_FLAGGED_COMMENTERS_FAILURE = 'FETCH_FLAGGED_COMMENTERS_FAILURE'; + +export const SHOW_BANUSER_DIALOG = 'SHOW_BANUSER_DIALOG'; +export const HIDE_BANUSER_DIALOG = 'HIDE_BANUSER_DIALOG'; diff --git a/client/coral-admin/src/containers/Community/Community.css b/client/coral-admin/src/containers/Community/Community.css index 05a268d5a..6460c50d2 100644 --- a/client/coral-admin/src/containers/Community/Community.css +++ b/client/coral-admin/src/containers/Community/Community.css @@ -1,3 +1,5 @@ +@custom-media --big-viewport (min-width: 780px); + .container { padding: 10px; display: flex; @@ -100,3 +102,188 @@ cursor: pointer; } } + +.list { + padding: 8px 0; + list-style: none; + display: block; + + &.singleView .listItem { + display: none; + } + + &.singleView .listItem.activeItem { + display: block; + height: 100%; + font-size: 1.5em; + line-height: 1.5em; + border: none; + + .actions { + position: fixed; + bottom: 60px; + left: 25%; + margin: 0 auto; + display: flex; + justify-content: space-around; + width: 50%; + margin: 0; + } + + .actionButton { + transform: scale(1.4); + } + } +} + +.listItem { + border-bottom: 1px solid #e0e0e0; + font-size: 16px; + width: 100%; + max-width: 660px; + min-width: 400px; + margin: 0 auto; + padding: 16px 14px; + position: relative; + transition: box-shadow 200ms; + + + &:hover { + box-shadow: 0 3px 6px rgba(0,0,0,0.16), 0 3px 6px rgba(0,0,0,0.23); + } + + &:last-child { + border-bottom: none; + } + + .sideActions { + position: absolute; + right: 0; + height: 100%; + top: 0; + padding: 40px 18px; + box-sizing: border-box; + } + + .itemHeader { + display: inline; + + .author { + font-size: 24px; + min-width: 50px; + align-items: left; + margin-bottom: 15px; + } + } + + .itemBody { + display: block; + } + + .created { + color: #666; + font-size: 13px; + margin-left: 40px; + } + + .body { + margin-top: 20px; + flex: 1; + font-size: 0.88em; + color: black; + } + + .flagged { + color: rgba(255, 0, 0, .5); + padding-top: 15px; + padding-left: 10px; + } + + .flagCount{ + font-size: 12px; + color: #d32f2f; + } + +} + +.empty { + color: #444; + margin-top: 50px; + text-align: center; +} + + +@media (--big-viewport) { + .listItem { + border: 1px solid #e0e0e0; + margin-bottom: 30px; + + &:last-child { + border-bottom: 1px solid #e0e0e0; + } + + &.activeItem { + border: 2px solid #333; + } + } + +} + +.hasLinks { + color: #f00; + text-align: right; + display: flex; + align-items: center; + + i { + margin-right: 5px; + } +} + +.banned { + color: #f00; + text-align: left; + display: flex; + align-items: center; + + i { + margin-right: 5px; + } +} + +.ban { + display: block; + text-align: center; + margin-top: 5px; +} + +.banButton { + width: 114px; + letter-spacing: 1px; + + i { + vertical-align: middle; + margin-right: 10px; + font-size: 14px; + } +} + + +.actionButton { + transform: scale(.8); + margin: 0; +} + +.flaggedByCount { + display: block; + text-align: left; +} + +.flaggedBy { + display: inline; + padding: 3px; +} + +.flaggedByLabel { + font-weight: bold; +} diff --git a/client/coral-admin/src/containers/Community/CommunityContainer.js b/client/coral-admin/src/containers/Community/CommunityContainer.js index 0effb551c..7f83909b6 100644 --- a/client/coral-admin/src/containers/Community/CommunityContainer.js +++ b/client/coral-admin/src/containers/Community/CommunityContainer.js @@ -1,26 +1,27 @@ import React, {Component} from 'react'; import {connect} from 'react-redux'; -import {modUserFlaggedQuery} from 'coral-admin/src/graphql/queries'; import {compose} from 'react-apollo'; + +import {modUserFlaggedQuery} from 'coral-admin/src/graphql/queries'; +import {banUser} from '../../graphql/mutations'; + import { fetchAccounts, updateSorting, - newPage + newPage, + showBanUserDialog, + hideBanUserDialog } from '../../actions/community'; import CommunityMenu from './components/CommunityMenu'; +import BanUserDialog from './components/BanUserDialog'; + import People from './People'; import FlaggedAccounts from './FlaggedAccounts'; class CommunityContainer extends Component { - // static propTypes = { - // - // // list of actions (approve, reject, ban) associated with the users - // modActions: PropTypes.arrayOf(PropTypes.string).isRequired, - // } - constructor(props) { super(props); @@ -92,12 +93,23 @@ class CommunityContainer extends Component { } return ( - +
+ + +
); } @@ -122,10 +134,13 @@ const mapStateToProps = state => ({ }); const mapDispatchToProps = dispatch => ({ - fetchAccounts: query => dispatch(fetchAccounts(query)) + fetchAccounts: query => dispatch(fetchAccounts(query)), + showBanUserDialog: (user) => dispatch(showBanUserDialog(user)), + hideBanUserDialog: () => dispatch(hideBanUserDialog(false)) }); export default compose( connect(mapStateToProps, mapDispatchToProps), - modUserFlaggedQuery + modUserFlaggedQuery, + banUser )(CommunityContainer); diff --git a/client/coral-admin/src/containers/Community/FlaggedAccounts.js b/client/coral-admin/src/containers/Community/FlaggedAccounts.js index 28fc0e1f9..a6df24598 100644 --- a/client/coral-admin/src/containers/Community/FlaggedAccounts.js +++ b/client/coral-admin/src/containers/Community/FlaggedAccounts.js @@ -39,7 +39,8 @@ const FlaggedAccounts = ({...props}) => { return ; + index={index} + showBanUserDialog={props.showBanUserDialog}/>; }) : {lang.t('community.no-flagged-accounts')} } diff --git a/client/coral-admin/src/containers/Community/UserModerationList.css b/client/coral-admin/src/containers/Community/UserModerationList.css deleted file mode 100644 index dc0e6b57b..000000000 --- a/client/coral-admin/src/containers/Community/UserModerationList.css +++ /dev/null @@ -1,187 +0,0 @@ - -@custom-media --big-viewport (min-width: 780px); - -.list { - padding: 8px 0; - list-style: none; - display: block; - - &.singleView .listItem { - display: none; - } - - &.singleView .listItem.activeItem { - display: block; - height: 100%; - font-size: 1.5em; - line-height: 1.5em; - border: none; - - .actions { - position: fixed; - bottom: 60px; - left: 25%; - margin: 0 auto; - display: flex; - justify-content: space-around; - width: 50%; - margin: 0; - } - - .actionButton { - transform: scale(1.4); - } - } -} - -.listItem { - border-bottom: 1px solid #e0e0e0; - font-size: 16px; - width: 100%; - max-width: 660px; - min-width: 400px; - margin: 0 auto; - padding: 16px 14px; - position: relative; - transition: box-shadow 200ms; - - - &:hover { - box-shadow: 0 3px 6px rgba(0,0,0,0.16), 0 3px 6px rgba(0,0,0,0.23); - } - - &:last-child { - border-bottom: none; - } - - .sideActions { - position: absolute; - right: 0; - height: 100%; - top: 0; - padding: 40px 18px; - box-sizing: border-box; - } - - .itemHeader { - display: block; - - .author { - font-size: 24px; - min-width: 50px; - align-items: left; - margin-bottom: 15px; - } - } - - .itemBody { - display: block; - } - - .created { - color: #666; - font-size: 13px; - margin-left: 40px; - } - - .body { - margin-top: 20px; - flex: 1; - font-size: 0.88em; - color: black; - } - - .flagged { - color: rgba(255, 0, 0, .5); - padding-top: 15px; - padding-left: 10px; - } - - .flagCount{ - font-size: 12px; - color: #d32f2f; - } - -} - -.empty { - color: #444; - margin-top: 50px; - text-align: center; -} - - -@media (--big-viewport) { - .listItem { - border: 1px solid #e0e0e0; - margin-bottom: 30px; - - &:last-child { - border-bottom: 1px solid #e0e0e0; - } - - &.activeItem { - border: 2px solid #333; - } - } - -} - -.hasLinks { - color: #f00; - text-align: right; - display: flex; - align-items: center; - - i { - margin-right: 5px; - } -} - -.banned { - color: #f00; - text-align: left; - display: flex; - align-items: center; - - i { - margin-right: 5px; - } -} - -.ban { - display: block; - text-align: center; - margin-top: 5px; -} - -.banButton { - width: 114px; - letter-spacing: 1px; - - i { - vertical-align: middle; - margin-right: 10px; - font-size: 14px; - } -} - - -.actionButton { - transform: scale(.8); - margin: 0; -} - -.flaggedByCount { - display: block; - text-align: left; -} - -.flaggedBy { - display: inline; - padding: 3px; -} - -.flaggedByLabel { - font-weight: bold; -} diff --git a/client/coral-admin/src/containers/Community/components/ActionButton.js b/client/coral-admin/src/containers/Community/components/ActionButton.js new file mode 100644 index 000000000..9cd3da6e1 --- /dev/null +++ b/client/coral-admin/src/containers/Community/components/ActionButton.js @@ -0,0 +1,22 @@ +import React from 'react'; +import styles from '../Community.css'; +import BanUserButton from '../../../components/BanUserButton'; +import {FabButton} from 'coral-ui'; +import {menuActionsMap} from '../../../containers/ModerationQueue/helpers/moderationQueueActionsMap'; + +const ActionButton = ({type = '', user, ...props}) => { + if (type === 'BAN') { + return props.showBanUserDialog(user)} />; + } + + return ( + + ); +}; + +export default ActionButton; diff --git a/client/coral-admin/src/containers/Community/components/BanUserDialog.css b/client/coral-admin/src/containers/Community/components/BanUserDialog.css new file mode 100644 index 000000000..a46b9da32 --- /dev/null +++ b/client/coral-admin/src/containers/Community/components/BanUserDialog.css @@ -0,0 +1,164 @@ +.dialog { + border: none; + box-shadow: 0 9px 46px 8px rgba(0, 0, 0, 0.14), 0 11px 15px -7px rgba(0, 0, 0, 0.12), 0 24px 38px 3px rgba(0, 0, 0, 0.2); + width: 500px; + top: 50%; + transform: translateY(-50%); + height: 184px; + padding: 20px; + + h2 { + color: black; + font-size: 1.76em; + font-weight: 500; + margin: 0; + } + + h3 { + color: black; + font-size: 1.4em; + font-weight: 500; + margin: 0; + } +} + +.textField { + margin-top: 15px; +} + +.textField label { + font-size: 1.08em; + font-weight: bold; + margin-bottom: 5px; +} + +.textField input { + width: 100%; + display: block; + border: none; + outline: none; + border: 1px solid rgba(0,0,0,.12); + padding: 10px 6px; + box-sizing: border-box; + border-radius: 2px; + margin: 5px auto; +} + +.footer { + margin: 20px auto 10px; + text-align: center; +} + +.footer span { + display: block; + margin-bottom: 5px; +} + +.footer a { + color: #2c69b6; + cursor: pointer; + margin: 0 5px; +} + +.socialConnections { + margin-bottom: 20px; +} + +.signInButton { + margin-top: 10px; +} + +.close { + font-size: 20px; + line-height: 14px; + top: 10px; + right: 10px; + position: absolute; + display: block; + font-weight: bold; + color: #363636; + cursor: pointer; +} + +.close:hover { + color: #6b6b6b; +} + +input.error{ + border: solid 2px #f44336; +} + +.errorMsg, .hint { + color: grey; + font-weight: 600; + padding: 3px 0 16px; +} + +.alert { + padding: 10px; + margin-bottom: 20px; + border-radius: 2px; +} + +.alert--success { + border: solid 1px #1ec00e; + background: #cbf1b8; + color: #006900; +} + +.alert--error { + background: #FFEBEE; + color: #B71C1C; +} + +.userBox a { + color: #2c69b6; + cursor: pointer; + margin: 0px; +} + +.attention { + display: inline-block; + width: 15px; + height: 15px; + background: #B71C1C; + color: #FFEBEE; + font-weight: bolder; + padding: 4px; + vertical-align: middle; + border-radius: 20px; + box-sizing: border-box; + font-size: 9px; + line-height: 7px; + text-align: center; + margin-right: 5px; +} + +.action { + margin-top: 15px; +} + +.passwordRequestSuccess { + border: 1px solid green; + background-color: lightgreen; + padding: 10px; +} + +.passwordRequestFailure { + border: 1px solid orange; + background-color: 1px solid coral; + padding: 10px; +} + +.cancel { + margin-right: 10px; + width: 47%; +} + +.ban { + width: 47%; +} + +.buttons { + margin: 20px 0; +} diff --git a/client/coral-admin/src/containers/Community/components/BanUserDialog.js b/client/coral-admin/src/containers/Community/components/BanUserDialog.js new file mode 100644 index 000000000..25169ffee --- /dev/null +++ b/client/coral-admin/src/containers/Community/components/BanUserDialog.js @@ -0,0 +1,53 @@ +import React, {PropTypes} from 'react'; +import {Dialog} from 'coral-ui'; +import styles from './BanUserDialog.css'; + +import Button from 'coral-ui/components/Button'; + +import I18n from 'coral-framework/modules/i18n/i18n'; +import translations from '../../../translations'; +const lang = new I18n(translations); + +const BanUserDialog = ({open, handleClose, handleBanUser, user}) => ( + + × +
+
+

{lang.t('community.ban_user')}

+
+
+

{lang.t('community.are_you_sure', user.username)}

+ {lang.t('community.note')} +
+
+ + +
+
+
+); + +BanUserDialog.propTypes = { + handleBanUser: PropTypes.func.isRequired, + handleClose: PropTypes.func.isRequired, + user: PropTypes.object.isRequired, +}; + +export default BanUserDialog; diff --git a/client/coral-admin/src/components/SuspendUserModal.css b/client/coral-admin/src/containers/Community/components/SuspendUserModal.css similarity index 100% rename from client/coral-admin/src/components/SuspendUserModal.css rename to client/coral-admin/src/containers/Community/components/SuspendUserModal.css diff --git a/client/coral-admin/src/components/SuspendUserModal.js b/client/coral-admin/src/containers/Community/components/SuspendUserModal.js similarity index 100% rename from client/coral-admin/src/components/SuspendUserModal.js rename to client/coral-admin/src/containers/Community/components/SuspendUserModal.js diff --git a/client/coral-admin/src/containers/Community/components/User.js b/client/coral-admin/src/containers/Community/components/User.js index 1f18a186f..b15d3d2ae 100644 --- a/client/coral-admin/src/containers/Community/components/User.js +++ b/client/coral-admin/src/containers/Community/components/User.js @@ -1,5 +1,5 @@ import React from 'react'; -import styles from '../UserModerationList.css'; +import styles from '../Community.css'; import I18n from 'coral-framework/modules/i18n/i18n'; import translations from '../../../translations.json'; @@ -7,7 +7,7 @@ import translations from '../../../translations.json'; const lang = new I18n(translations); // import {Icon} from 'react-mdl'; -// import ActionButton from './ActionButton'; +import ActionButton from './ActionButton'; // Render a single user for the list const User = props => { @@ -19,9 +19,13 @@ const User = props => { return (userStatus === 'PENDING' || userStatus === 'BANNED') &&
  • -
    - {user.username} -
    + {user.username} +
    diff --git a/client/coral-admin/src/graphql/mutations/index.js b/client/coral-admin/src/graphql/mutations/index.js index fe3a1faf9..05e089f20 100644 --- a/client/coral-admin/src/graphql/mutations/index.js +++ b/client/coral-admin/src/graphql/mutations/index.js @@ -9,11 +9,35 @@ export const banUser = graphql(SET_USER_STATUS, { variables: { userId, status: 'BANNED' - } + }, + refetchQueries: ['Users'] }); }}), }); +export const setUserStatus = graphql(SET_USER_STATUS, { + props: ({mutate}) => ({ + acceptUser: (userId) => { + return mutate({ + variables: { + userId, + status: 'APPROVED' + }, + refetchQueries: ['modUserFlaggedQuery'] + }); + }, + rejectUser: (userId) => { + return mutate({ + variables: { + userId, + status: 'BANNED' + }, + refetchQueries: ['modUserFlaggedQuery'] + }); + } + }) +}); + export const setCommentStatus = graphql(SET_COMMENT_STATUS, { props: ({mutate}) => ({ acceptComment: ({commentId}) => { diff --git a/client/coral-admin/src/reducers/community.js b/client/coral-admin/src/reducers/community.js index 50641118c..c8868510f 100644 --- a/client/coral-admin/src/reducers/community.js +++ b/client/coral-admin/src/reducers/community.js @@ -6,7 +6,9 @@ import { FETCH_COMMENTERS_SUCCESS, SORT_UPDATE, SET_ROLE, - SET_COMMENTER_STATUS + SET_COMMENTER_STATUS, + SHOW_BANUSER_DIALOG, + HIDE_BANUSER_DIALOG } from '../constants/community'; const initialState = Map({ @@ -17,7 +19,9 @@ const initialState = Map({ fieldPeople: 'created_at', ascPeople: false, totalPagesPeople: 0, - pagePeople: 0 + pagePeople: 0, + user: Map({}), + banDialog: false }); export default function community (state = initialState, action) { @@ -63,6 +67,15 @@ export default function community (state = initialState, action) { return state .set('fieldPeople', action.sort.field) .set('ascPeople', !state.get('ascPeople')); + case HIDE_BANUSER_DIALOG: + return state + .set('banDialog', false); + case SHOW_BANUSER_DIALOG: + return state + .merge({ + user: Map(action.user), + banDialog: true + }); default : return state; } diff --git a/client/coral-admin/src/translations.json b/client/coral-admin/src/translations.json index eae4543ee..04cd8b142 100644 --- a/client/coral-admin/src/translations.json +++ b/client/coral-admin/src/translations.json @@ -17,10 +17,16 @@ "flaggedaccounts": "Flagged Usernames", "people": "People", "no-flagged-accounts": "The Account Flags queue is currently empty.", + "I don't like this username": "I don't like this username", "This user is impersonating": "Impersonation", "This looks like an ad/marketing": "Spam/Ads", "This username is offensive": "Offensive", - "Other": "Other" + "Other": "Other", + "ban_user": "Ban User?", + "are_you_sure": "Are you sure you would like to ban {0}?", + "note": "Note: Banning this user will not let them edit, comment or remove anything.", + "cancel": "Cancel", + "yes_ban_user": "Yes, Ban User" }, "modqueue": { "likes": "likes", @@ -156,10 +162,16 @@ "flaggedaccounts": "Nombres de Usuario Reportados", "people": "Gente", "no-flagged-accounts": "No hay ninguna cuenta reportada.", + "I don't like this username": "No me gusta ese nombre de usuario", "This user is impersonating": "Suplantación", "This looks like an ad/marketing": "Spam/Propaganda", "This username is offensive": "Ofensivo", - "Other": "Otros" + "Other": "Otros", + "ban_user": "Quieres suspender el Usuario?", + "are_you_sure": "Estas segura que quieres suspender a {0}?", + "note": "Nota: Suspender a este usuario no le va a permitir borrar ni editar ni comentar.", + "cancel": "Cancelar", + "yes_ban_user": "Si, Suspendan el usuario" }, "modqueue": { "likes": "gustos", From acc9b2d9bfc248788dfc8484782e768dcb518377 Mon Sep 17 00:00:00 2001 From: gaba Date: Wed, 1 Mar 2017 14:21:49 -0800 Subject: [PATCH 07/20] Approve and Reject buttons. Still misssing suspending --- .../Community/CommunityContainer.js | 7 +++-- .../containers/Community/FlaggedAccounts.js | 21 ++++---------- .../Community/components/ActionButton.js | 6 ++-- .../containers/Community/components/User.js | 28 +++++++++---------- .../src/graphql/mutations/index.js | 6 ++-- .../coral-admin/src/graphql/queries/index.js | 4 +-- client/coral-plugin-flags/FlagComment.js | 4 +-- 7 files changed, 34 insertions(+), 42 deletions(-) diff --git a/client/coral-admin/src/containers/Community/CommunityContainer.js b/client/coral-admin/src/containers/Community/CommunityContainer.js index 7f83909b6..eee8aa561 100644 --- a/client/coral-admin/src/containers/Community/CommunityContainer.js +++ b/client/coral-admin/src/containers/Community/CommunityContainer.js @@ -3,7 +3,7 @@ import {connect} from 'react-redux'; import {compose} from 'react-apollo'; import {modUserFlaggedQuery} from 'coral-admin/src/graphql/queries'; -import {banUser} from '../../graphql/mutations'; +import {banUser, setUserStatus} from '../../graphql/mutations'; import { fetchAccounts, @@ -99,7 +99,7 @@ class CommunityContainer extends Component { isFetching={data.loading} error={data.error} showBanUserDialog={props.showBanUserDialog} - acceptUser={props.acceptUser} + approveUser={props.approveUser} rejectUser={props.rejectUser} {...this} /> @@ -142,5 +142,6 @@ const mapDispatchToProps = dispatch => ({ export default compose( connect(mapStateToProps, mapDispatchToProps), modUserFlaggedQuery, - banUser + banUser, + setUserStatus )(CommunityContainer); diff --git a/client/coral-admin/src/containers/Community/FlaggedAccounts.js b/client/coral-admin/src/containers/Community/FlaggedAccounts.js index a6df24598..817f51a37 100644 --- a/client/coral-admin/src/containers/Community/FlaggedAccounts.js +++ b/client/coral-admin/src/containers/Community/FlaggedAccounts.js @@ -10,25 +10,10 @@ import Loading from './Loading'; import EmptyCard from '../../components/EmptyCard'; import User from './components/User'; -// actions={commenter.actions} - const FlaggedAccounts = ({...props}) => { const {commenters, isFetching} = props; const hasResults = !isFetching && commenters && !!commenters.length; - // const menuOptions = { - // 'reject': {status: 'REJECTED', icon: 'close', key: 'r'}, - // 'approve': {status: 'ACCEPTED', icon: 'done', key: 't'}, - // 'ban': {status: 'BANNED', icon: 'not interested'} - // }; - // - // - // onClickAction={this.onClickAction} - // onClickShowBanDialog={this.onClickShowBanDialog} - // acceptCommenter={props.acceptCommenter} - // rejectCommenter={props.rejectCommenter} - // menuOptions={menuOptions} - return (
    @@ -40,7 +25,11 @@ const FlaggedAccounts = ({...props}) => { user={commenter} key={index} index={index} - showBanUserDialog={props.showBanUserDialog}/>; + modActionButtons={['REJECT', 'APPROVE']} + showBanUserDialog={props.showBanUserDialog} + approveUser={props.approveUser} + rejectUser={props.rejectUser} + />; }) : {lang.t('community.no-flagged-accounts')} } diff --git a/client/coral-admin/src/containers/Community/components/ActionButton.js b/client/coral-admin/src/containers/Community/components/ActionButton.js index 9cd3da6e1..305cf8d1a 100644 --- a/client/coral-admin/src/containers/Community/components/ActionButton.js +++ b/client/coral-admin/src/containers/Community/components/ActionButton.js @@ -6,7 +6,7 @@ import {menuActionsMap} from '../../../containers/ModerationQueue/helpers/modera const ActionButton = ({type = '', user, ...props}) => { if (type === 'BAN') { - return props.showBanUserDialog(user)} />; + return props.showBanUserDialog(user)} />; } return ( @@ -14,7 +14,9 @@ const ActionButton = ({type = '', user, ...props}) => { className={`${type.toLowerCase()} ${styles.actionButton}`} cStyle={type.toLowerCase()} icon={menuActionsMap[type].icon} - onClick={type === 'APPROVE' ? props.acceptUser : props.rejectUser} + onClick={() => { + type === 'APPROVE' ? props.approveUser({userId: user.id}) : props.rejectUser({userId: user.id}); + }} /> ); }; diff --git a/client/coral-admin/src/containers/Community/components/User.js b/client/coral-admin/src/containers/Community/components/User.js index b15d3d2ae..9ef3cb324 100644 --- a/client/coral-admin/src/containers/Community/components/User.js +++ b/client/coral-admin/src/containers/Community/components/User.js @@ -1,17 +1,16 @@ import React from 'react'; import styles from '../Community.css'; +import ActionButton from './ActionButton'; + import I18n from 'coral-framework/modules/i18n/i18n'; import translations from '../../../translations.json'; const lang = new I18n(translations); -// import {Icon} from 'react-mdl'; -import ActionButton from './ActionButton'; - // Render a single user for the list const User = props => { - const {user} = props; + const {user, modActionButtons} = props; let userStatus = user.status; // Do not display unless the user status is 'pending' or 'banned'. @@ -50,16 +49,17 @@ const User = props => {
    - {/* props.modActions.map( - (action, i) => - return - )*/} + {modActionButtons.map( + (action, i) => { + return ; + } + )}
    diff --git a/client/coral-admin/src/graphql/mutations/index.js b/client/coral-admin/src/graphql/mutations/index.js index 05e089f20..0fa442264 100644 --- a/client/coral-admin/src/graphql/mutations/index.js +++ b/client/coral-admin/src/graphql/mutations/index.js @@ -17,13 +17,13 @@ export const banUser = graphql(SET_USER_STATUS, { export const setUserStatus = graphql(SET_USER_STATUS, { props: ({mutate}) => ({ - acceptUser: (userId) => { + approveUser: (userId) => { return mutate({ variables: { userId, status: 'APPROVED' }, - refetchQueries: ['modUserFlaggedQuery'] + refetchQueries: ['Users'] }); }, rejectUser: (userId) => { @@ -32,7 +32,7 @@ export const setUserStatus = graphql(SET_USER_STATUS, { userId, status: 'BANNED' }, - refetchQueries: ['modUserFlaggedQuery'] + refetchQueries: ['Users'] }); } }) diff --git a/client/coral-admin/src/graphql/queries/index.js b/client/coral-admin/src/graphql/queries/index.js index 81aa24410..3cc66c21a 100644 --- a/client/coral-admin/src/graphql/queries/index.js +++ b/client/coral-admin/src/graphql/queries/index.js @@ -2,7 +2,7 @@ import {graphql} from 'react-apollo'; import MOST_FLAGS from './mostFlags.graphql'; import MOD_QUEUE_QUERY from './modQueueQuery.graphql'; -import USER_FLAGGED_QUERY from './modUserFlaggedQuery.graphql'; +import MOD_USER_FLAGGED_QUERY from './modUserFlaggedQuery.graphql'; export const mostFlags = graphql(MOST_FLAGS, { options: () => { @@ -30,4 +30,4 @@ export const modQueueQuery = graphql(MOD_QUEUE_QUERY, { } }); -export const modUserFlaggedQuery = graphql(USER_FLAGGED_QUERY); +export const modUserFlaggedQuery = graphql(MOD_USER_FLAGGED_QUERY); diff --git a/client/coral-plugin-flags/FlagComment.js b/client/coral-plugin-flags/FlagComment.js index 24fde6b0f..3f3051f28 100644 --- a/client/coral-plugin-flags/FlagComment.js +++ b/client/coral-plugin-flags/FlagComment.js @@ -23,14 +23,14 @@ const getPopupMenu = [ {val: 'This comment is offensive', text: lang.t('comment-offensive')}, {val: 'This looks like an ad/marketing', text: lang.t('marketing')}, {val: 'I don\'t agree with this comment', text: lang.t('no-agree-comment')}, - {val: 'other', text: lang.t('other')} + {val: 'Other', text: lang.t('other')} ] : [ {val: 'This username is offensive', text: lang.t('username-offensive')}, {val: 'I don\'t like this username', text: lang.t('no-like-username')}, {val: 'This user is impersonating', text: lang.t('user-impersonating')}, {val: 'This looks like an ad/marketing', text: lang.t('marketing')}, - {val: 'other', text: lang.t('other')} + {val: 'Other', text: lang.t('other')} ]; return { header: lang.t('step-2-header'), From 3626d2678eac613cd65e3fbbe6dd56ea720efd18 Mon Sep 17 00:00:00 2001 From: gaba Date: Wed, 1 Mar 2017 23:43:25 -0800 Subject: [PATCH 08/20] Accept button working. Add suspend user dialog. --- client/coral-admin/src/actions/community.js | 8 +++++++- client/coral-admin/src/constants/community.js | 3 +++ .../src/containers/Community/CommunityContainer.js | 13 ++++++++++--- .../Community/components/SuspendUserModal.js | 2 +- client/coral-admin/src/graphql/mutations/index.js | 4 ++-- client/coral-admin/src/reducers/community.js | 13 ++++++++++++- 6 files changed, 35 insertions(+), 8 deletions(-) diff --git a/client/coral-admin/src/actions/community.js b/client/coral-admin/src/actions/community.js index 82cee32f4..ea9a28efa 100644 --- a/client/coral-admin/src/actions/community.js +++ b/client/coral-admin/src/actions/community.js @@ -9,7 +9,9 @@ import { SET_ROLE, SET_COMMENTER_STATUS, SHOW_BANUSER_DIALOG, - HIDE_BANUSER_DIALOG + HIDE_BANUSER_DIALOG, + SHOW_SUSPENDUSER_DIALOG, + HIDE_SUSPENDUSER_DIALOG } from '../constants/community'; import coralApi from '../../../coral-framework/helpers/response'; @@ -62,3 +64,7 @@ export const setCommenterStatus = (id, status) => (dispatch) => { // Ban User Dialog export const showBanUserDialog = (user) => ({type: SHOW_BANUSER_DIALOG, user}); export const hideBanUserDialog = (showDialog) => ({type: HIDE_BANUSER_DIALOG, showDialog}); + +// Suspend User Dialog +export const showSuspendUserDialog = (user) => ({type: SHOW_SUSPENDUSER_DIALOG, user}); +export const hideSuspendUserDialog = (showDialog) => ({type: HIDE_SUSPENDUSER_DIALOG, showDialog}); diff --git a/client/coral-admin/src/constants/community.js b/client/coral-admin/src/constants/community.js index 9ea4568ff..a46e721a6 100644 --- a/client/coral-admin/src/constants/community.js +++ b/client/coral-admin/src/constants/community.js @@ -12,3 +12,6 @@ export const FETCH_FLAGGED_COMMENTERS_FAILURE = 'FETCH_FLAGGED_COMMENTERS_FAILUR export const SHOW_BANUSER_DIALOG = 'SHOW_BANUSER_DIALOG'; export const HIDE_BANUSER_DIALOG = 'HIDE_BANUSER_DIALOG'; + +export const SHOW_SUSPENDUSER_DIALOG = 'SHOW_SUSPENDUSER_DIALOG'; +export const HIDE_SUSPENDUSER_DIALOG = 'HIDE_SUSPENDUSER_DIALOG'; diff --git a/client/coral-admin/src/containers/Community/CommunityContainer.js b/client/coral-admin/src/containers/Community/CommunityContainer.js index eee8aa561..c9cc245c5 100644 --- a/client/coral-admin/src/containers/Community/CommunityContainer.js +++ b/client/coral-admin/src/containers/Community/CommunityContainer.js @@ -10,12 +10,13 @@ import { updateSorting, newPage, showBanUserDialog, - hideBanUserDialog + hideBanUserDialog, + hideSuspendUserDialog } from '../../actions/community'; import CommunityMenu from './components/CommunityMenu'; - import BanUserDialog from './components/BanUserDialog'; +import SuspendUserModal from './components/SuspendUserModal'; import People from './People'; import FlaggedAccounts from './FlaggedAccounts'; @@ -109,6 +110,11 @@ class CommunityContainer extends Component { handleClose={props.hideBanUserDialog} handleBanUser={props.banUser} /> +
    ); } @@ -136,7 +142,8 @@ const mapStateToProps = state => ({ const mapDispatchToProps = dispatch => ({ fetchAccounts: query => dispatch(fetchAccounts(query)), showBanUserDialog: (user) => dispatch(showBanUserDialog(user)), - hideBanUserDialog: () => dispatch(hideBanUserDialog(false)) + hideBanUserDialog: () => dispatch(hideBanUserDialog(false)), + hideSuspendUserDialog: () => dispatch(hideSuspendUserDialog(false)) }); export default compose( diff --git a/client/coral-admin/src/containers/Community/components/SuspendUserModal.js b/client/coral-admin/src/containers/Community/components/SuspendUserModal.js index 5afb8ffee..7fbcd87d0 100644 --- a/client/coral-admin/src/containers/Community/components/SuspendUserModal.js +++ b/client/coral-admin/src/containers/Community/components/SuspendUserModal.js @@ -1,5 +1,5 @@ import I18n from 'coral-framework/modules/i18n/i18n'; -import translations from '../translations.json'; +import translations from '../../../translations.json'; import React, {Component, PropTypes} from 'react'; import Modal from 'components/Modal'; import styles from './SuspendUserModal.css'; diff --git a/client/coral-admin/src/graphql/mutations/index.js b/client/coral-admin/src/graphql/mutations/index.js index 0fa442264..f10df6339 100644 --- a/client/coral-admin/src/graphql/mutations/index.js +++ b/client/coral-admin/src/graphql/mutations/index.js @@ -17,7 +17,7 @@ export const banUser = graphql(SET_USER_STATUS, { export const setUserStatus = graphql(SET_USER_STATUS, { props: ({mutate}) => ({ - approveUser: (userId) => { + approveUser: ({userId}) => { return mutate({ variables: { userId, @@ -26,7 +26,7 @@ export const setUserStatus = graphql(SET_USER_STATUS, { refetchQueries: ['Users'] }); }, - rejectUser: (userId) => { + rejectUser: ({userId}) => { return mutate({ variables: { userId, diff --git a/client/coral-admin/src/reducers/community.js b/client/coral-admin/src/reducers/community.js index c8868510f..e1382caaf 100644 --- a/client/coral-admin/src/reducers/community.js +++ b/client/coral-admin/src/reducers/community.js @@ -8,7 +8,9 @@ import { SET_ROLE, SET_COMMENTER_STATUS, SHOW_BANUSER_DIALOG, - HIDE_BANUSER_DIALOG + HIDE_BANUSER_DIALOG, + SHOW_SUSPENDUSER_DIALOG, + HIDE_SUSPENDUSER_DIALOG } from '../constants/community'; const initialState = Map({ @@ -76,6 +78,15 @@ export default function community (state = initialState, action) { user: Map(action.user), banDialog: true }); + case HIDE_SUSPENDUSER_DIALOG: + return state + .set('suspendDialog', false); + case SHOW_SUSPENDUSER_DIALOG: + return state + .merge({ + user: Map(action.user), + suspendDialog: true + }); default : return state; } From 1899dcbb7eb724d459e0809b0473e1ea62fede7c Mon Sep 17 00:00:00 2001 From: gaba Date: Thu, 2 Mar 2017 09:27:53 -0800 Subject: [PATCH 09/20] Suspend User dialog. --- client/coral-admin/src/actions/community.js | 4 +- client/coral-admin/src/components/Modal.js | 1 - .../Community/CommunityContainer.js | 16 ++- .../containers/Community/FlaggedAccounts.js | 2 +- .../Community/components/ActionButton.js | 2 +- ...endUserModal.css => SuspendUserDialog.css} | 0 .../Community/components/SuspendUserDialog.js | 119 ++++++++++++++++++ .../Community/components/SuspendUserModal.js | 106 ---------------- .../containers/Community/components/User.js | 2 +- .../src/graphql/mutations/index.js | 5 +- client/coral-admin/src/reducers/community.js | 3 +- client/coral-admin/src/translations.json | 17 ++- 12 files changed, 155 insertions(+), 122 deletions(-) rename client/coral-admin/src/containers/Community/components/{SuspendUserModal.css => SuspendUserDialog.css} (100%) create mode 100644 client/coral-admin/src/containers/Community/components/SuspendUserDialog.js delete mode 100644 client/coral-admin/src/containers/Community/components/SuspendUserModal.js diff --git a/client/coral-admin/src/actions/community.js b/client/coral-admin/src/actions/community.js index ea9a28efa..0f53ea3da 100644 --- a/client/coral-admin/src/actions/community.js +++ b/client/coral-admin/src/actions/community.js @@ -63,8 +63,8 @@ export const setCommenterStatus = (id, status) => (dispatch) => { // Ban User Dialog export const showBanUserDialog = (user) => ({type: SHOW_BANUSER_DIALOG, user}); -export const hideBanUserDialog = (showDialog) => ({type: HIDE_BANUSER_DIALOG, showDialog}); +export const hideBanUserDialog = () => ({type: HIDE_BANUSER_DIALOG}); // Suspend User Dialog export const showSuspendUserDialog = (user) => ({type: SHOW_SUSPENDUSER_DIALOG, user}); -export const hideSuspendUserDialog = (showDialog) => ({type: HIDE_SUSPENDUSER_DIALOG, showDialog}); +export const hideSuspendUserDialog = () => ({type: HIDE_SUSPENDUSER_DIALOG}); diff --git a/client/coral-admin/src/components/Modal.js b/client/coral-admin/src/components/Modal.js index 99c27c3a1..b667258c2 100644 --- a/client/coral-admin/src/components/Modal.js +++ b/client/coral-admin/src/components/Modal.js @@ -1,4 +1,3 @@ - import React from 'react'; import {Button, Icon} from 'react-mdl'; import styles from './Modal.css'; diff --git a/client/coral-admin/src/containers/Community/CommunityContainer.js b/client/coral-admin/src/containers/Community/CommunityContainer.js index c9cc245c5..5e02b193b 100644 --- a/client/coral-admin/src/containers/Community/CommunityContainer.js +++ b/client/coral-admin/src/containers/Community/CommunityContainer.js @@ -11,12 +11,13 @@ import { newPage, showBanUserDialog, hideBanUserDialog, + showSuspendUserDialog, hideSuspendUserDialog } from '../../actions/community'; import CommunityMenu from './components/CommunityMenu'; import BanUserDialog from './components/BanUserDialog'; -import SuspendUserModal from './components/SuspendUserModal'; +import SuspendUserDialog from './components/SuspendUserDialog'; import People from './People'; import FlaggedAccounts from './FlaggedAccounts'; @@ -93,6 +94,7 @@ class CommunityContainer extends Component { ); } + console.log('debug props', props); return (
    -
    ); @@ -143,7 +146,8 @@ const mapDispatchToProps = dispatch => ({ fetchAccounts: query => dispatch(fetchAccounts(query)), showBanUserDialog: (user) => dispatch(showBanUserDialog(user)), hideBanUserDialog: () => dispatch(hideBanUserDialog(false)), - hideSuspendUserDialog: () => dispatch(hideSuspendUserDialog(false)) + showSuspendUserDialog: () => dispatch(showSuspendUserDialog()), + hideSuspendUserDialog: () => dispatch(hideSuspendUserDialog()) }); export default compose( diff --git a/client/coral-admin/src/containers/Community/FlaggedAccounts.js b/client/coral-admin/src/containers/Community/FlaggedAccounts.js index 817f51a37..154e74ef0 100644 --- a/client/coral-admin/src/containers/Community/FlaggedAccounts.js +++ b/client/coral-admin/src/containers/Community/FlaggedAccounts.js @@ -27,8 +27,8 @@ const FlaggedAccounts = ({...props}) => { index={index} modActionButtons={['REJECT', 'APPROVE']} showBanUserDialog={props.showBanUserDialog} + showSuspendUserDialog={props.showSuspendUserDialog} approveUser={props.approveUser} - rejectUser={props.rejectUser} />; }) : {lang.t('community.no-flagged-accounts')} diff --git a/client/coral-admin/src/containers/Community/components/ActionButton.js b/client/coral-admin/src/containers/Community/components/ActionButton.js index 305cf8d1a..7ae8aa20b 100644 --- a/client/coral-admin/src/containers/Community/components/ActionButton.js +++ b/client/coral-admin/src/containers/Community/components/ActionButton.js @@ -15,7 +15,7 @@ const ActionButton = ({type = '', user, ...props}) => { cStyle={type.toLowerCase()} icon={menuActionsMap[type].icon} onClick={() => { - type === 'APPROVE' ? props.approveUser({userId: user.id}) : props.rejectUser({userId: user.id}); + type === 'APPROVE' ? props.approveUser({userId: user.id}) : props.showSuspendUserDialog({user: user}); }} /> ); diff --git a/client/coral-admin/src/containers/Community/components/SuspendUserModal.css b/client/coral-admin/src/containers/Community/components/SuspendUserDialog.css similarity index 100% rename from client/coral-admin/src/containers/Community/components/SuspendUserModal.css rename to client/coral-admin/src/containers/Community/components/SuspendUserDialog.css diff --git a/client/coral-admin/src/containers/Community/components/SuspendUserDialog.js b/client/coral-admin/src/containers/Community/components/SuspendUserDialog.js new file mode 100644 index 000000000..e385403c6 --- /dev/null +++ b/client/coral-admin/src/containers/Community/components/SuspendUserDialog.js @@ -0,0 +1,119 @@ +import React, {Component, PropTypes} from 'react'; + +import {Dialog, Button} from 'coral-ui'; +import styles from './SuspendUserDialog.css'; + +import I18n from 'coral-framework/modules/i18n/i18n'; +import translations from '../../../translations.json'; + +const lang = new I18n(translations); + +const stages = [ + { + title: 'suspenduser.title_0', + description: 'suspenduser.description_0', + options: { + 'j': 'suspenduser.no_cancel', + 'k': 'suspenduser.yes_suspend' + } + }, + { + title: 'suspenduser.title_1', + description: 'suspenduser.description_1', + options: { + 'j': 'bandialog.cancel', + 'k': 'suspenduser.send' + } + } +]; + +class SuspendUserDialog extends Component { + + state = {email: '', stage: 0} + + static propTypes = { + stage: PropTypes.number, + onClose: PropTypes.func.isRequired, + suspendUser: PropTypes.func.isRequired + } + + componentDidMount() { + this.setState({email: lang.t('suspenduser.email'), about: lang.t('suspenduser.username')}); + } + + /* + * When an admin clicks to suspend a user a dialog is shown, this function + * handles the possible actions for that dialog. + */ + onActionClick = (stage, menuOption) => () => { + const {suspendUser, user} = this.props; + const {stage, email} = this.state; + + console.log('DEBUG props --> ', this.props); + console.log('DEBUG user', user); + + const cancel = this.props.onClose; + const next = () => this.setState({stage: stage + 1}); + const suspend = () => { + suspendUser({userId: user.id, email}) + .then(() => { + this.props.onClose; + }); + }; + + const suspendModalActions = [ + [ cancel, next ], + [ cancel, suspend ] + ]; + return suspendModalActions[stage][menuOption](); + } + + onEmailChange = (e) => { + console.log('debug', e.target); + this.setState({email: e.target.value}); + } + + render () { + const {open, onClose} = this.props; + const {stage} = this.state; + + return +
    + {lang.t(stages[stage].title, lang.t('suspenduser.username'))} +
    +
    +
    + {lang.t(stages[stage].description, lang.t('suspenduser.username'))} +
    + { + stage === 1 && +
    +
    {lang.t('suspenduser.write_message')}
    +
    +