From 9e1b470cb52647cadaaee27623187f4fba9f849c Mon Sep 17 00:00:00 2001 From: riley Date: Wed, 3 May 2017 09:32:05 -0600 Subject: [PATCH 01/58] saving my place --- .../coral-admin/src/components/UserDetail.css | 0 .../coral-admin/src/components/UserDetail.js | 39 +++++++++++++++++++ .../ModerationQueue/ModerationContainer.js | 16 +++++++- .../ModerationQueue/ModerationQueue.js | 13 ++++++- .../ModerationQueue/components/Comment.js | 11 ++++-- .../ModerationQueue/components/styles.css | 11 ++++++ .../coral-admin/src/graphql/queries/index.js | 15 +++++++ .../src/graphql/queries/userDetail.graphql | 6 +++ client/coral-ui/components/Drawer.css | 12 ++++++ client/coral-ui/components/Drawer.js | 17 ++++++++ client/coral-ui/index.js | 1 + graph/resolvers/root_query.js | 10 +++++ graph/typeDefs.graphql | 3 ++ 13 files changed, 148 insertions(+), 6 deletions(-) create mode 100644 client/coral-admin/src/components/UserDetail.css create mode 100644 client/coral-admin/src/components/UserDetail.js create mode 100644 client/coral-admin/src/graphql/queries/userDetail.graphql create mode 100644 client/coral-ui/components/Drawer.css create mode 100644 client/coral-ui/components/Drawer.js diff --git a/client/coral-admin/src/components/UserDetail.css b/client/coral-admin/src/components/UserDetail.css new file mode 100644 index 000000000..e69de29bb diff --git a/client/coral-admin/src/components/UserDetail.js b/client/coral-admin/src/components/UserDetail.js new file mode 100644 index 000000000..aa6997dc4 --- /dev/null +++ b/client/coral-admin/src/components/UserDetail.js @@ -0,0 +1,39 @@ +import React, {PropTypes} from 'react'; +import {Button, Drawer} from 'coral-ui'; +import styles from './UserDetail.js'; + +const UserDetail = ({user}) => { + const localProfile = user.profiles.find(p => p.provider === 'local'); + const facebookProfile = user.profiles.find(p => p.provider === 'facebook'); + let profile; + if (localProfile) { + profile = profile.id; + } else if (facebookProfile) { + profile = {facebookProfile.id}; + } + + return ( + +

{user.username}

+

{profile}

+ Member since {user.created_at} +
+ Account summary +
Data represents the last six months of activity +
+
+
+ ); +}; + +UserDetail.propTypes = { + user: PropTypes.shape({ + username: PropTypes.string.isRequired, + profiles: PropTypes.arrayOf(PropTypes.shape({ + id: PropTypes.string, + provider: PropTypes.string + })) + }) +}; + +export default UserDetail; diff --git a/client/coral-admin/src/containers/ModerationQueue/ModerationContainer.js b/client/coral-admin/src/containers/ModerationQueue/ModerationContainer.js index eb59f369e..fac092a77 100644 --- a/client/coral-admin/src/containers/ModerationQueue/ModerationContainer.js +++ b/client/coral-admin/src/containers/ModerationQueue/ModerationContainer.js @@ -5,7 +5,7 @@ import key from 'keymaster'; import isEqual from 'lodash/isEqual'; import styles from './components/styles.css'; -import {modQueueQuery} from '../../graphql/queries'; +import {modQueueQuery, getUserDetail} from '../../graphql/queries'; import {banUser, setCommentStatus} from '../../graphql/mutations'; import {fetchSettings} from 'actions/settings'; @@ -19,6 +19,7 @@ import ModerationMenu from './components/ModerationMenu'; import ModerationHeader from './components/ModerationHeader'; import NotFoundAsset from './components/NotFoundAsset'; import ModerationKeysModal from '../../components/ModerationKeysModal'; +import UserDetail from '../../components/UserDetail'; class ModerationContainer extends Component { state = { @@ -76,6 +77,15 @@ class ModerationContainer extends Component { } } + viewUserDetail = (id) => { + console.log('getting user detail', id, typeof getUserDetail); + try { + getUserDetail({id}); + } catch (e) { + console.log(e); + } + } + selectSort = (sort) => { this.setState({sort}); this.props.modQueueResort(sort); @@ -180,6 +190,7 @@ class ModerationContainer extends Component { assetId={providedAssetId} sort={this.state.sort} commentCount={activeTabCount} + viewUserDetail={this.viewUserDetail} /> - + {data.user && } ); } diff --git a/client/coral-admin/src/containers/ModerationQueue/ModerationQueue.js b/client/coral-admin/src/containers/ModerationQueue/ModerationQueue.js index f986b947b..093c2f5dd 100644 --- a/client/coral-admin/src/containers/ModerationQueue/ModerationQueue.js +++ b/client/coral-admin/src/containers/ModerationQueue/ModerationQueue.js @@ -9,7 +9,16 @@ import translations from 'coral-admin/src/translations'; import LoadMore from './components/LoadMore'; const lang = new I18n(translations); -const ModerationQueue = ({comments, selectedIndex, commentCount, singleView, loadMore, activeTab, sort, ...props}) => { +const ModerationQueue = ({ + comments, + selectedIndex, + commentCount, + singleView, + loadMore, + activeTab, + sort, + viewUserDetail, + ...props}) => { return (
    @@ -26,6 +35,7 @@ const ModerationQueue = ({comments, selectedIndex, commentCount, singleView, loa bannedWords={props.bannedWords} actions={actionsMap[status]} showBanUserDialog={props.showBanUserDialog} + viewUserDetail={viewUserDetail} acceptComment={props.acceptComment} rejectComment={props.rejectComment} currentAsset={props.currentAsset} @@ -47,6 +57,7 @@ const ModerationQueue = ({comments, selectedIndex, commentCount, singleView, loa }; ModerationQueue.propTypes = { + viewUserDetail: PropTypes.func.isRequired, bannedWords: PropTypes.arrayOf(PropTypes.string).isRequired, suspectWords: PropTypes.arrayOf(PropTypes.string).isRequired, currentAsset: PropTypes.object, diff --git a/client/coral-admin/src/containers/ModerationQueue/components/Comment.js b/client/coral-admin/src/containers/ModerationQueue/components/Comment.js index 0fdf435bd..b35c688cd 100644 --- a/client/coral-admin/src/containers/ModerationQueue/components/Comment.js +++ b/client/coral-admin/src/containers/ModerationQueue/components/Comment.js @@ -18,7 +18,7 @@ import I18n from 'coral-framework/modules/i18n/i18n'; import translations from 'coral-admin/src/translations.json'; const lang = new I18n(translations); -const Comment = ({actions = [], comment, ...props}) => { +const Comment = ({actions = [], comment, viewUserDetail, ...props}) => { const links = linkify.getMatches(comment.body); const linkText = links ? links.map(link => link.raw) : []; const flagActionSummaries = getActionSummary('FlagActionSummary', comment); @@ -35,7 +35,10 @@ const Comment = ({actions = [], comment, ...props}) => {
    - + { + console.log('clickt', comment.user.id); + viewUserDetail(comment.user.id); + }}> {comment.user.name} @@ -89,6 +92,7 @@ const Comment = ({actions = [], comment, ...props}) => { }; Comment.propTypes = { + viewUserDetail: PropTypes.func.isRequired, acceptComment: PropTypes.func.isRequired, rejectComment: PropTypes.func.isRequired, suspectWords: PropTypes.arrayOf(PropTypes.string).isRequired, @@ -100,8 +104,9 @@ Comment.propTypes = { actions: PropTypes.array, created_at: PropTypes.string.isRequired, user: PropTypes.shape({ + id: PropTypes.string, status: PropTypes.string - }), + }).isRequired, asset: PropTypes.shape({ title: PropTypes.string, id: PropTypes.string diff --git a/client/coral-admin/src/containers/ModerationQueue/components/styles.css b/client/coral-admin/src/containers/ModerationQueue/components/styles.css index a22fbbc64..baa098d91 100644 --- a/client/coral-admin/src/containers/ModerationQueue/components/styles.css +++ b/client/coral-admin/src/containers/ModerationQueue/components/styles.css @@ -423,3 +423,14 @@ span { position: relative; top: 7px; } + +.username { + color: blue; + text-decoration: underline; + padding: 5px; + cursor: pointer; + + &:hover { + background-color: rgba(255, 0, 0, .1); + } +} diff --git a/client/coral-admin/src/graphql/queries/index.js b/client/coral-admin/src/graphql/queries/index.js index 9d2357ce7..aa5ffa505 100644 --- a/client/coral-admin/src/graphql/queries/index.js +++ b/client/coral-admin/src/graphql/queries/index.js @@ -4,6 +4,7 @@ import MOD_QUEUE_QUERY from './modQueueQuery.graphql'; import MOD_QUEUE_LOAD_MORE from './loadMore.graphql'; import MOD_USER_FLAGGED_QUERY from './modUserFlaggedQuery.graphql'; import METRICS from './metricsQuery.graphql'; +import USER_DETAIL from './userDetail.graphql'; export const modQueueQuery = graphql(MOD_QUEUE_QUERY, { options: ({params: {id = null}}) => { @@ -93,3 +94,17 @@ export const modQueueResort = (id, fetchMore) => (sort) => { updateQuery: (oldData, {fetchMoreResult:{data}}) => data }); }; + +export const getUserDetail = ({id}) => { + console.log('close', id); + return graphql(USER_DETAIL, { + options: () => { + console.log('so close!', id); + return { + variables: { + id + } + }; + } + }); +}; diff --git a/client/coral-admin/src/graphql/queries/userDetail.graphql b/client/coral-admin/src/graphql/queries/userDetail.graphql new file mode 100644 index 000000000..f489e16d9 --- /dev/null +++ b/client/coral-admin/src/graphql/queries/userDetail.graphql @@ -0,0 +1,6 @@ +query UserDetail ($id: ID!) { + user(id: $id) { + id + username + } +} diff --git a/client/coral-ui/components/Drawer.css b/client/coral-ui/components/Drawer.css new file mode 100644 index 000000000..57a44075e --- /dev/null +++ b/client/coral-ui/components/Drawer.css @@ -0,0 +1,12 @@ +.wrapper { + transition: opacity 250ms; + position: fixed; + z-index: 10000; + background-color: rgba(0, 0, 0, .2); + height: 100%; + width: 100%; +} + +.drawer { + transition: transform 500ms ease-in-out; +} diff --git a/client/coral-ui/components/Drawer.js b/client/coral-ui/components/Drawer.js new file mode 100644 index 000000000..d56677d76 --- /dev/null +++ b/client/coral-ui/components/Drawer.js @@ -0,0 +1,17 @@ +import React, {PropTypes} from 'react'; +import styles from './Drawer.css'; + +const Drawer = (props) => { + return ( +
    +
    {props.children}
    +
    ×
    +
    + ); +}; + +Drawer.propTypes = { + active: PropTypes.bool +}; + +export default Drawer; diff --git a/client/coral-ui/index.js b/client/coral-ui/index.js index 148da0383..3e9456727 100644 --- a/client/coral-ui/index.js +++ b/client/coral-ui/index.js @@ -23,3 +23,4 @@ export {default as Select} from './components/Select'; export {default as Option} from './components/Option'; export {default as SnackBar} from './components/SnackBar'; export {default as TextArea} from './components/TextArea'; +export {default as Drawer} from './components/Drawer'; diff --git a/graph/resolvers/root_query.js b/graph/resolvers/root_query.js index 4dcb7ea11..622584310 100644 --- a/graph/resolvers/root_query.js +++ b/graph/resolvers/root_query.js @@ -96,6 +96,16 @@ const RootQuery = { return await Users.getByQuery({ids: currentUser.ignoresUsers}); }, + // this returns an arbitrary user + user(_, {id}, {user, loaders: {Users}}) { + console.log("user id!", id); + if (user == null || !user.hasRoles('ADMIN')) { + return null; + } + + return Users.getByID.load(id); + }, + // 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. users(_, {query: {action_type, limit, cursor, sort}}, {user, loaders: {Users, Actions}}) { diff --git a/graph/typeDefs.graphql b/graph/typeDefs.graphql index 81105ff58..4fa7a1531 100644 --- a/graph/typeDefs.graphql +++ b/graph/typeDefs.graphql @@ -585,6 +585,9 @@ type RootQuery { # Users returned based on a query. users(query: UsersQuery): [User] + # a single User by id + user(id: ID!): User + # Asset metrics related to user actions are saturated into the assets # returned. Parameters `from` and `to` are related to the action created_at field. assetMetrics(from: Date!, to: Date!, sort: ASSET_METRICS_SORT!, limit: Int = 10): [Asset!] From 4a42436b7b4b41921f28bc580a842faa8f250313 Mon Sep 17 00:00:00 2001 From: riley Date: Wed, 3 May 2017 12:54:02 -0600 Subject: [PATCH 02/58] load user via HOC UserDetail --- client/coral-admin/src/actions/moderation.js | 2 + .../coral-admin/src/components/UserDetail.js | 39 ---------------- .../coral-admin/src/constants/moderation.js | 2 + .../ModerationQueue/ModerationContainer.js | 29 ++++++------ .../containers/ModerationQueue/UserDetail.js | 46 +++++++++++++++++++ .../ModerationQueue/components/Comment.js | 5 +- .../coral-admin/src/graphql/queries/index.js | 20 +++----- client/coral-admin/src/reducers/moderation.js | 5 ++ graph/resolvers/root_query.js | 1 - graph/resolvers/user.js | 9 ++++ graph/typeDefs.graphql | 11 +++++ 11 files changed, 97 insertions(+), 72 deletions(-) delete mode 100644 client/coral-admin/src/components/UserDetail.js create mode 100644 client/coral-admin/src/containers/ModerationQueue/UserDetail.js diff --git a/client/coral-admin/src/actions/moderation.js b/client/coral-admin/src/actions/moderation.js index b7117181e..557954b70 100644 --- a/client/coral-admin/src/actions/moderation.js +++ b/client/coral-admin/src/actions/moderation.js @@ -18,3 +18,5 @@ export const hideShortcutsNote = () => { return {type: actions.HIDE_SHORTCUTS_NOTE}; }; + +export const viewUserDetail = userId => ({type: actions.VIEW_USER_DETAIL, userId}); diff --git a/client/coral-admin/src/components/UserDetail.js b/client/coral-admin/src/components/UserDetail.js deleted file mode 100644 index aa6997dc4..000000000 --- a/client/coral-admin/src/components/UserDetail.js +++ /dev/null @@ -1,39 +0,0 @@ -import React, {PropTypes} from 'react'; -import {Button, Drawer} from 'coral-ui'; -import styles from './UserDetail.js'; - -const UserDetail = ({user}) => { - const localProfile = user.profiles.find(p => p.provider === 'local'); - const facebookProfile = user.profiles.find(p => p.provider === 'facebook'); - let profile; - if (localProfile) { - profile = profile.id; - } else if (facebookProfile) { - profile = {facebookProfile.id}; - } - - return ( - -

    {user.username}

    -

    {profile}

    - Member since {user.created_at} -
    - Account summary -
    Data represents the last six months of activity -
    -
    -
    - ); -}; - -UserDetail.propTypes = { - user: PropTypes.shape({ - username: PropTypes.string.isRequired, - profiles: PropTypes.arrayOf(PropTypes.shape({ - id: PropTypes.string, - provider: PropTypes.string - })) - }) -}; - -export default UserDetail; diff --git a/client/coral-admin/src/constants/moderation.js b/client/coral-admin/src/constants/moderation.js index 14672146e..1d09b0e1a 100644 --- a/client/coral-admin/src/constants/moderation.js +++ b/client/coral-admin/src/constants/moderation.js @@ -3,3 +3,5 @@ export const SINGLE_VIEW = 'SINGLE_VIEW'; export const SHOW_BANUSER_DIALOG = 'SHOW_BANUSER_DIALOG'; export const HIDE_BANUSER_DIALOG = 'HIDE_BANUSER_DIALOG'; export const HIDE_SHORTCUTS_NOTE = 'HIDE_SHORTCUTS_NOTE'; +export const VIEW_USER_DETAIL = 'VIEW_USER_DETAIL'; +export const HIDE_USER_DETAIL = 'HIDE_USER_DETAIL'; diff --git a/client/coral-admin/src/containers/ModerationQueue/ModerationContainer.js b/client/coral-admin/src/containers/ModerationQueue/ModerationContainer.js index fac092a77..b76de1d05 100644 --- a/client/coral-admin/src/containers/ModerationQueue/ModerationContainer.js +++ b/client/coral-admin/src/containers/ModerationQueue/ModerationContainer.js @@ -5,12 +5,19 @@ import key from 'keymaster'; import isEqual from 'lodash/isEqual'; import styles from './components/styles.css'; -import {modQueueQuery, getUserDetail} from '../../graphql/queries'; +import {modQueueQuery} from '../../graphql/queries'; import {banUser, setCommentStatus} from '../../graphql/mutations'; import {fetchSettings} from 'actions/settings'; import {updateAssets} from 'actions/assets'; -import {toggleModal, singleView, showBanUserDialog, hideBanUserDialog, hideShortcutsNote} from 'actions/moderation'; +import { + toggleModal, + singleView, + showBanUserDialog, + hideBanUserDialog, + hideShortcutsNote, + viewUserDetail +} from 'actions/moderation'; import {Spinner} from 'coral-ui'; import BanUserDialog from '../../components/BanUserDialog'; @@ -19,7 +26,7 @@ import ModerationMenu from './components/ModerationMenu'; import ModerationHeader from './components/ModerationHeader'; import NotFoundAsset from './components/NotFoundAsset'; import ModerationKeysModal from '../../components/ModerationKeysModal'; -import UserDetail from '../../components/UserDetail'; +import UserDetail from './UserDetail'; class ModerationContainer extends Component { state = { @@ -77,15 +84,6 @@ class ModerationContainer extends Component { } } - viewUserDetail = (id) => { - console.log('getting user detail', id, typeof getUserDetail); - try { - getUserDetail({id}); - } catch (e) { - console.log(e); - } - } - selectSort = (sort) => { this.setState({sort}); this.props.modQueueResort(sort); @@ -120,7 +118,7 @@ class ModerationContainer extends Component { } render () { - const {data, moderation, settings, assets, onClose, ...props} = this.props; + const {data, moderation, settings, assets, onClose, viewUserDetail, ...props} = this.props; const providedAssetId = this.props.params.id; const activeTab = this.props.route.path === ':id' ? 'premod' : this.props.route.path; @@ -190,7 +188,7 @@ class ModerationContainer extends Component { assetId={providedAssetId} sort={this.state.sort} commentCount={activeTabCount} - viewUserDetail={this.viewUserDetail} + viewUserDetail={viewUserDetail} /> - {data.user && } + {moderation.userDetailId && }
    ); } @@ -224,6 +222,7 @@ const mapDispatchToProps = dispatch => ({ singleView: () => dispatch(singleView()), updateAssets: assets => dispatch(updateAssets(assets)), fetchSettings: () => dispatch(fetchSettings()), + viewUserDetail: id => dispatch(viewUserDetail(id)), showBanUserDialog: (user, commentId, showRejectedNote) => dispatch(showBanUserDialog(user, commentId, showRejectedNote)), hideBanUserDialog: () => dispatch(hideBanUserDialog(false)), hideShortcutsNote: () => dispatch(hideShortcutsNote()), diff --git a/client/coral-admin/src/containers/ModerationQueue/UserDetail.js b/client/coral-admin/src/containers/ModerationQueue/UserDetail.js new file mode 100644 index 000000000..45d98c56c --- /dev/null +++ b/client/coral-admin/src/containers/ModerationQueue/UserDetail.js @@ -0,0 +1,46 @@ +import React, {PropTypes} from 'react'; +import {Button, Drawer} from 'coral-ui'; +import styles from './UserDetail.js'; +import {compose} from 'react-apollo'; +import {getUserDetail} from 'coral-admin/src/graphql/queries'; + +class UserDetail extends React.Component { + static propTypes = { + id: PropTypes.string.isRequired + } + + render () { + const {data} = this.props; + + if (!('user' in data)) { + return null; + } + + const {user} = data; + const localProfile = user.profiles.find(p => p.provider === 'local'); + const facebookProfile = user.profiles.find(p => p.provider === 'facebook'); + let profile; + if (localProfile) { + profile = localProfile.id; + } else if (facebookProfile) { + profile = {facebookProfile.id}; + } + + return ( + +

    {user.username}

    +

    {profile}

    + Member since {user.created_at} +
    + Account summary +
    Data represents the last six months of activity +
    +
    +
    + ); + } +} + +export default compose( + getUserDetail +)(UserDetail); diff --git a/client/coral-admin/src/containers/ModerationQueue/components/Comment.js b/client/coral-admin/src/containers/ModerationQueue/components/Comment.js index b35c688cd..8956e6e03 100644 --- a/client/coral-admin/src/containers/ModerationQueue/components/Comment.js +++ b/client/coral-admin/src/containers/ModerationQueue/components/Comment.js @@ -35,10 +35,7 @@ const Comment = ({actions = [], comment, viewUserDetail, ...props}) => {
    - { - console.log('clickt', comment.user.id); - viewUserDetail(comment.user.id); - }}> + viewUserDetail(comment.user.id)}> {comment.user.name} diff --git a/client/coral-admin/src/graphql/queries/index.js b/client/coral-admin/src/graphql/queries/index.js index aa5ffa505..8ab3c7292 100644 --- a/client/coral-admin/src/graphql/queries/index.js +++ b/client/coral-admin/src/graphql/queries/index.js @@ -95,16 +95,10 @@ export const modQueueResort = (id, fetchMore) => (sort) => { }); }; -export const getUserDetail = ({id}) => { - console.log('close', id); - return graphql(USER_DETAIL, { - options: () => { - console.log('so close!', id); - return { - variables: { - id - } - }; - } - }); -}; +export const getUserDetail = graphql(USER_DETAIL, { + options: ({id}) => { + return { + variables: {id} + }; + } +}); diff --git a/client/coral-admin/src/reducers/moderation.js b/client/coral-admin/src/reducers/moderation.js index a40ac865f..d8b9c59bb 100644 --- a/client/coral-admin/src/reducers/moderation.js +++ b/client/coral-admin/src/reducers/moderation.js @@ -6,6 +6,7 @@ const initialState = Map({ modalOpen: false, user: Map({}), commentId: null, + userDetailId: null, banDialog: false, shortcutsNoteVisible: window.localStorage.getItem('coral:shortcutsNote') || 'show' }); @@ -35,6 +36,10 @@ export default function moderation (state = initialState, action) { case actions.HIDE_SHORTCUTS_NOTE: return state .set('shortcutsNoteVisible', 'hide'); + case actions.VIEW_USER_DETAIL: + return state.set('userDetailId', action.userId); + case actions.HIDE_USER_DETAIL: + return state.set('userDetailId', null); default : return state; } diff --git a/graph/resolvers/root_query.js b/graph/resolvers/root_query.js index 622584310..9dd78c2fc 100644 --- a/graph/resolvers/root_query.js +++ b/graph/resolvers/root_query.js @@ -98,7 +98,6 @@ const RootQuery = { // this returns an arbitrary user user(_, {id}, {user, loaders: {Users}}) { - console.log("user id!", id); if (user == null || !user.hasRoles('ADMIN')) { return null; } diff --git a/graph/resolvers/user.js b/graph/resolvers/user.js index d8ed7ee15..4f6528454 100644 --- a/graph/resolvers/user.js +++ b/graph/resolvers/user.js @@ -20,6 +20,15 @@ const User = { return null; }, + profiles({id, profiles}, _, {user}) { + + // if the user is not an admin, do not return the profiles + if (user && (user.hasRoles('ADMIN') || user.id === id)) { + return profiles; + } + + return null; + }, roles({id, roles}, _, {user}) { // If the user is not an admin, only return the current user's roles. diff --git a/graph/typeDefs.graphql b/graph/typeDefs.graphql index 4fa7a1531..f1d0acac4 100644 --- a/graph/typeDefs.graphql +++ b/graph/typeDefs.graphql @@ -20,6 +20,14 @@ enum USER_ROLES { MODERATOR } +type UserProfile { + # the id is an identifier for the user profile (email, facebook id, etc) + id: String! + + # name of the provider attached to the authentication mode + provider: String! +} + # Any person who can author comments, create actions, and view comments on a # stream. type User { @@ -39,6 +47,9 @@ type User { # the current roles of the user. roles: [USER_ROLES] + # the current profiles of the user. + profiles: [UserProfile] + # determines whether the user can edit their username canEditName: Boolean From 57b0b5ec67b71a2d2cc0d269a0459f98d13020aa Mon Sep 17 00:00:00 2001 From: riley Date: Thu, 4 May 2017 13:58:43 -0600 Subject: [PATCH 03/58] add a drawer for the user details --- client/coral-admin/src/actions/moderation.js | 1 + .../coral-admin/src/components/UserDetail.css | 0 .../ModerationQueue/ModerationContainer.js | 9 +++-- .../containers/ModerationQueue/UserDetail.css | 12 +++++++ .../containers/ModerationQueue/UserDetail.js | 30 +++++++++++----- .../src/graphql/queries/userDetail.graphql | 5 +++ client/coral-ui/components/Drawer.css | 35 ++++++++++++++----- client/coral-ui/components/Drawer.js | 14 ++++---- graph/resolvers/user.js | 11 ++++-- graph/typeDefs.graphql | 3 ++ package.json | 6 ++-- yarn.lock | 2 +- 12 files changed, 96 insertions(+), 32 deletions(-) delete mode 100644 client/coral-admin/src/components/UserDetail.css create mode 100644 client/coral-admin/src/containers/ModerationQueue/UserDetail.css diff --git a/client/coral-admin/src/actions/moderation.js b/client/coral-admin/src/actions/moderation.js index 557954b70..5107669cf 100644 --- a/client/coral-admin/src/actions/moderation.js +++ b/client/coral-admin/src/actions/moderation.js @@ -20,3 +20,4 @@ export const hideShortcutsNote = () => { }; export const viewUserDetail = userId => ({type: actions.VIEW_USER_DETAIL, userId}); +export const hideUserDetail = () => ({type: actions.HIDE_USER_DETAIL}); diff --git a/client/coral-admin/src/components/UserDetail.css b/client/coral-admin/src/components/UserDetail.css deleted file mode 100644 index e69de29bb..000000000 diff --git a/client/coral-admin/src/containers/ModerationQueue/ModerationContainer.js b/client/coral-admin/src/containers/ModerationQueue/ModerationContainer.js index 29612b0a3..480666637 100644 --- a/client/coral-admin/src/containers/ModerationQueue/ModerationContainer.js +++ b/client/coral-admin/src/containers/ModerationQueue/ModerationContainer.js @@ -16,7 +16,8 @@ import { showBanUserDialog, hideBanUserDialog, hideShortcutsNote, - viewUserDetail + viewUserDetail, + hideUserDetail } from 'actions/moderation'; import {Spinner} from 'coral-ui'; @@ -118,7 +119,7 @@ class ModerationContainer extends Component { } render () { - const {data, moderation, settings, assets, onClose, viewUserDetail, ...props} = this.props; + const {data, moderation, settings, assets, onClose, viewUserDetail, hideUserDetail, ...props} = this.props; const providedAssetId = this.props.params.id; const activeTab = this.props.route.path === ':id' ? 'premod' : this.props.route.path; @@ -189,6 +190,7 @@ class ModerationContainer extends Component { sort={this.state.sort} commentCount={activeTabCount} viewUserDetail={viewUserDetail} + hideUserDetail={hideUserDetail} /> - {moderation.userDetailId && } + {moderation.userDetailId && }
    ); } @@ -223,6 +225,7 @@ const mapDispatchToProps = dispatch => ({ updateAssets: assets => dispatch(updateAssets(assets)), fetchSettings: () => dispatch(fetchSettings()), viewUserDetail: id => dispatch(viewUserDetail(id)), + hideUserDetail: () => dispatch(hideUserDetail()), showBanUserDialog: (user, commentId, showRejectedNote) => dispatch(showBanUserDialog(user, commentId, showRejectedNote)), hideBanUserDialog: () => dispatch(hideBanUserDialog(false)), hideShortcutsNote: () => dispatch(hideShortcutsNote()), diff --git a/client/coral-admin/src/containers/ModerationQueue/UserDetail.css b/client/coral-admin/src/containers/ModerationQueue/UserDetail.css new file mode 100644 index 000000000..9f41a9d8d --- /dev/null +++ b/client/coral-admin/src/containers/ModerationQueue/UserDetail.css @@ -0,0 +1,12 @@ +.copyButton { + float: right; + top: -10px; +} + +.memberSince { + clear: both; +} + +.small { + color: #aaa; +} diff --git a/client/coral-admin/src/containers/ModerationQueue/UserDetail.js b/client/coral-admin/src/containers/ModerationQueue/UserDetail.js index 45d98c56c..06587065e 100644 --- a/client/coral-admin/src/containers/ModerationQueue/UserDetail.js +++ b/client/coral-admin/src/containers/ModerationQueue/UserDetail.js @@ -1,16 +1,27 @@ import React, {PropTypes} from 'react'; import {Button, Drawer} from 'coral-ui'; -import styles from './UserDetail.js'; +import styles from './UserDetail.css'; import {compose} from 'react-apollo'; import {getUserDetail} from 'coral-admin/src/graphql/queries'; class UserDetail extends React.Component { static propTypes = { - id: PropTypes.string.isRequired + id: PropTypes.string.isRequired, + hideUserDetail: PropTypes.func.isRequired + } + + copyPermalink () { + this.profile.select(); + try { + document.execCommand('copy'); + } catch (e) { + + /* nothing */ + } } render () { - const {data} = this.props; + const {data, hideUserDetail} = this.props; if (!('user' in data)) { return null; @@ -27,13 +38,16 @@ class UserDetail extends React.Component { } return ( - +

    {user.username}

    -

    {profile}

    - Member since {user.created_at} + +

    this.profile = ref} contentEditable="true">{profile}

    +

    Member since {new Date(user.created_at).toLocaleString()}


    - Account summary -
    Data represents the last six months of activity +

    + Account summary +
    Data represents the last six months of activity +

    diff --git a/client/coral-admin/src/graphql/queries/userDetail.graphql b/client/coral-admin/src/graphql/queries/userDetail.graphql index f489e16d9..6d42682ea 100644 --- a/client/coral-admin/src/graphql/queries/userDetail.graphql +++ b/client/coral-admin/src/graphql/queries/userDetail.graphql @@ -2,5 +2,10 @@ query UserDetail ($id: ID!) { user(id: $id) { id username + created_at + profiles { + id + provider + } } } diff --git a/client/coral-ui/components/Drawer.css b/client/coral-ui/components/Drawer.css index 57a44075e..b967b20ab 100644 --- a/client/coral-ui/components/Drawer.css +++ b/client/coral-ui/components/Drawer.css @@ -1,12 +1,31 @@ -.wrapper { - transition: opacity 250ms; +.drawer { + max-width: 700px; + padding: 20px; position: fixed; - z-index: 10000; - background-color: rgba(0, 0, 0, .2); - height: 100%; - width: 100%; + top: 0; + right: 0; + bottom: 0; + background-color: white; + transition: transform 500ms ease-in-out; + box-shadow: -3px 0px 4px 0px rgba(0,0,0,0.15); } -.drawer { - transition: transform 500ms ease-in-out; +.closeButton { + position: absolute; + width: 40px; + height: 40px; + left: -40px; + background-color: white; + border-radius: 4px 0 0 4px; + box-sizing: border-box; + font-size: 32px; + top: 60px; + box-shadow: -1px 3px 4px 0px rgba(0,0,0,0.15); + text-align: center; + padding-top: 10px; + cursor: pointer; + + &:hover { + color: #ccc; + } } diff --git a/client/coral-ui/components/Drawer.js b/client/coral-ui/components/Drawer.js index d56677d76..d896203a8 100644 --- a/client/coral-ui/components/Drawer.js +++ b/client/coral-ui/components/Drawer.js @@ -1,17 +1,19 @@ import React, {PropTypes} from 'react'; import styles from './Drawer.css'; +import onClickOutside from 'react-onclickoutside'; -const Drawer = (props) => { +const Drawer = ({children, handleClickOutside}) => { return ( -
    -
    {props.children}
    -
    ×
    +
    +
    ×
    + {children}
    ); }; Drawer.propTypes = { - active: PropTypes.bool + active: PropTypes.bool, + handleClickOutside: PropTypes.func.isRequired }; -export default Drawer; +export default onClickOutside(Drawer); diff --git a/graph/resolvers/user.js b/graph/resolvers/user.js index 4f6528454..4dbdc43c4 100644 --- a/graph/resolvers/user.js +++ b/graph/resolvers/user.js @@ -10,6 +10,13 @@ const User = { } }, + created_at({roles, created_at}, _, {user}) { + if (user && user.hasRoles('ADMIN')) { + return created_at; + } + + return null; + }, comments({id}, _, {loaders: {Comments}, user}) { // If the user is not an admin, only return comment list for the owner of @@ -20,10 +27,10 @@ const User = { return null; }, - profiles({id, profiles}, _, {user}) { + profiles({profiles}, _, {user}) { // if the user is not an admin, do not return the profiles - if (user && (user.hasRoles('ADMIN') || user.id === id)) { + if (user && user.hasRoles('ADMIN')) { return profiles; } diff --git a/graph/typeDefs.graphql b/graph/typeDefs.graphql index a505f558e..7a5f0b76f 100644 --- a/graph/typeDefs.graphql +++ b/graph/typeDefs.graphql @@ -38,6 +38,9 @@ type User { # Username of a user. username: String! + # creation date of user + created_at: String! + # Action summaries against the user. action_summaries: [ActionSummary]! diff --git a/package.json b/package.json index bfca21643..d61431b3a 100644 --- a/package.json +++ b/package.json @@ -98,12 +98,10 @@ "react-recaptcha": "^2.2.6", "recompose": "^0.23.1", "redis": "^2.7.1", - "uuid": "^3.0.1", - "simplemde": "^1.11.2", - "subscriptions-transport-ws": "^0.5.5-alpha.0", "resolve": "^1.3.2", "semver": "^5.3.0", "simplemde": "^1.11.2", + "subscriptions-transport-ws": "^0.5.5-alpha.0", "uuid": "^3.0.1" }, "devDependencies": { @@ -174,7 +172,7 @@ "react-linkify": "^0.1.3", "react-mdl": "^1.7.2", "react-mdl-selectfield": "^0.2.0", - "react-onclickoutside": "^5.7.1", + "react-onclickoutside": "^5.11.1", "react-redux": "^4.4.5", "react-router": "^3.0.0", "react-tagsinput": "^3.14.0", diff --git a/yarn.lock b/yarn.lock index 99703bdf3..7a5163706 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6780,7 +6780,7 @@ react-mdl@^1.7.1, react-mdl@^1.7.2: lodash.isequal "^4.4.0" prop-types "^15.5.0" -react-onclickoutside@^5.7.1: +react-onclickoutside@^5.11.1: version "5.11.1" resolved "https://registry.yarnpkg.com/react-onclickoutside/-/react-onclickoutside-5.11.1.tgz#00314e52567cf55faba94cabbacd119619070623" dependencies: From 084f6b28ed0120fab1abed1a5c3092857b5bfe09 Mon Sep 17 00:00:00 2001 From: Riley Davis Date: Tue, 9 May 2017 10:06:41 -0600 Subject: [PATCH 04/58] merge master --- README.md | 38 +++- client/coral-admin/src/AppRouter.js | 4 +- .../ModerationQueue/components/Comment.js | 14 +- .../components/ModerationMenu.js | 12 +- .../ModerationQueue/components/styles.css | 21 ++ .../src/graphql/fragments/commentView.graphql | 1 + client/coral-admin/src/translations.json | 2 + .../components/ConfigureCommentStream.css | 6 +- .../components/ConfigureCommentStream.js | 2 +- docs/frontend/PLUGINS-experimental.md | 102 ++++++++++ docs/frontend/PLUGINS.md | 186 ++++++++++++++++++ graph/pubsub.js | 2 +- 12 files changed, 371 insertions(+), 19 deletions(-) create mode 100644 docs/frontend/PLUGINS-experimental.md create mode 100644 docs/frontend/PLUGINS.md diff --git a/README.md b/README.md index ef621b59c..74097d48c 100644 --- a/README.md +++ b/README.md @@ -42,9 +42,43 @@ available in the format: `://` without the path. Refer to the wiki page on [Configuration Loading](https://github.com/coralproject/talk/wiki/Configuration-Loading) for alternative methods of loading configuration during development. -### License +## Supported Browsers - Copyright 2016 Mozilla Foundation +### Web + +- Chrome: latest 2 versions +- Firefox: latest 2 versions, and most recent extended support version, if any +- Safari: latest 2 versions +- Internet Explorer: IE Edge, 11 + +### iOS Devices + +- iPad +- iPad Pro +- iPhone 6 Plus +- iPhone 6 +- iPhone 5 + +### iOS Browsers + +- Chrome for iOS: latest version +- Firefox for iOS: latest version +- Safari for iOS: latest version + +### Android Devices + +- Galaxy S5 +- Nexus 5X +- Nexus 6P + +### Android Browsers + +- Chrome for Android: latest version +- Firefox for Android: latest version + +## License + + Copyright 2017 Mozilla Foundation Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/client/coral-admin/src/AppRouter.js b/client/coral-admin/src/AppRouter.js index af3423acc..989316dd9 100644 --- a/client/coral-admin/src/AppRouter.js +++ b/client/coral-admin/src/AppRouter.js @@ -1,5 +1,5 @@ import React from 'react'; -import {Router, Route, IndexRoute, IndexRedirect, browserHistory} from 'react-router'; +import {Router, Route, IndexRedirect, browserHistory} from 'react-router'; import Stories from 'containers/Stories/Stories'; import Configure from 'containers/Configure/Configure'; @@ -18,7 +18,7 @@ const routes = (
    - + diff --git a/client/coral-admin/src/containers/ModerationQueue/components/Comment.js b/client/coral-admin/src/containers/ModerationQueue/components/Comment.js index 8294e8c43..155d0c551 100644 --- a/client/coral-admin/src/containers/ModerationQueue/components/Comment.js +++ b/client/coral-admin/src/containers/ModerationQueue/components/Comment.js @@ -18,7 +18,7 @@ import I18n from 'coral-framework/modules/i18n/i18n'; import translations from 'coral-admin/src/translations.json'; const lang = new I18n(translations); -const Comment = ({actions = [], comment, viewUserDetail, ...props}) => { +const Comment = ({actions = [], comment, viewUserDetail, suspectWords, bannedWords, ...props}) => { const links = linkify.getMatches(comment.body); const linkText = links ? links.map(link => link.raw) : []; const flagActionSummaries = getActionSummary('FlagActionSummary', comment); @@ -30,6 +30,13 @@ const Comment = ({actions = [], comment, viewUserDetail, ...props}) => { commentType = 'flagged'; } + // since words are checked against word boundaries on the backend, + // this should be the behavior on the front end as well. + // currently the highlighter plugin does not support this out of the box. + const searchWords = [...suspectWords, ...bannedWords].filter(w => { + return new RegExp(`(^|\\s)${w}(\\s|$)`).test(comment.body); + }).concat(linkText); + return (
  • @@ -60,8 +67,8 @@ const Comment = ({actions = [], comment, viewUserDetail, ...props}) => {

    + searchWords={searchWords} + textToHighlight={comment.body} /> {lang.t('comment.view_context')}

    {links ? Contains Link : null} @@ -108,6 +115,7 @@ Comment.propTypes = { }).isRequired, asset: PropTypes.shape({ title: PropTypes.string, + url: PropTypes.string, id: PropTypes.string }) }) diff --git a/client/coral-admin/src/containers/ModerationQueue/components/ModerationMenu.js b/client/coral-admin/src/containers/ModerationQueue/components/ModerationMenu.js index 39594274e..7a50e6270 100644 --- a/client/coral-admin/src/containers/ModerationQueue/components/ModerationMenu.js +++ b/client/coral-admin/src/containers/ModerationQueue/components/ModerationMenu.js @@ -28,12 +28,6 @@ const ModerationMenu = ( activeClassName={styles.active}> {lang.t('modqueue.all')} - - {lang.t('modqueue.approved')} - {lang.t('modqueue.flagged')} + + {lang.t('modqueue.approved')} + (

    {lang.t('configureCommentStream.title')}

    -

    {lang.t('configureCommentStream.description')}

    +

    {lang.t('configureCommentStream.description')}

    • diff --git a/docs/frontend/PLUGINS-experimental.md b/docs/frontend/PLUGINS-experimental.md new file mode 100644 index 000000000..895be5900 --- /dev/null +++ b/docs/frontend/PLUGINS-experimental.md @@ -0,0 +1,102 @@ +# Experimental plugins + +Talk plugins are, in essence, small programs that hook into the core application in a variety of ways. Ultimately, this code can do anything that javascript is capable of. In addition, plugins can import any core code to hook into talk at any level. + +If you want to write plugins that integrate with core code beyond the api described in [PLUGINS.md](PLUGINS.md), please keep the following things in mind: + +* core code may change and break your plugin +* you may introduce inefficiencies with your plugin that could hurt performance/crash Talk +* you may cause bugs in other areas of Talk + +If you'd like to build a supported plugin but don't have the hooks you need, please file an issue on this repo and we can discuss deepening the supported plugin api! + +With that said, here's some of the prime experimental integration points: + +## Reducers and Actions : Redux + +Talk is powered by Redux and our plugins can too! Our plugins can have their own reducers and actions. + +```js +import MyButton from './MyButton'; +import reducer from './reducer'; + +export default { + slots: { + commentDetail: [MyButton], + }, + reducer +}; +``` + +## Import Actions from Talk +We can easily trigger `Talk` actions in our plugin Components. + +```js +import React, {Component} from 'react'; +import {connect} from 'react-redux'; +import {bindActionCreators} from 'redux'; +import {addTag, removeTag} from 'coral-plugin-commentbox/actions'; + +class MyButton extends Component { + render() { + return ; + } +} + +const mapStateToProps = ({commentBox}) => ({commentBox}); + +const mapDispatchToProps = dispatch => + bindActionCreators({addTag, removeTag}, dispatch); + +export default connect(mapStateToProps, mapDispatchToProps)(OffTopicCheckbox); +``` + +## ESlint and Babel +In talk we use `eslint:recommended` and Babel with the latest ECMAScript Features. But you can use your own! +While building your plugin you need to specify a `.eslintrc.json` file and a`.babelrc` file. + +#### `.eslintrc.json` +```json +{ + "env": { + "browser": true, + "es6": true, + "mocha": true + }, + "parserOptions": { + "sourceType": "module", + "ecmaFeatures": { + "experimentalObjectRestSpread": true, + "jsx": true + } + }, + "parser": "babel-eslint", + "plugins": [ + "react" + ], + "rules": { + "react/jsx-uses-react": "error", + "react/jsx-uses-vars": "error", + "no-console": ["warn", { "allow": ["warn", "error"] }] + } +} +```` + + +#### `. babelrc ` +```json +{ + "presets": [ + "es2015" + ], + "plugins": [ + "add-module-exports", + "transform-class-properties", + "transform-decorators-legacy", + "transform-object-assign", + "transform-object-rest-spread", + "transform-async-to-generator", + "transform-react-jsx" + ] +} +```` diff --git a/docs/frontend/PLUGINS.md b/docs/frontend/PLUGINS.md new file mode 100644 index 000000000..fa582877c --- /dev/null +++ b/docs/frontend/PLUGINS.md @@ -0,0 +1,186 @@ +# Plugins +We can build plugins to extend the functionality of Talk. + +This guide is a walkthrough of our plugin architecture and components that we provide that allow you to build on top of Core coral components without having to understand the concepts there in. It is organized into three sections: + +* Plugin architecture +* Using our building block components +* Styling + +Advanced users will quickly realize that our plugins have complete access to core code. If you would like to write advanced plugins that reach outside of our published API as described in this document, please see [our notes on experimental pluginss](PLUGINS-experimental.md). + +Under the hood our plugins are powered by *React*, *Redux* and *GraphQL*. We can also build them with simple vanilla javascript. + +## Plugin Architecture + +The plugins live in the `/plugins` folder. Each plugin must have an `index.js` file and two folders `client` and `server`. + +### The Client Folder +The frontend of our plugin lives inside the `client` folder. The `client` folder must have an `index.js` file that exports the configuration of our plugin. + +``` +my-plugin/ + ├── client/ + │ └── index.js <-- index for client side functionality + ├── server/ + └── index.js <-- base plugin index +``` + +For now our base plugin `index.js` file should look like this: + +```js +export default { + // We will add more here later. +}; +``` + +### Creating a Component + +We can add our components (or any other javascript code) within the `client` folder. + +``` +my-plugin/ + ├── client/ + │ ├── MyComponent.js + │ └── index.js + ├── server/ + └── index.js +``` + +Our component could look like this: + +```js +import React, {Component} from 'react'; + +class MyButton extends Component { + render() { + return ; + } +} + +export default MyButton; +``` + +Here we create a component that renders a `button`. Now that we created our component we need to specify where it should get injected within Talk! + +To tell Talk where that Component should get injected we need to specify which *Slots* to insert it into. + +```js +import React from 'react'; +export default = () => ; +``` + +### Slots +In Talk we have defined specific *Slots* where we can inject components. + +Here is how we specify our slots config in `my-plugin/index.js` + +```js +import MyButton from './MyButton'; + +export default { + slots: { + commentDetail: [MyButton] + } +}; +``` + +Here I’m specifying that the MyComponent Component will take place within the `commentDetail` in Talk. + +`commentDetail` it’s a specific slot in the CommentStream. It means that it will be embedded inside de comment detail. + +Slots properties take an`Array` so we can add as many components as we want. + +## Building Blocks (TBD) + +`Note: the concepts in this section are still to be implemented. Code samples are for discussion and may change.` + +In order to allow you to build more complex plugins, we have wrapped some of our functionality in higher order componenets that expose a simple api. + +### Reactions + +Reactions provide users the ability to 'like', 'respect', etc... comments. + +``` + + + +``` + +Note: some server side work will need to accompany this client side component. See the like and respect plugins as examples. + +### Comment Stream + +Comment streams may be created with filtering and ordering in place: + +* filter by user +* filter by tag +* sort by date ascending / descrnding + +``` + +``` + +### Comment Commit hooks + +// docs for the pre/post comment submit commit hooks + +### Mod Queues + +Moderation queues can be added via configuration objects passed in through plugins. + +Basic mod queues will resemble the current moderation queues but can be generated from different lists of comments. + +* filter by user tag +* filter by comment tag +* filter by comment status +* Custom queries (paired with back end plugins that provide queries to get the data) + +#### Advanced mod queues + +Advanced mod queues can be created giving plugin authors the power to create the cards that appear in the queue, create actions and custom buttons, etc... + +### Custom Configuration + +Plugins may rely on configuration options that admins/moderators can set in the Configuration section. + +Basic settings can be added via json configuration in a plugin. + +* Setting headline +* Setting description +* Setting input type +* Default value +* Variable name + +#### Advanced Custom Configuration (low prioritiy) + +Users can inject configuration interfaces that they create into the configuration allowing for more advanced configuration. + + +## Styling Plugins +Talk uses CSS Modules. This basically means that you can also add your CSS Module to your plugin without colliding with the rest of Talk! + +##### My Component +```js +import styles from './style.css'; + +class MyCoralButton extends Component { + render() { + return ; + } +} +```` + +Our `style.css` should could look like this. +```css + +.button { + background: coral; + border-radius: 3px; +} +``` + + +### The server folder and the index file +Read more about the `/server` and how to extend Talk here. +[talk/PLUGINS.md at master · coralproject/talk · GitHub](https://github.com/coralproject/talk/blob/master/PLUGINS.md) diff --git a/graph/pubsub.js b/graph/pubsub.js index a5ee95b80..704f83725 100644 --- a/graph/pubsub.js +++ b/graph/pubsub.js @@ -2,4 +2,4 @@ const {RedisPubSub} = require('graphql-redis-subscriptions'); const {connectionOptions} = require('../services/redis'); -module.exports = new RedisPubSub(connectionOptions); +module.exports = new RedisPubSub({connection: connectionOptions}); From fe279587b59df8a63856b1ec3b357e16a16ad7e5 Mon Sep 17 00:00:00 2001 From: Riley Davis Date: Wed, 10 May 2017 09:25:39 -0600 Subject: [PATCH 05/58] add staff to dropdown --- client/coral-admin/src/containers/Community/Table.js | 1 + models/user.js | 3 ++- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/client/coral-admin/src/containers/Community/Table.js b/client/coral-admin/src/containers/Community/Table.js index aacc9c82f..2924e7fd3 100644 --- a/client/coral-admin/src/containers/Community/Table.js +++ b/client/coral-admin/src/containers/Community/Table.js @@ -65,6 +65,7 @@ class Table extends Component { label={lang.t('community.role')} onChange={role => this.onRoleChange(row.id, role)}> + diff --git a/models/user.js b/models/user.js index 458ac68f0..ec215b3fa 100644 --- a/models/user.js +++ b/models/user.js @@ -5,7 +5,8 @@ const uuid = require('uuid'); // USER_ROLES is the array of roles that is permissible as a user role. const USER_ROLES = [ 'ADMIN', - 'MODERATOR' + 'MODERATOR', + 'STAFF' ]; // USER_STATUS is the list of statuses that are permitted for the user status. From 40ff265ab1b4bbc4781358dc1eb7d08b7b45523b Mon Sep 17 00:00:00 2001 From: Riley Davis Date: Wed, 10 May 2017 11:07:48 -0600 Subject: [PATCH 06/58] merge master --- client/coral-admin/src/actions/moderation.js | 2 +- client/coral-admin/src/components/BanUserDialog.js | 8 ++++---- .../coral-admin/src/components/ModerationList.css | 2 ++ .../src/containers/Dashboard/Dashboard.js | 8 +------- .../ModerationQueue/ModerationContainer.js | 10 ++++++---- .../ModerationQueue/components/Comment.js | 6 +++--- client/coral-admin/src/reducers/moderation.js | 5 ++++- docs/frontend/PLUGINS.md | 14 ++------------ 8 files changed, 23 insertions(+), 32 deletions(-) diff --git a/client/coral-admin/src/actions/moderation.js b/client/coral-admin/src/actions/moderation.js index 5107669cf..a65b8c704 100644 --- a/client/coral-admin/src/actions/moderation.js +++ b/client/coral-admin/src/actions/moderation.js @@ -4,7 +4,7 @@ export const toggleModal = open => ({type: actions.TOGGLE_MODAL, open}); export const singleView = () => ({type: actions.SINGLE_VIEW}); // Ban User Dialog -export const showBanUserDialog = (user, commentId, showRejectedNote) => ({type: actions.SHOW_BANUSER_DIALOG, user, commentId, showRejectedNote}); +export const showBanUserDialog = (user, commentId, commentStatus, showRejectedNote) => ({type: actions.SHOW_BANUSER_DIALOG, user, commentId, commentStatus, showRejectedNote}); export const hideBanUserDialog = (showDialog) => ({type: actions.HIDE_BANUSER_DIALOG, showDialog}); // hide shortcuts note diff --git a/client/coral-admin/src/components/BanUserDialog.js b/client/coral-admin/src/components/BanUserDialog.js index ee5830249..b259012f3 100644 --- a/client/coral-admin/src/components/BanUserDialog.js +++ b/client/coral-admin/src/components/BanUserDialog.js @@ -8,14 +8,14 @@ import I18n from 'coral-framework/modules/i18n/i18n'; import translations from '../translations'; const lang = new I18n(translations); -const onBanClick = (userId, commentId, handleBanUser, rejectComment, handleClose) => (e) => { +const onBanClick = (userId, commentId, commentStatus, handleBanUser, rejectComment, handleClose) => (e) => { e.preventDefault(); handleBanUser({userId}) .then(handleClose) - .then(() => rejectComment({commentId})); + .then(() => commentStatus === 'REJECTED' ? null : rejectComment({commentId})); }; -const BanUserDialog = ({open, handleClose, handleBanUser, rejectComment, user, commentId, showRejectedNote}) => ( +const BanUserDialog = ({open, handleClose, handleBanUser, rejectComment, user, commentId, commentStatus, showRejectedNote}) => ( {lang.t('bandialog.cancel')} -
    diff --git a/client/coral-admin/src/components/ModerationList.css b/client/coral-admin/src/components/ModerationList.css index 1e871f97c..4d4f37c9a 100644 --- a/client/coral-admin/src/components/ModerationList.css +++ b/client/coral-admin/src/components/ModerationList.css @@ -193,10 +193,12 @@ box-shadow: none; color: white; background-color: #519954; + cursor: not-allowed; } .reject__active, .rejected__active { color: white; background-color: #D03235; box-shadow: none; + cursor: not-allowed; } diff --git a/client/coral-admin/src/containers/Dashboard/Dashboard.js b/client/coral-admin/src/containers/Dashboard/Dashboard.js index f5d0aa39b..e6cfa27cd 100644 --- a/client/coral-admin/src/containers/Dashboard/Dashboard.js +++ b/client/coral-admin/src/containers/Dashboard/Dashboard.js @@ -6,7 +6,6 @@ import {getMetrics} from 'coral-admin/src/graphql/queries'; import FlagWidget from './FlagWidget'; import ActivityWidget from './ActivityWidget'; import CountdownTimer from 'coral-admin/src/components/CountdownTimer'; -import {showBanUserDialog, hideBanUserDialog} from 'coral-admin/src/actions/moderation'; import {Spinner} from 'coral-ui'; @@ -43,12 +42,7 @@ const mapStateToProps = state => { }; }; -const mapDispatchToProps = dispatch => ({ - showBanUserDialog: (user, commentId) => dispatch(showBanUserDialog(user, commentId)), - hideBanUserDialog: () => dispatch(hideBanUserDialog(false)) -}); - export default compose( - connect(mapStateToProps, mapDispatchToProps), + connect(mapStateToProps), getMetrics )(Dashboard); diff --git a/client/coral-admin/src/containers/ModerationQueue/ModerationContainer.js b/client/coral-admin/src/containers/ModerationQueue/ModerationContainer.js index 480666637..121871de7 100644 --- a/client/coral-admin/src/containers/ModerationQueue/ModerationContainer.js +++ b/client/coral-admin/src/containers/ModerationQueue/ModerationContainer.js @@ -52,12 +52,13 @@ class ModerationContainer extends Component { const {acceptComment, rejectComment} = this.props; const {selectedIndex} = this.state; const comments = this.getComments(); - const commentId = {commentId: comments[selectedIndex].id}; + const comment = comments[selectedIndex]; + const commentId = {commentId: comment.id}; if (accept) { - acceptComment(commentId); + comment.status !== 'ACCEPTED' && acceptComment(commentId); } else { - rejectComment(commentId); + comment.status !== 'REJECTED' && rejectComment(commentId); } } @@ -196,6 +197,7 @@ class ModerationContainer extends Component { open={moderation.banDialog} user={moderation.user} commentId={moderation.commentId} + commentStatus={moderation.commentStatus} handleClose={props.hideBanUserDialog} handleBanUser={props.banUser} showRejectedNote={moderation.showRejectedNote} @@ -226,7 +228,7 @@ const mapDispatchToProps = dispatch => ({ fetchSettings: () => dispatch(fetchSettings()), viewUserDetail: id => dispatch(viewUserDetail(id)), hideUserDetail: () => dispatch(hideUserDetail()), - showBanUserDialog: (user, commentId, showRejectedNote) => dispatch(showBanUserDialog(user, commentId, showRejectedNote)), + showBanUserDialog: (user, commentId, commentStatus, showRejectedNote) => dispatch(showBanUserDialog(user, commentId, commentStatus, showRejectedNote)), hideBanUserDialog: () => dispatch(hideBanUserDialog(false)), hideShortcutsNote: () => dispatch(hideShortcutsNote()), }); diff --git a/client/coral-admin/src/containers/ModerationQueue/components/Comment.js b/client/coral-admin/src/containers/ModerationQueue/components/Comment.js index 155d0c551..b38e0c4f0 100644 --- a/client/coral-admin/src/containers/ModerationQueue/components/Comment.js +++ b/client/coral-admin/src/containers/ModerationQueue/components/Comment.js @@ -48,7 +48,7 @@ const Comment = ({actions = [], comment, viewUserDetail, suspectWords, bannedWor {timeago().format(comment.created_at || (Date.now() - props.index * 60 * 1000), lang.getLocale().replace('-', '_'))} - props.showBanUserDialog(comment.user, comment.id, comment.status !== 'REJECTED')} /> + props.showBanUserDialog(comment.user, comment.id, comment.status, comment.status !== 'REJECTED')} />
    {comment.user.status === 'banned' ? @@ -81,8 +81,8 @@ const Comment = ({actions = [], comment, viewUserDetail, suspectWords, bannedWor user={comment.user} status={comment.status} active={active} - acceptComment={() => props.acceptComment({commentId: comment.id})} - rejectComment={() => props.rejectComment({commentId: comment.id})} />; + acceptComment={() => comment.status === 'ACCEPTED' ? null : props.acceptComment({commentId: comment.id})} + rejectComment={() => comment.status === 'REJECTED' ? null : props.rejectComment({commentId: comment.id})} />; })}
    diff --git a/client/coral-admin/src/reducers/moderation.js b/client/coral-admin/src/reducers/moderation.js index d8b9c59bb..b0ff99617 100644 --- a/client/coral-admin/src/reducers/moderation.js +++ b/client/coral-admin/src/reducers/moderation.js @@ -6,6 +6,7 @@ const initialState = Map({ modalOpen: false, user: Map({}), commentId: null, + commentStatus: null, userDetailId: null, banDialog: false, shortcutsNoteVisible: window.localStorage.getItem('coral:shortcutsNote') || 'show' @@ -15,12 +16,14 @@ export default function moderation (state = initialState, action) { switch (action.type) { case actions.HIDE_BANUSER_DIALOG: return state - .set('banDialog', false); + .set('banDialog', false) + .set('commentStatus', null); case actions.SHOW_BANUSER_DIALOG: return state .merge({ user: Map(action.user), commentId: action.commentId, + commentStatus: action.commentStatus, showRejectedNote: action.showRejectedNote, banDialog: true }); diff --git a/docs/frontend/PLUGINS.md b/docs/frontend/PLUGINS.md index fa582877c..8377d0359 100644 --- a/docs/frontend/PLUGINS.md +++ b/docs/frontend/PLUGINS.md @@ -95,18 +95,12 @@ Slots properties take an`Array` so we can add as many components as we want. `Note: the concepts in this section are still to be implemented. Code samples are for discussion and may change.` -In order to allow you to build more complex plugins, we have wrapped some of our functionality in higher order componenets that expose a simple api. +In order to allow you to build more complex plugins, we have wrapped some of our functionality in higher order components that expose a simple api. ### Reactions Reactions provide users the ability to 'like', 'respect', etc... comments. -``` - - - -``` - Note: some server side work will need to accompany this client side component. See the like and respect plugins as examples. ### Comment Stream @@ -115,11 +109,7 @@ Comment streams may be created with filtering and ordering in place: * filter by user * filter by tag -* sort by date ascending / descrnding - -``` - -``` +* sort by date ascending / descending ### Comment Commit hooks From 602c3d0be286e617007f0f2b092992074818075f Mon Sep 17 00:00:00 2001 From: riley Date: Wed, 10 May 2017 12:28:33 -0600 Subject: [PATCH 07/58] use a slot instead of trying to use facebook directly --- .../src/containers/ModerationQueue/UserDetail.js | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/client/coral-admin/src/containers/ModerationQueue/UserDetail.js b/client/coral-admin/src/containers/ModerationQueue/UserDetail.js index 06587065e..a305118a7 100644 --- a/client/coral-admin/src/containers/ModerationQueue/UserDetail.js +++ b/client/coral-admin/src/containers/ModerationQueue/UserDetail.js @@ -3,6 +3,7 @@ import {Button, Drawer} from 'coral-ui'; import styles from './UserDetail.css'; import {compose} from 'react-apollo'; import {getUserDetail} from 'coral-admin/src/graphql/queries'; +import Slot from 'coral-framework/components/Slot'; class UserDetail extends React.Component { static propTypes = { @@ -29,19 +30,17 @@ class UserDetail extends React.Component { const {user} = data; const localProfile = user.profiles.find(p => p.provider === 'local'); - const facebookProfile = user.profiles.find(p => p.provider === 'facebook'); let profile; if (localProfile) { profile = localProfile.id; - } else if (facebookProfile) { - profile = {facebookProfile.id}; } return (

    {user.username}

    -

    this.profile = ref} contentEditable="true">{profile}

    + {profile &&

    this.profile = ref} contentEditable="true">{profile}

    } +

    Member since {new Date(user.created_at).toLocaleString()}


    From 14b483d0894a07ca16b37da71158194f74e93be9 Mon Sep 17 00:00:00 2001 From: Riley Davis Date: Thu, 11 May 2017 14:23:02 -0600 Subject: [PATCH 08/58] create roles helper --- client/coral-admin/src/actions/auth.js | 8 +++----- client/coral-admin/src/containers/LayoutContainer.js | 7 ++++--- client/coral-admin/src/reducers/auth.js | 2 -- client/coral-embed-stream/src/components/Embed.js | 5 +++-- client/coral-embed-stream/src/components/Stream.js | 5 +++-- client/coral-framework/actions/auth.js | 7 +++---- client/coral-framework/reducers/auth.js | 6 +----- client/coral-framework/utils/roles.js | 10 ++++++++++ services/users.js | 11 ++++------- 9 files changed, 31 insertions(+), 30 deletions(-) create mode 100644 client/coral-framework/utils/roles.js diff --git a/client/coral-admin/src/actions/auth.js b/client/coral-admin/src/actions/auth.js index 89b56d4dc..68da3577e 100644 --- a/client/coral-admin/src/actions/auth.js +++ b/client/coral-admin/src/actions/auth.js @@ -14,8 +14,7 @@ export const handleLogin = (email, password, recaptchaResponse) => dispatch => { return dispatch(checkLoginFailure('not logged in')); } - const isAdmin = !!user.roles.filter(i => i === 'ADMIN').length; - dispatch(checkLoginSuccess(user, isAdmin)); + dispatch(checkLoginSuccess(user)); }) .catch(error => { @@ -41,7 +40,7 @@ export const requestPasswordReset = email => dispatch => { // Check Login const checkLoginRequest = () => ({type: actions.CHECK_LOGIN_REQUEST}); -const checkLoginSuccess = (user, isAdmin) => ({type: actions.CHECK_LOGIN_SUCCESS, user, isAdmin}); +const checkLoginSuccess = (user) => ({type: actions.CHECK_LOGIN_SUCCESS, user}); const checkLoginFailure = error => ({type: actions.CHECK_LOGIN_FAILURE, error}); export const checkLogin = () => dispatch => { @@ -52,8 +51,7 @@ export const checkLogin = () => dispatch => { return dispatch(checkLoginFailure('not logged in')); } - const isAdmin = !!user.roles.filter(i => i === 'ADMIN').length; - dispatch(checkLoginSuccess(user, isAdmin)); + dispatch(checkLoginSuccess(user)); }) .catch(error => { console.error(error); diff --git a/client/coral-admin/src/containers/LayoutContainer.js b/client/coral-admin/src/containers/LayoutContainer.js index 42e2baade..fea730c98 100644 --- a/client/coral-admin/src/containers/LayoutContainer.js +++ b/client/coral-admin/src/containers/LayoutContainer.js @@ -6,6 +6,7 @@ import {toggleModal as toggleShortcutModal} from '../actions/moderation'; import {fetchConfig} from '../actions/config'; import {FullLoading} from '../components/FullLoading'; import AdminLogin from '../components/AdminLogin'; +import roleUtils from 'coral-framework/utils/roles'; class LayoutContainer extends Component { componentWillMount () { @@ -16,7 +17,7 @@ class LayoutContainer extends Component { } render () { const { - isAdmin, + user, loggedIn, loadingUser, loginError, @@ -26,7 +27,7 @@ class LayoutContainer extends Component { const {handleLogout, toggleShortcutModal, TALK_RECAPTCHA_PUBLIC} = this.props; if (loadingUser) { return ; } - if (!isAdmin) { + if (roleUtils.canAccessAdmin(user)) { return ; } - if (isAdmin && loggedIn) { + if (roleUtils.canAccessAdmin(user) && loggedIn) { return ; } return ; diff --git a/client/coral-admin/src/reducers/auth.js b/client/coral-admin/src/reducers/auth.js index a7054ddfa..2bd42d112 100644 --- a/client/coral-admin/src/reducers/auth.js +++ b/client/coral-admin/src/reducers/auth.js @@ -4,7 +4,6 @@ import * as actions from '../constants/auth'; const initialState = Map({ loggedIn: false, user: null, - isAdmin: false, loginError: null, loginMaxExceeded: false, passwordRequestSuccess: null @@ -24,7 +23,6 @@ export default function auth (state = initialState, action) { return state .set('loggedIn', true) .set('loadingUser', false) - .set('isAdmin', action.isAdmin) .set('user', action.user); case actions.LOGOUT_SUCCESS: return initialState; diff --git a/client/coral-embed-stream/src/components/Embed.js b/client/coral-embed-stream/src/components/Embed.js index 0d26cf740..908fd4a90 100644 --- a/client/coral-embed-stream/src/components/Embed.js +++ b/client/coral-embed-stream/src/components/Embed.js @@ -1,6 +1,7 @@ import React from 'react'; import I18n from 'coral-framework/modules/i18n/i18n'; import translations from 'coral-framework/translations'; +import rolesHelper from 'coral-framework/utils/roles'; const lang = new I18n(translations); import {TabBar, Tab, TabContent, Button} from 'coral-ui'; @@ -38,7 +39,7 @@ export default class Embed extends React.Component { render () { const {activeTab, logout, viewAllComments, commentId} = this.props; const {asset: {totalCommentCount}} = this.props.root; - const {loggedIn, isAdmin, user} = this.props.auth; + const {loggedIn, user} = this.props.auth; const userBox = ; @@ -48,7 +49,7 @@ export default class Embed extends React.Component { {lang.t('myProfile')} - Configure Stream + Configure Stream { commentId && diff --git a/client/coral-embed-stream/src/components/Stream.js b/client/coral-embed-stream/src/components/Stream.js index 46ac75dca..7efadc921 100644 --- a/client/coral-embed-stream/src/components/Stream.js +++ b/client/coral-embed-stream/src/components/Stream.js @@ -11,6 +11,7 @@ import QuestionBox from 'coral-plugin-questionbox/QuestionBox'; import IgnoredCommentTombstone from './IgnoredCommentTombstone'; import SuspendedAccount from 'coral-framework/components/SuspendedAccount'; import RestrictedContent from 'coral-framework/components/RestrictedContent'; +import rolesHelper from 'coral-framework/utils/roles'; import ChangeUsernameContainer from 'coral-sign-in/containers/ChangeUsernameContainer'; @@ -37,7 +38,7 @@ class Stream extends React.Component { removeCommentTag, pluginProps, ignoreUser, - auth: {loggedIn, isAdmin, user}, + auth: {loggedIn, user}, commentCountCache, editName } = this.props; @@ -111,7 +112,7 @@ class Stream extends React.Component { {loggedIn && user && } - {loggedIn && } + {loggedIn && } {/* the highlightedComment is isolated after the user followed a permalink */} {highlightedComment diff --git a/client/coral-framework/actions/auth.js b/client/coral-framework/actions/auth.js index d689aeb63..527369b60 100644 --- a/client/coral-framework/actions/auth.js +++ b/client/coral-framework/actions/auth.js @@ -108,7 +108,7 @@ export const cleanState = () => ({type: actions.CLEAN_STATE}); const signInRequest = () => ({type: actions.FETCH_SIGNIN_REQUEST}); // TODO: revisit login redux flow. -// const signInSuccess = (user, isAdmin) => ({type: actions.FETCH_SIGNIN_SUCCESS, user, isAdmin}); +// const signInSuccess = (user) => ({type: actions.FETCH_SIGNIN_SUCCESS, user}); // const signInFailure = error => ({type: actions.FETCH_SIGNIN_FAILURE, error}); @@ -236,7 +236,7 @@ export const invalidForm = error => ({type: actions.INVALID_FORM, error}); // Check Login const checkLoginRequest = () => ({type: actions.CHECK_LOGIN_REQUEST}); -const checkLoginSuccess = (user, isAdmin) => ({type: actions.CHECK_LOGIN_SUCCESS, user, isAdmin}); +const checkLoginSuccess = (user) => ({type: actions.CHECK_LOGIN_SUCCESS, user}); const checkLoginFailure = error => ({type: actions.CHECK_LOGIN_FAILURE, error}); export const checkLogin = () => dispatch => { @@ -247,8 +247,7 @@ export const checkLogin = () => dispatch => { throw new Error('Not logged in'); } - const isAdmin = !!result.user.roles.filter(i => i === 'ADMIN').length; - dispatch(checkLoginSuccess(result.user, isAdmin)); + dispatch(checkLoginSuccess(result.user)); }) .catch(error => { console.error(error); diff --git a/client/coral-framework/reducers/auth.js b/client/coral-framework/reducers/auth.js index 83ea49ce5..5ac862431 100644 --- a/client/coral-framework/reducers/auth.js +++ b/client/coral-framework/reducers/auth.js @@ -4,7 +4,6 @@ import * as actions from '../constants/auth'; const initialState = Map({ isLoading: false, loggedIn: false, - isAdmin: false, user: null, showSignInDialog: false, showCreateUsernameDialog: false, @@ -79,12 +78,10 @@ export default function auth (state = initialState, action) { return state .set('checkedInitialLogin', true) .set('loggedIn', true) - .set('isAdmin', action.isAdmin) .set('user', purge(action.user)); case actions.FETCH_SIGNIN_SUCCESS: return state .set('loggedIn', true) - .set('isAdmin', action.isAdmin) .set('user', purge(action.user)); case actions.FETCH_SIGNIN_FAILURE: return state @@ -120,8 +117,7 @@ export default function auth (state = initialState, action) { return state .set('user', null) .set('isLoading', false) - .set('loggedIn', false) - .set('isAdmin', false); + .set('loggedIn', false); case actions.INVALID_FORM: return state .set('error', action.error); diff --git a/client/coral-framework/utils/roles.js b/client/coral-framework/utils/roles.js new file mode 100644 index 000000000..b457ff1b3 --- /dev/null +++ b/client/coral-framework/utils/roles.js @@ -0,0 +1,10 @@ +import includes from 'lodash/includes'; + +export default { + canAccessConfig: (user) => includes(user.roles, 'ADMIN'), + canChangeRoles: user => includes(user.roles, 'ADMIN'), + hasStaffTag: user => includes(user.roles, 'ADMIN', 'MODERATOR', 'STAFF'), + canViewUserEmails: user => includes(user.roles, 'ADMIN'), + canModerate: user => includes(user.roles, 'ADMIN', 'MODERATOR'), + canAccessAdmin: user => includes(user.roles, 'ADMIN', 'MODERATOR') +}; diff --git a/services/users.js b/services/users.js index 14301ccf4..246d3fded 100644 --- a/services/users.js +++ b/services/users.js @@ -395,13 +395,10 @@ module.exports = class UsersService { return Promise.reject(new Error(`role ${role} is not supported`)); } - return UserModel.update({ - id: id - }, { - $addToSet: { - roles: role - } - }); + // 5.11.2017 - Restricting this to a hierarchical system like WordPress + // where you can only set one role at a time. + // I'm not changing the data structure here, because I don't want a migration + return UserModel.update({id}, {$set: {roles: [role]}}); } /** From e32ab99edf17a85a41c7e1bc7e4f2cfabb3b83d6 Mon Sep 17 00:00:00 2001 From: riley Date: Fri, 12 May 2017 09:57:07 -0600 Subject: [PATCH 09/58] remove hasRoles --- .../src/containers/LayoutContainer.js | 2 +- graph/loaders/comments.js | 6 +- graph/mutators/comment.js | 2 +- graph/resolvers/comment.js | 7 +- graph/resolvers/root_query.js | 12 +-- graph/resolvers/user.js | 6 +- models/user.js | 78 ++++++++++++++++--- test/server/graph/mutations/addCommentTag.js | 1 + 8 files changed, 87 insertions(+), 27 deletions(-) diff --git a/client/coral-admin/src/containers/LayoutContainer.js b/client/coral-admin/src/containers/LayoutContainer.js index fea730c98..8a3e0b0d2 100644 --- a/client/coral-admin/src/containers/LayoutContainer.js +++ b/client/coral-admin/src/containers/LayoutContainer.js @@ -27,7 +27,7 @@ class LayoutContainer extends Component { const {handleLogout, toggleShortcutModal, TALK_RECAPTCHA_PUBLIC} = this.props; if (loadingUser) { return ; } - if (roleUtils.canAccessAdmin(user)) { + if (!loggedIn) { return { */ const genComments = ({user}, ids) => { let comments; - if (user && user.hasRoles('ADMIN')) { + if (user && user.canViewOthersComments()) { comments = CommentModel.find({ id: { $in: ids diff --git a/graph/mutators/comment.js b/graph/mutators/comment.js index e2f2d3bac..9557c1dcf 100644 --- a/graph/mutators/comment.js +++ b/graph/mutators/comment.js @@ -22,7 +22,7 @@ const createComment = ({user, loaders: {Comments}, pubsub}, {body, asset_id, par tags = tags.map(tag => ({name: tag})); // If admin or moderator, adding STAFF tag - if (user.hasRoles('ADMIN') || user.hasRoles('MODERATOR')) { + if (user.isStaff()) { tags.push({name: 'STAFF'}); } diff --git a/graph/resolvers/comment.js b/graph/resolvers/comment.js index 19ea11efe..fb01a1585 100644 --- a/graph/resolvers/comment.js +++ b/graph/resolvers/comment.js @@ -23,14 +23,13 @@ const Comment = { }, replyCount({id}, {excludeIgnored}, {user, loaders: {Comments}}) { if (user && excludeIgnored) { - return Comments.countByParentIDPersonalized({id, excludeIgnored}); + return Comments.countByParentIDPersonalized({id, excludeIgnored}); } - return Comments.countByParentID.load(id); + return Comments.countByParentID.load(id); }, actions({id}, _, {user, loaders: {Actions}}) { - // Only return the actions if the user is not an admin. - if (user && user.hasRoles('ADMIN')) { + if (user && user.canViewActions()) { return Actions.getByID.load(id); } diff --git a/graph/resolvers/root_query.js b/graph/resolvers/root_query.js index 4dcb7ea11..9deaba5f6 100644 --- a/graph/resolvers/root_query.js +++ b/graph/resolvers/root_query.js @@ -1,6 +1,6 @@ const RootQuery = { assets(_, args, {loaders: {Assets}, user}) { - if (user == null || !user.hasRoles('ADMIN')) { + if (user == null || !user.canQueryAssets()) { return null; } @@ -22,7 +22,7 @@ const RootQuery = { comments(_, {query: {action_type, statuses, asset_id, parent_id, limit, cursor, sort, excludeIgnored}}, {user, loaders: {Comments, Actions}}) { let query = {statuses, asset_id, parent_id, limit, cursor, sort, excludeIgnored}; - if (user != null && user.hasRoles('ADMIN') && action_type) { + if (user != null && user.canViewOthersComments() && action_type) { return Actions.getByTypes({action_type, item_type: 'COMMENTS'}) .then((ids) => { @@ -37,7 +37,7 @@ const RootQuery = { return Comments.get.load(id); }, commentCount(_, {query: {action_type, statuses, asset_id, parent_id}}, {user, loaders: {Actions, Comments}}) { - if (user == null || !user.hasRoles('ADMIN')) { + if (user == null || !user.canViewOthersComments()) { return null; } @@ -54,7 +54,7 @@ const RootQuery = { }, assetMetrics(_, {from, to, sort, limit = 10}, {user, loaders: {Metrics: {Assets}}}) { - if (user == null || !user.hasRoles('ADMIN')) { + if (user == null || !user.canQueryAssets()) { return null; } @@ -66,7 +66,7 @@ const RootQuery = { }, commentMetrics(_, {from, to, sort, limit = 10}, {user, loaders: {Metrics: {Comments}}}) { - if (user == null || !user.hasRoles('ADMIN')) { + if (user == null || !user.canViewCommentMetrics()) { return null; } @@ -100,7 +100,7 @@ const RootQuery = { // so hide it in the event that we aren't an admin. users(_, {query: {action_type, limit, cursor, sort}}, {user, loaders: {Users, Actions}}) { - if (user == null || !user.hasRoles('ADMIN')) { + if (user == null || !user.canViewOtherUsers()) { return null; } diff --git a/graph/resolvers/user.js b/graph/resolvers/user.js index d8ed7ee15..03e1b121a 100644 --- a/graph/resolvers/user.js +++ b/graph/resolvers/user.js @@ -5,7 +5,7 @@ const User = { actions({id}, _, {user, loaders: {Actions}}) { // Only return the actions if the user is not an admin. - if (user && user.hasRoles('ADMIN')) { + if (user && user.canViewActions()) { return Actions.getByID.load(id); } @@ -14,7 +14,7 @@ const User = { // If the user is not an admin, only return comment list for the owner of // the comments. - if (user && (user.hasRoles('ADMIN') || user.id === id)) { + if (user && (user.canViewOthersComments() || user.id === id)) { return Comments.getByQuery({author_id: id, sort: 'REVERSE_CHRONOLOGICAL'}); } @@ -23,7 +23,7 @@ const User = { roles({id, roles}, _, {user}) { // If the user is not an admin, only return the current user's roles. - if (user && (user.hasRoles('ADMIN') || user.id === id)) { + if (user && (user.canChangeRoles() || user.id === id)) { return roles; } diff --git a/models/user.js b/models/user.js index ec215b3fa..179e6f7a5 100644 --- a/models/user.js +++ b/models/user.js @@ -1,6 +1,7 @@ const mongoose = require('../services/mongoose'); const bcrypt = require('bcrypt'); const uuid = require('uuid'); +const intersection = require('lodash/intersection'); // USER_ROLES is the array of roles that is permissible as a user role. const USER_ROLES = [ @@ -158,14 +159,74 @@ UserSchema.index({ }); /** - * Returns true if the user has all the roles specified. + * returns true if the user can look up assets through the api */ -UserSchema.method('hasRoles', function(...roles) { - return roles.every((role) => { +UserSchema.method('canQueryAssets', function () { + return !!intersection(['ADMIN', 'MODERATOR'], this.roles).length; +}); - // TODO: remove toUpperCase() once we've migrated usage. - return this.roles.indexOf(role.toUpperCase()) >= 0; - }); +/** + * returns true if the user can view actions + */ +UserSchema.method('canViewActions', function () { + return !!intersection(['ADMIN', 'MODERATOR'], this.roles).length; +}); + +/** + * returns true if the user can view non-null or non-ACCEPTED comments + */ +UserSchema.method('canViewNonNullOrAcceptedComments', function () { + return !!intersection(['ADMIN', 'MODERATOR'], this.roles).length; +}); + +/** + * returns true when a user can view comments that are not their own + */ +UserSchema.method('canViewOthersComments', function () { + return !!intersection(['ADMIN', 'MODERATOR'], this.roles).length; +}); + +/** + * returns true when a user can view comment metrics + */ +UserSchema.method('canViewCommentMetrics', function () { + return !!intersection(['ADMIN', 'MODERATOR'], this.roles).length; +}); + +/** + * returns true if a commenter is staff + */ +UserSchema.method('isStaff', function () { + return !!intersection(['ADMIN', 'MODERATOR', 'STAFF'], this.roles).length; +}); + +/** + * returns true when a user can see other user info + */ +UserSchema.method('canViewOtherUsers', function () { + return !!intersection(['ADMIN', 'MODERATOR'], this.roles).length; +}); + +/** + * when a user can modify tags + */ +UserSchema.method('canModifyTags', function () { + return !!intersection(['ADMIN', 'MODERATOR'], this.roles).length; +}); + +/** + * when a user can change roles + */ +UserSchema.method('canChangeUserRoles', function () { + return !!intersection(['ADMIN', 'MODERATOR'], this.roles).length; +}); + +UserSchema.method('canSetCommentStatus', function () { + return !!intersection(['ADMIN', 'MODERATOR'], this.roles).length; +}); + +UserSchema.method('canSetUserStatus', function () { + return !!intersection(['ADMIN', 'MODERATOR'], this.roles).length; }); /** @@ -216,13 +277,12 @@ UserSchema.method('can', function(...actions) { return false; } - if (actions.some((action) => action === 'mutation:setUserStatus' || action === 'mutation:suspendUser' || action === 'mutation:setCommentStatus') && !this.hasRoles('ADMIN')) { + if (actions.some((action) => action === 'mutation:setUserStatus' || action === 'mutation:suspendUser' || action === 'mutation:setCommentStatus') && !this.canSetUserStatus()) { return false; } // {add,remove}CommentTag - requires admin and/or moderator role - const userCanModifyTags = user => ['ADMIN', 'MODERATOR'].some(r => user.hasRoles(r)); - if (actions.some(a => ['mutation:removeCommentTag', 'mutation:addCommentTag'].includes(a)) && ! userCanModifyTags(this)) { + if (actions.some(a => ['mutation:removeCommentTag', 'mutation:addCommentTag'].includes(a)) && ! this.canModifyTags()) { return false; } diff --git a/test/server/graph/mutations/addCommentTag.js b/test/server/graph/mutations/addCommentTag.js index 018e96631..1386d8339 100644 --- a/test/server/graph/mutations/addCommentTag.js +++ b/test/server/graph/mutations/addCommentTag.js @@ -44,6 +44,7 @@ describe('graph.mutations.addCommentTag', () => { Object.entries({ 'anonymous': undefined, 'regular commenter': new UserModel({}), + 'staff': new UserModel({roles: ['STAFF']}), 'banned moderator': new UserModel({roles: ['MODERATOR'], status: 'BANNED'}) }).forEach(([ userDescription, user ]) => { it(userDescription, async function () { From 63fa3c4de79e5fb0c04eeac11966f909239adfa2 Mon Sep 17 00:00:00 2001 From: Chi Vinh Le Date: Mon, 15 May 2017 22:28:04 +0700 Subject: [PATCH 10/58] Implement suspend user components --- client/coral-admin/src/actions/moderation.js | 6 + .../src/components/ActionsMenu.css | 53 ++++ .../coral-admin/src/components/ActionsMenu.js | 64 +++++ .../src/components/ActionsMenuItem.js | 9 + client/coral-admin/src/components/App.js | 10 +- .../src/components/ToastContainer.css | 226 ++++++++++++++++++ .../src/components/ToastContainer.js | 7 + .../coral-admin/src/constants/moderation.js | 2 + .../Community/components/ActionButton.js | 2 +- .../Community}/components/BanUserButton.css | 0 .../Community}/components/BanUserButton.js | 0 .../Community/components/SuspendUserDialog.js | 10 +- .../ModerationQueue/ModerationContainer.js | 38 ++- .../ModerationQueue/ModerationQueue.js | 2 + .../components/BanUserDialog.css | 7 +- .../components/BanUserDialog.js | 2 +- .../ModerationQueue/components/Comment.js | 19 +- .../components/SuspendUserDialog.css | 90 +++++++ .../components/SuspendUserDialog.js | 147 ++++++++++++ client/coral-admin/src/reducers/moderation.js | 28 ++- client/coral-admin/src/translations.json | 41 ++-- client/coral-ui/components/Button.css | 36 ++- client/coral-ui/components/Button.js | 2 +- package.json | 5 +- yarn.lock | 24 ++ 25 files changed, 770 insertions(+), 60 deletions(-) create mode 100644 client/coral-admin/src/components/ActionsMenu.css create mode 100644 client/coral-admin/src/components/ActionsMenu.js create mode 100644 client/coral-admin/src/components/ActionsMenuItem.js create mode 100644 client/coral-admin/src/components/ToastContainer.css create mode 100644 client/coral-admin/src/components/ToastContainer.js rename client/coral-admin/src/{ => containers/Community}/components/BanUserButton.css (100%) rename client/coral-admin/src/{ => containers/Community}/components/BanUserButton.js (100%) rename client/coral-admin/src/{ => containers/ModerationQueue}/components/BanUserDialog.css (97%) rename client/coral-admin/src/{ => containers/ModerationQueue}/components/BanUserDialog.js (97%) create mode 100644 client/coral-admin/src/containers/ModerationQueue/components/SuspendUserDialog.css create mode 100644 client/coral-admin/src/containers/ModerationQueue/components/SuspendUserDialog.js diff --git a/client/coral-admin/src/actions/moderation.js b/client/coral-admin/src/actions/moderation.js index c93dc0c76..52d5134a5 100644 --- a/client/coral-admin/src/actions/moderation.js +++ b/client/coral-admin/src/actions/moderation.js @@ -7,6 +7,12 @@ export const singleView = () => ({type: actions.SINGLE_VIEW}); export const showBanUserDialog = (user, commentId, commentStatus, showRejectedNote) => ({type: actions.SHOW_BANUSER_DIALOG, user, commentId, commentStatus, showRejectedNote}); export const hideBanUserDialog = (showDialog) => ({type: actions.HIDE_BANUSER_DIALOG, showDialog}); +// Suspend User Dialog +export const showSuspendUserDialog = (userId, username, commentId, commentStatus) => + ({type: actions.SHOW_SUSPEND_USER_DIALOG, userId, username, commentId, commentStatus}); + +export const hideSuspendUserDialog = () => ({type: actions.HIDE_SUSPEND_USER_DIALOG}); + // hide shortcuts note export const hideShortcutsNote = () => { try { diff --git a/client/coral-admin/src/components/ActionsMenu.css b/client/coral-admin/src/components/ActionsMenu.css new file mode 100644 index 000000000..1d2f91f74 --- /dev/null +++ b/client/coral-admin/src/components/ActionsMenu.css @@ -0,0 +1,53 @@ +.button { + -webkit-transform: scale(.8); + transform: scale(.8); + margin: 0; +} + +.root { + color: black; + > :global(.mdl-menu__container) { + margin-left: 10px; + > :global(.mdl-menu__outline) { + box-shadow: none; + } + } +} + +.buttonOpen { + box-shadow: none; + color: white; + background-color: #616161; +} + +.arrowIcon { + margin-left: 6px; + margin-right: 0; + vertical-align: middle; + margin-right: 0; + font-size: 14px; +} + +.menu { + padding: 0; +} + +.menuItem { + background-color: #2a2a2a; + color: white; + &:first-child { + margin-bottom: 1px; + border-radius: 2px 2px 0px 0px; + } + &:last-child { + border-radius: 0px 0px 2px 2px; + } + &:hover, &:active, &:focus { + background-color: #767676; + } + &[disabled], &[disabled]:hover, &[disabled]:focus, &[disabled]:active { + background-color: #262626; + color: rgba(255, 255, 255, 0.5); + } +} + diff --git a/client/coral-admin/src/components/ActionsMenu.js b/client/coral-admin/src/components/ActionsMenu.js new file mode 100644 index 000000000..eb173eb81 --- /dev/null +++ b/client/coral-admin/src/components/ActionsMenu.js @@ -0,0 +1,64 @@ +import React, {PropTypes} from 'react'; +import {Button, Icon} from 'coral-ui'; +import {Menu} from 'react-mdl'; +import cn from 'classnames'; +import {findDOMNode} from 'react-dom'; +import styles from './ActionsMenu.css'; + +import I18n from 'coral-framework/modules/i18n/i18n'; +import translations from 'coral-admin/src/translations.json'; +const lang = new I18n(translations); + +let count = 0; + +class ActionsMenu extends React.Component { + id = `actions-dropdown-${count++}`; + menu = null; + state = {open: false}; + timeout = null; + + componentWillUnmount() { + clearTimeout(this.timeout); + } + + handleRef = (ref) => { + this.menu = ref ? findDOMNode(ref).parentNode : null; + } + + syncOpenState = () => { + clearTimeout(this.timeout); + this.timeout = setTimeout(() => { + this.setState({open: this.menu.className.indexOf('is-visible') >= 0}); + }, 150); + }; + + render() { + return ( +

    + + + {this.props.children} + +
    + ); + } +} + +ActionsMenu.propTypes = { + icon: PropTypes.string, +}; + +export default ActionsMenu; diff --git a/client/coral-admin/src/components/ActionsMenuItem.js b/client/coral-admin/src/components/ActionsMenuItem.js new file mode 100644 index 000000000..82dab96f0 --- /dev/null +++ b/client/coral-admin/src/components/ActionsMenuItem.js @@ -0,0 +1,9 @@ +import React from 'react'; +import cn from 'classnames'; +import {MenuItem} from 'react-mdl'; +import styles from './ActionsMenu.css'; + +const ActionsMenuItem = (props) => + ; + +export default ActionsMenuItem; diff --git a/client/coral-admin/src/components/App.js b/client/coral-admin/src/components/App.js index 3c6e88a14..1a15c72c3 100644 --- a/client/coral-admin/src/components/App.js +++ b/client/coral-admin/src/components/App.js @@ -1,16 +1,16 @@ import React from 'react'; -import {Provider} from 'react-redux'; +import ToastContainer from './ToastContainer'; import 'material-design-lite'; -import store from 'services/store'; import AppRouter from '../AppRouter'; export default class App extends React.Component { render () { return ( - - - +
    + + +
    ); } } diff --git a/client/coral-admin/src/components/ToastContainer.css b/client/coral-admin/src/components/ToastContainer.css new file mode 100644 index 000000000..319872ceb --- /dev/null +++ b/client/coral-admin/src/components/ToastContainer.css @@ -0,0 +1,226 @@ +@keyframes :global(bounceInRight) { + from, 60%, 75%, 90%, to { + animation-timing-function: cubic-bezier(0.215, 0.61, 0.355, 1); } + from { + opacity: 0; + transform: translate3d(3000px, 0, 0); } + 60% { + opacity: 1; + transform: translate3d(-25px, 0, 0); } + 75% { + transform: translate3d(10px, 0, 0); } + 90% { + transform: translate3d(-5px, 0, 0); } + to { + transform: none; } } + +@keyframes :global(bounceOutRight) { + 20% { + opacity: 1; + transform: translate3d(-20px, 0, 0); } + to { + opacity: 0; + transform: translate3d(2000px, 0, 0); } } + +@keyframes :global(bounceInLeft) { + from, 60%, 75%, 90%, to { + animation-timing-function: cubic-bezier(0.215, 0.61, 0.355, 1); } + 0% { + opacity: 0; + transform: translate3d(-3000px, 0, 0); } + 60% { + opacity: 1; + transform: translate3d(25px, 0, 0); } + 75% { + transform: translate3d(-10px, 0, 0); } + 90% { + transform: translate3d(5px, 0, 0); } + to { + transform: none; } } + +@keyframes :global(bounceOutLeft) { + 20% { + opacity: 1; + transform: translate3d(20px, 0, 0); } + to { + opacity: 0; + transform: translate3d(-2000px, 0, 0); } } + +@keyframes :global(bounceInUp) { + from, 60%, 75%, 90%, to { + animation-timing-function: cubic-bezier(0.215, 0.61, 0.355, 1); } + from { + opacity: 0; + transform: translate3d(0, 3000px, 0); } + 60% { + opacity: 1; + transform: translate3d(0, -20px, 0); } + 75% { + transform: translate3d(0, 10px, 0); } + 90% { + transform: translate3d(0, -5px, 0); } + to { + transform: translate3d(0, 0, 0); } } + +@keyframes :global(bounceOutUp) { + 20% { + transform: translate3d(0, -10px, 0); } + 40%, 45% { + opacity: 1; + transform: translate3d(0, 20px, 0); } + to { + opacity: 0; + transform: translate3d(0, -2000px, 0); } } + +@keyframes :global(bounceInDown) { + from, 60%, 75%, 90%, to { + animation-timing-function: cubic-bezier(0.215, 0.61, 0.355, 1); } + 0% { + opacity: 0; + transform: translate3d(0, -3000px, 0); } + 60% { + opacity: 1; + transform: translate3d(0, 25px, 0); } + 75% { + transform: translate3d(0, -10px, 0); } + 90% { + transform: translate3d(0, 5px, 0); } + to { + transform: none; } } + +@keyframes :global(bounceOutDown) { + 20% { + transform: translate3d(0, 10px, 0); } + 40%, 45% { + opacity: 1; + transform: translate3d(0, -20px, 0); } + to { + opacity: 0; + transform: translate3d(0, 2000px, 0); } } + +@keyframes :global(track-progress) { + 0% { + width: 100%; } + 100% { + width: 0; } } + +:global { + .bounceOutRight, .toast-exit--top-right, .toast-exit--bottom-right { + animation-name: bounceOutRight; } + + .bounceInRight, .toast-enter--top-right, .toast-enter--bottom-right { + animation-name: bounceInRight; } + + .bounceInLeft, .toast-enter--top-left, .toast-enter--bottom-left { + animation-name: bounceInLeft; } + + .bounceOutLeft, .toast-exit--top-left, .toast-exit--bottom-left { + animation-name: bounceOutLeft; } + + .bounceInUp, .toast-enter--bottom-center { + animation-name: bounceInUp; } + .bounceOutUp, .toast-exit--top-center { + animation-name: bounceOutUp; } + + .bounceInDown, .toast-enter--top-center { + animation-name: bounceInDown; } + + .bounceOutDown, .toast-exit--bottom-center { + animation-name: bounceOutDown; } + + .animated { + animation-duration: 0.75s; + animation-fill-mode: both; } + + .toastify { + z-index: 999; + position: fixed; + padding: 4px; + width: 350px; + max-width: 98%; + color: #999; + box-sizing: border-box; } + .toastify--top-left { + top: 1em; + left: 1em; } + .toastify--top-center { + top: 1em; + left: 50%; + margin-left: -175px; } + .toastify--top-right { + top: 1em; + right: 2em; } + .toastify--bottom-left { + bottom: 1em; + left: 1em; } + .toastify--bottom-center { + bottom: 1em; + left: 50%; + margin-left: -175px; } + .toastify--bottom-right { + bottom: 1em; + right: 2em; } + .toastify__img { + float: left; + margin-right: 8px; + vertical-align: middle; } + + .toastify__close { + position: absolute; + top: 18px; + left: 12px; + width: 20px; + height: 16px; + padding: 0; + text-align: center; + text-decoration: none; + color: white; + font-weight: bold; + font-size: 14px; + background: transparent; + outline: none; + border: none; + cursor: pointer; + opacity: 0.8; + transition: .3s ease; } + .toastify__close:hover, .toastify__close:focus { + opacity: 1; + } + + .toastify-content { + position: relative; + width: 100%; + margin-bottom: 12px; + padding: 18px 24px 20px 48px; + box-sizing: border-box; + background: #404040; + border-radius: 2px; + box-shadow: 0 2px 15px 0 rgba(0, 0, 0, 0.1), 0 3px 20px 0 rgba(0, 0, 0, 0.05); } + .toastify-content--info { + background: #2488cb; } + .toastify-content--success { + background: #008577; } + .toastify-content--warning { + background: #ef6c2b; } + .toastify-content--error { + background: #ef342b; } + + .toastify__body { + color: white; + font-size: 15px; + font-weight: 400; + } + + .toastify__progress { + position: absolute; + bottom: 0; + left: 0; + width: 0; + height: 4px; + z-index: 999; + opacity: 0.8; + border-radius: 2px; + animation: track-progress linear 1; + background-color: white; + } +} diff --git a/client/coral-admin/src/components/ToastContainer.js b/client/coral-admin/src/components/ToastContainer.js new file mode 100644 index 000000000..a751d2714 --- /dev/null +++ b/client/coral-admin/src/components/ToastContainer.js @@ -0,0 +1,7 @@ +import './ToastContainer.css'; +import {defaultProps} from 'recompose'; +import {ToastContainer} from 'react-toastify'; + +export default defaultProps({ + autoClose: 5000, +})(ToastContainer); diff --git a/client/coral-admin/src/constants/moderation.js b/client/coral-admin/src/constants/moderation.js index 14672146e..7e918ed4a 100644 --- a/client/coral-admin/src/constants/moderation.js +++ b/client/coral-admin/src/constants/moderation.js @@ -3,3 +3,5 @@ export const SINGLE_VIEW = 'SINGLE_VIEW'; export const SHOW_BANUSER_DIALOG = 'SHOW_BANUSER_DIALOG'; export const HIDE_BANUSER_DIALOG = 'HIDE_BANUSER_DIALOG'; export const HIDE_SHORTCUTS_NOTE = 'HIDE_SHORTCUTS_NOTE'; +export const SHOW_SUSPEND_USER_DIALOG = 'SHOW_SUSPEND_USER_DIALOG'; +export const HIDE_SUSPEND_USER_DIALOG = 'HIDE_SUSPEND_USER_DIALOG'; diff --git a/client/coral-admin/src/containers/Community/components/ActionButton.js b/client/coral-admin/src/containers/Community/components/ActionButton.js index 6352480fd..ad5346ac5 100644 --- a/client/coral-admin/src/containers/Community/components/ActionButton.js +++ b/client/coral-admin/src/containers/Community/components/ActionButton.js @@ -1,6 +1,6 @@ import React from 'react'; import styles from '../Community.css'; -import BanUserButton from '../../../components/BanUserButton'; +import BanUserButton from './BanUserButton'; import {Button} from 'coral-ui'; import {menuActionsMap} from '../../../containers/ModerationQueue/helpers/moderationQueueActionsMap'; diff --git a/client/coral-admin/src/components/BanUserButton.css b/client/coral-admin/src/containers/Community/components/BanUserButton.css similarity index 100% rename from client/coral-admin/src/components/BanUserButton.css rename to client/coral-admin/src/containers/Community/components/BanUserButton.css diff --git a/client/coral-admin/src/components/BanUserButton.js b/client/coral-admin/src/containers/Community/components/BanUserButton.js similarity index 100% rename from client/coral-admin/src/components/BanUserButton.js rename to client/coral-admin/src/containers/Community/components/BanUserButton.js diff --git a/client/coral-admin/src/containers/Community/components/SuspendUserDialog.js b/client/coral-admin/src/containers/Community/components/SuspendUserDialog.js index 20e221c48..827352c42 100644 --- a/client/coral-admin/src/containers/Community/components/SuspendUserDialog.js +++ b/client/coral-admin/src/containers/Community/components/SuspendUserDialog.js @@ -10,16 +10,16 @@ const lang = new I18n(translations); const stages = [ { - title: 'suspenduser.title_0', - description: 'suspenduser.description_0', + title: 'suspenduser.title_reject', + description: 'suspenduser.description_reject', options: { 'j': 'suspenduser.no_cancel', 'k': 'suspenduser.yes_suspend' } }, { - title: 'suspenduser.title_1', - description: 'suspenduser.description_1', + title: 'suspenduser.title_notify', + description: 'suspenduser.description_notify', options: { 'j': 'bandialog.cancel', 'k': 'suspenduser.send' @@ -38,7 +38,7 @@ class SuspendUserDialog extends Component { } componentDidMount() { - this.setState({email: lang.t('suspenduser.email'), about: lang.t('suspenduser.username')}); + this.setState({email: lang.t('suspenduser.email_message_reject'), about: lang.t('suspenduser.username')}); } /* diff --git a/client/coral-admin/src/containers/ModerationQueue/ModerationContainer.js b/client/coral-admin/src/containers/ModerationQueue/ModerationContainer.js index 9606be543..e8535f854 100644 --- a/client/coral-admin/src/containers/ModerationQueue/ModerationContainer.js +++ b/client/coral-admin/src/containers/ModerationQueue/ModerationContainer.js @@ -1,6 +1,8 @@ import React, {Component} from 'react'; import {connect} from 'react-redux'; +import {bindActionCreators} from 'redux'; import {compose} from 'react-apollo'; +import {toast} from 'react-toastify'; import key from 'keymaster'; import isEqual from 'lodash/isEqual'; import styles from './components/styles.css'; @@ -10,10 +12,19 @@ import {banUser, setCommentStatus} from '../../graphql/mutations'; import {fetchSettings} from 'actions/settings'; import {updateAssets} from 'actions/assets'; -import {toggleModal, singleView, showBanUserDialog, hideBanUserDialog, hideShortcutsNote} from 'actions/moderation'; +import { + toggleModal, + singleView, + showBanUserDialog, + hideBanUserDialog, + showSuspendUserDialog, + hideSuspendUserDialog, + hideShortcutsNote, +} from 'actions/moderation'; import {Spinner} from 'coral-ui'; -import BanUserDialog from '../../components/BanUserDialog'; +import BanUserDialog from './components/BanUserDialog'; +import SuspendUserDialog from './components/SuspendUserDialog'; import ModerationQueue from './ModerationQueue'; import ModerationMenu from './components/ModerationMenu'; import ModerationHeader from './components/ModerationHeader'; @@ -175,6 +186,7 @@ class ModerationContainer extends Component { bannedWords={settings.wordlist.banned} suspectWords={settings.wordlist.suspect} showBanUserDialog={props.showBanUserDialog} + showSuspendUserDialog={props.showSuspendUserDialog} acceptComment={props.acceptComment} rejectComment={props.rejectComment} loadMore={props.loadMore} @@ -192,6 +204,12 @@ class ModerationContainer extends Component { showRejectedNote={moderation.showRejectedNote} rejectComment={props.rejectComment} /> + toast('User admin has been suspended for 24 hours. this suspension will automatically end after 24 hours.', {type: 'success'}) && props.hideSuspendUserDialog()} + /> ({ }); const mapDispatchToProps = dispatch => ({ - toggleModal: toggle => dispatch(toggleModal(toggle)), onClose: () => dispatch(toggleModal(false)), - singleView: () => dispatch(singleView()), - updateAssets: assets => dispatch(updateAssets(assets)), - fetchSettings: () => dispatch(fetchSettings()), - showBanUserDialog: (user, commentId, commentStatus, showRejectedNote) => dispatch(showBanUserDialog(user, commentId, commentStatus, showRejectedNote)), hideBanUserDialog: () => dispatch(hideBanUserDialog(false)), - hideShortcutsNote: () => dispatch(hideShortcutsNote()), + ...bindActionCreators({ + toggleModal, + singleView, + updateAssets, + fetchSettings, + showBanUserDialog, + hideShortcutsNote, + showSuspendUserDialog, + hideSuspendUserDialog, + }, dispatch), }); export default compose( diff --git a/client/coral-admin/src/containers/ModerationQueue/ModerationQueue.js b/client/coral-admin/src/containers/ModerationQueue/ModerationQueue.js index c4bf0f85c..b93f185f0 100644 --- a/client/coral-admin/src/containers/ModerationQueue/ModerationQueue.js +++ b/client/coral-admin/src/containers/ModerationQueue/ModerationQueue.js @@ -16,6 +16,7 @@ class ModerationQueue extends React.Component { suspectWords: PropTypes.arrayOf(PropTypes.string).isRequired, currentAsset: PropTypes.object, showBanUserDialog: PropTypes.func.isRequired, + showSuspendUserDialog: PropTypes.func.isRequired, rejectComment: PropTypes.func.isRequired, acceptComment: PropTypes.func.isRequired, comments: PropTypes.array.isRequired @@ -51,6 +52,7 @@ class ModerationQueue extends React.Component { bannedWords={props.bannedWords} actions={actionsMap[status]} showBanUserDialog={props.showBanUserDialog} + showSuspendUserDialog={props.showSuspendUserDialog} acceptComment={props.acceptComment} rejectComment={props.rejectComment} currentAsset={props.currentAsset} diff --git a/client/coral-admin/src/components/BanUserDialog.css b/client/coral-admin/src/containers/ModerationQueue/components/BanUserDialog.css similarity index 97% rename from client/coral-admin/src/components/BanUserDialog.css rename to client/coral-admin/src/containers/ModerationQueue/components/BanUserDialog.css index a46b9da32..f13f0e6aa 100644 --- a/client/coral-admin/src/components/BanUserDialog.css +++ b/client/coral-admin/src/containers/ModerationQueue/components/BanUserDialog.css @@ -152,13 +152,14 @@ input.error{ .cancel { margin-right: 10px; - width: 47%; + width: 48%; } .ban { - width: 47%; + width: 48%; } .buttons { - margin: 20px 0; + margin: 20px; + text-align: center; } diff --git a/client/coral-admin/src/components/BanUserDialog.js b/client/coral-admin/src/containers/ModerationQueue/components/BanUserDialog.js similarity index 97% rename from client/coral-admin/src/components/BanUserDialog.js rename to client/coral-admin/src/containers/ModerationQueue/components/BanUserDialog.js index b259012f3..87a5fe55a 100644 --- a/client/coral-admin/src/components/BanUserDialog.js +++ b/client/coral-admin/src/containers/ModerationQueue/components/BanUserDialog.js @@ -5,7 +5,7 @@ import styles from './BanUserDialog.css'; import Button from 'coral-ui/components/Button'; import I18n from 'coral-framework/modules/i18n/i18n'; -import translations from '../translations'; +import translations from '../../../translations'; const lang = new I18n(translations); const onBanClick = (userId, commentId, commentStatus, handleBanUser, rejectComment, handleClose) => (e) => { diff --git a/client/coral-admin/src/containers/ModerationQueue/components/Comment.js b/client/coral-admin/src/containers/ModerationQueue/components/Comment.js index ffd4db2a6..2e940d0d8 100644 --- a/client/coral-admin/src/containers/ModerationQueue/components/Comment.js +++ b/client/coral-admin/src/containers/ModerationQueue/components/Comment.js @@ -9,7 +9,8 @@ import {Icon} from 'coral-ui'; import FlagBox from './FlagBox'; import CommentType from './CommentType'; import ActionButton from 'coral-admin/src/components/ActionButton'; -import BanUserButton from 'coral-admin/src/components/BanUserButton'; +import ActionsMenu from 'coral-admin/src/components/ActionsMenu'; +import ActionsMenuItem from 'coral-admin/src/components/ActionsMenuItem'; import {getActionSummary} from 'coral-framework/utils'; const linkify = new Linkify(); @@ -48,7 +49,19 @@ const Comment = ({actions = [], comment, suspectWords, bannedWords, ...props}) = {timeago().format(comment.created_at || (Date.now() - props.index * 60 * 1000), lang.getLocale().replace('-', '_'))} - props.showBanUserDialog(comment.user, comment.id, comment.status, comment.status !== 'REJECTED')} /> + + + props.showSuspendUserDialog(comment.user.id, comment.user.name, comment.id, comment.status)}> + Suspend User + props.showBanUserDialog(comment.user, comment.id, comment.status, comment.status !== 'REJECTED')}> + Ban User + + +
  • {comment.user.status === 'banned' ? @@ -103,6 +116,8 @@ Comment.propTypes = { suspectWords: PropTypes.arrayOf(PropTypes.string).isRequired, bannedWords: PropTypes.arrayOf(PropTypes.string).isRequired, currentAsset: PropTypes.object, + showBanUserDialog: PropTypes.func.isRequired, + showSuspendUserDialog: PropTypes.func.isRequired, comment: PropTypes.shape({ body: PropTypes.string.isRequired, action_summaries: PropTypes.array, diff --git a/client/coral-admin/src/containers/ModerationQueue/components/SuspendUserDialog.css b/client/coral-admin/src/containers/ModerationQueue/components/SuspendUserDialog.css new file mode 100644 index 000000000..1c96f509a --- /dev/null +++ b/client/coral-admin/src/containers/ModerationQueue/components/SuspendUserDialog.css @@ -0,0 +1,90 @@ +.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: 400px; + top: 50%; + transform: translateY(-50%); + padding: 20px; + border-radius: 4px; +} + +.header { + color: black; + font-size: 1.5em; + font-weight: 500; + margin: 0 0 8px 0; +} + +.close { + display: block; + position: absolute; + top: 24px; + right: 20px; +} + +.closeButton { + userSelect: none; + outline: none; + border: none; + touchAction: manipulation; + &::-moz-focus-inner: { + border: 0; + } + background: 0; + padding: 0; + font-size: 24px; + line-height: 14px; + cursor: pointer; + color: #363636; + &:hover { + color: #6b6b6b; + } +} + +.legend { + padding: 0; + font-weight: bold; +} + +div.radioGroup { + margin-top: 6px; +} + +label.radioGroup { + + &:global(.is-checked) > :global(.mdl-radio__outer-circle), + > :global(.mdl-radio__outer-circle) { + border-color: #212121; + } + + > :global(.mdl-radio__inner-circle) { + background: #212121; + } + + > :global(.mdl-radio__label) { + font-size: 14px; + line-height: 20px; + } +} + +.messageInput { + border-radius: 3px; + width: 100%; + padding: 10px; + font-size: 14px; + box-sizing: border-box; +} + +.cancel { + margin-right: 5px; +} + +.perform { + min-width: 90px; +} + +.buttons { + margin-top: 8px; + margin-bottom: 6px; + text-align: right; +} diff --git a/client/coral-admin/src/containers/ModerationQueue/components/SuspendUserDialog.js b/client/coral-admin/src/containers/ModerationQueue/components/SuspendUserDialog.js new file mode 100644 index 000000000..0ce0a43d1 --- /dev/null +++ b/client/coral-admin/src/containers/ModerationQueue/components/SuspendUserDialog.js @@ -0,0 +1,147 @@ +import React, {PropTypes} from 'react'; +import {Dialog} from 'coral-ui'; +import {RadioGroup, Radio} from 'react-mdl'; +import styles from './SuspendUserDialog.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 initialState = {step: 0, duration: '3', message: lang.t('suspenduser.email_message_suspend')}; + +class SuspendUserDialog extends React.Component { + + state = initialState; + + componentWillReceiveProps(next) { + if (this.props.open && !next.open) { + this.setState(initialState); + } + } + + handleDurationChange = (event) => { + this.setState({duration: event.target.value}); + } + + handleMessageChange = (event) => { + this.setState({message: event.target.value}); + } + + increaseStep = () => { + this.setState({step: this.state.step + 1}); + } + + resetStep = () => { + this.setState({step: 0}); + } + + handlePerform = () => { + this.props.onPerform({ + duration: this.state.duration, + message: this.state.message, + }); + }; + + renderStep0() { + const {onCancel, username} = this.props; + const {duration} = this.state; + return ( +
    +

    + {lang.t('suspenduser.title_suspend')} +

    +

    + {lang.t('suspenduser.description_suspend', username)} +

    +
    + {lang.t('suspenduser.select_duration')} + + {lang.t('suspenduser.hours', 3)} + {lang.t('suspenduser.hours', 24)} + {lang.t('suspenduser.days', 7)} + +
    +
    + + +
    +
    + ); + } + + renderStep1() { + const {onCancel, username} = this.props; + const {message} = this.state; + return ( +
    +

    + {lang.t('suspenduser.title_notify')} +

    +

    + {lang.t('suspenduser.description_notify', username)} +

    +
    + {lang.t('suspenduser.write_message')} +