Refactor and pluginize comment details

This commit is contained in:
Chi Vinh Le
2017-09-29 19:42:44 +07:00
parent 5d718f6dd4
commit f9693c7154
37 changed files with 486 additions and 302 deletions
+1
View File
@@ -26,5 +26,6 @@ plugins/*
!plugins/talk-plugin-toxic-comments
!plugins/talk-plugin-remember-sort
!plugins/talk-plugin-deep-reply-count
!plugins/talk-plugin-flag-details
node_modules
+1
View File
@@ -43,5 +43,6 @@ plugins/*
!plugins/talk-plugin-toxic-comments
!plugins/talk-plugin-remember-sort
!plugins/talk-plugin-deep-reply-count
!plugins/talk-plugin-flag-details
**/node_modules/*
@@ -1,24 +0,0 @@
.actionButton {
transform: scale(.8);
margin: 0;
}
.minimal {
width: 45px;
min-width: 0;
}
.approve__active {
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;
}
@@ -1,38 +0,0 @@
import React from 'react';
import PropTypes from 'prop-types';
import styles from './ActionButton.css';
import {Button} from 'coral-ui';
import {menuActionsMap} from '../utils/moderationQueueActionsMap';
import t from 'coral-framework/services/i18n';
const ActionButton = ({type = '', active, ...props}) => {
const typeName = type.toLowerCase();
let text = menuActionsMap[type].text;
if (text === 'approve' && active) {
text = 'approved';
} else if (text === 'reject' && active) {
text = 'rejected';
}
return (
<Button
className={`${typeName} ${styles.actionButton} ${props.minimal ? styles.minimal : ''} ${active ? styles[`${typeName}__active`] : ''}`}
cStyle={typeName}
icon={menuActionsMap[type].icon}
onClick={type === 'APPROVE' ? props.acceptComment : props.rejectComment}
>{props.minimal ? '' : t(`modqueue.${text}`)}</Button>
);
};
ActionButton.propTypes = {
active: PropTypes.bool,
type: PropTypes.oneOf(['APPROVE', 'REJECT']),
minimal: PropTypes.bool,
acceptComment: PropTypes.func,
rejectComment: PropTypes.func,
};
export default ActionButton;
@@ -0,0 +1,44 @@
.root {
display: block;
color: #519954;
border: solid 2px rgba(81, 153, 84, 0.75);
background: white;
padding: 10px 12px;
box-sizing: border-box;
vertical-align: middle;
line-height: 24px;
font-size: 17px;
height: 47px;
border-radius: 3px;
text-transform: capitalize;
box-shadow: 0 2px 2px 0 rgba(0, 0, 0, 0.03), 0 3px 1px -2px rgba(0,0,0,.2), 0 1px 5px 0 rgba(0,0,0,.09);
width: 129px;
transform: scale(.8);
margin: 0;
&:hover {
box-shadow: none;
color: white;
background-color: #519954;
cursor: pointer;
}
}
.active {
box-shadow: none;
color: white;
background-color: #519954;
&:hover {
cursor: not-allowed;
}
}
.minimal {
width: 45px;
min-width: 0;
}
.icon {
margin-right: 5px;
}
@@ -0,0 +1,30 @@
import React from 'react';
import PropTypes from 'prop-types';
import cn from 'classnames';
import styles from './ApproveButton.css';
import {Icon} from 'coral-ui';
import t from 'coral-framework/services/i18n';
const ApproveButton = ({active, minimal, onClick}) => {
const text = active ? t('modqueue.approved') : t('modqueue.approve');
return (
<button
className={cn(styles.root, {[styles.minimal]: minimal, [styles.active]: active})}
onClick={onClick}
>
<Icon name={'done'} className={styles.icon} />
{!minimal && text}
</button>
);
};
ApproveButton.propTypes = {
active: PropTypes.bool,
minimal: PropTypes.bool,
onClick: PropTypes.func,
};
export default ApproveButton;
@@ -2,7 +2,7 @@ import React, {Component} from 'react';
import PropTypes from 'prop-types';
import styles from './CommentDetails.css';
import t from 'coral-framework/services/i18n';
import FlagDetails from './FlagDetails';
import Slot from 'coral-framework/components/Slot';
class CommentDetails extends Component {
state = {
@@ -23,15 +23,20 @@ class CommentDetails extends Component {
}
render() {
const {actions, viewUserDetail} = this.props;
const {data, root, comment} = this.props;
const {showDetail} = this.state;
const queryData = {
root,
comment,
};
return (
<div className={styles.root}>
<a onClick={this.toggleDetail} className={styles.moreDetail}>{showDetail ? t('modqueue.less_detail') : t('modqueue.more_detail')}</a>
<FlagDetails
actions={actions}
viewUserDetail={viewUserDetail}
<Slot
fill="adminCommentDetailArea"
data={data}
queryData={queryData}
more={showDetail}
/>
</div>
@@ -40,11 +45,9 @@ class CommentDetails extends Component {
}
CommentDetails.propTypes = {
actions: PropTypes.arrayOf(PropTypes.shape({
message: PropTypes.string,
user: PropTypes.shape({username: PropTypes.string})
})).isRequired,
viewUserDetail: PropTypes.func.isRequired,
data: PropTypes.object.isRequired,
root: PropTypes.object.isRequired,
comment: PropTypes.object.isRequired,
};
export default CommentDetails;
@@ -0,0 +1,44 @@
.root {
display: block;
color: #D03235;
border: solid 1px #D03235;
background: white;
padding: 10px 11px;
box-sizing: border-box;
vertical-align: middle;
line-height: 24px;
font-size: 17px;
height: 47px;
border-radius: 3px;
text-transform: capitalize;
box-shadow: 0 2px 2px 0 rgba(0, 0, 0, 0.03), 0 3px 1px -2px rgba(0,0,0,.2), 0 1px 5px 0 rgba(0,0,0,.09);
width: 129px;
transform: scale(.8);
margin: 0;
&:hover {
color: white;
background-color: #D03235;
box-shadow: none;
cursor: pointer;
}
}
.active {
color: white;
background-color: #D03235;
box-shadow: none;
&:hover {
cursor: not-allowed;
}
}
.minimal {
width: 45px;
min-width: 0;
}
.icon {
margin-right: 5px;
}
@@ -0,0 +1,30 @@
import React from 'react';
import PropTypes from 'prop-types';
import cn from 'classnames';
import styles from './RejectButton.css';
import {Icon} from 'coral-ui';
import t from 'coral-framework/services/i18n';
const RejectButton = ({active, minimal, onClick}) => {
const text = active ? t('modqueue.rejected') : t('modqueue.reject');
return (
<button
className={cn(styles.root, {[styles.minimal]: minimal, [styles.active]: active})}
onClick={onClick}
>
<Icon name={'close'} className={styles.icon} />
{!minimal && text}
</button>
);
};
RejectButton.propTypes = {
active: PropTypes.bool,
minimal: PropTypes.bool,
onClick: PropTypes.func,
};
export default RejectButton;
@@ -5,7 +5,6 @@ import styles from './UserDetail.css';
import {Icon, Button, Drawer, Spinner} from 'coral-ui';
import {Slot} from 'coral-framework/components';
import ButtonCopyToClipboard from './ButtonCopyToClipboard';
import {actionsMap} from '../utils/moderationQueueActionsMap';
import ClickOutside from 'coral-framework/components/ClickOutside';
import LoadMore from '../components/LoadMore';
import cn from 'classnames';
@@ -70,6 +69,7 @@ export default class UserDetail extends React.Component {
renderLoaded() {
const {
data,
root,
root: {
user,
@@ -177,15 +177,15 @@ export default class UserDetail extends React.Component {
<div>
{
nodes.map((comment) => {
const status = comment.action_summaries ? 'FLAGGED' : comment.status;
const selected = selectedCommentIds.indexOf(comment.id) !== -1;
return <Comment
key={comment.id}
user={user}
root={root}
data={data}
comment={comment}
suspectWords={suspectWords}
bannedWords={bannedWords}
actions={actionsMap[status]}
acceptComment={this.acceptThenReload}
rejectComment={this.rejectThenReload}
selected={selected}
@@ -5,33 +5,39 @@ import {Link} from 'react-router';
import {Icon} from 'coral-ui';
import CommentDetails from './CommentDetails';
import styles from './UserDetailComment.css';
import ActionButton from 'coral-admin/src/components/ActionButton';
import CommentBodyHighlighter from 'coral-admin/src/components/CommentBodyHighlighter';
import IfHasLink from 'coral-admin/src/components/IfHasLink';
import cn from 'classnames';
import CommentAnimatedEdit from './CommentAnimatedEdit';
import CommentLabels from '../containers/CommentLabels';
import ApproveButton from './ApproveButton';
import RejectButton from 'coral-admin/src/components/RejectButton';
import t, {timeago} from 'coral-framework/services/i18n';
class UserDetailComment extends React.Component {
approve = () => (this.props.comment.status === 'ACCEPTED'
? null
: this.props.acceptComment({commentId: this.props.comment.id})
);
reject = () => (this.props.comment.status === 'REJECTED'
? null
: this.props.rejectComment({commentId: this.props.comment.id})
);
render() {
const {
actions = [],
comment,
viewUserDetail,
suspectWords,
bannedWords,
selected,
toggleSelect,
className,
user,
...props
data,
} = this.props;
const flagActions = comment.actions && comment.actions.filter((a) => a.__typename === 'FlagAction');
return (
<li
tabIndex={0}
@@ -86,40 +92,26 @@ class UserDetailComment extends React.Component {
</span>
</IfHasLink>
<div className={styles.actions}>
{actions.map((action, i) => {
const active =
(action === 'REJECT' && comment.status === 'REJECTED') ||
(action === 'APPROVE' && comment.status === 'ACCEPTED');
return (
<ActionButton
minimal={true}
key={i}
type={action}
user={user}
status={comment.status}
active={active}
acceptComment={() =>
(comment.status === 'ACCEPTED'
? null
: props.acceptComment({commentId: comment.id}))}
rejectComment={() =>
(comment.status === 'REJECTED'
? null
: props.rejectComment({commentId: comment.id}))}
/>
);
})}
<ApproveButton
active={comment.status === 'ACCEPTED'}
onClick={this.approve}
minimal
/>
<RejectButton
active={comment.status === 'REJECTED'}
onClick={this.reject}
minimal
/>
</div>
</div>
</div>
</CommentAnimatedEdit>
</div>
{flagActions && flagActions.length
? <CommentDetails
actions={flagActions}
viewUserDetail={viewUserDetail}
/>
: null}
<CommentDetails
data={data}
root={root}
comment={comment}
/>
</li>
);
}
@@ -135,6 +127,8 @@ UserDetailComment.propTypes = {
bannedWords: PropTypes.arrayOf(PropTypes.string).isRequired,
toggleSelect: PropTypes.func,
comment: PropTypes.shape({
id: PropTypes.string.isRequired,
status: PropTypes.string.isRequired,
body: PropTypes.string.isRequired,
actions: PropTypes.array,
created_at: PropTypes.string.isRequired,
@@ -0,0 +1,23 @@
import {gql} from 'react-apollo';
import CommentDetails from '../components/CommentDetails';
import {getSlotFragmentSpreads} from 'coral-framework/utils';
import withFragments from 'coral-framework/hocs/withFragments';
const slots = [
'adminCommentDetailArea',
];
export default withFragments({
root: gql`
fragment CoralAdmin_CommentDetails_root on RootQuery {
__typename
${getSlotFragmentSpreads(slots, 'root')}
}
`,
comment: gql`
fragment CoralAdmin_CommentDetails_comment on Comment {
__typename
${getSlotFragmentSpreads(slots, 'comment')}
}
`
})(CommentDetails);
@@ -8,6 +8,12 @@ const slots = [
];
export default withFragments({
root: gql`
fragment CoralAdmin_CommentLabels_root on RootQuery {
__typename
${getSlotFragmentSpreads(slots, 'root')}
}
`,
comment: gql`
fragment CoralAdmin_CommentLabels_comment on Comment {
hasParent
@@ -160,8 +160,10 @@ export const withUserDetailQuery = withQuery(gql`
}) {
...CoralAdmin_Moderation_CommentConnection
}
...${getDefinitionName(UserDetailComment.fragments.root)}
${getSlotFragmentSpreads(slots, 'root')}
}
${UserDetailComment.fragments.root}
${commentConnectionFragment}
`, {
options: ({userId, statuses}) => {
@@ -3,10 +3,20 @@ import UserDetailComment from '../components/UserDetailComment';
import withFragments from 'coral-framework/hocs/withFragments';
import {getDefinitionName} from 'coral-framework/utils';
import CommentLabels from './CommentLabels';
import CommentDetails from './CommentDetails';
export default withFragments({
root: gql`
fragment CoralAdmin_UserDetailComment_root on RootQuery {
__typename
...${getDefinitionName(CommentLabels.fragments.root)}
...${getDefinitionName(CommentDetails.fragments.root)}
}
${CommentLabels.fragments.root}
${CommentDetails.fragments.root}
`,
comment: gql`
fragment CoralAdmin_UserDetail_comment on Comment {
fragment CoralAdmin_UserDetailComment_comment on Comment {
id
body
created_at
@@ -17,22 +27,13 @@ export default withFragments({
title
url
}
actions {
... on FlagAction {
id
reason
message
user {
id
username
}
}
}
editing {
edited
}
...${getDefinitionName(CommentLabels.fragments.comment)}
...${getDefinitionName(CommentDetails.fragments.comment)}
}
${CommentLabels.fragments.comment}
${CommentDetails.fragments.comment}
`
})(UserDetailComment);
@@ -1,4 +0,0 @@
.actionButton {
transform: scale(.8);
margin: 0;
}
@@ -1,22 +0,0 @@
import React from 'react';
import styles from './ActionButton.css';
import {Button} from 'coral-ui';
import {menuActionsMap} from '../../../utils/moderationQueueActionsMap';
import t from 'coral-framework/services/i18n';
// TODO: Needs refactoring.
const ActionButton = ({type = '', user, ...props}) => {
return (
<Button
className={`${type.toLowerCase()} ${styles.actionButton}`}
cStyle={type.toLowerCase()}
icon={menuActionsMap[type].icon}
onClick={() => {
type === 'APPROVE' ? props.approveUser({userId: user.id}) : props.showRejectUsernameDialog({user: user});
}}
>{t(`modqueue.${menuActionsMap[type].text}`)}</Button>
);
};
export default ActionButton;
@@ -47,7 +47,6 @@ class FlaggedAccounts extends React.Component {
<FlaggedUser
user={user}
key={user.id}
modActionButtons={['APPROVE', 'REJECT']}
showBanUserDialog={showBanUserDialog}
showSuspendUserDialog={showSuspendUserDialog}
showRejectUsernameDialog={showRejectUsernameDialog}
@@ -1,13 +1,13 @@
import React from 'react';
import styles from './FlaggedUser.css';
import ActionButton from './ActionButton';
// TODO: Should not rely on plugin.
import {username} from 'talk-plugin-flags/helpers/flagReasons';
import ActionsMenu from 'coral-admin/src/components/ActionsMenu';
import ActionsMenuItem from 'coral-admin/src/components/ActionsMenuItem';
import ApproveButton from 'coral-admin/src/components/ApproveButton';
import RejectButton from 'coral-admin/src/components/RejectButton';
import cn from 'classnames';
import t from 'coral-framework/services/i18n';
@@ -35,15 +35,16 @@ class User extends React.Component {
});
viewAuthorDetail = () => this.props.viewUserDetail(this.props.user.id);
showRejectUsernameDialog = () => this.props.showRejectUsernameDialog({id: this.props.user.id});
approveUser = () => this.props.approveUser({
userId: this.props.user.id,
});
render() {
const {
user,
modActionButtons,
viewUserDetail,
selected,
approveUser,
showRejectUsernameDialog,
me,
className,
} = this.props;
@@ -125,14 +126,12 @@ class User extends React.Component {
</div>
<div className={styles.sideActions}>
<div className={styles.actions}>
{modActionButtons.map((action, i) =>
<ActionButton key={i}
type={action.toUpperCase()}
user={user}
approveUser={approveUser}
showRejectUsernameDialog={showRejectUsernameDialog}
/>
)}
<ApproveButton
onClick={this.approveUser}
/>
<RejectButton
onClick={this.showRejectUsernameDialog}
/>
</div>
</div>
</div>
@@ -51,7 +51,7 @@ class RejectUsernameDialog extends Component {
const next = () => this.setState({stage: stage + 1});
const suspend = async () => {
try {
await rejectUsername({id: user.user.id, message: this.state.email});
await rejectUsername({id: user.id, message: this.state.email});
this.props.handleClose();
} catch (err) {
@@ -78,7 +78,11 @@ const withData = withQuery(gql`
${FlaggedAccounts.fragments.root}
${FlaggedUser.fragments.root}
${FlaggedUser.fragments.me}
`);
`, {
options: {
fetchPolicy: 'network-only',
},
});
export default compose(
connect(mapStateToProps, mapDispatchToProps),
@@ -8,13 +8,13 @@ import styles from './Comment.css';
import CommentLabels from 'coral-admin/src/components/CommentLabels';
import CommentAnimatedEdit from 'coral-admin/src/components/CommentAnimatedEdit';
import Slot from 'coral-framework/components/Slot';
import {getActionSummary} from 'coral-framework/utils';
import ActionButton from 'coral-admin/src/components/ActionButton';
import ActionsMenu from 'coral-admin/src/components/ActionsMenu';
import ActionsMenuItem from 'coral-admin/src/components/ActionsMenuItem';
import CommentBodyHighlighter from 'coral-admin/src/components/CommentBodyHighlighter';
import IfHasLink from 'coral-admin/src/components/IfHasLink';
import cn from 'classnames';
import ApproveButton from 'coral-admin/src/components/ApproveButton';
import RejectButton from 'coral-admin/src/components/RejectButton';
import t, {timeago} from 'coral-framework/services/i18n';
@@ -45,11 +45,19 @@ class Comment extends React.Component {
return viewUserDetail(comment.user.id);
};
approve = () => (this.props.comment.status === 'ACCEPTED'
? null
: this.props.acceptComment({commentId: this.props.comment.id})
);
reject = () => (this.props.comment.status === 'REJECTED'
? null
: this.props.rejectComment({commentId: this.props.comment.id})
);
render() {
const {
actions = [],
comment,
viewUserDetail,
suspectWords,
bannedWords,
selected,
@@ -58,15 +66,9 @@ class Comment extends React.Component {
root,
currentUserId,
currentAsset,
acceptComment,
rejectComment,
} = this.props;
const flagActionSummaries = getActionSummary('FlagActionSummary', comment);
const flagActions = comment.actions && comment.actions.filter((a) => a.__typename === 'FlagAction');
const selectionStateCSS = selected ? 'mdl-shadow--16dp' : 'mdl-shadow--2dp';
const queryData = {root, comment, asset: comment.asset};
return (
@@ -153,28 +155,14 @@ class Comment extends React.Component {
</span>
</IfHasLink>
<div className={`actions ${styles.actions}`}>
{actions.map((action, i) => {
const active =
(action === 'REJECT' && comment.status === 'REJECTED') ||
(action === 'APPROVE' && comment.status === 'ACCEPTED');
return (
<ActionButton
key={i}
type={action}
user={comment.user}
status={comment.status}
active={active}
acceptComment={() =>
(comment.status === 'ACCEPTED'
? null
: acceptComment({commentId: comment.id}))}
rejectComment={() =>
(comment.status === 'REJECTED'
? null
: rejectComment({commentId: comment.id}))}
/>
);
})}
<ApproveButton
active={comment.status === 'ACCEPTED'}
onClick={this.approve}
/>
<RejectButton
active={comment.status === 'REJECTED'}
onClick={this.reject}
/>
</div>
<Slot
fill="adminSideActions"
@@ -185,17 +173,10 @@ class Comment extends React.Component {
</div>
</CommentAnimatedEdit>
</div>
{flagActions && flagActions.length
? <CommentDetails
actions={flagActions}
actionSummaries={flagActionSummaries}
viewUserDetail={viewUserDetail}
/>
: null}
<Slot
fill="adminCommentDetailArea"
<CommentDetails
data={data}
queryData={queryData}
root={root}
comment={comment}
/>
</li>
);
@@ -214,6 +195,8 @@ Comment.propTypes = {
showSuspendUserDialog: PropTypes.func.isRequired,
currentUserId: PropTypes.string.isRequired,
comment: PropTypes.shape({
id: PropTypes.string.isRequired,
status: PropTypes.string.isRequired,
body: PropTypes.string.isRequired,
action_summaries: PropTypes.array,
actions: PropTypes.array,
@@ -230,7 +213,6 @@ Comment.propTypes = {
}),
data: PropTypes.object.isRequired,
root: PropTypes.object.isRequired,
actions: PropTypes.array.isRequired,
selected: PropTypes.bool,
};
@@ -4,7 +4,6 @@ import PropTypes from 'prop-types';
import Comment from '../containers/Comment';
import styles from './ModerationQueue.css';
import EmptyCard from '../../../components/EmptyCard';
import {actionsMap} from '../../../utils/moderationQueueActionsMap';
import LoadMore from '../../../components/LoadMore';
import ViewMore from './ViewMore';
import t from 'coral-framework/services/i18n';
@@ -140,7 +139,6 @@ class ModerationQueue extends React.Component {
if (singleView) {
const index = comments.findIndex((comment) => comment.id === selectedCommentId);
const comment = comments[index];
const status = comment.action_summaries ? 'FLAGGED' : comment.status;
return (
<div className={styles.root}>
<Comment
@@ -152,7 +150,6 @@ class ModerationQueue extends React.Component {
suspectWords={props.suspectWords}
bannedWords={props.bannedWords}
viewUserDetail={viewUserDetail}
actions={actionsMap[status]}
showBanUserDialog={props.showBanUserDialog}
showSuspendUserDialog={props.showSuspendUserDialog}
acceptComment={props.acceptComment}
@@ -190,7 +187,6 @@ class ModerationQueue extends React.Component {
{
view
.map((comment) => {
const status = comment.action_summaries ? 'FLAGGED' : comment.status;
return <Comment
data={this.props.data}
root={this.props.root}
@@ -200,7 +196,6 @@ class ModerationQueue extends React.Component {
suspectWords={props.suspectWords}
bannedWords={props.bannedWords}
viewUserDetail={viewUserDetail}
actions={actionsMap[status]}
showBanUserDialog={props.showBanUserDialog}
showSuspendUserDialog={props.showSuspendUserDialog}
acceptComment={props.acceptComment}
@@ -1,6 +1,7 @@
import {gql} from 'react-apollo';
import Comment from '../components/Comment';
import CommentLabels from '../../../containers/CommentLabels';
import CommentDetails from '../../../containers/CommentDetails';
import withFragments from 'coral-framework/hocs/withFragments';
import {getSlotFragmentSpreads, getDefinitionName} from 'coral-framework/utils';
@@ -16,8 +17,12 @@ export default withFragments({
fragment CoralAdmin_ModerationComment_root on RootQuery {
__typename
${getSlotFragmentSpreads(slots, 'root')}
...${getDefinitionName(CommentLabels.fragments.root)}
...${getDefinitionName(CommentDetails.fragments.root)}
}
`,
${CommentLabels.fragments.root}
${CommentDetails.fragments.root}
`,
comment: gql`
fragment CoralAdmin_ModerationComment_comment on Comment {
id
@@ -34,33 +39,15 @@ export default withFragments({
title
url
}
action_summaries {
count
... on FlagActionSummary {
reason
__typename
}
}
actions {
... on FlagAction {
id
reason
message
user {
id
username
}
__typename
}
__typename
}
editing {
edited
}
hasParent
${getSlotFragmentSpreads(slots, 'comment')}
...${getDefinitionName(CommentLabels.fragments.comment)}
...${getDefinitionName(CommentDetails.fragments.comment)}
}
${CommentLabels.fragments.comment}
${CommentDetails.fragments.comment}
`
})(Comment);
@@ -363,7 +363,9 @@ const withModQueueQuery = withQuery(({queueConfig}) => gql`
organizationName
moderation
}
...${getDefinitionName(Comment.fragments.root)}
}
${Comment.fragments.root}
${commentConnectionFragment}
`, {
options: (props) => {
@@ -1,14 +0,0 @@
export const actionsMap = {
PREMOD: ['APPROVE', 'REJECT'],
FLAGGED: ['APPROVE', 'REJECT'],
REJECTED: ['APPROVE', 'REJECTED']
};
export const menuActionsMap = {
'REJECT': {status: 'REJECTED', text: 'reject', icon: 'close', key: 'f'},
'REJECTED': {status: 'REJECTED', text: 'rejected', icon: 'close'},
'APPROVE': {status: 'ACCEPTED', text: 'approve', icon: 'done', key: 'd'},
'FLAGGED': {status: 'FLAGGED', text: 'flag', icon: 'flag', filter: 'Untouched'},
'BAN': {status: 'BANNED', text: 'ban_user', icon: 'not interested'},
'': {icon: 'done'}
};
-53
View File
@@ -130,59 +130,6 @@
background: #00a291;
}
.type--approve {
display: block;
color: #519954;
border: solid 2px rgba(81, 153, 84, 0.75);
background: white;
padding: 10px 12px;
box-sizing: border-box;
vertical-align: middle;
line-height: 24px;
font-size: 17px;
height: 47px;
border-radius: 3px;
text-transform: capitalize;
box-shadow: 0 2px 2px 0 rgba(0, 0, 0, 0.03), 0 3px 1px -2px rgba(0,0,0,.2), 0 1px 5px 0 rgba(0,0,0,.09);
width: 129px;
&:hover {
box-shadow: none;
color: white;
background-color: #519954;
}
}
.type--reject, .type--rejected {
display: block;
color: #D03235;
border: solid 1px #D03235;
background: white;
padding: 10px 11px;
box-sizing: border-box;
vertical-align: middle;
line-height: 24px;
font-size: 17px;
height: 47px;
border-radius: 3px;
text-transform: capitalize;
box-shadow: 0 2px 2px 0 rgba(0, 0, 0, 0.03), 0 3px 1px -2px rgba(0,0,0,.2), 0 1px 5px 0 rgba(0,0,0,.09);
width: 129px;
&:hover {
color: white;
background-color: #D03235;
box-shadow: none;
}
}
.type--rejected {
color: white;
background-color: #D03235;
box-shadow: none;
cursor: not-allowed;
}
.type--ban, .type--actions {
display: block;
color: #616161;
@@ -0,0 +1,3 @@
{
"extends": "@coralproject/eslint-config-talk"
}
@@ -0,0 +1,14 @@
{
"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"
]
}
@@ -0,0 +1,3 @@
{
"extends": "@coralproject/eslint-config-talk/client"
}
@@ -0,0 +1,43 @@
.info {
vertical-align: middle;
list-style: none;
display: inline-block;
padding: 0;
font-size: 12px;
}
.detail {
padding: 0;
list-style: none;
font-size: 12px;
font-weight: 500;
}
.subDetail {
margin-left:10px;
padding: 0;
list-style: none;
font-size: 12px;
font-weight: normal;
color: #888;
}
.lessDetail {
display: inline-block;
margin-right: 10px;
}
.username {
color: #393B44;
text-decoration: none;
cursor: pointer;
font-weight: 600;
padding: 2px 5px;
border-radius: 2px;
margin-left: -5px;
transition: background-color 200ms ease;
&:hover {
background-color: #E0E0E0;
}
}
@@ -0,0 +1,79 @@
import React, {Component} from 'react';
import PropTypes from 'prop-types';
import styles from './FlagDetails.css';
import {t} from 'plugin-api/beta/client/services';
import CommentDetail from 'coral-admin/src/components/CommentDetail';
class FlagDetails extends Component {
render() {
const {comment: {actions}, viewUserDetail, more} = this.props;
const flagActions = actions && actions.filter((a) => a.__typename === 'FlagAction');
const summaries = flagActions.reduce((sum, action) => {
if (!(action.reason in sum)) {
sum[action.reason] = {count: 0, userFlagged: false, actions: []};
}
sum[action.reason].count++;
if (action.user) {
sum[action.reason].userFlagged = true;
}
sum[action.reason].actions.push(action);
return sum;
}, {});
const userFlagReasons = Object.keys(summaries).filter((reason) => summaries[reason].userFlagged);
return (
<CommentDetail
icon={'flag'}
header={`${t('community.flags')} (${Object.keys(summaries).length})`}
info={
<ul className={styles.info}>
{Object.keys(summaries).map((reason) =>
<li key={reason} className={styles.lessDetail}>
{reason} {summaries[reason].userFlagged && `(${summaries[reason].count})`}
</li>
)}
</ul>
}>
{more && userFlagReasons.length > 0 && (
<ul className={styles.detail}>
{userFlagReasons
.map((reason) => (
<li key={reason}>
{reason} ({summaries[reason].count})
<ul className={styles.subDetail}>
{summaries[reason].actions.map((action) =>
<li key={action.user.id}>
{action.user &&
<a className={styles.username} onClick={() => viewUserDetail(action.user.id)}>
{action.user.username}
</a>
}
{action.message}
</li>
)}
</ul>
</li>
))
}
</ul>
)}
</CommentDetail>
);
}
}
FlagDetails.propTypes = {
more: PropTypes.bool,
comment: PropTypes.shape({
actions: PropTypes.arrayOf(PropTypes.shape({
message: PropTypes.string,
user: PropTypes.shape({username: PropTypes.string})
})).isRequired,
}).isRequired,
viewUserDetail: PropTypes.func.isRequired,
};
export default FlagDetails;
@@ -0,0 +1,37 @@
import {compose, gql} from 'react-apollo';
import FlagDetails from '../components/FlagDetails';
import {bindActionCreators} from 'redux';
import {withFragments, excludeIf} from 'plugin-api/beta/client/hocs';
import {viewUserDetail} from 'coral-admin/src/actions/userDetail';
import {connect} from 'react-redux';
const mapDispatchToProps = (dispatch) => ({
...bindActionCreators({
viewUserDetail,
}, dispatch)
});
const enhance = compose(
connect(null, mapDispatchToProps),
withFragments({
comment: gql`
fragment CoralAdmin_FlagDetails_comment on Comment {
actions {
__typename
... on FlagAction {
id
reason
message
user {
id
username
}
}
}
}
`
}),
excludeIf(({comment: {actions}}) => !actions.some((action) => action.__typename === 'FlagAction')),
);
export default enhance(FlagDetails);
@@ -0,0 +1,9 @@
import FlagDetails from './containers/FlagDetails';
import translations from './translations.yml';
export default {
translations,
slots: {
adminCommentDetailArea: [FlagDetails],
}
};
@@ -0,0 +1,3 @@
en:
talk-plugin-fag-details:
@@ -0,0 +1 @@
module.exports = {};
@@ -15,7 +15,7 @@ const enhance = compose(
}
}`,
}),
excludeIf(({comment: {toxicity}}) => toxicity === null),
excludeIf(({comment: {toxicity}, more}) => !more || toxicity === null),
);
export default enhance(ToxicDetail);