mirror of
https://github.com/wassname/talk.git
synced 2026-07-26 13:37:38 +08:00
Merge branch 'master' into talk-product-guide
This commit is contained in:
@@ -1,9 +1,8 @@
|
||||
{
|
||||
"presets": [
|
||||
"es2015"
|
||||
["es2015", {modules: false}]
|
||||
],
|
||||
"plugins": [
|
||||
"add-module-exports",
|
||||
"transform-class-properties",
|
||||
"transform-decorators-legacy",
|
||||
"transform-object-assign",
|
||||
@@ -11,5 +10,12 @@
|
||||
"transform-async-to-generator",
|
||||
"transform-react-jsx",
|
||||
"syntax-dynamic-import"
|
||||
]
|
||||
}
|
||||
],
|
||||
"env": {
|
||||
"test": {
|
||||
"plugins": [
|
||||
["transform-es2015-modules-commonjs", "dynamic-import-node"]
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -26,5 +26,7 @@ plugins/*
|
||||
!plugins/talk-plugin-toxic-comments
|
||||
!plugins/talk-plugin-remember-sort
|
||||
!plugins/talk-plugin-deep-reply-count
|
||||
!plugins/talk-plugin-subscriber
|
||||
!plugins/talk-plugin-flag-details
|
||||
|
||||
node_modules
|
||||
|
||||
@@ -43,5 +43,7 @@ plugins/*
|
||||
!plugins/talk-plugin-toxic-comments
|
||||
!plugins/talk-plugin-remember-sort
|
||||
!plugins/talk-plugin-deep-reply-count
|
||||
!plugins/talk-plugin-subscriber
|
||||
!plugins/talk-plugin-flag-details
|
||||
|
||||
**/node_modules/*
|
||||
|
||||
@@ -1,14 +0,0 @@
|
||||
{
|
||||
"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"
|
||||
]
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
.root {
|
||||
min-height: 25px;
|
||||
padding: 0 16px;
|
||||
}
|
||||
|
||||
.moreDetail {
|
||||
position: absolute;
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
color: black;
|
||||
right: 16px;
|
||||
|
||||
&:hover {
|
||||
opacity: 0.9;
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,66 @@
|
||||
import React, {Component} from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import styles from './CommentDetails.css';
|
||||
import t from 'coral-framework/services/i18n';
|
||||
import Slot from 'coral-framework/components/Slot';
|
||||
import IfSlotIsNotEmpty from 'coral-framework/components/IfSlotIsNotEmpty';
|
||||
|
||||
class CommentDetails extends Component {
|
||||
state = {
|
||||
showDetail: false
|
||||
};
|
||||
|
||||
constructor () {
|
||||
super();
|
||||
this.state = {
|
||||
showDetail: false
|
||||
};
|
||||
}
|
||||
|
||||
toggleDetail = () => {
|
||||
this.setState((state) => ({
|
||||
showDetail: !state.showDetail
|
||||
}));
|
||||
}
|
||||
|
||||
render() {
|
||||
const {data, root, comment} = this.props;
|
||||
const {showDetail} = this.state;
|
||||
const queryData = {
|
||||
root,
|
||||
comment,
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={styles.root}>
|
||||
<IfSlotIsNotEmpty
|
||||
queryData={queryData}
|
||||
slot={['adminCommentMoreDetails', 'adminCommentMoreFlagDetails']}
|
||||
>
|
||||
<a onClick={this.toggleDetail} className={styles.moreDetail}>
|
||||
{showDetail ? t('modqueue.less_detail') : t('modqueue.more_detail')}
|
||||
</a>
|
||||
</IfSlotIsNotEmpty>
|
||||
<Slot
|
||||
fill="adminCommentDetailArea"
|
||||
data={data}
|
||||
queryData={queryData}
|
||||
more={showDetail}
|
||||
/>
|
||||
{showDetail && <Slot
|
||||
fill="adminCommentMoreDetails"
|
||||
data={data}
|
||||
queryData={queryData}
|
||||
/>}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
CommentDetails.propTypes = {
|
||||
data: PropTypes.object.isRequired,
|
||||
root: PropTypes.object.isRequired,
|
||||
comment: PropTypes.object.isRequired,
|
||||
};
|
||||
|
||||
export default CommentDetails;
|
||||
@@ -1,83 +0,0 @@
|
||||
.flagBox {
|
||||
border-top: 1px solid rgba(66, 66, 66, 0.12);
|
||||
margin-top: 10px;
|
||||
.container {
|
||||
padding: 0 14px;
|
||||
}
|
||||
|
||||
.detail {
|
||||
padding: 0 20px 16px;
|
||||
ul {
|
||||
padding: 0;
|
||||
list-style: none;
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
}
|
||||
}
|
||||
|
||||
.header {
|
||||
position: relative;
|
||||
.moreDetail {
|
||||
float: right;
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
margin-right: 10px;
|
||||
margin-top: 8px;
|
||||
color: black;
|
||||
|
||||
&:hover {
|
||||
opacity: 0.9;
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
i {
|
||||
vertical-align: middle;
|
||||
font-size: 12px;
|
||||
}
|
||||
ul {
|
||||
vertical-align: middle;
|
||||
list-style: none;
|
||||
display: inline-block;
|
||||
padding: 0;
|
||||
margin-left: 10px;
|
||||
font-size: 12px;
|
||||
}
|
||||
}
|
||||
|
||||
h3 {
|
||||
vertical-align: middle;
|
||||
margin: 0;
|
||||
font-weight: 500;
|
||||
display: inline-block;
|
||||
margin-left: 7px;
|
||||
font-size: 12px;
|
||||
}
|
||||
}
|
||||
|
||||
.lessDetail {
|
||||
display: inline-block;
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
.subDetail {
|
||||
font-weight: normal;
|
||||
color: #888;
|
||||
|
||||
span {
|
||||
color: black;
|
||||
}
|
||||
}
|
||||
|
||||
.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;
|
||||
}
|
||||
}
|
||||
@@ -1,97 +0,0 @@
|
||||
import React, {Component} from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import {Icon} from 'coral-ui';
|
||||
import styles from './FlagBox.css';
|
||||
import t from 'coral-framework/services/i18n';
|
||||
|
||||
const shortReasons = {
|
||||
'This comment is offensive': t('modqueue.offensive'),
|
||||
'This looks like an ad/marketing': t('modqueue.spam_ads'),
|
||||
'This user is impersonating': t('modqueue.impersonating'),
|
||||
'I don\'t like this username': t('modqueue.dont_like_username'),
|
||||
'Other': t('modqueue.other')
|
||||
};
|
||||
|
||||
class FlagBox extends Component {
|
||||
constructor () {
|
||||
super();
|
||||
this.state = {
|
||||
showDetail: false
|
||||
};
|
||||
}
|
||||
|
||||
toggleDetail = () => {
|
||||
this.setState((state) => ({
|
||||
showDetail: !state.showDetail
|
||||
}));
|
||||
}
|
||||
|
||||
reasonMap = (reason) => {
|
||||
const shortReason = shortReasons[reason];
|
||||
|
||||
// if the short reason isn't found, just return the long one.
|
||||
return shortReason ? shortReason : reason;
|
||||
}
|
||||
|
||||
render() {
|
||||
const {actionSummaries, actions, viewUserDetail} = this.props;
|
||||
const {showDetail} = this.state;
|
||||
|
||||
return (
|
||||
<div className={styles.flagBox}>
|
||||
<div className={styles.container}>
|
||||
<div className={styles.header}>
|
||||
<Icon name='flag'/><h3>{t('community.flags')} ({actionSummaries.length}):</h3>
|
||||
<ul>
|
||||
{actionSummaries.map((action, i) =>
|
||||
<li key={i} className={styles.lessDetail}> {this.reasonMap(action.reason)} (<strong>{action.count}</strong>)</li>
|
||||
)}
|
||||
</ul>
|
||||
<a onClick={this.toggleDetail} className={styles.moreDetail}>{showDetail ? t('modqueue.less_detail') : t('modqueue.more_detail')}</a>
|
||||
</div>
|
||||
{showDetail && (
|
||||
<div className={styles.detail}>
|
||||
<ul>
|
||||
{actionSummaries.map((summary, i) => {
|
||||
|
||||
const actionList = actions.filter((a) => a.reason === summary.reason);
|
||||
|
||||
return (
|
||||
<li key={i}>
|
||||
{this.reasonMap(summary.reason)} (<strong>{summary.count}</strong>)
|
||||
<ul>
|
||||
{actionList.map((action, j) =>
|
||||
<li key={`${i}_${j}`} className={styles.subDetail}>
|
||||
{action.user &&
|
||||
<a className={styles.username} onClick={() => viewUserDetail(action.user.id)}>
|
||||
{action.user.username}
|
||||
</a>
|
||||
}
|
||||
{action.message}
|
||||
</li>
|
||||
)}
|
||||
</ul>
|
||||
</li>
|
||||
);
|
||||
})}
|
||||
</ul>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
FlagBox.propTypes = {
|
||||
actionSummaries: PropTypes.arrayOf(PropTypes.shape({
|
||||
reason: PropTypes.string,
|
||||
count: PropTypes.number
|
||||
})).isRequired,
|
||||
actions: PropTypes.arrayOf(PropTypes.shape({
|
||||
message: PropTypes.string,
|
||||
user: PropTypes.shape({username: PropTypes.string})
|
||||
})).isRequired
|
||||
};
|
||||
|
||||
export default FlagBox;
|
||||
@@ -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}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
.root {
|
||||
position: relative;
|
||||
display: block;
|
||||
margin: 0;
|
||||
border-bottom: 1px solid #e0e0e0;
|
||||
|
||||
@@ -3,37 +3,41 @@ import PropTypes from 'prop-types';
|
||||
import {Link} from 'react-router';
|
||||
|
||||
import {Icon} from 'coral-ui';
|
||||
import FlagBox from './FlagBox';
|
||||
import CommentDetails from './CommentDetails';
|
||||
import styles from './UserDetailComment.css';
|
||||
import {getActionSummary} from 'coral-framework/utils';
|
||||
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 flagActionSummaries = getActionSummary('FlagActionSummary', comment);
|
||||
const flagActions = comment.actions && comment.actions.filter((a) => a.__typename === 'FlagAction');
|
||||
|
||||
return (
|
||||
<li
|
||||
tabIndex={0}
|
||||
@@ -66,7 +70,7 @@ class UserDetailComment extends React.Component {
|
||||
</div>
|
||||
<CommentAnimatedEdit body={comment.body}>
|
||||
<div className={styles.bodyContainer}>
|
||||
<p className={styles.body}>
|
||||
<div className={styles.body}>
|
||||
<CommentBodyHighlighter
|
||||
suspectWords={suspectWords}
|
||||
bannedWords={bannedWords}
|
||||
@@ -80,7 +84,7 @@ class UserDetailComment extends React.Component {
|
||||
>
|
||||
<Icon name="open_in_new" /> {t('comment.view_context')}
|
||||
</a>
|
||||
</p>
|
||||
</div>
|
||||
<div className={styles.sideActions}>
|
||||
<IfHasLink text={comment.body}>
|
||||
<span className={styles.hasLinks}>
|
||||
@@ -88,41 +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
|
||||
? <FlagBox
|
||||
actions={flagActions}
|
||||
actionSummaries={flagActionSummaries}
|
||||
viewUserDetail={viewUserDetail}
|
||||
/>
|
||||
: null}
|
||||
<CommentDetails
|
||||
data={data}
|
||||
root={root}
|
||||
comment={comment}
|
||||
/>
|
||||
</li>
|
||||
);
|
||||
}
|
||||
@@ -138,8 +127,9 @@ 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,
|
||||
action_summaries: PropTypes.array,
|
||||
actions: PropTypes.array,
|
||||
created_at: PropTypes.string.isRequired,
|
||||
asset: PropTypes.shape({
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
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',
|
||||
'adminCommentMoreDetails',
|
||||
];
|
||||
|
||||
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,28 +27,13 @@ export default withFragments({
|
||||
title
|
||||
url
|
||||
}
|
||||
action_summaries {
|
||||
count
|
||||
... on FlagActionSummary {
|
||||
reason
|
||||
}
|
||||
}
|
||||
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),
|
||||
|
||||
@@ -58,6 +58,7 @@
|
||||
font-size: 14px;
|
||||
line-height: 1.5;
|
||||
font-weight: 300;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.body {
|
||||
|
||||
@@ -3,18 +3,18 @@ import PropTypes from 'prop-types';
|
||||
import {Link} from 'react-router';
|
||||
|
||||
import {Icon} from 'coral-ui';
|
||||
import FlagBox from 'coral-admin/src/components/FlagBox';
|
||||
import CommentDetails from 'coral-admin/src/components/CommentDetails';
|
||||
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,18 +173,11 @@ class Comment extends React.Component {
|
||||
</div>
|
||||
</CommentAnimatedEdit>
|
||||
</div>
|
||||
<Slot
|
||||
fill="adminCommentDetailArea"
|
||||
<CommentDetails
|
||||
data={data}
|
||||
queryData={queryData}
|
||||
root={root}
|
||||
comment={comment}
|
||||
/>
|
||||
{flagActions && flagActions.length
|
||||
? <FlagBox
|
||||
actions={flagActions}
|
||||
actionSummaries={flagActionSummaries}
|
||||
viewUserDetail={viewUserDetail}
|
||||
/>
|
||||
: null}
|
||||
</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'}
|
||||
};
|
||||
@@ -8,7 +8,7 @@ import styles from './QuestionBoxBuilder.css';
|
||||
class QuestionBoxBuilder extends React.Component {
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
|
||||
this.state = {
|
||||
loading: true
|
||||
};
|
||||
@@ -17,9 +17,9 @@ class QuestionBoxBuilder extends React.Component {
|
||||
componentWillMount() {
|
||||
this.loadEditor();
|
||||
}
|
||||
|
||||
|
||||
async loadEditor() {
|
||||
const MarkdownEditor = await import('coral-framework/components/MarkdownEditor');
|
||||
const {default: MarkdownEditor} = await import('coral-framework/components/MarkdownEditor');
|
||||
|
||||
return this.setState({
|
||||
loading : false,
|
||||
@@ -79,17 +79,17 @@ class QuestionBoxBuilder extends React.Component {
|
||||
</ul>
|
||||
|
||||
<QuestionBox
|
||||
className={styles.qb}
|
||||
className={styles.qb}
|
||||
enable={true}
|
||||
icon={questionBoxIcon}
|
||||
icon={questionBoxIcon}
|
||||
content={questionBoxContent}
|
||||
/>
|
||||
|
||||
<MarkdownEditor
|
||||
<MarkdownEditor
|
||||
value={questionBoxContent}
|
||||
onChange={(value) => handleChange({}, {questionBoxContent: value})}
|
||||
/>
|
||||
|
||||
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -45,6 +45,12 @@
|
||||
border-left: 3px solid rgb(35,118,216);
|
||||
}
|
||||
|
||||
.bylineSecondary {
|
||||
display: flex;
|
||||
color: #696969;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.pendingComment {
|
||||
filter: blur(2px);
|
||||
pointer-events: none;
|
||||
|
||||
@@ -96,6 +96,11 @@ export default {
|
||||
user {
|
||||
id
|
||||
username
|
||||
tags {
|
||||
tag {
|
||||
name
|
||||
}
|
||||
}
|
||||
}
|
||||
action_summaries {
|
||||
count
|
||||
@@ -129,7 +134,19 @@ export default {
|
||||
user: {
|
||||
__typename: 'User',
|
||||
id: auth.user.id,
|
||||
username: auth.user.username
|
||||
username: auth.user.username,
|
||||
tags: tags.map((tag) => ({
|
||||
tag: {
|
||||
name: tag,
|
||||
created_at: new Date().toISOString(),
|
||||
__typename: 'Tag'
|
||||
},
|
||||
assigned_by: {
|
||||
id: auth.user.id,
|
||||
__typename: 'User'
|
||||
},
|
||||
__typename: 'TagLink'
|
||||
})),
|
||||
},
|
||||
created_at: new Date().toISOString(),
|
||||
body,
|
||||
@@ -140,10 +157,6 @@ export default {
|
||||
created_at: new Date().toISOString(),
|
||||
__typename: 'Tag'
|
||||
},
|
||||
assigned_by: {
|
||||
id: auth.user.id,
|
||||
__typename: 'User'
|
||||
},
|
||||
__typename: 'TagLink'
|
||||
})),
|
||||
status: 'NONE',
|
||||
|
||||
@@ -270,6 +270,7 @@ button.comment__action-button[disabled],
|
||||
width: 75%;
|
||||
font-size: 16px;
|
||||
border: 1px solid #ccc;
|
||||
max-width: calc(100% - 40px);
|
||||
}
|
||||
|
||||
/* Close comments */
|
||||
|
||||
@@ -2,101 +2,98 @@ import URLSearchParams from 'url-search-params';
|
||||
import Stream from './Stream';
|
||||
import StreamInterface from './StreamInterface';
|
||||
|
||||
// This function should return value of window.Coral
|
||||
const Coral = {};
|
||||
const Talk = (Coral.Talk = {});
|
||||
export class Talk {
|
||||
|
||||
/**
|
||||
* Render a Talk stream
|
||||
* @param {HTMLElement} el - Element to render the stream in
|
||||
* @param {Object} opts - Configuration options for talk
|
||||
* @param {String} opts.talk - Talk base URL
|
||||
* @param {String} [opts.title] - Title of Stream (rendered in iframe)
|
||||
* @param {String} [opts.asset_url] - Asset URL
|
||||
* @param {String} [opts.asset_id] - Asset ID
|
||||
* @param {String} [opts.auth_token] - (optional) A jwt representing the session
|
||||
* @return {Object}
|
||||
*
|
||||
* Example:
|
||||
* ```
|
||||
* const embed = Talk.render(document.getElementById('talkStreamEmbed'), opts);
|
||||
*
|
||||
* // trigger a login with optional token.
|
||||
* embed.login(token);
|
||||
*
|
||||
* // trigger a logout.
|
||||
* embed.logout();
|
||||
*
|
||||
* // listen to events (in this case all events).
|
||||
* embed.on('**', function(value) {
|
||||
* console.log(this.event, value);
|
||||
* });
|
||||
* ```
|
||||
*/
|
||||
Talk.render = (el, opts) => {
|
||||
if (!el) {
|
||||
throw new Error('Please provide Coral.Talk.render() the HTMLElement you want to render Talk in.');
|
||||
}
|
||||
if (typeof el !== 'object') {
|
||||
throw new Error(`Coral.Talk.render() expected HTMLElement but got ${el} (${typeof el})`);
|
||||
}
|
||||
|
||||
opts = opts || {};
|
||||
|
||||
// TODO: infer this URL without explicit user input (if possible, may have to be added at build/render time of this script)
|
||||
if (!opts.talk) {
|
||||
throw new Error(
|
||||
'Coral.Talk.render() expects opts.talk as the Talk Base URL'
|
||||
);
|
||||
}
|
||||
|
||||
// Ensure el has an id, as pym can't directly accept the HTMLElement.
|
||||
if (!el.id) {
|
||||
el.id = `_${Math.random()}`;
|
||||
}
|
||||
|
||||
// Compose the query to send down to the Talk API so it knows what to load.
|
||||
const query = {};
|
||||
|
||||
// Parse the url parameters to extract some of the information.
|
||||
const urlParams = new URLSearchParams(window.location.search);
|
||||
if (urlParams.get('commentId')) {
|
||||
query.comment_id = urlParams.get('commentId');
|
||||
}
|
||||
|
||||
// Extract the asset id from the options.
|
||||
if (opts.asset_id) {
|
||||
query.asset_id = opts.asset_id;
|
||||
}
|
||||
|
||||
// Extract the asset url.
|
||||
if (opts.asset_url) {
|
||||
query.asset_url = opts.asset_url;
|
||||
} else if (!opts.asset_id) {
|
||||
|
||||
// The asset url was not provided and the asset id was also not provided,
|
||||
// we need to infer the asset url from details on the page.
|
||||
|
||||
try {
|
||||
query.asset_url = document.querySelector('link[rel="canonical"]').href;
|
||||
} catch (e) {
|
||||
console.warn(
|
||||
'This page does not include a canonical link tag. Talk has inferred this asset_url from the window object. Query params have been stripped, which may cause a single thread to be present across multiple pages.'
|
||||
);
|
||||
|
||||
if (!window.location.origin) {
|
||||
window.location.origin = `${window.location.protocol}//${window.location.hostname}${window.location.port ? `:${window.location.port}` : ''}`;
|
||||
}
|
||||
|
||||
query.asset_url = window.location.origin + window.location.pathname;
|
||||
/**
|
||||
* Render a Talk stream
|
||||
* @param {HTMLElement} el - Element to render the stream in
|
||||
* @param {Object} opts - Configuration options for talk
|
||||
* @param {String} opts.talk - Talk base URL
|
||||
* @param {String} [opts.title] - Title of Stream (rendered in iframe)
|
||||
* @param {String} [opts.asset_url] - Asset URL
|
||||
* @param {String} [opts.asset_id] - Asset ID
|
||||
* @param {String} [opts.auth_token] - (optional) A jwt representing the session
|
||||
* @return {Object}
|
||||
*
|
||||
* Example:
|
||||
* ```
|
||||
* const embed = Talk.render(document.getElementById('talkStreamEmbed'), opts);
|
||||
*
|
||||
* // trigger a login with optional token.
|
||||
* embed.login(token);
|
||||
*
|
||||
* // trigger a logout.
|
||||
* embed.logout();
|
||||
*
|
||||
* // listen to events (in this case all events).
|
||||
* embed.on('**', function(value) {
|
||||
* console.log(this.event, value);
|
||||
* });
|
||||
* ```
|
||||
*/
|
||||
static render(el, opts) {
|
||||
if (!el) {
|
||||
throw new Error('Please provide Coral.Talk.render() the HTMLElement you want to render Talk in.');
|
||||
}
|
||||
if (typeof el !== 'object') {
|
||||
throw new Error(`Coral.Talk.render() expected HTMLElement but got ${el} (${typeof el})`);
|
||||
}
|
||||
|
||||
opts = opts || {};
|
||||
|
||||
// TODO: infer this URL without explicit user input (if possible, may have to be added at build/render time of this script)
|
||||
if (!opts.talk) {
|
||||
throw new Error(
|
||||
'Coral.Talk.render() expects opts.talk as the Talk Base URL'
|
||||
);
|
||||
}
|
||||
|
||||
// Ensure el has an id, as pym can't directly accept the HTMLElement.
|
||||
if (!el.id) {
|
||||
el.id = `_${Math.random()}`;
|
||||
}
|
||||
|
||||
// Compose the query to send down to the Talk API so it knows what to load.
|
||||
const query = {};
|
||||
|
||||
// Parse the url parameters to extract some of the information.
|
||||
const urlParams = new URLSearchParams(window.location.search);
|
||||
if (urlParams.get('commentId')) {
|
||||
query.comment_id = urlParams.get('commentId');
|
||||
}
|
||||
|
||||
// Extract the asset id from the options.
|
||||
if (opts.asset_id) {
|
||||
query.asset_id = opts.asset_id;
|
||||
}
|
||||
|
||||
// Extract the asset url.
|
||||
if (opts.asset_url) {
|
||||
query.asset_url = opts.asset_url;
|
||||
} else if (!opts.asset_id) {
|
||||
|
||||
// The asset url was not provided and the asset id was also not provided,
|
||||
// we need to infer the asset url from details on the page.
|
||||
|
||||
try {
|
||||
query.asset_url = document.querySelector('link[rel="canonical"]').href;
|
||||
} catch (e) {
|
||||
console.warn(
|
||||
'This page does not include a canonical link tag. Talk has inferred this asset_url from the window object. Query params have been stripped, which may cause a single thread to be present across multiple pages.'
|
||||
);
|
||||
|
||||
if (!window.location.origin) {
|
||||
window.location.origin = `${window.location.protocol}//${window.location.hostname}${window.location.port ? `:${window.location.port}` : ''}`;
|
||||
}
|
||||
|
||||
query.asset_url = window.location.origin + window.location.pathname;
|
||||
}
|
||||
}
|
||||
|
||||
// Create the new Stream.
|
||||
const stream = new Stream(el, opts.talk, query, opts);
|
||||
|
||||
// Return the public interface for the stream.
|
||||
return new StreamInterface(stream);
|
||||
}
|
||||
|
||||
// Create the new Stream.
|
||||
const stream = new Stream(el, opts.talk, query, opts);
|
||||
|
||||
// Return the public interface for the stream.
|
||||
return new StreamInterface(stream);
|
||||
};
|
||||
|
||||
export default Coral;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
.root {
|
||||
}
|
||||
|
||||
.headerContainer {
|
||||
display: flex;
|
||||
justify-content: flex-start;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.header {
|
||||
vertical-align: middle;
|
||||
margin: 0;
|
||||
font-weight: 500;
|
||||
display: inline-block;
|
||||
font-size: 12px;
|
||||
line-height: 12px;
|
||||
margin-right: 7px;
|
||||
}
|
||||
|
||||
.info {
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.details {
|
||||
padding: 0 20px 16px;
|
||||
font-size: 12px;
|
||||
|
||||
&:empty {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.icon {
|
||||
vertical-align: middle;
|
||||
font-size: 12px;
|
||||
margin-right: 7px;
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import cn from 'classnames';
|
||||
import styles from './CommentDetail.css';
|
||||
import {Icon} from 'coral-ui';
|
||||
|
||||
const CommentDetail = ({icon, header, info, children, className}) => {
|
||||
return (
|
||||
<div className={cn(className, styles.root)}>
|
||||
<div className={styles.headerContainer}>
|
||||
{icon && <Icon className={styles.icon} name={icon}/>}
|
||||
<h3 className={styles.header}>{header}:</h3>
|
||||
<div className={styles.info}>{info}</div>
|
||||
</div>
|
||||
{children &&
|
||||
<div className={styles.details}>
|
||||
{children}
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
CommentDetail.propTypes = {
|
||||
className: PropTypes.string,
|
||||
header: PropTypes.node,
|
||||
icon: PropTypes.string,
|
||||
info: PropTypes.node,
|
||||
children: PropTypes.node,
|
||||
};
|
||||
|
||||
export default CommentDetail;
|
||||
@@ -23,8 +23,9 @@ class IfSlotIsEmpty extends React.Component {
|
||||
}
|
||||
|
||||
isSlotEmpty(props = this.props) {
|
||||
const {slot, className: _a, reduxState, component: _b = 'div', children: _c, ...rest} = props;
|
||||
return this.context.plugins.isSlotEmpty(slot, reduxState, rest);
|
||||
const {slot, className: _a, reduxState, component: _b = 'div', children: _c, queryData, ...rest} = props;
|
||||
const slots = Array.isArray(slot) ? slot : [slot];
|
||||
return slots.every((slot) => this.context.plugins.isSlotEmpty(slot, reduxState, rest, queryData));
|
||||
}
|
||||
|
||||
render() {
|
||||
@@ -34,7 +35,7 @@ class IfSlotIsEmpty extends React.Component {
|
||||
}
|
||||
|
||||
IfSlotIsEmpty.propTypes = {
|
||||
slot: PropTypes.string,
|
||||
slot: PropTypes.oneOfType([PropTypes.string, PropTypes.array]),
|
||||
};
|
||||
|
||||
const mapStateToProps = (state) => ({
|
||||
|
||||
@@ -23,8 +23,9 @@ class IfSlotIsNotEmpty extends React.Component {
|
||||
}
|
||||
|
||||
isSlotEmpty(props = this.props) {
|
||||
const {slot, className: _a, reduxState, component: _b = 'div', children: _c, ...rest} = props;
|
||||
return this.context.plugins.isSlotEmpty(slot, reduxState, rest);
|
||||
const {slot, className: _a, reduxState, component: _b = 'div', children: _c, queryData, ...rest} = props;
|
||||
const slots = Array.isArray(slot) ? slot : [slot];
|
||||
return slots.every((slot) => this.context.plugins.isSlotEmpty(slot, reduxState, rest, queryData));
|
||||
}
|
||||
|
||||
render() {
|
||||
@@ -34,7 +35,7 @@ class IfSlotIsNotEmpty extends React.Component {
|
||||
}
|
||||
|
||||
IfSlotIsNotEmpty.propTypes = {
|
||||
slot: PropTypes.string,
|
||||
slot: PropTypes.oneOfType([PropTypes.string, PropTypes.array]),
|
||||
};
|
||||
|
||||
const mapStateToProps = (state) => ({
|
||||
|
||||
+2
-3
@@ -1,6 +1,5 @@
|
||||
import React from 'react';
|
||||
import {shallow} from 'enzyme';
|
||||
import {expect} from 'chai';
|
||||
import Markdown from '../Markdown';
|
||||
|
||||
const render = (props) => shallow(<Markdown {...props} />);
|
||||
@@ -9,12 +8,12 @@ describe('Markdown', () => {
|
||||
it('should convert Markdown to html', () => {
|
||||
const wrapper = render({content: '*test*'});
|
||||
const html = wrapper.html();
|
||||
expect(html).to.contain('<em>');
|
||||
expect(html).toMatch('<em>');
|
||||
});
|
||||
|
||||
it('should set target="_parent" for links', () => {
|
||||
const wrapper = render({content: '[link](https://coralproject.net)'});
|
||||
const html = wrapper.html();
|
||||
expect(html).to.contain('target="_parent"');
|
||||
expect(html).toMatch('target="_parent"');
|
||||
});
|
||||
});
|
||||
@@ -1,4 +1,19 @@
|
||||
export default (condition) => (BaseComponent) => {
|
||||
BaseComponent.isExcluded = condition;
|
||||
return BaseComponent;
|
||||
};
|
||||
import React from 'react';
|
||||
import hoistStatics from 'recompose/hoistStatics';
|
||||
|
||||
/**
|
||||
* ExcludeIf provides a property `emit: (eventName, value)`
|
||||
* to the wrapped component.
|
||||
*/
|
||||
export default (condition) => hoistStatics((WrappedComponent) => {
|
||||
class ExcludeIf extends React.Component {
|
||||
render() {
|
||||
return <WrappedComponent
|
||||
{...this.props}
|
||||
/>;
|
||||
}
|
||||
}
|
||||
|
||||
WrappedComponent.isExcluded = condition;
|
||||
return ExcludeIf;
|
||||
});
|
||||
|
||||
@@ -21,7 +21,7 @@ module.exports = function(source) {
|
||||
this.cacheable();
|
||||
const config = this.exec(source, this.resourcePath);
|
||||
const plugins = getPluginList(config).map((plugin) => `{
|
||||
module: require('${plugin}/client'),
|
||||
module: require('${plugin}/client').default,
|
||||
name: '${plugin}'
|
||||
}`);
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import ApolloClient, {addTypename, IntrospectionFragmentMatcher, createNetworkInterface} from 'apollo-client';
|
||||
import ApolloClient, {IntrospectionFragmentMatcher, createNetworkInterface} from 'apollo-client';
|
||||
import {SubscriptionClient, addGraphQLSubscriptions} from 'subscriptions-transport-ws';
|
||||
import MessageTypes from 'subscriptions-transport-ws/dist/message-types';
|
||||
|
||||
@@ -64,7 +64,6 @@ export function createClient(options = {}) {
|
||||
connectToDevTools: true,
|
||||
addTypename: true,
|
||||
fragmentMatcher: new IntrospectionFragmentMatcher({introspectionQueryResultData: introspectionData}),
|
||||
queryTransformer: addTypename,
|
||||
dataIdFromObject: (result) => {
|
||||
if (result.id && result.__typename) { // eslint-disable-line no-underscore-dangle
|
||||
return `${result.__typename}_${result.id}`; // eslint-disable-line no-underscore-dangle
|
||||
|
||||
@@ -13,7 +13,7 @@ import pt_BR from '../../../locales/pt_BR.yml';
|
||||
|
||||
// Translations are happening at https://translate.lingohub.com/the-coral-project/dashboard
|
||||
|
||||
const defaultLanguage = 'en';
|
||||
const defaultLanguage = process.env.TALK_DEFAULT_LANG;
|
||||
const translations = {...en, ...es, ...fr, ...pt_BR};
|
||||
|
||||
let lang;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -1,26 +0,0 @@
|
||||
import React from 'react';
|
||||
import {shallow} from 'enzyme';
|
||||
import {expect} from 'chai';
|
||||
import CommentBox from '../CommentBox';
|
||||
|
||||
describe('CommentBox', () => {
|
||||
let comment;
|
||||
let render;
|
||||
beforeEach(() => {
|
||||
comment = {};
|
||||
const postItem = (item) => {
|
||||
comment.posted = item;
|
||||
return Promise.resolve(4);
|
||||
};
|
||||
render = shallow(<CommentBox
|
||||
postItem={postItem}
|
||||
updateItem={(e) => comment.text = e.target.value}
|
||||
item_id={'1'}
|
||||
comments={['1', '2', '3']}/>);
|
||||
});
|
||||
|
||||
it('should render the CommentBox appropriately', () => {
|
||||
expect(render.contains('<div class="CommentBox"')).to.be.true;
|
||||
expect(render.contains('<button class="postCommentButton"')).to.be.true;
|
||||
});
|
||||
});
|
||||
@@ -1,5 +1 @@
|
||||
import reducer from './reducer';
|
||||
|
||||
export default {
|
||||
reducer
|
||||
};
|
||||
export {default as reducer} from './reducer';
|
||||
|
||||
@@ -1,15 +1,14 @@
|
||||
export default {
|
||||
username: {
|
||||
offensive: 'USERNAME_OFFENSIVE',
|
||||
nolike: 'USERNAME_NOLIKE',
|
||||
impersonating: 'USERNAME_IMPERSONATING',
|
||||
spam: 'USERNAME_SPAM',
|
||||
other: 'USERNAME_OTHER'
|
||||
},
|
||||
comment: {
|
||||
offensive: 'COMMENT_OFFENSIVE',
|
||||
spam: 'COMMENT_SPAM',
|
||||
noagree: 'COMMENT_NOAGREE',
|
||||
other: 'COMMENT_OTHER'
|
||||
}
|
||||
export const username = {
|
||||
offensive: 'USERNAME_OFFENSIVE',
|
||||
nolike: 'USERNAME_NOLIKE',
|
||||
impersonating: 'USERNAME_IMPERSONATING',
|
||||
spam: 'USERNAME_SPAM',
|
||||
other: 'USERNAME_OTHER'
|
||||
};
|
||||
|
||||
export const comment = {
|
||||
offensive: 'COMMENT_OFFENSIVE',
|
||||
spam: 'COMMENT_SPAM',
|
||||
noagree: 'COMMENT_NOAGREE',
|
||||
other: 'COMMENT_OTHER'
|
||||
};
|
||||
|
||||
+14
-7
@@ -1,29 +1,36 @@
|
||||
import React from 'react';
|
||||
import {shallow} from 'enzyme';
|
||||
import {expect} from 'chai';
|
||||
import InfoBox from '../InfoBox';
|
||||
import renderer from 'react-test-renderer';
|
||||
|
||||
const render = (props) => shallow(<InfoBox {...props} />);
|
||||
|
||||
describe('InfoBox', () => {
|
||||
it('renders correctly', () => {
|
||||
const tree = renderer.create(
|
||||
<InfoBox content='test' enable/>
|
||||
).toJSON();
|
||||
expect(tree).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('should render hidden InfoBox', () => {
|
||||
const wrapper = render();
|
||||
const className = wrapper.prop('className');
|
||||
expect(className).to.include('-info');
|
||||
expect(className).to.include('hidden');
|
||||
expect(className).toMatch('-info');
|
||||
expect(className).toMatch('hidden');
|
||||
});
|
||||
|
||||
it('should render enabled InfoBox', () => {
|
||||
const wrapper = render({enable: true});
|
||||
const className = wrapper.prop('className');
|
||||
expect(className).to.include('-info');
|
||||
expect(className).to.not.include('hidden');
|
||||
expect(className).toMatch('-info');
|
||||
expect(className).not.toMatch('hidden');
|
||||
});
|
||||
|
||||
it('should render Markdown', () => {
|
||||
const wrapper = render({content: 'x'});
|
||||
const Markddown = wrapper.find('Markdown');
|
||||
expect(Markddown).to.have.length(1);
|
||||
expect(Markddown.prop('content')).to.equal('x');
|
||||
expect(Markddown).toHaveLength(1);
|
||||
expect(Markddown.prop('content')).toEqual('x');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,16 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`InfoBox renders correctly 1`] = `
|
||||
<div
|
||||
className="talk-plugin-infobox-info "
|
||||
>
|
||||
<div
|
||||
dangerouslySetInnerHTML={
|
||||
Object {
|
||||
"__html": "<p>test</p>
|
||||
",
|
||||
}
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
`;
|
||||
@@ -24,6 +24,10 @@ otherwise the application will fail to start.
|
||||
Configure the duration for which comment counts are cached for, parsed by
|
||||
[ms](https://www.npmjs.com/package/ms){:target="_blank"}. (Default `1hr`)
|
||||
|
||||
## TALK_DEFAULT_LANG
|
||||
|
||||
Specify the default translation language. (Default `en`)
|
||||
|
||||
## TALK_DEFAULT_STREAM_TAB
|
||||
|
||||
Specify the default stream tab in the admin. (Default `all`)
|
||||
@@ -439,4 +443,4 @@ Could be read as:
|
||||
again, then they must have two of their comments approved in order to get
|
||||
added back to the queue.
|
||||
- At the moment of writing, behavior is not attached to the flagging
|
||||
reliability, but it is recorded.
|
||||
reliability, but it is recorded.
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
const path = require('path');
|
||||
const {pluginsPath} = require('./plugins');
|
||||
|
||||
const buildTargets = [
|
||||
'coral-admin',
|
||||
'coral-docs'
|
||||
];
|
||||
|
||||
const buildEmbeds = [
|
||||
'stream'
|
||||
];
|
||||
|
||||
// jest.config.js
|
||||
module.exports = {
|
||||
testMatch: ['**/client/**/__tests__/**/*.js?(x)'],
|
||||
setupTestFrameworkScriptFile: '<rootDir>/test/client/setupJest.js',
|
||||
modulePaths: [
|
||||
'<rootDir>/plugins',
|
||||
'<rootDir>/client',
|
||||
...buildTargets.map((target) => path.join('<rootDir>', 'client', target, 'src')),
|
||||
...buildEmbeds.map((embed) => path.join('<rootDir>', 'client', `coral-embed-${embed}`, 'src')),
|
||||
],
|
||||
moduleFileExtensions: ['js', 'jsx', 'json', 'yaml', 'yml'],
|
||||
moduleDirectories: ['node_modules'],
|
||||
|
||||
transform: {
|
||||
'^.+\\.jsx?$': 'babel-jest',
|
||||
'\\.ya?ml$': '<rootDir>/test/client/yamlTransformer.js'
|
||||
},
|
||||
|
||||
moduleNameMapper: {
|
||||
'^plugin-api\\/(.*)$': '<rootDir>/plugin-api/$1',
|
||||
'^plugins\\/(.*)$': '<rootDir>/plugins/$1',
|
||||
'^pluginsConfig$': pluginsPath,
|
||||
|
||||
'\\.(scss|css|less)$': 'identity-obj-proxy',
|
||||
'\\.(gif|ttf|eot|svg)$': '<rootDir>/test/client/fileMock.js'
|
||||
}
|
||||
};
|
||||
@@ -262,11 +262,9 @@ en:
|
||||
ban_user: "Ban"
|
||||
billion: B
|
||||
close: Close
|
||||
dont_like_username: "Don't like username"
|
||||
empty_queue: "No more comments to moderate! You're all caught up. Go have some ☕️"
|
||||
flagged: flagged
|
||||
reported: reported
|
||||
impersonating: Impersonating
|
||||
less_detail: "Less detail"
|
||||
likes: likes
|
||||
million: M
|
||||
@@ -277,9 +275,7 @@ en:
|
||||
newest_first: "Newest First"
|
||||
navigation: Navigation
|
||||
next_comment: "Go to the next comment"
|
||||
offensive: Offensive
|
||||
oldest_first: "Oldest First"
|
||||
other: Other
|
||||
premod: pre-mod
|
||||
prev_comment: "Go to the previous comment"
|
||||
reject: "Reject"
|
||||
@@ -290,7 +286,6 @@ en:
|
||||
shortcuts: "Shortcuts"
|
||||
show_shortcuts: "Show Shortcuts"
|
||||
singleview: "Toggle single comment edit view"
|
||||
spam_ads: Spam/Ads
|
||||
thismenu: "Open this menu"
|
||||
thousand: k
|
||||
try_these: "Try these"
|
||||
|
||||
@@ -255,10 +255,8 @@ es:
|
||||
approved: "Aprobado"
|
||||
billion: B
|
||||
close: Cerrar
|
||||
dont_like_username: "No me gusta el nombre de usuario"
|
||||
empty_queue: "¡No hay más comentarios para moderar! Tiempo para un ☕️"
|
||||
flagged: Reportado
|
||||
impersonating: Impersonando
|
||||
less_detail: "Menos detalles"
|
||||
likes: likes
|
||||
million: M
|
||||
@@ -269,9 +267,7 @@ es:
|
||||
newest_first: "Primero el más nuevo"
|
||||
navigation: Navegación
|
||||
next_comment: "Ir al siguiente comentario"
|
||||
offensive: Ofensivo
|
||||
oldest_first: "Primero el más antiguo"
|
||||
other: Otro
|
||||
premod: pre-mod
|
||||
prev_comment: "Ir al comentario anterior"
|
||||
reject: "Rechazar"
|
||||
@@ -282,7 +278,6 @@ es:
|
||||
shortcuts: Atajos
|
||||
show_shortcuts: "Mostrar Atajos"
|
||||
singleview: "Colocar vista de edición de comentario único"
|
||||
spam_ads: Spam/Publicidad
|
||||
thismenu: "Abrir este menu"
|
||||
thousand: k
|
||||
try_these: "Intentar estos"
|
||||
|
||||
@@ -41,22 +41,17 @@ fr:
|
||||
banned: Banni
|
||||
banned_user: "Utilisateur banni"
|
||||
cancel: Signalé
|
||||
dont_like_username: "Je n'aime pas ce nom d'utilisateur"
|
||||
flaggedaccounts: "Noms d'utilisateurs signalés"
|
||||
flags: Signalements
|
||||
impersonating: "Cet utilisateur se fait passer pour quelqu'un d'autre"
|
||||
loading: "Chargement des résultats"
|
||||
moderator: Modérateur
|
||||
newsroom_role: "Rôle de la salle de presse"
|
||||
no_flagged_accounts: "La liste des comptes signalés est vide."
|
||||
no_results: "Aucun utilisateur n'a été trouvé avec ce nom d'utilisateur ou cette adresse de messagerie. Ils se cachent !"
|
||||
note: "Remarque: bannir cet utilisateur ne lui permettra pas de modifier les commentaires ou de supprimer quoi que ce soit."
|
||||
offensive: "Ce commentaire est offensant"
|
||||
other: Autre
|
||||
people: Gens
|
||||
role: "Sélectionnez le rôle ..."
|
||||
select_status: "Sélectionnez l'état ..."
|
||||
spam_ads: "Spam / Annonces"
|
||||
staff: "Équipe"
|
||||
status: Statut
|
||||
username_and_email: "Nom d'utilisateur et e-mail"
|
||||
|
||||
@@ -51,7 +51,6 @@ pt_BR:
|
||||
banned: Proibida
|
||||
banned_user: "Usuário proibido"
|
||||
cancel: Cancelar
|
||||
dont_like_username: "Não gostei do nome de usuário"
|
||||
flaggedaccounts: "Nomes de usuários marcados"
|
||||
flags: Marcadas
|
||||
impersonating: "Representação"
|
||||
@@ -60,12 +59,9 @@ pt_BR:
|
||||
newsroom_role: "Papel de empresa"
|
||||
no_flagged_accounts: "A fila de nomes de usuários marcados está atualmente vazia."
|
||||
no_results: "Nenhum usuário encontrado com esse nome de usuário ou e-mail. Eles estão se escondendo!"
|
||||
offensive: "Ofensiva"
|
||||
other: Outra
|
||||
people: Pessoas
|
||||
role: "Selecione um papel..."
|
||||
select_status: "Selecione um status..."
|
||||
spam_ads: "Spam/anúncios"
|
||||
staff: "Funcionários"
|
||||
status: Status
|
||||
username_and_email: "Nome de usuário e email"
|
||||
|
||||
+12
-4
@@ -14,7 +14,8 @@
|
||||
"build-watch": "WEBPACK=TRUE NODE_ENV=development webpack --progress --config webpack.config.js --watch",
|
||||
"lint": "eslint --ext=.js --ext=.json bin/* .",
|
||||
"lint-fix": "yarn lint --fix",
|
||||
"test": "TEST_MODE=unit NODE_ENV=test mocha -R ${MOCHA_REPORTER:-spec}",
|
||||
"jest-watch": "TEST_MODE=unit NODE_ENV=test jest --watch",
|
||||
"test": "TEST_MODE=unit NODE_ENV=test jest && TEST_MODE=unit NODE_ENV=test mocha -R ${MOCHA_REPORTER:-spec}",
|
||||
"test-cover": "TEST_MODE=unit NODE_ENV=test istanbul cover _mocha --report text --check-coverage -- -R spec",
|
||||
"heroku-postbuild": "./bin/cli plugins reconcile && yarn build",
|
||||
"generate-introspection": "WEBPACK=TRUE NODE_ENV=test ./scripts/generateIntrospectionResult.js"
|
||||
@@ -63,7 +64,6 @@
|
||||
"babel-core": "^6.26.0",
|
||||
"babel-eslint": "^7.2.1",
|
||||
"babel-loader": "^7.1.2",
|
||||
"babel-plugin-add-module-exports": "^0.2.1",
|
||||
"babel-plugin-syntax-dynamic-import": "^6.18.0",
|
||||
"babel-plugin-transform-async-to-generator": "^6.16.0",
|
||||
"babel-plugin-transform-class-properties": "^6.23.0",
|
||||
@@ -160,6 +160,7 @@
|
||||
"react-redux": "^4.4.5",
|
||||
"react-router": "^3.0.0",
|
||||
"react-tagsinput": "^3.17.0",
|
||||
"react-test-renderer": "15.5",
|
||||
"react-toastify": "^1.5.0",
|
||||
"react-transition-group": "^1.1.3",
|
||||
"recompose": "^0.23.1",
|
||||
@@ -180,17 +181,24 @@
|
||||
"url-search-params": "^0.9.0",
|
||||
"uuid": "^3.1.0",
|
||||
"webpack": "^2.3.1",
|
||||
"webpack-sources": "^1.0.1",
|
||||
"yaml-loader": "^0.4.0",
|
||||
"yamljs": "^0.2.10"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@coralproject/eslint-config-talk": "^0.0.3",
|
||||
"@coralproject/eslint-config-talk": "^0.0.4",
|
||||
"babel-jest": "^21.2.0",
|
||||
"babel-plugin-dynamic-import-node": "^1.1.0",
|
||||
"babel-plugin-transform-es2015-modules-commonjs": "^6.26.0",
|
||||
"chai": "^3.5.0",
|
||||
"chai-as-promised": "^6.0.0",
|
||||
"chai-http": "^3.0.0",
|
||||
"enzyme": "^2.9.1",
|
||||
"enzyme": "^3.0.0",
|
||||
"enzyme-adapter-react-15": "^1.0.0",
|
||||
"eslint": "^4.5.0",
|
||||
"eslint-plugin-mocha": "^4.11.0",
|
||||
"identity-obj-proxy": "^3.0.0",
|
||||
"jest": "^21.2.1",
|
||||
"mocha": "^3.1.2",
|
||||
"mocha-junit-reporter": "^1.12.1",
|
||||
"nodemon": "^1.11.0",
|
||||
|
||||
@@ -1,14 +0,0 @@
|
||||
{
|
||||
"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 @@
|
||||
export {viewUserDetail} from 'coral-admin/src/actions/userDetail';
|
||||
@@ -4,4 +4,5 @@ export {default as IfSlotIsEmpty} from 'coral-framework/components/IfSlotIsEmpty
|
||||
export {default as IfSlotIsNotEmpty} from 'coral-framework/components/IfSlotIsNotEmpty';
|
||||
export {default as CommentAuthorName} from 'coral-framework/components/CommentAuthorName';
|
||||
export {default as CommentTimestamp} from 'coral-framework/components/CommentTimestamp';
|
||||
export {default as CommentDetail} from 'coral-framework/components/CommentDetail';
|
||||
export {default as CommentContent} from 'coral-framework/components/CommentContent';
|
||||
|
||||
@@ -21,6 +21,7 @@
|
||||
"talk-plugin-author-menu",
|
||||
"talk-plugin-member-since",
|
||||
"talk-plugin-ignore-user",
|
||||
"talk-plugin-moderation-actions"
|
||||
"talk-plugin-moderation-actions",
|
||||
"talk-plugin-flag-details"
|
||||
]
|
||||
}
|
||||
|
||||
@@ -1,14 +0,0 @@
|
||||
{
|
||||
"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"
|
||||
]
|
||||
}
|
||||
@@ -1,14 +0,0 @@
|
||||
{
|
||||
"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"
|
||||
]
|
||||
}
|
||||
@@ -1,14 +0,0 @@
|
||||
{
|
||||
"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"
|
||||
]
|
||||
}
|
||||
@@ -1,14 +0,0 @@
|
||||
{
|
||||
"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"
|
||||
]
|
||||
}
|
||||
@@ -1,14 +0,0 @@
|
||||
{
|
||||
"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"
|
||||
]
|
||||
}
|
||||
@@ -3,7 +3,7 @@ import cn from 'classnames';
|
||||
import styles from './ModTag.css';
|
||||
import {t} from 'plugin-api/beta/client/services';
|
||||
import {Icon} from 'plugin-api/beta/client/components/ui';
|
||||
import * as notification from 'coral-admin/src/services/notification';
|
||||
import {getErrorMessages} from 'plugin-api/beta/client/utils';
|
||||
|
||||
export default class ModTag extends React.Component {
|
||||
constructor() {
|
||||
@@ -32,10 +32,10 @@ export default class ModTag extends React.Component {
|
||||
postTag = async () => {
|
||||
try {
|
||||
await this.props.postTag();
|
||||
notification.success(t('talk-plugin-featured-comments.notify_self_featured', this.props.comment.user.username));
|
||||
this.props.notify('success', t('talk-plugin-featured-comments.notify_self_featured', this.props.comment.user.username));
|
||||
}
|
||||
catch(err) {
|
||||
notification.showMutationErrors(err);
|
||||
this.props.notify('error', getErrorMessages(err));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import React from 'react';
|
||||
import Comment from '../containers/Comment';
|
||||
import LoadMore from './LoadMore';
|
||||
import {forEachError} from 'plugin-api/beta/client/utils';
|
||||
import {getErrorMessages} from 'plugin-api/beta/client/utils';
|
||||
|
||||
class TabPane extends React.Component {
|
||||
state = {
|
||||
@@ -16,7 +16,7 @@ class TabPane extends React.Component {
|
||||
})
|
||||
.catch((error) => {
|
||||
this.setState({loadingState: 'error'});
|
||||
forEachError(error, ({msg}) => {this.props.addNotification('error', msg);});
|
||||
this.props.notify('error', getErrorMessages(error));
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,13 @@
|
||||
import ModTag from '../components/ModTag';
|
||||
import {withTags} from 'plugin-api/beta/client/hocs';
|
||||
import {gql} from 'react-apollo';
|
||||
import {withTags, connect} from 'plugin-api/beta/client/hocs';
|
||||
import {gql, compose} from 'react-apollo';
|
||||
import {bindActionCreators} from 'redux';
|
||||
import {notify} from 'plugin-api/beta/client/actions/notification';
|
||||
|
||||
const mapDispatchToProps = (dispatch) =>
|
||||
bindActionCreators({
|
||||
notify,
|
||||
}, dispatch);
|
||||
|
||||
const fragments = {
|
||||
comment: gql`
|
||||
@@ -11,6 +18,10 @@ const fragments = {
|
||||
}
|
||||
`
|
||||
};
|
||||
const enhance = compose(
|
||||
withTags('featured', {fragments}),
|
||||
connect(null, mapDispatchToProps),
|
||||
);
|
||||
|
||||
export default withTags('featured', {fragments})(ModTag);
|
||||
export default enhance(ModTag);
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ import {compose, gql} from 'react-apollo';
|
||||
import TabPane from '../components/TabPane';
|
||||
import {withFragments, connect} from 'plugin-api/beta/client/hocs';
|
||||
import Comment from '../containers/Comment';
|
||||
import {addNotification} from 'plugin-api/beta/client/actions/notification';
|
||||
import {notify} from 'plugin-api/beta/client/actions/notification';
|
||||
import {viewComment} from 'coral-embed-stream/src/actions/stream';
|
||||
import {appendNewNodes, getDefinitionName} from 'plugin-api/beta/client/utils';
|
||||
import update from 'immutability-helper';
|
||||
@@ -81,7 +81,7 @@ const LOAD_MORE_QUERY = gql`
|
||||
const mapDispatchToProps = (dispatch) =>
|
||||
bindActionCreators({
|
||||
viewComment,
|
||||
addNotification,
|
||||
notify,
|
||||
}, dispatch);
|
||||
|
||||
const enhance = compose(
|
||||
|
||||
@@ -87,7 +87,7 @@ module.exports = {
|
||||
name: 'FEATURED',
|
||||
permissions: {
|
||||
public: true,
|
||||
self: true,
|
||||
self: false,
|
||||
roles: []
|
||||
},
|
||||
models: ['COMMENTS'],
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"extends": "@coralproject/eslint-config-talk"
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"extends": "@coralproject/eslint-config-talk/client"
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
|
||||
.info {
|
||||
vertical-align: middle;
|
||||
list-style: none;
|
||||
display: inline-block;
|
||||
padding: 0;
|
||||
font-size: 12px;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.detail {
|
||||
margin: 0;
|
||||
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,72 @@
|
||||
import React, {Component} from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import styles from './FlagDetails.css';
|
||||
import {t} from 'plugin-api/beta/client/services';
|
||||
import {Slot, IfSlotIsNotEmpty, CommentDetail} from 'plugin-api/beta/client/components';
|
||||
|
||||
class FlagDetails extends Component {
|
||||
|
||||
render() {
|
||||
const {comment: {actions}, more, data, root, comment} = 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, actions: []};
|
||||
}
|
||||
sum[action.reason].count++;
|
||||
if (action.user) {
|
||||
sum[action.reason].userFlagged = true;
|
||||
}
|
||||
return sum;
|
||||
}, {});
|
||||
|
||||
const reasons = Object.keys(summaries);
|
||||
const queryData = {
|
||||
root,
|
||||
comment,
|
||||
};
|
||||
|
||||
return (
|
||||
<CommentDetail
|
||||
icon={'flag'}
|
||||
header={`${t('talk-plugin-flag-details.flags')} (${Object.keys(summaries).length})`}
|
||||
info={
|
||||
<ul className={styles.info}>
|
||||
{reasons.map((reason) =>
|
||||
<li key={reason} className={styles.lessDetail}>
|
||||
{reason} {summaries[reason].userFlagged && `(${summaries[reason].count})`}
|
||||
</li>
|
||||
)}
|
||||
</ul>
|
||||
}>
|
||||
{more && (
|
||||
<IfSlotIsNotEmpty
|
||||
slot="adminCommentMoreFlagDetails"
|
||||
queryData={queryData}
|
||||
>
|
||||
<Slot
|
||||
fill="adminCommentMoreFlagDetails"
|
||||
data={data}
|
||||
queryData={queryData}
|
||||
/>
|
||||
</IfSlotIsNotEmpty>
|
||||
)}
|
||||
</CommentDetail>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
FlagDetails.propTypes = {
|
||||
more: PropTypes.bool,
|
||||
data: PropTypes.object,
|
||||
root: PropTypes.object,
|
||||
comment: PropTypes.shape({
|
||||
actions: PropTypes.arrayOf(PropTypes.shape({
|
||||
message: PropTypes.string,
|
||||
user: PropTypes.shape({username: PropTypes.string})
|
||||
})).isRequired,
|
||||
}).isRequired,
|
||||
};
|
||||
|
||||
export default FlagDetails;
|
||||
@@ -0,0 +1,45 @@
|
||||
|
||||
.info {
|
||||
vertical-align: middle;
|
||||
list-style: none;
|
||||
display: inline-block;
|
||||
padding: 0;
|
||||
font-size: 12px;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.detail {
|
||||
margin: 0;
|
||||
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,59 @@
|
||||
import React, {Component} from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import styles from './UserFlagDetails.css';
|
||||
|
||||
class UserFlagDetails extends Component {
|
||||
|
||||
render() {
|
||||
const {comment: {actions}, viewUserDetail} = this.props;
|
||||
|
||||
const flagActions = actions && actions.filter((a) => a.__typename === 'FlagAction');
|
||||
const summaries = flagActions.reduce((sum, action) => {
|
||||
if (!action.user) {
|
||||
return sum;
|
||||
}
|
||||
if (!(action.reason in sum)) {
|
||||
sum[action.reason] = {count: 0, actions: []};
|
||||
}
|
||||
sum[action.reason].count++;
|
||||
sum[action.reason].actions.push(action);
|
||||
return sum;
|
||||
}, {});
|
||||
|
||||
return (
|
||||
<ul className={styles.detail}>
|
||||
{Object.keys(summaries)
|
||||
.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>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
UserFlagDetails.propTypes = {
|
||||
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 UserFlagDetails;
|
||||
@@ -0,0 +1,39 @@
|
||||
import {compose, gql} from 'react-apollo';
|
||||
import FlagDetails from '../components/FlagDetails';
|
||||
import {withFragments, excludeIf} from 'plugin-api/beta/client/hocs';
|
||||
import {getSlotFragmentSpreads} from 'plugin-api/beta/client/utils';
|
||||
|
||||
const slots = [
|
||||
'adminCommentMoreFlagDetails',
|
||||
];
|
||||
|
||||
const enhance = compose(
|
||||
withFragments({
|
||||
root: gql`
|
||||
fragment CoralAdmin_FlagDetails_root on RootQuery {
|
||||
__typename
|
||||
${getSlotFragmentSpreads(slots, 'root')}
|
||||
}
|
||||
`,
|
||||
comment: gql`
|
||||
fragment CoralAdmin_FlagDetails_comment on Comment {
|
||||
actions {
|
||||
__typename
|
||||
... on FlagAction {
|
||||
id
|
||||
reason
|
||||
message
|
||||
user {
|
||||
id
|
||||
username
|
||||
}
|
||||
}
|
||||
}
|
||||
${getSlotFragmentSpreads(slots, 'comment')}
|
||||
}
|
||||
`
|
||||
}),
|
||||
excludeIf(({comment: {actions}}) => !actions.some((action) => action.__typename === 'FlagAction')),
|
||||
);
|
||||
|
||||
export default enhance(FlagDetails);
|
||||
@@ -0,0 +1,39 @@
|
||||
import {compose, gql} from 'react-apollo';
|
||||
import UserFlagDetails from '../components/UserFlagDetails';
|
||||
import {bindActionCreators} from 'redux';
|
||||
import {withFragments, excludeIf} from 'plugin-api/beta/client/hocs';
|
||||
import {viewUserDetail} from 'plugin-api/beta/client/actions/admin';
|
||||
import {connect} from 'react-redux';
|
||||
|
||||
const mapDispatchToProps = (dispatch) => ({
|
||||
...bindActionCreators({
|
||||
viewUserDetail,
|
||||
}, dispatch)
|
||||
});
|
||||
|
||||
const enhance = compose(
|
||||
connect(null, mapDispatchToProps),
|
||||
withFragments({
|
||||
comment: gql`
|
||||
fragment CoralAdmin_UserFlagDetails_comment on Comment {
|
||||
actions {
|
||||
__typename
|
||||
... on FlagAction {
|
||||
id
|
||||
reason
|
||||
message
|
||||
user {
|
||||
id
|
||||
username
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`
|
||||
}),
|
||||
excludeIf(({comment: {actions}}) =>
|
||||
!actions.some((action) => action.__typename === 'FlagAction' && action.user
|
||||
)),
|
||||
);
|
||||
|
||||
export default enhance(UserFlagDetails);
|
||||
@@ -0,0 +1,11 @@
|
||||
import FlagDetails from './containers/FlagDetails';
|
||||
import UserFlagDetails from './containers/UserFlagDetails';
|
||||
import translations from './translations.yml';
|
||||
|
||||
export default {
|
||||
translations,
|
||||
slots: {
|
||||
adminCommentDetailArea: [FlagDetails],
|
||||
adminCommentMoreFlagDetails: [UserFlagDetails],
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,6 @@
|
||||
en:
|
||||
talk-plugin-flag-details:
|
||||
flags: Flags
|
||||
es:
|
||||
talk-plugin-flag-details:
|
||||
flags: Reportes
|
||||
@@ -0,0 +1 @@
|
||||
module.exports = {};
|
||||
@@ -1,14 +0,0 @@
|
||||
{
|
||||
"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"
|
||||
]
|
||||
}
|
||||
@@ -1,14 +0,0 @@
|
||||
{
|
||||
"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"
|
||||
]
|
||||
}
|
||||
@@ -1,14 +0,0 @@
|
||||
{
|
||||
"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"
|
||||
]
|
||||
}
|
||||
@@ -1,14 +0,0 @@
|
||||
{
|
||||
"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"
|
||||
]
|
||||
}
|
||||
@@ -1,14 +0,0 @@
|
||||
{
|
||||
"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"
|
||||
]
|
||||
}
|
||||
@@ -1,14 +0,0 @@
|
||||
{
|
||||
"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"
|
||||
]
|
||||
}
|
||||
@@ -1,14 +0,0 @@
|
||||
{
|
||||
"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"
|
||||
]
|
||||
}
|
||||
@@ -1,14 +0,0 @@
|
||||
{
|
||||
"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"
|
||||
]
|
||||
}
|
||||
@@ -1,14 +0,0 @@
|
||||
{
|
||||
"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"
|
||||
]
|
||||
}
|
||||
@@ -1,14 +0,0 @@
|
||||
{
|
||||
"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"
|
||||
]
|
||||
}
|
||||
@@ -1,14 +0,0 @@
|
||||
{
|
||||
"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"
|
||||
]
|
||||
}
|
||||
@@ -1,14 +0,0 @@
|
||||
{
|
||||
"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"
|
||||
]
|
||||
}
|
||||
@@ -1,14 +0,0 @@
|
||||
{
|
||||
"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"
|
||||
]
|
||||
}
|
||||
@@ -1,14 +0,0 @@
|
||||
{
|
||||
"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"
|
||||
]
|
||||
}
|
||||
@@ -1,14 +0,0 @@
|
||||
{
|
||||
"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"
|
||||
]
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user