Merge branch 'master' into talk-product-guide

This commit is contained in:
Kim Gardner
2017-10-05 13:53:44 +01:00
committed by GitHub
130 changed files with 2137 additions and 1139 deletions
+10 -4
View File
@@ -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"]
]
}
}
}
+2
View File
@@ -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
+2
View File
@@ -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/*
-14
View File
@@ -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;
+18 -5
View File
@@ -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 */
+92 -95
View File
@@ -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) => ({
@@ -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"');
});
});
+19 -4
View File
@@ -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 -2
View File
@@ -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
+1 -1
View File
@@ -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;
-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;
@@ -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
View File
@@ -1,5 +1 @@
import reducer from './reducer';
export default {
reducer
};
export {default as reducer} from './reducer';
+13 -14
View File
@@ -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'
};
@@ -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>
`;
+5 -1
View File
@@ -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.
+39
View File
@@ -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'
}
};
-5
View File
@@ -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"
-5
View File
@@ -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"
-5
View File
@@ -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"
-4
View File
@@ -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
View File
@@ -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",
-14
View File
@@ -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
View File
@@ -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';
+2 -1
View File
@@ -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"
]
}
-14
View File
@@ -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"
]
}
-14
View File
@@ -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"
]
}
-14
View File
@@ -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