mirror of
https://github.com/wassname/talk.git
synced 2026-09-14 11:36:51 +08:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2d43eef508 | ||
|
|
fa22e45a4d | ||
|
|
abdf5335da | ||
|
|
c24454e234 | ||
|
|
97e2e61a4b | ||
|
|
57ced3c91f | ||
|
|
c664438ade | ||
|
|
a5b5043a0d | ||
|
|
03a0d904d2 | ||
|
|
315d3babde | ||
|
|
4d6055b9b8 | ||
|
|
77c8d966ab | ||
|
|
cabadf467f | ||
|
|
0807acc98f | ||
|
|
3c5b56c599 | ||
|
|
899bba7327 | ||
|
|
41dd3eb180 | ||
|
|
81d90cb138 | ||
|
|
2882dfbadb | ||
|
|
f2b9db9dbc | ||
|
|
c563a13e7c | ||
|
|
5976595871 | ||
|
|
8a4a7d3589 | ||
|
|
5073e60df5 | ||
|
|
5a71db2568 | ||
|
|
ac0acea22b | ||
|
|
6198c9e730 | ||
|
|
9ac06281e9 | ||
|
|
78e5cb7aa6 | ||
|
|
882bf57bbc | ||
|
|
789fb3dfce | ||
|
|
819b1c597c | ||
|
|
38148ad51c | ||
|
|
8f6ff46244 | ||
|
|
4727c52de4 | ||
|
|
f461d27810 | ||
|
|
fbc5a9d74b | ||
|
|
6ff4994d0d | ||
|
|
c793a840ea | ||
|
|
ebda51dc0f | ||
|
|
b0573749ee | ||
|
|
933903de2f | ||
|
|
4373c382e1 | ||
|
|
af6787bc0f | ||
|
|
dd1fbf30ca | ||
|
|
a5aa59e255 | ||
|
|
19b8d0a6ea | ||
|
|
bcf8370359 | ||
|
|
0da0b1a37e | ||
|
|
31ee8139b6 | ||
|
|
51977dcf54 | ||
|
|
0b18bb06e7 | ||
|
|
9abb3d30da | ||
|
|
f0839c57df | ||
|
|
b2929e9a83 | ||
|
|
ca7b790962 | ||
|
|
42a477ae41 | ||
|
|
53461df287 | ||
|
|
9b18a78ce0 | ||
|
|
da35f39baf | ||
|
|
e885178dfc | ||
|
|
24e8aab0fe | ||
|
|
44d6ad340d | ||
|
|
63b6315435 | ||
|
|
95ca43ff8c | ||
|
|
55e4aed511 | ||
|
|
6d5adc8f54 | ||
|
|
247e05383e | ||
|
|
3988fb9e93 | ||
|
|
7d524efaad | ||
|
|
d7a88cdc2a | ||
|
|
18f5b0cc5e | ||
|
|
2df18f07cb | ||
|
|
786e85686d | ||
|
|
9a651880a2 | ||
|
|
82e01eac00 | ||
|
|
5efdc9865f | ||
|
|
62d01cf2a0 | ||
|
|
81bf63836a | ||
|
|
54ac2985ba | ||
|
|
e9bd5703f1 | ||
|
|
a394ba29b3 | ||
|
|
a2470fa01b | ||
|
|
9b98e3cac9 | ||
|
|
4f89c1e1c3 | ||
|
|
c3e551deb5 | ||
|
|
0678ea0344 | ||
|
|
b09b56bd24 | ||
|
|
2a0f70f084 | ||
|
|
bf2e11ef9a | ||
|
|
0fc810a7b1 | ||
|
|
918ad915b8 | ||
|
|
d51b6b31f6 | ||
|
|
e88445beac | ||
|
|
33be7531a0 | ||
|
|
2131638f48 | ||
|
|
39a980b22f | ||
|
|
ce0b63fe75 | ||
|
|
c0cb641df3 | ||
|
|
cee68ae471 | ||
|
|
33cd783b37 | ||
|
|
82ef16a4ee | ||
|
|
736ab6066f | ||
|
|
3327604a58 | ||
|
|
0c3cfd21a9 | ||
|
|
903e70d641 | ||
|
|
21f6caa942 | ||
|
|
f70ccf641a | ||
|
|
dd4e3d7ec8 | ||
|
|
886a468993 | ||
|
|
de0dad6243 | ||
|
|
7226c475e3 | ||
|
|
173c9262d1 | ||
|
|
5e7ee344ec | ||
|
|
b2d0c58a72 | ||
|
|
b34b033a18 | ||
|
|
4195d8b419 | ||
|
|
bfb2da519f | ||
|
|
8821c0aa5c | ||
|
|
a56031ac47 | ||
|
|
a3eeec34df | ||
|
|
c4ad667788 | ||
|
|
d20666adb9 | ||
|
|
96b44b2b99 | ||
|
|
b92b4027b0 | ||
|
|
f1423abdea | ||
|
|
c31e6347d0 | ||
|
|
48179b413c | ||
|
|
c955fcd29e | ||
|
|
457322e3d0 | ||
|
|
9ba8cbcbe1 | ||
|
|
93363a0650 | ||
|
|
7f442ce91e | ||
|
|
42f0d76d98 | ||
|
|
1578316363 | ||
|
|
5c033dbfdf | ||
|
|
3cdf2a3118 | ||
|
|
4e5e989d22 | ||
|
|
62660ed144 | ||
|
|
2d44f7abf4 | ||
|
|
ea98f0275d | ||
|
|
07e6c15c3a | ||
|
|
05e91129f4 | ||
|
|
350e679abf | ||
|
|
ae07dfb2cd | ||
|
|
88d012755c | ||
|
|
0cdefa0066 | ||
|
|
e4688f9876 | ||
|
|
e091fa552a | ||
|
|
53b1104cf8 | ||
|
|
975fe550e0 | ||
|
|
8adff14263 | ||
|
|
5f07d0256c | ||
|
|
f573504a3c | ||
|
|
8c0118106b | ||
|
|
4ce720e510 | ||
|
|
81c669d63f | ||
|
|
97a8f95b93 | ||
|
|
7bf114c01a | ||
|
|
6460cbd536 | ||
|
|
05b326b91c | ||
|
|
9af206da1a | ||
|
|
0c3a9db443 | ||
|
|
511f4f0975 | ||
|
|
95545a0e7d | ||
|
|
12a71fa117 | ||
|
|
302d30cde1 | ||
|
|
3da4a70442 | ||
|
|
6d7afc7bbc | ||
|
|
b6786f614e | ||
|
|
a6a903c459 | ||
|
|
34b58779e8 | ||
|
|
82a332c787 | ||
|
|
bfe82218ed | ||
|
|
5f5c64870a | ||
|
|
ad4a5595f9 | ||
|
|
b7137fbcd4 | ||
|
|
b84daf0422 | ||
|
|
4089d09681 | ||
|
|
a7eb588f7f | ||
|
|
cd92dbf682 | ||
|
|
89ab71eb1d | ||
|
|
c1cfe3b200 | ||
|
|
d7ed679384 | ||
|
|
59acdc5c02 | ||
|
|
b06ae9001e | ||
|
|
1f170c1dca | ||
|
|
9217239caf | ||
|
|
fac028c22b |
+11
-4
@@ -1,11 +1,18 @@
|
||||
FROM coralproject/talk:latest
|
||||
|
||||
# Setup the build arguments
|
||||
ONBUILD ARG TALK_THREADING_LEVEL=3
|
||||
ONBUILD ARG TALK_DEFAULT_STREAM_TAB=all
|
||||
ONBUILD ARG TALK_DEFAULT_LANG=en
|
||||
ONBUILD ARG TALK_PLUGINS_JSON
|
||||
|
||||
# Bundle app source
|
||||
ONBUILD COPY . /usr/src/app
|
||||
|
||||
# At this stage, we need to install the development dependancies again because
|
||||
# we need to have webpack available. We then build the new dependancies and
|
||||
# clear out the development dependancies again. After this we of course need to
|
||||
# At this stage, we need to install the development dependencies again because
|
||||
# we need to have webpack available. We then build the new dependencies and
|
||||
# clear out the development dependencies again. After this we of course need to
|
||||
# clear out the yarn cache, this saves quite a lot of size.
|
||||
ONBUILD RUN cli plugins reconcile && \
|
||||
yarn build
|
||||
yarn build && \
|
||||
yarn cache clean
|
||||
@@ -4,8 +4,12 @@ Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
Unless required by applicable law or agreed to in writing,
|
||||
software distributed under the License is distributed on an
|
||||
"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND,
|
||||
either express or implied.
|
||||
|
||||
See the License for the specific language governing permissions and limitations under the License.
|
||||
See the License for the specific language governing permissions
|
||||
and limitations under the License.
|
||||
|
||||
@@ -1,46 +1,37 @@
|
||||
# Talk
|
||||
# Talk · [](https://circleci.com/gh/coralproject/talk) · [](https://nodesecurity.io/orgs/coralproject/projects/07ce2e4c-99fb-48f8-b50b-69d2d2c081b8) · [](CONTRIBUTING.md#pull-requests)
|
||||
|
||||
[](https://circleci.com/gh/coralproject/talk)
|
||||
[](https://dashboard.heroku.com/new?template=https%3A%2F%2Fgithub.com%2Fcoralproject%2Ftalk&env[TALK_FACEBOOK_APP_ID]=ignore&env[TALK_FACEBOOK_APP_SECRET]=ignore)
|
||||
[](https://nodesecurity.io/orgs/coralproject/projects/07ce2e4c-99fb-48f8-b50b-69d2d2c081b8)
|
||||
|
||||
Online comments are broken. Our open-source Talk tool rethinks how moderation, comment display, and conversation function, creating the opportunity for safer, smarter discussions around your work. [Read more about Talk here](https://coralproject.net/products/talk.html).
|
||||
Online comments are broken. Our open-source commenting platform, Talk, rethinks how moderation, comment display, and conversation function, creating the opportunity for safer, smarter discussions around your work. [Read more about Talk here](https://coralproject.net/products/talk.html).
|
||||
|
||||
Built with <3 by The Coral Project & Mozilla.
|
||||
|
||||
## Getting Started
|
||||
## Try Talk!
|
||||
|
||||
Check out our Docs: https://coralproject.github.io/talk/
|
||||
You're just one click away from trying Talk - all you need is a Heroku account and a few minutes of your time.
|
||||
|
||||
[](https://dashboard.heroku.com/new?template=https%3A%2F%2Fgithub.com%2Fcoralproject%2Ftalk&env[TALK_FACEBOOK_APP_ID]=ignore&env[TALK_FACEBOOK_APP_SECRET]=ignore)
|
||||
|
||||
## Technical Documentation
|
||||
|
||||
From getting up and running, to advanced configuration, to how to scale Talk, our [Talk Technical Docs](https://coralproject.github.io/talk/) have everything you need to know.
|
||||
|
||||
## Product Guide
|
||||
|
||||
Learn more about Talk, including a deep dive into features for commenters and moderators, and FAQs in our [Talk Product Guide](https://coralproject.github.io/talk/how-talk-works).
|
||||
|
||||
## Relevant Links
|
||||
|
||||
- Blog: https://blog.coralproject.net/
|
||||
- Community Forums: https://community.coralproject.net/
|
||||
- Community Guides for Journalism: https://guides.coralproject.net/
|
||||
- Project: https://coralproject.net/
|
||||
- Roadmap: https://www.pivotaltracker.com/n/projects/1863625
|
||||
- [Our Blog](https://blog.coralproject.net/)
|
||||
- [Community Forums](https://community.coralproject.net/)
|
||||
- [Community Guides for Journalism](https://guides.coralproject.net/)
|
||||
- [More About Us](https://coralproject.net/)
|
||||
- [Talk Roadmap](https://www.pivotaltracker.com/n/projects/1863625)
|
||||
|
||||
## End-to-End Testing
|
||||
|
||||
Talk uses [Nightwatch](http://nightwatchjs.org/) to write e2e tests. The testing infrastructure that allows us to run our tests in real browsers is provided with love by our friends at Browserstack.
|
||||
Talk uses [Nightwatch](https://nightwatchjs.org/) as our e2e testing framework. The testing infrastructure that allows us to run our tests in real browsers is provided with love by our friends at [Browserstack](https://browserstack.com).
|
||||
|
||||
|
||||

|
||||
[](https://browserstack.com)
|
||||
|
||||
## License
|
||||
|
||||
Copyright 2017 Mozilla Foundation
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing,
|
||||
software distributed under the License is distributed on an
|
||||
"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND,
|
||||
either express or implied.
|
||||
|
||||
See the License for the specific language governing permissions
|
||||
and limitations under the License.
|
||||
Talk is released under the [Apache License, v2.0](/LICENSE).
|
||||
|
||||
+1
-1
@@ -50,7 +50,7 @@ test:
|
||||
- MOCHA_FILE=$CIRCLE_TEST_REPORTS/junit/test-results.xml MOCHA_REPORTER=mocha-junit-reporter yarn test
|
||||
# Check dependancies using nsp.
|
||||
- nsp check
|
||||
# - yarn e2e-ci
|
||||
- yarn e2e-ci
|
||||
|
||||
deployment:
|
||||
release:
|
||||
|
||||
@@ -32,8 +32,9 @@ class ActionsMenu extends React.Component {
|
||||
};
|
||||
|
||||
render() {
|
||||
const {className = ''} = this.props;
|
||||
return (
|
||||
<div className={styles.root} onBlur={this.syncOpenState} >
|
||||
<div className={cn(styles.root, className)} onBlur={this.syncOpenState} >
|
||||
<Button
|
||||
cStyle='actions'
|
||||
className={cn(styles.button, {[styles.buttonOpen]: this.state.open})}
|
||||
@@ -59,6 +60,7 @@ class ActionsMenu extends React.Component {
|
||||
ActionsMenu.propTypes = {
|
||||
icon: PropTypes.string,
|
||||
children: PropTypes.node,
|
||||
className: PropTypes.string,
|
||||
};
|
||||
|
||||
export default ActionsMenu;
|
||||
|
||||
@@ -3,12 +3,14 @@ import cn from 'classnames';
|
||||
import {MenuItem} from 'react-mdl';
|
||||
import PropTypes from 'prop-types';
|
||||
import styles from './ActionsMenu.css';
|
||||
import camelCase from 'lodash/camelCase';
|
||||
|
||||
const ActionsMenuItem = (props) =>
|
||||
<MenuItem className={cn(styles.menuItem, props.className)} {...props} />;
|
||||
|
||||
<MenuItem className={cn(styles.menuItem, props.className, 'action-menu-item')} {...props} id={camelCase(props.children)}/>;
|
||||
|
||||
ActionsMenuItem.propTypes = {
|
||||
className: PropTypes.string,
|
||||
children: PropTypes.string,
|
||||
};
|
||||
|
||||
export default ActionsMenuItem;
|
||||
|
||||
@@ -7,11 +7,11 @@ import {Icon} from 'coral-ui';
|
||||
|
||||
import t from 'coral-framework/services/i18n';
|
||||
|
||||
const ApproveButton = ({active, minimal, onClick}) => {
|
||||
const ApproveButton = ({active, minimal, onClick, className}) => {
|
||||
const text = active ? t('modqueue.approved') : t('modqueue.approve');
|
||||
return (
|
||||
<button
|
||||
className={cn(styles.root, {[styles.minimal]: minimal, [styles.active]: active})}
|
||||
className={cn(styles.root, {[styles.minimal]: minimal, [styles.active]: active}, className)}
|
||||
onClick={onClick}
|
||||
>
|
||||
<Icon name={'done'} className={styles.icon} />
|
||||
@@ -21,6 +21,7 @@ const ApproveButton = ({active, minimal, onClick}) => {
|
||||
};
|
||||
|
||||
ApproveButton.propTypes = {
|
||||
className: PropTypes.string,
|
||||
active: PropTypes.bool,
|
||||
minimal: PropTypes.bool,
|
||||
onClick: PropTypes.func,
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import React from 'react';
|
||||
import cn from 'classnames';
|
||||
import PropTypes from 'prop-types';
|
||||
import {Dialog} from 'coral-ui';
|
||||
import styles from './BanUserDialog.css';
|
||||
@@ -8,28 +9,34 @@ import t from 'coral-framework/services/i18n';
|
||||
|
||||
const BanUserDialog = ({open, onCancel, onPerform, username, info}) => (
|
||||
<Dialog
|
||||
className={styles.dialog}
|
||||
className={cn(styles.dialog, 'talk-ban-user-dialog')}
|
||||
id="banUserDialog"
|
||||
open={open}
|
||||
onCancel={onCancel}
|
||||
title={t('bandialog.ban_user')}>
|
||||
<span className={styles.close} onClick={onCancel}>×</span>
|
||||
<div>
|
||||
<div className={styles.header}>
|
||||
<h2>{t('bandialog.ban_user')}</h2>
|
||||
</div>
|
||||
<div className={styles.separator}>
|
||||
<h3>{t('bandialog.are_you_sure', username)}</h3>
|
||||
<i>{info}</i>
|
||||
</div>
|
||||
<div className={styles.buttons}>
|
||||
<Button cStyle="cancel" className={styles.cancel} onClick={onCancel} raised>
|
||||
{t('bandialog.cancel')}
|
||||
</Button>
|
||||
<Button cStyle="black" className={styles.ban} onClick={onPerform} raised>
|
||||
{t('bandialog.yes_ban_user')}
|
||||
</Button>
|
||||
</div>
|
||||
<div className={styles.header}>
|
||||
<h2>{t('bandialog.ban_user')}</h2>
|
||||
</div>
|
||||
<div className={styles.separator}>
|
||||
<h3>{t('bandialog.are_you_sure', username)}</h3>
|
||||
<i>{info}</i>
|
||||
</div>
|
||||
<div className={styles.buttons}>
|
||||
<Button
|
||||
className={cn(styles.cancel, 'talk-ban-user-dialog-button-cancel')}
|
||||
cStyle="cancel"
|
||||
onClick={onCancel}
|
||||
raised >
|
||||
{t('bandialog.cancel')}
|
||||
</Button>
|
||||
<Button
|
||||
className={cn(styles.ban, 'talk-ban-user-dialog-button-confirm')}
|
||||
cStyle="black"
|
||||
onClick={onPerform}
|
||||
raised >
|
||||
{t('bandialog.yes_ban_user')}
|
||||
</Button>
|
||||
</div>
|
||||
</Dialog>
|
||||
);
|
||||
|
||||
@@ -15,7 +15,15 @@ function generateRegExp(phrases) {
|
||||
.join('[\\s"?!.]+')
|
||||
).join('|');
|
||||
|
||||
return new RegExp(`(^|[^\\w])(${inner})(?=[^\\w]|$)`, 'iu');
|
||||
const pattern = `(^|[^\\w])(${inner})(?=[^\\w]|$)`;
|
||||
try {
|
||||
return new RegExp(pattern, 'iu');
|
||||
}
|
||||
catch (_err) {
|
||||
|
||||
// IE does not support unicode support, so we'll create one without.
|
||||
return new RegExp(pattern, 'i');
|
||||
}
|
||||
}
|
||||
|
||||
// Generate a regular expression detecting `suspectWords` and `bannedWords` phrases.
|
||||
|
||||
@@ -6,10 +6,21 @@ import FlagLabel from 'coral-ui/components/FlagLabel';
|
||||
import cn from 'classnames';
|
||||
import styles from './CommentLabels.css';
|
||||
|
||||
const staffRoles = ['ADMIN', 'STAFF', 'MODERATOR'];
|
||||
|
||||
function isUserFlagged(actions) {
|
||||
return actions.some((action) => action.__typename === 'FlagAction' && action.user);
|
||||
}
|
||||
|
||||
function getUserFlaggedType(actions) {
|
||||
return actions
|
||||
.some((action) =>
|
||||
action.__typename === 'FlagAction' &&
|
||||
action.user &&
|
||||
action.user.roles.some((role) => staffRoles.includes(role))
|
||||
) ? 'Staff' : 'User';
|
||||
}
|
||||
|
||||
function hasSuspectedWords(actions) {
|
||||
return actions.some((action) => action.__typename === 'FlagAction' && action.reason === 'Matched suspect word filter');
|
||||
}
|
||||
@@ -24,7 +35,7 @@ const CommentLabels = ({comment, comment: {className, status, actions, hasParent
|
||||
<div className={styles.coreLabels}>
|
||||
{hasParent && <Label iconName="reply" className={styles.replyLabel}>reply</Label>}
|
||||
{status === 'PREMOD' && <Label iconName="query_builder" className={styles.premodLabel}>Pre-Mod</Label>}
|
||||
{isUserFlagged(actions) && <FlagLabel iconName="person">User</FlagLabel>}
|
||||
{isUserFlagged(actions) && <FlagLabel iconName="person">{getUserFlaggedType(actions)}</FlagLabel>}
|
||||
{hasSuspectedWords(actions) && <FlagLabel iconName="sms_failed">Suspect</FlagLabel>}
|
||||
{hasHistoryFlag(actions) && <FlagLabel iconName="sentiment_very_dissatisfied">History</FlagLabel>}
|
||||
</div>
|
||||
|
||||
@@ -5,14 +5,15 @@ import {IndexLink, Link} from 'react-router';
|
||||
import styles from './Drawer.css';
|
||||
import t from 'coral-framework/services/i18n';
|
||||
import {can} from 'coral-framework/services/perms';
|
||||
import cn from 'classnames';
|
||||
|
||||
const CoralDrawer = ({handleLogout, auth = {}}) => (
|
||||
<Drawer className={styles.drawer}>
|
||||
<Drawer className={cn('talk-admin-drawer-nav', styles.drawer)}>
|
||||
{ auth && auth.user && can(auth.user, 'ACCESS_ADMIN') ?
|
||||
<div>
|
||||
<Navigation className={styles.nav}>
|
||||
<IndexLink
|
||||
className={styles.navLink}
|
||||
className={cn('talk-admin-nav-dashboard', styles.navLink)}
|
||||
to="/admin/dashboard"
|
||||
activeClassName={styles.active}>
|
||||
{t('configure.dashboard')}
|
||||
@@ -20,19 +21,21 @@ const CoralDrawer = ({handleLogout, auth = {}}) => (
|
||||
{
|
||||
can(auth.user, 'MODERATE_COMMENTS') && (
|
||||
<Link
|
||||
className={styles.navLink}
|
||||
className={cn('talk-admin-nav-moderate', styles.navLink)}
|
||||
to="/admin/moderate"
|
||||
activeClassName={styles.active}>
|
||||
{t('configure.moderate')}
|
||||
</Link>
|
||||
)
|
||||
}
|
||||
<Link className={styles.navLink}
|
||||
<Link
|
||||
className={cn('talk-admin-nav-stories', styles.navLink)}
|
||||
to="/admin/stories"
|
||||
activeClassName={styles.active}>
|
||||
{t('configure.stories')}
|
||||
</Link>
|
||||
<Link className={styles.navLink}
|
||||
<Link
|
||||
className={cn('talk-admin-nav-community', styles.navLink)}
|
||||
to="/admin/community"
|
||||
activeClassName={styles.active}>
|
||||
{t('configure.community')}
|
||||
@@ -41,7 +44,7 @@ const CoralDrawer = ({handleLogout, auth = {}}) => (
|
||||
can(auth.user, 'UPDATE_CONFIG') &&
|
||||
(
|
||||
<Link
|
||||
className={styles.navLink}
|
||||
className={cn('talk-admin-nav-configure', styles.navLink)}
|
||||
to="/admin/configure"
|
||||
activeClassName={styles.active}>
|
||||
{t('configure.configure')}
|
||||
|
||||
@@ -4,34 +4,38 @@ import Modal from 'components/Modal';
|
||||
import styles from './ModerationKeysModal.css';
|
||||
import t from 'coral-framework/services/i18n';
|
||||
|
||||
const shortcuts = [
|
||||
{
|
||||
title: 'modqueue.navigation',
|
||||
shortcuts: {
|
||||
'j': 'modqueue.next_comment',
|
||||
'k': 'modqueue.prev_comment',
|
||||
'ctrl+f': 'modqueue.toggle_search',
|
||||
't': 'modqueue.next_queue',
|
||||
's': 'modqueue.singleview',
|
||||
'?': 'modqueue.thismenu'
|
||||
}
|
||||
},
|
||||
{
|
||||
title: 'modqueue.actions',
|
||||
shortcuts: {
|
||||
'd': 'modqueue.approve',
|
||||
'f': 'modqueue.reject'
|
||||
}
|
||||
}
|
||||
];
|
||||
|
||||
export default class ModerationKeysModal extends React.Component {
|
||||
|
||||
static propTypes = {
|
||||
open: PropTypes.bool.isRequired,
|
||||
onClose: PropTypes.func.isRequired,
|
||||
hideShortcutsNote: PropTypes.func.isRequired,
|
||||
shortcutsNoteVisible: PropTypes.string.isRequired
|
||||
shortcutsNoteVisible: PropTypes.string.isRequired,
|
||||
queueCount: PropTypes.number.isRequired
|
||||
}
|
||||
|
||||
buildShortcuts = () => {
|
||||
return [
|
||||
{
|
||||
title: 'modqueue.navigation',
|
||||
shortcuts: {
|
||||
'j': 'modqueue.next_comment',
|
||||
'k': 'modqueue.prev_comment',
|
||||
'ctrl+f': 'modqueue.toggle_search',
|
||||
't': 'modqueue.next_queue',
|
||||
[`1...${this.props.queueCount}`]: 'modqueue.jump_to_queue',
|
||||
's': 'modqueue.singleview',
|
||||
'?': 'modqueue.thismenu'
|
||||
}
|
||||
},
|
||||
{
|
||||
title: 'modqueue.actions',
|
||||
shortcuts: {
|
||||
'd': 'modqueue.approve',
|
||||
'f': 'modqueue.reject'
|
||||
}
|
||||
}
|
||||
];
|
||||
}
|
||||
|
||||
render () {
|
||||
@@ -51,7 +55,7 @@ export default class ModerationKeysModal extends React.Component {
|
||||
<Modal open={open} onClose={onClose}>
|
||||
<h3>{t('modqueue.shortcuts')}</h3>
|
||||
<div className={styles.container}>
|
||||
{shortcuts.map((shortcut, i) => (
|
||||
{this.buildShortcuts().map((shortcut, i) => (
|
||||
<table className={styles.table} key={i}>
|
||||
<thead>
|
||||
<tr>
|
||||
|
||||
@@ -7,11 +7,11 @@ import {Icon} from 'coral-ui';
|
||||
|
||||
import t from 'coral-framework/services/i18n';
|
||||
|
||||
const RejectButton = ({active, minimal, onClick}) => {
|
||||
const RejectButton = ({active, minimal, onClick, className}) => {
|
||||
const text = active ? t('modqueue.rejected') : t('modqueue.reject');
|
||||
return (
|
||||
<button
|
||||
className={cn(styles.root, {[styles.minimal]: minimal, [styles.active]: active})}
|
||||
className={cn(styles.root, {[styles.minimal]: minimal, [styles.active]: active}, className)}
|
||||
onClick={onClick}
|
||||
>
|
||||
<Icon name={'close'} className={styles.icon} />
|
||||
@@ -21,6 +21,7 @@ const RejectButton = ({active, minimal, onClick}) => {
|
||||
};
|
||||
|
||||
RejectButton.propTypes = {
|
||||
className: PropTypes.string,
|
||||
active: PropTypes.bool,
|
||||
minimal: PropTypes.bool,
|
||||
onClick: PropTypes.func,
|
||||
|
||||
@@ -3,6 +3,7 @@ import PropTypes from 'prop-types';
|
||||
import {Dialog} from 'coral-ui';
|
||||
import {RadioGroup, Radio} from 'react-mdl';
|
||||
import styles from './SuspendUserDialog.css';
|
||||
import cn from 'classnames';
|
||||
|
||||
import Button from 'coral-ui/components/Button';
|
||||
|
||||
@@ -87,7 +88,7 @@ class SuspendUserDialog extends React.Component {
|
||||
<Button cStyle="white" className={styles.cancel} onClick={onCancel} raised>
|
||||
{t('suspenduser.cancel')}
|
||||
</Button>
|
||||
<Button cStyle="black" className={styles.perform} onClick={this.goToStep1} raised>
|
||||
<Button cStyle="black" className={cn(styles.perform, 'talk-admin-suspend-user-dialog-confirm')} onClick={this.goToStep1} raised>
|
||||
{t('suspenduser.suspend_user')}
|
||||
</Button>
|
||||
</div>
|
||||
@@ -120,7 +121,7 @@ class SuspendUserDialog extends React.Component {
|
||||
</Button>
|
||||
<Button
|
||||
cStyle="black"
|
||||
className={styles.perform}
|
||||
className={cn(styles.perform, 'talk-admin-suspend-user-dialog-send')}
|
||||
onClick={this.handlePerform}
|
||||
disabled={this.state.message.length === 0}
|
||||
raised
|
||||
@@ -137,7 +138,7 @@ class SuspendUserDialog extends React.Component {
|
||||
const {step} = this.state;
|
||||
return (
|
||||
<Dialog
|
||||
className={styles.dialog}
|
||||
className={cn(styles.dialog, 'talk-admin-suspend-user-dialog')}
|
||||
onCancel={onCancel}
|
||||
open={open}
|
||||
>
|
||||
|
||||
@@ -76,9 +76,9 @@ const CoralHeader = ({
|
||||
}
|
||||
<div className={styles.rightPanel}>
|
||||
<ul>
|
||||
<li className={styles.settings}>
|
||||
<li className={cn(styles.settings, 'talk-admin-header-settings')}>
|
||||
<div>
|
||||
<IconButton name="settings" id="menu-settings"/>
|
||||
<IconButton name="settings" id="menu-settings" className="talk-admin-header-settings-button"/>
|
||||
<Menu target="menu-settings" align="right">
|
||||
<MenuItem onClick={() => showShortcuts(true)}>{t('configure.shortcuts')}</MenuItem>
|
||||
<MenuItem>
|
||||
@@ -91,7 +91,7 @@ const CoralHeader = ({
|
||||
Report a bug or give feedback
|
||||
</a>
|
||||
</MenuItem>
|
||||
<MenuItem onClick={handleLogout}>
|
||||
<MenuItem onClick={handleLogout} className="talk-admin-header-sign-out">
|
||||
{t('configure.sign_out')}
|
||||
</MenuItem>
|
||||
</Menu>
|
||||
|
||||
@@ -25,6 +25,7 @@ export default withFragments({
|
||||
}
|
||||
user {
|
||||
id
|
||||
roles
|
||||
}
|
||||
}
|
||||
${getSlotFragmentSpreads(slots, 'comment')}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import React, {Component} from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import {connect} from 'react-redux';
|
||||
import {bindActionCreators} from 'redux';
|
||||
import SuspendUserDialog from '../components/SuspendUserDialog';
|
||||
@@ -44,6 +45,12 @@ class SuspendUserDialogContainer extends Component {
|
||||
}
|
||||
}
|
||||
|
||||
SuspendUserDialogContainer.propTypes = {
|
||||
open: PropTypes.bool,
|
||||
hideSuspendUserDialog: PropTypes.func,
|
||||
username: PropTypes.string,
|
||||
};
|
||||
|
||||
const withOrganizationName = withQuery(gql`
|
||||
query CoralAdmin_SuspendUserDialog {
|
||||
__typename
|
||||
|
||||
@@ -13,11 +13,11 @@ const CommunityMenu = ({flaggedUsernamesCount = 0}) => {
|
||||
<div className='mdl-tabs'>
|
||||
<div className={`mdl-tabs__tab-bar ${styles.tabBar}`}>
|
||||
<div>
|
||||
<Link to={flaggedPath} className={`mdl-tabs__tab ${styles.tab}`} activeClassName={styles.active}>
|
||||
<Link to={flaggedPath} className={`mdl-tabs__tab ${styles.tab} talk-admin-nav-flagged-accounts`} activeClassName={styles.active}>
|
||||
{t('community.flaggedaccounts')}
|
||||
<CountBadge count={flaggedUsernamesCount} />
|
||||
</Link>
|
||||
<Link to={peoplePath} className={`mdl-tabs__tab ${styles.tab}`} activeClassName={styles.active}>
|
||||
<Link to={peoplePath} className={`mdl-tabs__tab ${styles.tab} talk-admin-nav-people`} activeClassName={styles.active}>
|
||||
{t('community.people')}
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import React from 'react';
|
||||
|
||||
import PropTypes from 'prop-types';
|
||||
import cn from 'classnames';
|
||||
import t from 'coral-framework/services/i18n';
|
||||
import EmptyCard from 'coral-admin/src/components/EmptyCard';
|
||||
import LoadMore from '../../../components/LoadMore';
|
||||
@@ -23,7 +24,7 @@ class FlaggedAccounts extends React.Component {
|
||||
const hasResults = users.nodes && !!users.nodes.length;
|
||||
|
||||
return (
|
||||
<div className={styles.container}>
|
||||
<div className={cn('talk-adnin-community-flagged-accounts', styles.container)}>
|
||||
<div className={styles.mainFlaggedContent}>
|
||||
{
|
||||
hasResults
|
||||
@@ -70,4 +71,15 @@ class FlaggedAccounts extends React.Component {
|
||||
}
|
||||
}
|
||||
|
||||
FlaggedAccounts.propTypes = {
|
||||
users: PropTypes.object,
|
||||
loadMore: PropTypes.func,
|
||||
showBanUserDialog: PropTypes.func,
|
||||
showSuspendUserDialog: PropTypes.func,
|
||||
showRejectUsernameDialog: PropTypes.func,
|
||||
approveUser: PropTypes.func,
|
||||
me: PropTypes.object,
|
||||
viewUserDetail: PropTypes.func,
|
||||
};
|
||||
|
||||
export default FlaggedAccounts;
|
||||
|
||||
@@ -1,18 +1,14 @@
|
||||
import React from 'react';
|
||||
import styles from './FlaggedUser.css';
|
||||
|
||||
// TODO: Should not rely on plugin.
|
||||
import PropTypes from 'prop-types';
|
||||
import cn from 'classnames';
|
||||
import t from 'coral-framework/services/i18n';
|
||||
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';
|
||||
|
||||
// TODO: Should work with custom flags too.
|
||||
const shortReasons = {
|
||||
[username.other]: t('community.other'),
|
||||
[username.spam]: t('community.spam_ads'),
|
||||
@@ -21,7 +17,6 @@ const shortReasons = {
|
||||
[username.impersonating]: t('community.impersonating'),
|
||||
};
|
||||
|
||||
// Render a single user for the list
|
||||
class User extends React.Component {
|
||||
|
||||
showSuspenUserDialog = () => this.props.showSuspendUserDialog({
|
||||
@@ -50,12 +45,10 @@ class User extends React.Component {
|
||||
} = this.props;
|
||||
|
||||
return (
|
||||
<li
|
||||
tabIndex={0}
|
||||
className={cn(className, styles.root, {[styles.rootSelected]: selected})}
|
||||
>
|
||||
<div className={styles.container}>
|
||||
<div className={styles.header}>
|
||||
<li tabIndex={0}
|
||||
className={cn(className, styles.root, {[styles.rootSelected]: selected})} >
|
||||
<div className={cn('talk-admin-community-flagged-user', styles.container)}>
|
||||
<div className={cn('talk-admin-community-flagged-user-header', styles.header)}>
|
||||
<div className={styles.author}>
|
||||
<button
|
||||
onClick={this.viewAuthorDetail}
|
||||
@@ -78,8 +71,7 @@ class User extends React.Component {
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className={styles.body}>
|
||||
<div className={cn('talk-admin-community-flagged-user-body', styles.body)}>
|
||||
<div className={styles.flagged}>
|
||||
<div className={styles.flaggedByCount}>
|
||||
<i className={cn('material-icons', styles.flagIcon)}>flag</i>
|
||||
@@ -127,9 +119,11 @@ class User extends React.Component {
|
||||
<div className={styles.sideActions}>
|
||||
<div className={styles.actions}>
|
||||
<ApproveButton
|
||||
className="talk-admin-flagged-user-approve-button"
|
||||
onClick={this.approveUser}
|
||||
/>
|
||||
<RejectButton
|
||||
className="talk-admin-flagged-user-reject-button"
|
||||
onClick={this.showRejectUsernameDialog}
|
||||
/>
|
||||
</div>
|
||||
@@ -141,4 +135,16 @@ class User extends React.Component {
|
||||
}
|
||||
}
|
||||
|
||||
User.propTypes = {
|
||||
showSuspendUserDialog: PropTypes.func,
|
||||
showBanUserDialog: PropTypes.func,
|
||||
viewUserDetail: PropTypes.func,
|
||||
showRejectUsernameDialog: PropTypes.func,
|
||||
approveUser: PropTypes.func,
|
||||
user: PropTypes.object,
|
||||
className: PropTypes.string,
|
||||
selected: PropTypes.bool,
|
||||
me: PropTypes.object,
|
||||
};
|
||||
|
||||
export default User;
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import React from 'react';
|
||||
import cn from 'classnames';
|
||||
import styles from './styles.css';
|
||||
import Table from './Table';
|
||||
import {Icon} from 'coral-ui';
|
||||
@@ -24,7 +25,7 @@ const People = (props) => {
|
||||
const hasResults = !!users.length;
|
||||
|
||||
return (
|
||||
<div className={styles.container}>
|
||||
<div className={cn(styles.container, 'talk-admin-community-people-container')}>
|
||||
<div className={styles.leftColumn}>
|
||||
<div className={styles.searchBox}>
|
||||
<Icon name='search' className={styles.searchIcon}/>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import React, {Component} from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import cn from 'classnames';
|
||||
import {Dialog, Button} from 'coral-ui';
|
||||
import styles from './RejectUsernameDialog.css';
|
||||
|
||||
@@ -29,12 +29,6 @@ class RejectUsernameDialog extends Component {
|
||||
|
||||
state = {email: '', stage: 0}
|
||||
|
||||
static propTypes = {
|
||||
stage: PropTypes.number,
|
||||
handleClose: PropTypes.func.isRequired,
|
||||
rejectUsername: PropTypes.func.isRequired
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.setState({email: t('reject_username.email_message_reject'), about: t('reject_username.username')});
|
||||
}
|
||||
@@ -76,7 +70,7 @@ class RejectUsernameDialog extends Component {
|
||||
const {stage} = this.state;
|
||||
|
||||
return <Dialog
|
||||
className={styles.suspendDialog}
|
||||
className={cn(styles.suspendDialog, 'talk-reject-username-dialog')}
|
||||
id="rejectUsernameDialog"
|
||||
open={open}
|
||||
onClose={handleClose}
|
||||
@@ -96,15 +90,18 @@ class RejectUsernameDialog extends Component {
|
||||
<div className={styles.emailContainer}>
|
||||
<textarea
|
||||
rows={5}
|
||||
className={styles.emailInput}
|
||||
className={cn(styles.emailInput, 'talk-reject-username-dialog-suspension-message')}
|
||||
value={this.state.email}
|
||||
onChange={this.onEmailChange}/>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
<div className={styles.modalButtons}>
|
||||
<div className={cn(styles.modalButtons, 'talk-reject-username-dialog-buttons')}>
|
||||
{Object.keys(stages[stage].options).map((key, i) => (
|
||||
<Button key={i} onClick={this.onActionClick(stage, i)}>
|
||||
<Button
|
||||
key={i}
|
||||
className={cn('talk-reject-username-dialog-button', `talk-reject-username-dialog-button-${key}`)}
|
||||
onClick={this.onActionClick(stage, i)} >
|
||||
{t(stages[stage].options[key], t('reject_username.username'))}
|
||||
</Button>
|
||||
))}
|
||||
@@ -114,4 +111,12 @@ class RejectUsernameDialog extends Component {
|
||||
}
|
||||
}
|
||||
|
||||
RejectUsernameDialog.propTypes = {
|
||||
stage: PropTypes.number,
|
||||
handleClose: PropTypes.func.isRequired,
|
||||
rejectUsername: PropTypes.func.isRequired,
|
||||
user: PropTypes.object,
|
||||
open: PropTypes.bool,
|
||||
};
|
||||
|
||||
export default RejectUsernameDialog;
|
||||
|
||||
@@ -42,7 +42,7 @@ const Table = ({users, setRole, onHeaderClickHandler, setCommenterStatus, viewUs
|
||||
</thead>
|
||||
<tbody>
|
||||
{users.map((row, i)=> (
|
||||
<tr key={i}>
|
||||
<tr key={i} className="talk-admin-community-people-row">
|
||||
<td className="mdl-data-table__cell--non-numeric">
|
||||
<button onClick={() => {viewUserDetail(row.id);}} className={cn(styles.username, styles.button)}>{row.username}</button>
|
||||
<span className={styles.email}>{row.profiles.map(({id}) => id)}</span>
|
||||
@@ -52,6 +52,7 @@ const Table = ({users, setRole, onHeaderClickHandler, setCommenterStatus, viewUs
|
||||
</td>
|
||||
<td className="mdl-data-table__cell--non-numeric">
|
||||
<Dropdown
|
||||
containerClassName="talk-admin-community-people-dd-status"
|
||||
value={row.status}
|
||||
placeholder={t('community.status')}
|
||||
onChange={(status) => setCommenterStatus(row.id, status)}>
|
||||
@@ -61,6 +62,7 @@ const Table = ({users, setRole, onHeaderClickHandler, setCommenterStatus, viewUs
|
||||
</td>
|
||||
<td className="mdl-data-table__cell--non-numeric">
|
||||
<Dropdown
|
||||
containerClassName="talk-admin-community-people-dd-role"
|
||||
value={row.roles[0] || ''}
|
||||
placeholder={t('community.role')}
|
||||
onChange={(role) => setRole(row.id, role)}>
|
||||
|
||||
@@ -73,7 +73,7 @@ class Comment extends React.Component {
|
||||
return (
|
||||
<li
|
||||
tabIndex={0}
|
||||
className={cn(className, 'mdl-card', selectionStateCSS, styles.root, {[styles.selected]: selected})}
|
||||
className={cn(className, 'mdl-card', selectionStateCSS, styles.root, {[styles.selected]: selected}, 'talk-admin-moderate-comment')}
|
||||
id={`comment_${comment.id}`}
|
||||
>
|
||||
<div className={styles.container}>
|
||||
@@ -95,7 +95,7 @@ class Comment extends React.Component {
|
||||
: null
|
||||
}
|
||||
{currentUserId !== comment.user.id &&
|
||||
<ActionsMenu icon="not_interested">
|
||||
<ActionsMenu icon="not_interested" className="talk-admin-moderate-comment-actions-menu">
|
||||
<ActionsMenuItem
|
||||
disabled={comment.user.status === 'BANNED'}
|
||||
onClick={this.showSuspendUserDialog}>
|
||||
|
||||
@@ -34,6 +34,8 @@ class Moderation extends Component {
|
||||
key('k', () => this.select(false));
|
||||
key('f', () => this.moderate(false));
|
||||
key('d', () => this.moderate(true));
|
||||
this.getMenuItems()
|
||||
.forEach((menuItem, idx) => key(`${idx + 1}`, () => this.selectQueue(menuItem)));
|
||||
}
|
||||
|
||||
onClose = () => {
|
||||
@@ -41,20 +43,23 @@ class Moderation extends Component {
|
||||
}
|
||||
|
||||
nextQueue = () => {
|
||||
const queueConfig = this.props.queueConfig;
|
||||
const activeTab = this.props.activeTab;
|
||||
const assetId = this.props.data.variables.asset_id;
|
||||
|
||||
const menuItems = Object.keys(queueConfig).map((queue) => ({
|
||||
key: queue
|
||||
}));
|
||||
const menuItems = this.getMenuItems();
|
||||
|
||||
const activeTabIndex = menuItems.findIndex((item) => item.key === activeTab);
|
||||
const activeTabIndex = menuItems.findIndex((item) => item === activeTab);
|
||||
const nextQueueIndex = (activeTabIndex === menuItems.length - 1) ? 0 : activeTabIndex + 1;
|
||||
|
||||
this.props.router.push(this.props.getModPath(menuItems[nextQueueIndex].key, assetId));
|
||||
this.selectQueue(menuItems[nextQueueIndex]);
|
||||
}
|
||||
|
||||
selectQueue = (key) => {
|
||||
const assetId = this.props.data.variables.asset_id;
|
||||
this.props.router.push(this.props.getModPath(key, assetId));
|
||||
}
|
||||
|
||||
getMenuItems = () => Object.keys(this.props.queueConfig);
|
||||
|
||||
closeSearch = () => {
|
||||
const {toggleStorySearch} = this.props;
|
||||
toggleStorySearch(false);
|
||||
@@ -175,6 +180,8 @@ class Moderation extends Component {
|
||||
key.unbind('k');
|
||||
key.unbind('f');
|
||||
key.unbind('d');
|
||||
this.getMenuItems()
|
||||
.forEach((menuItem, idx) => key.unbind(`${idx + 1}`));
|
||||
}
|
||||
|
||||
componentWillReceiveProps(nextProps) {
|
||||
@@ -276,6 +283,7 @@ class Moderation extends Component {
|
||||
shortcutsNoteVisible={moderation.shortcutsNoteVisible}
|
||||
open={moderation.modalOpen}
|
||||
onClose={this.onClose}
|
||||
queueCount={this.getMenuItems().length}
|
||||
/>
|
||||
</div>
|
||||
<StorySearch
|
||||
|
||||
@@ -47,6 +47,9 @@ export default withFragments({
|
||||
editing {
|
||||
edited
|
||||
}
|
||||
status_history {
|
||||
type
|
||||
}
|
||||
hasParent
|
||||
${getSlotFragmentSpreads(slots, 'comment')}
|
||||
...${getDefinitionName(CommentLabels.fragments.comment)}
|
||||
|
||||
@@ -71,10 +71,10 @@ class ModerationContainer extends Component {
|
||||
const id = getAssetId(this.props);
|
||||
const tab = getTab(this.props);
|
||||
|
||||
// Grab premod from asset or from settings
|
||||
const premod = !id ? settings.moderation : asset.settings.moderation;
|
||||
// Grab premod from asset or from settings if it's defined.
|
||||
const setting = id && asset && asset.settings ? asset.settings.moderation : settings.moderation;
|
||||
|
||||
const queue = isPremod(premod) ? 'premod' : 'new';
|
||||
const queue = isPremod(setting) ? 'premod' : 'new';
|
||||
const activeTab = tab ? tab : queue;
|
||||
|
||||
return activeTab;
|
||||
@@ -111,6 +111,17 @@ class ModerationContainer extends Component {
|
||||
return this.handleCommentChange(prev, comment, notifyText);
|
||||
},
|
||||
},
|
||||
{
|
||||
document: COMMENT_RESET_SUBSCRIPTION,
|
||||
variables,
|
||||
updateQuery: (prev, {subscriptionData: {data: {commentReset: comment}}}) => {
|
||||
const user = comment.status_history[comment.status_history.length - 1].assigned_by;
|
||||
const notifyText = this.props.auth.user.id === user.id
|
||||
? ''
|
||||
: t('modqueue.notify_reset', user.username, prepareNotificationText(comment.body));
|
||||
return this.handleCommentChange(prev, comment, notifyText);
|
||||
},
|
||||
},
|
||||
{
|
||||
document: COMMENT_EDITED_SUBSCRIPTION,
|
||||
variables,
|
||||
@@ -299,6 +310,23 @@ const COMMENT_REJECTED_SUBSCRIPTION = gql`
|
||||
${Comment.fragments.comment}
|
||||
`;
|
||||
|
||||
const COMMENT_RESET_SUBSCRIPTION = gql`
|
||||
subscription CommentReset($asset_id: ID){
|
||||
commentReset(asset_id: $asset_id){
|
||||
...${getDefinitionName(Comment.fragments.comment)}
|
||||
status_history {
|
||||
type
|
||||
created_at
|
||||
assigned_by {
|
||||
id
|
||||
username
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
${Comment.fragments.comment}
|
||||
`;
|
||||
|
||||
const LOAD_MORE_QUERY = gql`
|
||||
query CoralAdmin_Moderation_LoadMore($limit: Int = 10, $cursor: Cursor, $sortOrder: SORT_ORDER, $asset_id: ID, $tags:[String!], $statuses:[COMMENT_STATUS!], $action_type: ACTION_TYPE) {
|
||||
comments(query: {limit: $limit, cursor: $cursor, asset_id: $asset_id, statuses: $statuses, sortOrder: $sortOrder, action_type: $action_type, tags: $tags}) {
|
||||
|
||||
@@ -158,6 +158,7 @@
|
||||
}
|
||||
|
||||
.content {
|
||||
word-wrap: break-word;
|
||||
}
|
||||
|
||||
.footer {
|
||||
|
||||
@@ -182,6 +182,7 @@ export default class Comment extends React.Component {
|
||||
editableUntil: PropTypes.string,
|
||||
})
|
||||
}).isRequired,
|
||||
setCommentStatus: PropTypes.func.isRequired,
|
||||
|
||||
// edit a comment, passed (id, asset_id, { body })
|
||||
editComment: PropTypes.func,
|
||||
@@ -343,7 +344,12 @@ export default class Comment extends React.Component {
|
||||
} = this.props;
|
||||
|
||||
if (!highlighted && this.commentIsRejected(comment)) {
|
||||
return <CommentTombstone action='reject' />;
|
||||
return <CommentTombstone action='reject' onUndo={() => {
|
||||
this.props.setCommentStatus({
|
||||
commentId: comment.id,
|
||||
status: comment.status_history[comment.status_history.length - 2].type,
|
||||
});
|
||||
}}/>;
|
||||
}
|
||||
|
||||
if (this.commentIsIgnored(comment)) {
|
||||
|
||||
@@ -3,4 +3,10 @@
|
||||
text-align: center;
|
||||
padding: 1em;
|
||||
color: #3E4F71;
|
||||
}
|
||||
|
||||
.undo {
|
||||
cursor: pointer;
|
||||
text-decoration: underline;
|
||||
margin-left: 5px;
|
||||
}
|
||||
@@ -24,6 +24,9 @@ class CommentTombstone extends React.Component {
|
||||
<hr aria-hidden={true} />
|
||||
<p className={styles.commentTombstone}>
|
||||
{this.getCopy()}
|
||||
{this.props.action === 'reject' &&
|
||||
<span className={styles.undo} onClick={this.props.onUndo}>{t('comment.undo_reject')}</span>
|
||||
}
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
@@ -32,6 +35,7 @@ class CommentTombstone extends React.Component {
|
||||
|
||||
CommentTombstone.propTypes = {
|
||||
action: PropTypes.string,
|
||||
onUndo: PropTypes.func,
|
||||
};
|
||||
|
||||
export default CommentTombstone;
|
||||
|
||||
@@ -213,12 +213,14 @@ class Stream extends React.Component {
|
||||
const open = !asset.isClosed;
|
||||
|
||||
const banned = user && user.status === 'BANNED';
|
||||
const pending = user && user.status === 'PENDING';
|
||||
|
||||
const temporarilySuspended =
|
||||
user &&
|
||||
user.suspension.until &&
|
||||
new Date(user.suspension.until) > new Date();
|
||||
|
||||
const showCommentBox = loggedIn && ((!banned && !temporarilySuspended && !highlightedComment) || keepCommentBox);
|
||||
const showCommentBox = loggedIn && ((!banned && !pending & !temporarilySuspended && !highlightedComment) || keepCommentBox);
|
||||
const slotProps = {data};
|
||||
const slotQueryData = {root, asset};
|
||||
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
.editNameInput {
|
||||
margin-top: 10px;
|
||||
margin-bottom: 10px;
|
||||
padding: 10px;
|
||||
border-radius: 3px;
|
||||
border: solid 1px #d8d8d8;
|
||||
font-size: 0.9em;
|
||||
}
|
||||
|
||||
.alert {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import React, {Component} from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import cn from 'classnames';
|
||||
import t from 'coral-framework/services/i18n';
|
||||
import styles from './SuspendAccount.css';
|
||||
import {Button} from 'coral-ui';
|
||||
@@ -66,17 +67,16 @@ class SuspendedAccount extends Component {
|
||||
</label>
|
||||
<input
|
||||
type='text'
|
||||
className={styles.editNameInput}
|
||||
className={cn(styles.editNameInput, 'talk-suspended-account-username-input')}
|
||||
value={username}
|
||||
placeholder={t('framework.edit_name.label')}
|
||||
id='username'
|
||||
onChange={(e) => this.setState({username: e.target.value})}
|
||||
rows={3}/><br/>
|
||||
<Button
|
||||
onClick={this.onSubmitClick}>
|
||||
{
|
||||
t('framework.edit_name.button')
|
||||
}
|
||||
className="talk-suspended-account-submit-button"
|
||||
onClick={this.onSubmitClick} >
|
||||
{t('framework.edit_name.button')}
|
||||
</Button>
|
||||
</div> : null
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ import React from 'react';
|
||||
import Comment from '../components/Comment';
|
||||
import {withFragments} from 'coral-framework/hocs';
|
||||
import {getSlotFragmentSpreads} from 'coral-framework/utils';
|
||||
import {withSetCommentStatus} from 'coral-framework/graphql/mutations';
|
||||
import {THREADING_LEVEL} from '../constants/stream';
|
||||
import hoistStatics from 'recompose/hoistStatics';
|
||||
import {nest} from '../graphql/utils';
|
||||
@@ -75,6 +76,9 @@ const singleCommentFragment = gql`
|
||||
id
|
||||
username
|
||||
}
|
||||
status_history {
|
||||
type
|
||||
}
|
||||
action_summaries {
|
||||
__typename
|
||||
count
|
||||
@@ -130,6 +134,7 @@ const withCommentFragments = withFragments({
|
||||
const enhance = compose(
|
||||
withAnimateEnter,
|
||||
withCommentFragments,
|
||||
withSetCommentStatus,
|
||||
);
|
||||
|
||||
export default enhance(Comment);
|
||||
|
||||
@@ -102,6 +102,9 @@ export default {
|
||||
}
|
||||
}
|
||||
}
|
||||
status_history {
|
||||
type
|
||||
}
|
||||
action_summaries {
|
||||
count
|
||||
current_user {
|
||||
@@ -182,6 +185,7 @@ export default {
|
||||
editableUntil: new Date().toISOString(),
|
||||
edited: false,
|
||||
},
|
||||
status_history: [],
|
||||
id: `pending-${uuid()}`,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,16 +1,25 @@
|
||||
import React from 'react';
|
||||
|
||||
import PropTypes from 'prop-types';
|
||||
import t from 'coral-framework/services/i18n';
|
||||
import RestrictedMessageBox from './RestrictedMessageBox';
|
||||
|
||||
export default ({children, restricted, message = t('framework.content_not_available'), restrictedComp}) => {
|
||||
const RestrictedContent = ({children, restricted, message = t('framework.content_not_available'), restrictedComp}) => {
|
||||
if (restricted) {
|
||||
return restrictedComp ? restrictedComp : <RestrictedMessageBox message={message} />;
|
||||
} else {
|
||||
return (
|
||||
<div>
|
||||
<div className="talk-restricted-content">
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
RestrictedContent.propTypes = {
|
||||
children: PropTypes.node,
|
||||
restricted: PropTypes.bool,
|
||||
message: PropTypes.string,
|
||||
restrictedComp: PropTypes.node,
|
||||
};
|
||||
|
||||
export default RestrictedContent;
|
||||
|
||||
@@ -1,4 +1,13 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import cn from 'classnames';
|
||||
import styles from './RestrictedMessageBox.css';
|
||||
|
||||
export default ({children}) => <div className={styles.message}>{children}</div>;
|
||||
const RestrictedMessageBox = ({children}) =>
|
||||
<div className={cn(styles.message, 'talk-restricted-message-box')}>{children}</div>;
|
||||
|
||||
RestrictedMessageBox.propTypes = {
|
||||
children: PropTypes.node,
|
||||
};
|
||||
|
||||
export default RestrictedMessageBox;
|
||||
|
||||
@@ -3,6 +3,7 @@ import cn from 'classnames';
|
||||
import styles from './Slot.css';
|
||||
import {connect} from 'react-redux';
|
||||
import omit from 'lodash/omit';
|
||||
import kebabCase from 'lodash/kebabCase';
|
||||
import PropTypes from 'prop-types';
|
||||
import isEqual from 'lodash/isEqual';
|
||||
import {getShallowChanges} from 'coral-framework/utils';
|
||||
@@ -58,6 +59,7 @@ class Slot extends React.Component {
|
||||
childFactory,
|
||||
defaultComponent: DefaultComponent,
|
||||
queryData,
|
||||
fill,
|
||||
} = this.props;
|
||||
const {plugins} = this.context;
|
||||
let children = this.getChildren();
|
||||
@@ -72,7 +74,7 @@ class Slot extends React.Component {
|
||||
}
|
||||
|
||||
return (
|
||||
<Component className={cn({[styles.inline]: inline, [styles.debug]: pluginConfig.debug}, className)}>
|
||||
<Component className={cn({[styles.inline]: inline, [styles.debug]: pluginConfig.debug}, className, `talk-slot-${kebabCase(fill)}`)}>
|
||||
{children}
|
||||
</Component>
|
||||
);
|
||||
@@ -85,12 +87,22 @@ Slot.defaultProps = {
|
||||
|
||||
Slot.propTypes = {
|
||||
fill: PropTypes.string.isRequired,
|
||||
inline: PropTypes.bool,
|
||||
className: PropTypes.string,
|
||||
reduxState: PropTypes.object,
|
||||
defaultComponent: PropTypes.oneOfType([
|
||||
PropTypes.func,
|
||||
PropTypes.string,
|
||||
]),
|
||||
|
||||
/**
|
||||
* You may specify the component to use as the root wrapper.
|
||||
* Defaults to 'div'.
|
||||
*/
|
||||
component: PropTypes.any,
|
||||
component: PropTypes.oneOfType([
|
||||
PropTypes.func,
|
||||
PropTypes.string,
|
||||
]),
|
||||
|
||||
// props coming from graphql must be passed through this property.
|
||||
queryData: PropTypes.object,
|
||||
|
||||
@@ -138,6 +138,9 @@ export const withSetCommentStatus = withMutation(
|
||||
const fragment = gql`
|
||||
fragment Talk_SetCommentStatus on Comment {
|
||||
status
|
||||
status_history {
|
||||
type
|
||||
}
|
||||
}`;
|
||||
|
||||
const fragmentId = `Comment_${commentId}`;
|
||||
@@ -145,6 +148,8 @@ export const withSetCommentStatus = withMutation(
|
||||
const data = proxy.readFragment({fragment, id: fragmentId});
|
||||
|
||||
data.status = status;
|
||||
data.status_history = data.status_history ? data.status_history : [];
|
||||
data.status_history.push({__typename: 'CommentStatusHistory', type: status});
|
||||
|
||||
proxy.writeFragment({fragment, id: fragmentId, data});
|
||||
}
|
||||
|
||||
@@ -10,12 +10,12 @@ export default class Dialog extends Component {
|
||||
onCancel: PropTypes.func,
|
||||
onClose: PropTypes.func,
|
||||
open: PropTypes.bool,
|
||||
style: PropTypes.object
|
||||
style: PropTypes.object,
|
||||
};
|
||||
|
||||
static defaultProps = {
|
||||
onCancel: (e) => e.preventDefault(),
|
||||
onClose: (e) => e.preventDefault()
|
||||
onClose: (e) => e.preventDefault(),
|
||||
};
|
||||
|
||||
componentDidMount(){
|
||||
|
||||
@@ -131,7 +131,7 @@ class Dropdown extends React.Component {
|
||||
const {containerClassName, toggleClassName, toggleOpenClassName} = this.props;
|
||||
return (
|
||||
<ClickOutside onClickOutside={this.hideMenu}>
|
||||
<div className={cn(styles.dropdown, containerClassName)}>
|
||||
<div className={cn(styles.dropdown, containerClassName, 'dd dd-container')}>
|
||||
<div
|
||||
className={cn(styles.toggle, toggleClassName, {[cn(this.state.isOpen, toggleOpenClassName)]: this.state.isOpen})}
|
||||
onClick={this.handleClick}
|
||||
@@ -149,7 +149,7 @@ class Dropdown extends React.Component {
|
||||
{this.state.isOpen &&
|
||||
<div>
|
||||
<div tabIndex="0" onFocus={this.trapFocusBegin} />
|
||||
<ul className={cn(styles.list, {[styles.listActive] : this.state.isOpen})}>
|
||||
<ul className={cn(styles.list, {[styles.listActive] : this.state.isOpen}, 'dd-list-active')}>
|
||||
{React.Children.toArray(this.props.children)
|
||||
.map((child, i) =>
|
||||
React.cloneElement(child, {
|
||||
|
||||
@@ -16,8 +16,9 @@ class Option extends React.Component {
|
||||
|
||||
render() {
|
||||
const {className, label = '', onClick, onKeyDown} = this.props;
|
||||
const id = this.props.id ? this.props.id : this.props.value;
|
||||
return (
|
||||
<li className={cn(styles.option, className)} onClick={onClick} onKeyDown={onKeyDown} role="option" tabIndex="0" ref={this.handleRef}>
|
||||
<li className={cn(styles.option, className, 'dd-option')} onClick={onClick} onKeyDown={onKeyDown} role="option" tabIndex="0" ref={this.handleRef} id={id}>
|
||||
{label}
|
||||
</li>
|
||||
);
|
||||
@@ -26,6 +27,7 @@ class Option extends React.Component {
|
||||
|
||||
Option.propTypes = {
|
||||
className: PropTypes.string,
|
||||
id: PropTypes.string,
|
||||
label: PropTypes.string,
|
||||
onClick: PropTypes.func,
|
||||
onKeyDown: PropTypes.func,
|
||||
|
||||
@@ -158,9 +158,16 @@ export default class FlagButton extends Component {
|
||||
<ClickOutside onClickOutside={this.handleClickOutside}>
|
||||
<div className={`${name}-container`}>
|
||||
<button
|
||||
disabled={flagged}
|
||||
ref={(ref) => this.flagButton = ref}
|
||||
onClick={!this.props.banned && !flaggedByCurrentUser && !localPost ? this.onReportClick : null}
|
||||
className={cn(`${name}-button`, {[`${name}-button-flagged`]: flagged}, styles.button)}>
|
||||
className={
|
||||
cn(`${name}-button`, {
|
||||
[`${name}-button-flagged`]: flagged,
|
||||
[styles.flaggedButton]: flagged
|
||||
},
|
||||
styles.button)}
|
||||
>
|
||||
{
|
||||
flagged
|
||||
? <span className={`${name}-button-text`}>{t('reported')}</span>
|
||||
|
||||
@@ -3,6 +3,10 @@
|
||||
margin: 5px 0px 5px 10px;
|
||||
}
|
||||
|
||||
button[disabled] {
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.flaggedIcon {
|
||||
color: #f00
|
||||
}
|
||||
|
||||
@@ -13,6 +13,18 @@
|
||||
border-bottom: solid 1px #EBEBEB;
|
||||
}
|
||||
|
||||
.main {
|
||||
min-width: 70%;
|
||||
}
|
||||
|
||||
.sidebar {
|
||||
min-width: 30%;
|
||||
}
|
||||
|
||||
.commentBody {
|
||||
word-wrap: break-word;
|
||||
}
|
||||
|
||||
.assetURL {
|
||||
text-decoration: none;
|
||||
font-weight: bold;
|
||||
@@ -44,7 +56,8 @@
|
||||
margin-top: 0;
|
||||
margin-bottom: 0;
|
||||
list-style-type: none;
|
||||
min-width: 136px;
|
||||
min-width: 140px;
|
||||
padding: 0px 10px;
|
||||
}
|
||||
|
||||
li {
|
||||
|
||||
@@ -19,7 +19,7 @@ class Comment extends React.Component {
|
||||
|
||||
return (
|
||||
<div className={styles.myComment}>
|
||||
<div>
|
||||
<div className={styles.main}>
|
||||
<Slot
|
||||
fill="commentContent"
|
||||
defaultComponent={CommentContent}
|
||||
|
||||
@@ -95,10 +95,12 @@ FROM coralproject/talk:latest-onbuild
|
||||
And running the following to build the docker image:
|
||||
|
||||
```bash
|
||||
docker build -t my-awesome-talk-image .
|
||||
docker build -t my-awesome-talk-image --build-arg TALK_DEFAULT_LANG=es .
|
||||
```
|
||||
|
||||
Don't forget to replace `my-awesome-talk-image` with your own image name.
|
||||
Don't forget to replace `my-awesome-talk-image` with your own image name, and
|
||||
specify your build variables with the `--build-arg`. Refer to [Dockerfile.onbuild](https://github.com/coralproject/talk/blob/master/Dockerfile.onbuild){:target="_blank"} for the
|
||||
available build variables.
|
||||
{: .code-aside}
|
||||
|
||||
This accomplishes a lot:
|
||||
@@ -108,6 +110,9 @@ This accomplishes a lot:
|
||||
2. Installs any new dependencies that were required by any new plugins.
|
||||
3. Builds the new static bundles so that they are ready to serve when the image
|
||||
is running.
|
||||
4. Specifies a build time variable [TALK_DEFAULT_LANG]({{ "/advanced-configuration/#talk_default_lang" | relative_url }}){:.param}. Refer
|
||||
to [Dockerfile.onbuild](https://github.com/coralproject/talk/blob/master/Dockerfile.onbuild){:target="_blank"} for the
|
||||
available build variables.
|
||||
|
||||
This means that you can create a repository for your organization that simply
|
||||
includes the above `Dockerfile`, a directory of `plugins`, and a `plugins.json`
|
||||
|
||||
@@ -26,10 +26,18 @@ Configure the duration for which comment counts are cached for, parsed by
|
||||
|
||||
## TALK_DEFAULT_LANG
|
||||
|
||||
This is a **Build Variable** and must be consumed during build. If using the
|
||||
[Docker-onbuild]({{ "/installation-from-docker/#onbuild" | relative_url }})
|
||||
image you can specify it with `--build-arg TALK_DEFAULT_LANG=en`.
|
||||
|
||||
Specify the default translation language. (Default `en`)
|
||||
|
||||
## TALK_DEFAULT_STREAM_TAB
|
||||
|
||||
This is a **Build Variable** and must be consumed during build. If using the
|
||||
[Docker-onbuild]({{ "/installation-from-docker/#onbuild" | relative_url }})
|
||||
image you can specify it with `--build-arg TALK_DEFAULT_STREAM_TAB=all`.
|
||||
|
||||
Specify the default stream tab in the admin. (Default `all`)
|
||||
|
||||
## TALK_DISABLE_AUTOFLAG_SUSPECT_WORDS
|
||||
@@ -361,12 +369,12 @@ Then all the routes for the API will be expecting to be hit on `/talk/`, such as
|
||||
can perform the path stripping when serving an upstream proxy, but some CDN's
|
||||
cannot. You would use this option in the latter situation.
|
||||
|
||||
## TALK_SMTP_EMAIL
|
||||
## TALK_SMTP_FROM_ADDRESS
|
||||
|
||||
The email address to send emails from using the SMTP provider in the format:
|
||||
|
||||
```plain
|
||||
TALK_SMTP_EMAIL="The Coral Project" <support@coralproject.net>
|
||||
TALK_SMTP_FROM_ADDRESS="The Coral Project" <support@coralproject.net>
|
||||
```
|
||||
|
||||
Including the name and email address.
|
||||
@@ -397,6 +405,10 @@ CDN/Storage url. (Defaults to value of
|
||||
|
||||
## TALK_THREADING_LEVEL
|
||||
|
||||
This is a **Build Variable** and must be consumed during build. If using the
|
||||
[Docker-onbuild]({{ "/installation-from-docker/#onbuild" | relative_url }})
|
||||
image you can specify it with `--build-arg TALK_THREADING_LEVEL=3`.
|
||||
|
||||
Specify the maximum depth of the comment thread. (Default `3`)
|
||||
|
||||
**Note that a high value for `TALK_THREADING_LEVEL` will result in large
|
||||
|
||||
@@ -273,7 +273,8 @@ pre {
|
||||
p a:not(.plain-link) {
|
||||
@extend .coral-link;
|
||||
}
|
||||
ul:not(.toc__menu) li a {
|
||||
ul:not(.toc__menu) li a,
|
||||
ol li a {
|
||||
@extend .coral-link;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -33,8 +33,11 @@ const createAction = async ({user = {}, pubsub, loaders: {Comments}}, {item_id,
|
||||
|
||||
if (item_type === 'USERS' && action_type === 'FLAG') {
|
||||
|
||||
// Set the user as pending if it was a user flag.
|
||||
await UsersService.setStatus(item_id, 'PENDING');
|
||||
// Set the user as pending if it was a user flag and user has no Admin, Staff or Moderation roles
|
||||
let user = await UsersService.findById(item_id);
|
||||
if(!user.isStaff()){
|
||||
await UsersService.setStatus(item_id, 'PENDING');
|
||||
}
|
||||
}
|
||||
|
||||
if (comment) {
|
||||
|
||||
@@ -43,13 +43,11 @@ const RootMutation = {
|
||||
setCommentStatus: async (_, {id, status}, {mutators: {Comment}, pubsub}) => {
|
||||
const comment = await Comment.setStatus({id, status});
|
||||
if (status === 'ACCEPTED') {
|
||||
|
||||
// Publish the comment status change via the subscription.
|
||||
pubsub.publish('commentAccepted', comment);
|
||||
} else if (status === 'REJECTED') {
|
||||
|
||||
// Publish the comment status change via the subscription.
|
||||
pubsub.publish('commentRejected', comment);
|
||||
} else if (status === 'NONE') {
|
||||
pubsub.publish('commentReset', comment);
|
||||
}
|
||||
},
|
||||
addTag: async (_, {tag}, {mutators: {Tag}}) => {
|
||||
|
||||
@@ -11,6 +11,9 @@ const Subscription = {
|
||||
commentRejected(comment) {
|
||||
return comment;
|
||||
},
|
||||
commentReset(comment) {
|
||||
return comment;
|
||||
},
|
||||
commentFlagged(comment) {
|
||||
return comment;
|
||||
},
|
||||
|
||||
@@ -2,6 +2,7 @@ const {
|
||||
SUBSCRIBE_COMMENT_ACCEPTED,
|
||||
SUBSCRIBE_COMMENT_REJECTED,
|
||||
SUBSCRIBE_COMMENT_FLAGGED,
|
||||
SUBSCRIBE_COMMENT_RESET,
|
||||
SUBSCRIBE_ALL_COMMENT_EDITED,
|
||||
SUBSCRIBE_ALL_COMMENT_ADDED,
|
||||
SUBSCRIBE_ALL_USER_SUSPENDED,
|
||||
@@ -93,6 +94,16 @@ const setupFunctions = plugins.get('server', 'setupFunctions').reduce((acc, {plu
|
||||
}
|
||||
},
|
||||
}),
|
||||
commentReset: (options, args) => ({
|
||||
commentReset: {
|
||||
filter: (comment, context) => {
|
||||
if (!context.user || !context.user.can(SUBSCRIBE_COMMENT_RESET)) {
|
||||
return false;
|
||||
}
|
||||
return !args.asset_id || comment.asset_id === args.asset_id;
|
||||
}
|
||||
},
|
||||
}),
|
||||
userSuspended: (options, args) => ({
|
||||
userSuspended: {
|
||||
filter: (user, context) => {
|
||||
|
||||
@@ -1376,6 +1376,10 @@ type Subscription {
|
||||
# Requires the `ADMIN` or `MODERATOR` role.
|
||||
commentRejected(asset_id: ID): Comment
|
||||
|
||||
# Get an update whenever the status of a comment has been reset.
|
||||
# Requires the `ADMIN` or `MODERATOR` role.
|
||||
commentReset(asset_id: ID): Comment
|
||||
|
||||
# Get an update whenever a user has been suspended.
|
||||
# `user_id` must match id of current user except for
|
||||
# users with the `ADMIN` or `MODERATOR` role.
|
||||
|
||||
@@ -17,6 +17,7 @@ en:
|
||||
characters_remaining: "characters remaining"
|
||||
comment:
|
||||
anon: "Anonymous"
|
||||
undo_reject: "Undo"
|
||||
ban_user: "Ban User"
|
||||
comment: "Post a comment"
|
||||
edited: Edited
|
||||
@@ -263,6 +264,7 @@ en:
|
||||
notify_accepted: '{0} accepted comment "{1}"'
|
||||
notify_rejected: '{0} rejected comment "{1}"'
|
||||
notify_flagged: '{0} flagged comment "{1}"'
|
||||
notify_reset: '{0} reset status of comment "{1}"'
|
||||
approve: "Approve"
|
||||
approved: "Approved"
|
||||
ban_user: "Ban"
|
||||
@@ -296,6 +298,7 @@ en:
|
||||
show_shortcuts: "Show Shortcuts"
|
||||
singleview: "Toggle single comment edit view"
|
||||
thismenu: "Open this menu"
|
||||
jump_to_queue: "Jump to specific queue"
|
||||
thousand: k
|
||||
try_these: "Try these"
|
||||
view_more_shortcuts: "View more shortcuts"
|
||||
|
||||
@@ -22,22 +22,34 @@ const nightwatch_config = {
|
||||
'browserstack.user': process.env.BROWSERSTACK_USER || 'coralproject2',
|
||||
'browserstack.key': process.env.BROWSERSTACK_KEY,
|
||||
'browserstack.local': true,
|
||||
'browserstack.localIdentifier': process.env.BROWSERSTACK_LOCAL_IDENTIFIER ? process.env.BROWSERSTACK_LOCAL_IDENTIFIER : undefined,
|
||||
'browserstack.debug': true,
|
||||
|
||||
// Disable this, as it makes bs slow and brittle.
|
||||
'browserstack.networkLogs': false,
|
||||
}
|
||||
'resolution': '1600x1200',
|
||||
},
|
||||
screenshots : {
|
||||
enabled: true,
|
||||
on_failure: true,
|
||||
on_error: true,
|
||||
path: process.env.REPORTS_FOLDER || './test/e2e/reports',
|
||||
},
|
||||
},
|
||||
chrome: {
|
||||
desiredCapabilities: {
|
||||
browser: 'chrome',
|
||||
browser_version: '62',
|
||||
os: 'Windows',
|
||||
os_version: '10',
|
||||
},
|
||||
},
|
||||
firefox: {
|
||||
desiredCapabilities: {
|
||||
browser: 'firefox',
|
||||
browser_version: '56',
|
||||
os: 'Windows',
|
||||
os_version: '10',
|
||||
},
|
||||
},
|
||||
safari: {
|
||||
@@ -52,14 +64,21 @@ const nightwatch_config = {
|
||||
desiredCapabilities: {
|
||||
browser: 'internet explorer',
|
||||
os: 'Windows',
|
||||
os_version: '8.1',
|
||||
os_version: '10',
|
||||
browser_version: '11',
|
||||
|
||||
// The x64 bit IEDriver that is used by IE 11 has a known issue with sendKeys where
|
||||
// it may enter incorrect keys (shift + key).
|
||||
// This adds a delay for each character as temporary fix as advised from the browserstack support.
|
||||
'browserstack.customSendKeys': 800,
|
||||
},
|
||||
},
|
||||
edge: {
|
||||
desiredCapabilities: {
|
||||
browser: 'edge',
|
||||
browser_version: '15',
|
||||
os: 'Windows',
|
||||
os_version: '10',
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
+6
-4
@@ -9,13 +9,13 @@ module.exports = {
|
||||
globals_path: './test/e2e/globals',
|
||||
selenium: {
|
||||
start_process: true,
|
||||
server_path: 'node_modules/selenium-standalone/.selenium/selenium-server/3.5.3-server.jar',
|
||||
server_path: 'node_modules/selenium-standalone/.selenium/selenium-server/3.6.0-server.jar',
|
||||
log_path: './test/e2e/',
|
||||
host: '127.0.0.1',
|
||||
port: 6666,
|
||||
cli_args: {
|
||||
'webdriver.chrome.driver': 'node_modules/selenium-standalone/.selenium/chromedriver/2.32-x64-chromedriver',
|
||||
'webdriver.gecko.driver': 'node_modules/selenium-standalone/.selenium/geckodriver/0.18.0-x64-geckodriver',
|
||||
'webdriver.chrome.driver': 'node_modules/selenium-standalone/.selenium/chromedriver/2.33-x64-chromedriver',
|
||||
'webdriver.gecko.driver': 'node_modules/selenium-standalone/.selenium/geckodriver/0.19.0-x64-geckodriver',
|
||||
}
|
||||
},
|
||||
test_settings: {
|
||||
@@ -40,10 +40,12 @@ module.exports = {
|
||||
path: process.env.REPORTS_FOLDER || './test/e2e/reports',
|
||||
},
|
||||
},
|
||||
'chrome': {
|
||||
},
|
||||
'chrome-headless': {
|
||||
desiredCapabilities: {
|
||||
chromeOptions : {
|
||||
args: ['--headless', '--disable-gpu', 'window-size=1280,800'],
|
||||
args: ['--headless', '--disable-gpu', 'window-size=1600,1200'],
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
+3
-5
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "talk",
|
||||
"version": "3.7.0",
|
||||
"version": "3.8.0",
|
||||
"description": "A better commenting experience from Mozilla, The New York Times, and the Washington Post. https://coralproject.net",
|
||||
"main": "app.js",
|
||||
"private": true,
|
||||
@@ -16,9 +16,7 @@
|
||||
"lint-fix": "yarn lint --fix",
|
||||
"jest-watch": "TEST_MODE=unit NODE_ENV=test jest --watch",
|
||||
"e2e-ci": "./scripts/e2e-ci.sh",
|
||||
"e2e-browserstack": "NODE_ENV=test ./scripts/e2e-browserstack.js --config nightwatch-browserstack.conf.js",
|
||||
"pree2e": "selenium-standalone install",
|
||||
"e2e": "NODE_ENV=test nightwatch",
|
||||
"e2e": "./scripts/e2e.js",
|
||||
"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",
|
||||
@@ -172,7 +170,7 @@
|
||||
"redux": "^3.6.0",
|
||||
"redux-thunk": "^2.1.0",
|
||||
"resolve": "^1.4.0",
|
||||
"selenium-standalone": "6.9.0",
|
||||
"selenium-standalone": "^6.11.0",
|
||||
"semver": "^5.4.1",
|
||||
"simplemde": "^1.11.2",
|
||||
"smoothscroll-polyfill": "^0.3.5",
|
||||
|
||||
@@ -36,6 +36,7 @@ module.exports = {
|
||||
SUBSCRIBE_COMMENT_ACCEPTED: 'SUBSCRIBE_COMMENT_ACCEPTED',
|
||||
SUBSCRIBE_COMMENT_REJECTED: 'SUBSCRIBE_COMMENT_REJECTED',
|
||||
SUBSCRIBE_COMMENT_FLAGGED: 'SUBSCRIBE_COMMENT_FLAGGED',
|
||||
SUBSCRIBE_COMMENT_RESET: 'SUBSCRIBE_COMMENT_RESET',
|
||||
SUBSCRIBE_ALL_COMMENT_ADDED: 'SUBSCRIBE_ALL_COMMENT_ADDED',
|
||||
SUBSCRIBE_ALL_COMMENT_EDITED: 'SUBSCRIBE_ALL_COMMENT_EDITED',
|
||||
SUBSCRIBE_ALL_USER_SUSPENDED: 'SUBSCRIBE_ALL_USER_SUSPENDED',
|
||||
|
||||
@@ -9,6 +9,8 @@ module.exports = (user, perm) => {
|
||||
return check(user, ['ADMIN', 'MODERATOR']);
|
||||
case types.SUBSCRIBE_COMMENT_REJECTED:
|
||||
return check(user, ['ADMIN', 'MODERATOR']);
|
||||
case types.SUBSCRIBE_COMMENT_RESET:
|
||||
return check(user, ['ADMIN', 'MODERATOR']);
|
||||
case types.SUBSCRIBE_ALL_COMMENT_EDITED:
|
||||
return check(user, ['ADMIN', 'MODERATOR']);
|
||||
case types.SUBSCRIBE_ALL_COMMENT_ADDED:
|
||||
|
||||
@@ -44,6 +44,7 @@
|
||||
margin: 0;
|
||||
quotes: '\201c' '\201d';
|
||||
margin-bottom: 10px;
|
||||
word-wrap: break-word;
|
||||
}
|
||||
|
||||
.quote:before {
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import styles from './styles.css';
|
||||
import {t} from 'plugin-api/beta/client/services';
|
||||
import {Icon} from 'plugin-api/beta/client/components/ui';
|
||||
@@ -6,16 +7,23 @@ import cn from 'classnames';
|
||||
|
||||
const isApproved = (status) => (status === 'ACCEPTED');
|
||||
|
||||
export default ({approveComment, comment: {status}}) => (
|
||||
const ApproveCommentAction = ({approveComment, comment: {status}}) => (
|
||||
isApproved(status) ? (
|
||||
<span className={styles.approved}>
|
||||
<Icon name="check_circle" className={styles.icon} />
|
||||
{t('talk-plugin-moderation-actions.approved_comment')}
|
||||
</span>
|
||||
) : (
|
||||
<button className={cn(styles.button, 'talk-plugin-moderation-actions-reject')} onClick={approveComment}>
|
||||
<button className={cn(styles.button, 'talk-plugin-moderation-actions-approve')} onClick={approveComment}>
|
||||
<Icon name="done" className={styles.icon} />
|
||||
{t('talk-plugin-moderation-actions.approve_comment')}
|
||||
</button>
|
||||
)
|
||||
);
|
||||
|
||||
ApproveCommentAction.propTypes = {
|
||||
approveComment: PropTypes.func,
|
||||
comment: PropTypes.object,
|
||||
};
|
||||
|
||||
export default ApproveCommentAction;
|
||||
@@ -1,14 +1,21 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import styles from './styles.css';
|
||||
import {t} from 'plugin-api/beta/client/services';
|
||||
import {Icon} from 'plugin-api/beta/client/components/ui';
|
||||
import cn from 'classnames';
|
||||
|
||||
export default ({onBanUser}) => (
|
||||
const BanUserAction = ({onBanUser}) => (
|
||||
<button
|
||||
className={cn(styles.button, 'talk-plugin-moderation-actions-reject')}
|
||||
className={cn(styles.button, 'talk-plugin-moderation-actions-ban')}
|
||||
onClick={onBanUser} >
|
||||
<Icon name="block" className={styles.icon} />
|
||||
{t('talk-plugin-moderation-actions.ban_user')}
|
||||
</button>
|
||||
);
|
||||
|
||||
BanUserAction.propTypes = {
|
||||
onBanUser: PropTypes.func,
|
||||
};
|
||||
|
||||
export default BanUserAction;
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import cn from 'classnames';
|
||||
import styles from './BanUserDialog.css';
|
||||
import {t} from 'plugin-api/beta/client/services';
|
||||
import {Dialog, Button} from 'plugin-api/beta/client/components/ui';
|
||||
|
||||
export default ({showBanDialog, closeBanDialog, banUser}) => (
|
||||
<Dialog open={showBanDialog} className={styles.dialog}>
|
||||
const BanUserDialog = ({showBanDialog, closeBanDialog, banUser}) => (
|
||||
<Dialog open={showBanDialog} className={cn(styles.dialog, 'talk-ban-user-dialog')}>
|
||||
<span className={styles.close} onClick={closeBanDialog}>×</span>
|
||||
<h2>{t('talk-plugin-moderation-actions.ban_user_dialog_headline')}</h2>
|
||||
<h3>{t('talk-plugin-moderation-actions.ban_user_dialog_sub')}</h3>
|
||||
@@ -12,12 +14,28 @@ export default ({showBanDialog, closeBanDialog, banUser}) => (
|
||||
{t('talk-plugin-moderation-actions.ban_user_dialog_copy')}
|
||||
</p>
|
||||
<div className={styles.buttons}>
|
||||
<Button cStyle="cancel" onClick={closeBanDialog} className={styles.cancel} raised>
|
||||
<Button
|
||||
cStyle="cancel"
|
||||
onClick={closeBanDialog}
|
||||
className={cn(styles.cancel, 'talk-ban-user-dialog-button-cancel')}
|
||||
raised >
|
||||
{t('talk-plugin-moderation-actions.ban_user_dialog_cancel')}
|
||||
</Button>
|
||||
<Button cStyle="black" onClick={banUser} className={styles.confirm} raised>
|
||||
<Button
|
||||
cStyle="black"
|
||||
onClick={banUser}
|
||||
className={cn(styles.confirm, 'talk-ban-user-dialog-button-confirm')}
|
||||
raised >
|
||||
{t('talk-plugin-moderation-actions.ban_user_dialog_yes')}
|
||||
</Button>
|
||||
</div>
|
||||
</Dialog>
|
||||
);
|
||||
|
||||
BanUserDialog.propTypes = {
|
||||
showBanDialog: PropTypes.bool.isRequired,
|
||||
closeBanDialog: PropTypes.func.isRequired,
|
||||
banUser: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
export default BanUserDialog;
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import cn from 'classnames';
|
||||
import styles from './Menu.css';
|
||||
import {t} from 'plugin-api/beta/client/services';
|
||||
|
||||
export default ({className = '', children}) => (
|
||||
const Menu = ({className = '', children}) => (
|
||||
<div className={cn(styles.menu, className)}>
|
||||
<h3 className={styles.headline}>
|
||||
{t('talk-plugin-moderation-actions.moderation_actions')}
|
||||
@@ -11,3 +12,10 @@ export default ({className = '', children}) => (
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
|
||||
Menu.propTypes = {
|
||||
className: PropTypes.string,
|
||||
children: PropTypes.node,
|
||||
};
|
||||
|
||||
export default Menu;
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import cn from 'classnames';
|
||||
import Menu from './Menu';
|
||||
import styles from './ModerationActions.css';
|
||||
@@ -21,7 +22,7 @@ export default class ModerationActions extends React.Component {
|
||||
<Icon name="keyboard_arrow_down" className={styles.icon} />}
|
||||
</span>
|
||||
{menuVisible && (
|
||||
<Menu>
|
||||
<Menu className="talk-plugin-modetarion-actions-menu">
|
||||
<Slot
|
||||
className="talk-plugin-modetarion-actions-slot"
|
||||
fill="moderationActions"
|
||||
@@ -38,3 +39,13 @@ export default class ModerationActions extends React.Component {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
ModerationActions.propTypes = {
|
||||
comment: PropTypes.object,
|
||||
root: PropTypes.object,
|
||||
asset: PropTypes.object,
|
||||
data: PropTypes.object,
|
||||
menuVisible: PropTypes.bool,
|
||||
toogleMenu: PropTypes.func,
|
||||
hideMenu: PropTypes.func,
|
||||
};
|
||||
|
||||
@@ -1,12 +1,19 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import cn from 'classnames';
|
||||
import styles from './styles.css';
|
||||
import {t} from 'plugin-api/beta/client/services';
|
||||
import {Icon} from 'plugin-api/beta/client/components/ui';
|
||||
|
||||
export default ({rejectComment}) => (
|
||||
const RejectCommentAction = ({rejectComment}) => (
|
||||
<button className={cn(styles.button, 'talk-plugin-moderation-actions-reject')} onClick={rejectComment}>
|
||||
<Icon name="clear" className={styles.icon} />
|
||||
{t('talk-plugin-moderation-actions.reject_comment')}
|
||||
</button>
|
||||
);
|
||||
|
||||
RejectCommentAction.propTypes = {
|
||||
rejectComment: PropTypes.func,
|
||||
};
|
||||
|
||||
export default RejectCommentAction;
|
||||
|
||||
@@ -1,51 +0,0 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
const Nightwatch = require('nightwatch');
|
||||
const browserstack = require('browserstack-local');
|
||||
const {onshutdown} = require('../bin/util');
|
||||
|
||||
async function start() {
|
||||
try {
|
||||
const bs_local = new browserstack.Local();
|
||||
process.mainModule.filename = './node_modules/.bin/nightwatch';
|
||||
|
||||
// Code to start browserstack local before start of test
|
||||
console.log('Connecting local');
|
||||
Nightwatch.bs_local = bs_local;
|
||||
|
||||
bs_local.start({
|
||||
'key': process.env.BROWSERSTACK_KEY,
|
||||
'logFile': './test/e2e/bslocal.log'
|
||||
}, function(error) {
|
||||
if (error) {
|
||||
console.error(error);
|
||||
throw error;
|
||||
}
|
||||
|
||||
console.log('Connected. Now testing...');
|
||||
Nightwatch.cli(function(argv) {
|
||||
Nightwatch.CliRunner(argv)
|
||||
.setup(null, function(){
|
||||
|
||||
// Code to stop browserstack local after end of parallel test
|
||||
bs_local.stop(function(){});
|
||||
})
|
||||
.runTests(function(){
|
||||
|
||||
// Code to stop browserstack local after end of single test
|
||||
bs_local.stop(function(){});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
onshutdown([
|
||||
() => bs_local.stop(function(){}),
|
||||
]);
|
||||
} catch (ex) {
|
||||
console.log('There was an error while starting the test runner:\n\n');
|
||||
process.stderr.write(`${ex.stack}\n`);
|
||||
process.exit(2);
|
||||
}
|
||||
}
|
||||
|
||||
start();
|
||||
+12
-29
@@ -1,37 +1,20 @@
|
||||
#!/bin/bash
|
||||
|
||||
if [[ "${CIRCLE_BRANCH}" == "master" ]]; then
|
||||
REPORTS_FOLDER=${CIRCLE_TEST_REPORTS:-./test/e2e/reports}
|
||||
CIRCLE_BRANCH=${CIRCLE_BRANCH:-master}
|
||||
|
||||
exitCode=0
|
||||
# Amount of retries before failure.
|
||||
E2E_MAX_RETRIES=${E2E_MAX_RETRIES:-1}
|
||||
|
||||
browserstack() {
|
||||
REPORTS_FOLDER="$CIRCLE_TEST_REPORTS/$1" yarn e2e-browserstack -- --env "$1"
|
||||
# Safari >= 8 has issues connecting to browserstack-local. Safari < 8 is too old.
|
||||
# IE 64bit has issues with receiving keyboard input. Let's wait for them to fix it.
|
||||
BROWSERS="chrome,firefox,edge" #ie safari
|
||||
|
||||
# Determine exit code.
|
||||
result=$?
|
||||
if [ "$result" -gt "0" ]
|
||||
then
|
||||
exitCode=$result
|
||||
fi
|
||||
|
||||
# Sleep a bit to let browserstack-local to close properly.
|
||||
sleep 2
|
||||
}
|
||||
|
||||
# Test using browserstack.
|
||||
browserstack chrome
|
||||
browserstack firefox
|
||||
browserstack ie
|
||||
browserstack edge
|
||||
|
||||
# Safari >= 8 has issues connecting to browserstack-local. Safari < 8 is too old.
|
||||
# browserstack safari
|
||||
|
||||
exit $exitCode
|
||||
if [[ "${CIRCLE_BRANCH}" == "master" && -n "$BROWSERSTACK_KEY" ]]; then
|
||||
echo Testing on browserstack
|
||||
yarn e2e --reports-folder "$REPORTS_FOLDER" --bs-key "$BROWSERSTACK_KEY" --retries "$E2E_MAX_RETRIES" --browsers $BROWSERS
|
||||
else
|
||||
# When browserstack is not available test locally using chrome headless.
|
||||
REPORTS_FOLDER="$CIRCLE_TEST_REPORTS/chrome" yarn e2e -- --env chrome-headless
|
||||
|
||||
# Will exit with status of last command.
|
||||
exit $?
|
||||
echo Testing locally
|
||||
yarn e2e --reports-folder "$REPORTS_FOLDER" --retries "$E2E_MAX_RETRIES" --headless
|
||||
fi
|
||||
|
||||
Executable
+207
@@ -0,0 +1,207 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
process.env['NODE_ENV'] = 'test';
|
||||
|
||||
const browserstack = require('browserstack-local');
|
||||
const {onshutdown, shutdown} = require('../bin/util');
|
||||
const program = require('commander');
|
||||
const Table = require('cli-table');
|
||||
const serve = require('../serve');
|
||||
const childProcess = require('child_process');
|
||||
const uuid = require('uuid').v4;
|
||||
const mongoose = require('../services/mongoose');
|
||||
|
||||
// Make things colorful!
|
||||
require('colors');
|
||||
|
||||
function startTunnel(key, localIdentifier) {
|
||||
const bs_local = new browserstack.Local();
|
||||
|
||||
// Code to start browserstack local before start of test
|
||||
console.log('Connecting local');
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
bs_local.start({
|
||||
key,
|
||||
logFile: './test/e2e/bslocal.log',
|
||||
verbose: 'true',
|
||||
force: 'true',
|
||||
onlyAutomate: 'true',
|
||||
localIdentifier,
|
||||
}, (error) => {
|
||||
if (error) {
|
||||
reject(error);
|
||||
}
|
||||
resolve();
|
||||
});
|
||||
onshutdown([
|
||||
() => bs_local.stop(function(){}),
|
||||
]);
|
||||
});
|
||||
}
|
||||
|
||||
function seleniumInstall() {
|
||||
return new Promise((resolve, reject) => {
|
||||
try {
|
||||
const nw = childProcess.spawn(
|
||||
'./node_modules/.bin/selenium-standalone',
|
||||
['install'],
|
||||
{
|
||||
stdio: 'inherit',
|
||||
});
|
||||
nw.on('close', (code) => {
|
||||
code === 0 ? resolve() : reject();
|
||||
});
|
||||
}
|
||||
catch (ex) {
|
||||
reject(ex);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function nightwatch(env, config, reportsFolder, browserstack) {
|
||||
return new Promise((resolve, reject) => {
|
||||
try {
|
||||
const nw = childProcess.spawn(
|
||||
'./node_modules/.bin/nightwatch',
|
||||
['--config', config, '--env', env],
|
||||
{
|
||||
env: Object.assign({}, process.env, {
|
||||
'BROWSERSTACK_LOCAL_IDENTIFIER': browserstack.localIdentifier,
|
||||
'BROWSERSTACK_KEY': browserstack.key,
|
||||
'BROWSERSTACK_USER': browserstack.user,
|
||||
'REPORTS_FOLDER': `${reportsFolder}/${env}`,
|
||||
}),
|
||||
stdio: 'inherit',
|
||||
});
|
||||
nw.on('close', (code) => {
|
||||
code === 0 ? resolve() : reject();
|
||||
});
|
||||
}
|
||||
catch (ex) {
|
||||
reject(ex);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function printResults(browsers, succeeded, retries) {
|
||||
let table = new Table({
|
||||
head: [
|
||||
'Browser'.cyan,
|
||||
'Status'.cyan,
|
||||
'Retries'.cyan,
|
||||
]
|
||||
});
|
||||
|
||||
for (let browser of browsers) {
|
||||
const wasSuccessful = browser in succeeded;
|
||||
table.push([
|
||||
browser,
|
||||
wasSuccessful ? 'success'.green : 'failed'.red,
|
||||
wasSuccessful ? succeeded[browser] : retries,
|
||||
]);
|
||||
}
|
||||
|
||||
console.log(table.toString());
|
||||
}
|
||||
|
||||
function printSection(txt) {
|
||||
console.log('*****************************'.magenta);
|
||||
console.log(`${'*'.magenta} ${txt.cyan}`);
|
||||
console.log('*****************************'.magenta);
|
||||
}
|
||||
|
||||
async function runBrowserTests(browsers, config, retries = 1, reportsFolder, browserstack) {
|
||||
const succeeded = {};
|
||||
for (let browser of browsers) {
|
||||
for (let t = 0; t < retries + 1; t++) {
|
||||
try {
|
||||
printSection(`e2e test for ${browser} #${t}`);
|
||||
await nightwatch(browser, config, reportsFolder, browserstack);
|
||||
succeeded[browser] = t;
|
||||
console.log(`\n==> Succeeded e2e for ${browser} #${t}\n`.green);
|
||||
break;
|
||||
}
|
||||
catch (ex) {
|
||||
if (ex) {
|
||||
console.log('There was an error while starting the test runner:\n\n');
|
||||
process.stderr.write(`${ex.stack}\n`);
|
||||
}
|
||||
console.log(`\n==> Failed e2e for ${browser} #${t}\n`.red);
|
||||
}
|
||||
}
|
||||
}
|
||||
printResults(browsers, succeeded, retries);
|
||||
return Object.keys(succeeded).length === browsers.length;
|
||||
}
|
||||
|
||||
async function start(program) {
|
||||
const localIdentifier = uuid();
|
||||
let browsers = program.browsers.split(',');
|
||||
const retries = Number.parseInt(program.retries);
|
||||
const browserstack = {};
|
||||
const date = new Date().toISOString()
|
||||
.replace(/[T.]/g, '-')
|
||||
.replace(/:/g, '');
|
||||
const reportsFolder = `${program.reportsFolder}/${date}`;
|
||||
let exitCode = 0;
|
||||
let config = 'nightwatch.conf.js';
|
||||
try {
|
||||
if (program.tunnel && program.bsKey) {
|
||||
await startTunnel(program.bsKey, localIdentifier);
|
||||
}
|
||||
|
||||
console.log('Dropping test database');
|
||||
await mongoose.connection.dropDatabase();
|
||||
await serve();
|
||||
|
||||
if (program.bsKey) {
|
||||
config = 'nightwatch-browserstack.conf.js';
|
||||
browserstack.localIdentifier = localIdentifier;
|
||||
browserstack.key = program.bsKey;
|
||||
browserstack.user = program.bsUser;
|
||||
} else {
|
||||
|
||||
// Install selenium standalone.
|
||||
await seleniumInstall();
|
||||
if (program.headless) {
|
||||
browsers = browsers.map((b) => `${b}-headless`);
|
||||
}
|
||||
}
|
||||
|
||||
const succeeded = await runBrowserTests(
|
||||
browsers,
|
||||
config,
|
||||
retries,
|
||||
reportsFolder,
|
||||
browserstack,
|
||||
);
|
||||
if (!succeeded) {
|
||||
exitCode = 1;
|
||||
}
|
||||
}
|
||||
catch (ex) {
|
||||
console.log('There was an error:\n\n');
|
||||
process.stderr.write(`${ex.stack}\n`);
|
||||
process.exit(2);
|
||||
}
|
||||
finally {
|
||||
console.log('Shutting down');
|
||||
shutdown();
|
||||
}
|
||||
process.exit(exitCode);
|
||||
}
|
||||
|
||||
program
|
||||
.version('0.1.0')
|
||||
.description('Perform e2e testing locally or if browserstack credentials are provided on browserstack.')
|
||||
.option('-u, --bs-user [user]', 'Browserstack user', 'coralproject2')
|
||||
.option('-k, --bs-key [key]', 'Browserstack api key')
|
||||
.option('--no-tunnel', 'Dont start browserstack-local')
|
||||
.option('-b, --browsers [list of browsers]', 'Browsers to test', 'chrome')
|
||||
.option('-r, --retries [number]', 'Number of retries before failing', '1')
|
||||
.option('--headless', 'Start in headless mode for local e2e')
|
||||
.option('--reports-folder [folder]', 'Reports folder', './test/e2e/reports')
|
||||
.parse(process.argv);
|
||||
|
||||
start(program);
|
||||
@@ -0,0 +1 @@
|
||||
<%= body.replace(/\n/g, '<br />') %>
|
||||
@@ -0,0 +1 @@
|
||||
<%= body %>
|
||||
@@ -515,5 +515,6 @@ module.exports = {
|
||||
HandleAuthPopupCallback,
|
||||
HandleGenerateCredentials,
|
||||
HandleLogout,
|
||||
CheckBlacklisted
|
||||
CheckBlacklisted,
|
||||
CheckRecaptcha,
|
||||
};
|
||||
|
||||
+20
-1
@@ -398,7 +398,7 @@ module.exports = class UsersService {
|
||||
// TODO: current updating status behavior is weird.
|
||||
// once a user has been `APPROVED` its status cannot be
|
||||
// changed anymore.
|
||||
return UserModel.findOneAndUpdate({
|
||||
const user = await UserModel.findOneAndUpdate({
|
||||
id,
|
||||
status: {
|
||||
$ne: 'APPROVED'
|
||||
@@ -410,6 +410,25 @@ module.exports = class UsersService {
|
||||
}, {
|
||||
new: true,
|
||||
});
|
||||
|
||||
if (status === 'BANNED') {
|
||||
let localProfile = user.profiles.find((profile) => profile.provider === 'local');
|
||||
if (localProfile) {
|
||||
const options =
|
||||
{
|
||||
template: 'banned', // needed to know which template to render!
|
||||
locals: { // specifies the template locals.
|
||||
body: 'In accordance with The Coral Project’s community guidelines, your account has been banned. You are now longer allowed to comment, flag or engage with our community.'
|
||||
},
|
||||
subject: 'Your account has been banned',
|
||||
to: localProfile.id // This only works if the user has registered via e-mail.
|
||||
// We may want a standard way to access a user's e-mail address in the future
|
||||
};
|
||||
await MailerService.sendSimple(options);
|
||||
}
|
||||
|
||||
}
|
||||
return user;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
+1
-2
@@ -1,11 +1,10 @@
|
||||
const serve = require('../../serve');
|
||||
const mongoose = require('../../services/mongoose');
|
||||
const {shutdown} = require('../../bin/util');
|
||||
|
||||
module.exports = {
|
||||
before: async (done) => {
|
||||
console.log('Dropping test database');
|
||||
await mongoose.connection.dropDatabase();
|
||||
await serve();
|
||||
done();
|
||||
},
|
||||
after: (done) => {
|
||||
|
||||
+159
-11
@@ -7,17 +7,165 @@ module.exports = {
|
||||
return this
|
||||
.waitForElementVisible('body');
|
||||
},
|
||||
openDrawer() {
|
||||
this
|
||||
.waitForElementVisible('@drawerButton')
|
||||
.click('@drawerButton');
|
||||
this.expect.section('@drawer').to.be.visible;
|
||||
return this.section.drawer;
|
||||
},
|
||||
goToModerate() {
|
||||
this
|
||||
.click('@moderateNav')
|
||||
.expect.section('@moderate').to.be.visible;
|
||||
return this.section.moderate;
|
||||
},
|
||||
goToStories() {
|
||||
this
|
||||
.click('@storiesNav')
|
||||
.expect.section('@stories').to.be.visible;
|
||||
return this.section.stories;
|
||||
},
|
||||
goToCommunity() {
|
||||
this
|
||||
.click('@communityNav')
|
||||
.expect.section('@community').to.be.visible;
|
||||
return this.section.community;
|
||||
},
|
||||
logout() {
|
||||
this
|
||||
.waitForElementVisible('@settingsButton')
|
||||
.click('@settingsButton')
|
||||
.waitForElementVisible('@signOutButton')
|
||||
.click('@signOutButton');
|
||||
},
|
||||
navigateAndLogin(user) {
|
||||
this
|
||||
.navigate()
|
||||
.expect.section('@login').to.be.visible;
|
||||
return this.section.login.login(user);
|
||||
},
|
||||
}],
|
||||
elements: {
|
||||
'loginLayout': '.talk-admin-login',
|
||||
'signInForm': '.talk-admin-login-sign-in',
|
||||
'emailInput': '.talk-admin-login-sign-in #email',
|
||||
'passwordInput': '.talk-admin-login-sign-in #password',
|
||||
'signInButton': '.talk-admin-login-sign-in-button',
|
||||
'storiesNav': '.talk-admin-nav-stories',
|
||||
'storiesSection': '.talk-admin-stories',
|
||||
'communityNav': '.talk-admin-nav-community',
|
||||
'communitySection': '.talk-admin-community',
|
||||
'moderationContainer': '.talk-admin-moderation-container'
|
||||
}
|
||||
drawerButton: '.mdl-layout__drawer-button',
|
||||
drawerOverlay: 'div.mdl-layout__obfuscator.is-visible',
|
||||
storiesNav: '.talk-admin-nav-stories',
|
||||
communityNav: '.talk-admin-nav-community',
|
||||
moderateNav: '.talk-admin-nav-moderate',
|
||||
settingsButton: '.talk-admin-header-settings-button',
|
||||
signOutButton: '.talk-admin-header-sign-out',
|
||||
suspendUserDialog: '.talk-admin-suspend-user-dialog',
|
||||
suspendUserConfirmButton: '.talk-admin-suspend-user-dialog-confirm',
|
||||
supendUserSendButton: '.talk-admin-suspend-user-dialog-send',
|
||||
toast: '.toastify',
|
||||
toastClose: '.toastify__close',
|
||||
},
|
||||
sections: {
|
||||
moderate: {
|
||||
selector: '.talk-admin-moderation-container',
|
||||
elements: {
|
||||
comment: '.talk-admin-moderate-comment',
|
||||
commentActionMenu: '.talk-admin-moderate-comment-actions-menu',
|
||||
actionItemSuspendUser: '.action-menu-item#suspendUser',
|
||||
actionMenuButton: '.talk-admin-moderate-comment-actions-menu #actions-dropdown-0'
|
||||
}
|
||||
},
|
||||
stories: {
|
||||
selector: '.talk-admin-stories',
|
||||
},
|
||||
community: {
|
||||
selector: '.talk-admin-community',
|
||||
commands: [{
|
||||
url: function() {
|
||||
return `${this.api.launchUrl}/admin/community`;
|
||||
},
|
||||
ready() {
|
||||
return this
|
||||
.waitForElementVisible('body');
|
||||
},
|
||||
goToPeople() {
|
||||
this
|
||||
.click('@peopleNav')
|
||||
.expect.section('@people').to.be.visible;
|
||||
return this.section.people;
|
||||
},
|
||||
}],
|
||||
elements: {
|
||||
peopleNav: '.talk-admin-nav-people',
|
||||
flaggedAccountsNav: '.talk-admin-nav-flagged-accounts',
|
||||
flaggedAccountsContainer: '.talk-adnin-community-flagged-accounts',
|
||||
flaggedUser:'.talk-admin-community-flagged-user',
|
||||
flaggedUserApproveButton: '.talk-admin-flagged-user-approve-button',
|
||||
flaggedUserRejectButton: '.talk-admin-flagged-user-reject-button',
|
||||
usernameDialog: '.talk-reject-username-dialog',
|
||||
usernameDialogButtons: '.talk-reject-username-dialog-buttons',
|
||||
usernameDialogSuspend: '.talk-reject-username-dialog-button-k',
|
||||
usernameDialogSuspensionMessage: '.talk-reject-username-dialog-suspension-message'
|
||||
},
|
||||
sections: {
|
||||
people: {
|
||||
selector: '.talk-admin-community-people-container',
|
||||
elements: {
|
||||
firstRow: '.talk-admin-community-people-row:first-child',
|
||||
dropdownStatus: '.talk-admin-community-people-dd-status',
|
||||
dropdownRole: '.talk-admin-community-people-dd-role',
|
||||
dropdownStatusActive: '.talk-admin-community-people-dd-status .dd-list-active',
|
||||
optionActive: '.dd-option#ACTIVE',
|
||||
optionBanned: '.dd-option#BANNED',
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
drawer: {
|
||||
selector: '.talk-admin-drawer-nav',
|
||||
commands: [{
|
||||
goToStories() {
|
||||
this
|
||||
.click('@storiesButton');
|
||||
this.parent.expect.section('@stories').to.be.visible;
|
||||
this.close();
|
||||
return this.parent.section.stories;
|
||||
},
|
||||
goToCommunity() {
|
||||
this
|
||||
.click('@communityButton');
|
||||
this.parent.expect.section('@community').to.be.visible;
|
||||
this.close();
|
||||
return this.parent.section.stories;
|
||||
},
|
||||
close() {
|
||||
this.parent
|
||||
.click('@drawerOverlay')
|
||||
.waitForElementNotPresent('@drawerOverlay');
|
||||
return this.parent;
|
||||
},
|
||||
}],
|
||||
elements: {
|
||||
'storiesButton': '.talk-admin-drawer-nav .talk-admin-nav-stories',
|
||||
'communityButton': '.talk-admin-drawer-nav .talk-admin-nav-community',
|
||||
},
|
||||
},
|
||||
login: {
|
||||
commands: [{
|
||||
login(user) {
|
||||
this
|
||||
.waitForElementVisible('@signInForm')
|
||||
.setValue('@emailInput', user.email)
|
||||
.setValue('@passwordInput', user.password)
|
||||
.waitForElementVisible('@signInButton')
|
||||
.click('@signInButton');
|
||||
const adminPage = this.api.page.admin();
|
||||
adminPage.expect.section('@moderate').to.be.visible;
|
||||
return adminPage.section.moderate;
|
||||
},
|
||||
}],
|
||||
selector: '.talk-admin-login',
|
||||
elements: {
|
||||
'signInForm': '.talk-admin-login-sign-in',
|
||||
'emailInput': '.talk-admin-login-sign-in #email',
|
||||
'passwordInput': '.talk-admin-login-sign-in #password',
|
||||
'signInButton': '.talk-admin-login-sign-in-button',
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
@@ -1,16 +1,38 @@
|
||||
const iframeId = 'coralStreamEmbed_iframe';
|
||||
const SortedWindowHandler = require('../utils/SortedWindowHandler');
|
||||
|
||||
module.exports = {
|
||||
commands: [{
|
||||
ready: function() {
|
||||
this.switchToIframe();
|
||||
this.expect.section('@comments').to.be.visible;
|
||||
return this.section.comments;
|
||||
},
|
||||
goToProfileSection: function() {
|
||||
this.waitForElementVisible('@profileTabButton');
|
||||
this.click('@profileTabButton');
|
||||
this.expect.section('@profile').to.be.visible;
|
||||
return this.section.profile;
|
||||
},
|
||||
goToCommentsSection: function() {
|
||||
this.waitForElementVisible('@commentsTabButton');
|
||||
this.click('@commentsTabButton');
|
||||
this.expect.section('@comments').to.be.visible;
|
||||
return this.section.comments;
|
||||
},
|
||||
navigateToAsset: function(asset) {
|
||||
this.api.url(`${this.api.launchUrl}/assets/title/${asset}`);
|
||||
return this;
|
||||
},
|
||||
getEmbedSection: function() {
|
||||
switchToIframe: function() {
|
||||
this.waitForElementVisible('@iframe');
|
||||
|
||||
// Pause a bit to let iframe initialize in the hope that it'll
|
||||
// fix https://www.browserstack.com/automate/builds/96419cf46e3d6376a36ae6d3f90934112df1ed91/sessions/224f1a1566c1c8c7859e2e76ece51862200f0173#automate_button
|
||||
this.api.pause(1000);
|
||||
|
||||
this.api.frame(iframeId);
|
||||
this.expect.section('@embed').to.be.present;
|
||||
return this.section.embed;
|
||||
return this;
|
||||
},
|
||||
}],
|
||||
url: function() {
|
||||
@@ -18,47 +40,107 @@ module.exports = {
|
||||
},
|
||||
elements: {
|
||||
iframe: `#${iframeId}`,
|
||||
commentsTabButton: '.talk-embed-stream-comments-tab > button',
|
||||
profileTabButton: '.talk-embed-stream-profile-tab > button',
|
||||
banDialog: '.talk-ban-user-dialog',
|
||||
banDialogConfirmButton: '.talk-ban-user-dialog-button-confirm',
|
||||
},
|
||||
sections: {
|
||||
embed: {
|
||||
comments: {
|
||||
commands: [{
|
||||
getProfileSection: function() {
|
||||
this.click('@profileTabButton');
|
||||
this.expect.section('@profile').to.be.present;
|
||||
return this.section.profile;
|
||||
openLoginPopup(callback) {
|
||||
const windowHandler = new SortedWindowHandler(this.api);
|
||||
|
||||
this
|
||||
.waitForElementVisible('@signInButton')
|
||||
.click('@signInButton');
|
||||
|
||||
// Wait for window to be created
|
||||
// https://www.browserstack.com/automate/builds/1ceccf4efb4683b7feb890f45a32b5922b40ed3f/sessions/17b1a79682bef2498cb0be86eac317a08c976b0a#automate_button
|
||||
this.api.pause(200);
|
||||
|
||||
// Focusing on the Login PopUp
|
||||
windowHandler.windowHandles((handles) => {
|
||||
this.api.switchWindow(handles[1]);
|
||||
});
|
||||
|
||||
const popup = this.api.page.popup().ready();
|
||||
callback(popup);
|
||||
|
||||
// Give a tiny bit of time to let popup close.
|
||||
this.api.pause(50);
|
||||
|
||||
if (this.api.capabilities.browserName === 'MicrosoftEdge') {
|
||||
|
||||
// More time for edge.
|
||||
// https://www.browserstack.com/automate/builds/1ceccf4efb4683b7feb890f45a32b5922b40ed3f/sessions/7393dbfda8387e43b6d5851f359b0c07db414973
|
||||
this.api.pause(1000);
|
||||
}
|
||||
|
||||
// Focusing on the Embed Window
|
||||
windowHandler.windowHandles((handles) => {
|
||||
this.api.switchWindow(handles[0]);
|
||||
|
||||
// For some reasons firefox does not automatically load auth after login.
|
||||
// https://www.browserstack.com/automate/builds/37650cb4e66c6edce0ba0800a1c1b7e7f74bf991/sessions/7a4e9da69b0f9ecdf8b7fa9150639e47b1532cb0#automate_button
|
||||
if (this.api.capabilities.browserName === 'firefox') {
|
||||
this.parent.navigate().ready();
|
||||
} else {
|
||||
this.parent.switchToIframe();
|
||||
}
|
||||
});
|
||||
return this;
|
||||
},
|
||||
getCommentsSection: function() {
|
||||
this.click('@commentsTabButton');
|
||||
this.expect.section('@comments').to.be.present;
|
||||
return this.section.comments;
|
||||
logout() {
|
||||
this
|
||||
.waitForElementVisible('@logoutButton')
|
||||
.click('@logoutButton');
|
||||
},
|
||||
}],
|
||||
selector: '#talk-embed-stream-container',
|
||||
selector: '.talk-embed-stream-comments-tab-pane',
|
||||
elements: {
|
||||
logoutButton: '.talk-stream-userbox-logout',
|
||||
commentsTabButton: '.talk-embed-stream-comments-tab > button',
|
||||
profileTabButton: '.talk-embed-stream-profile-tab > button',
|
||||
signInButton: '#coralSignInButton',
|
||||
commentBoxTextarea: '#commentText',
|
||||
commentBoxPostButton: '.talk-plugin-commentbox-button',
|
||||
firstComment: '.talk-stream-comment.talk-stream-comment-level-0',
|
||||
firstCommentContent: '.talk-stream-comment.talk-stream-comment-level-0 .talk-stream-comment-content',
|
||||
respectButton: '.talk-stream-comment.talk-stream-comment-level-0 .talk-stream-comment-footer .talk-plugin-respect-button'
|
||||
flagButton: '.talk-stream-comment.talk-stream-comment-level-0 .talk-plugin-flags-button',
|
||||
respectButton: '.talk-stream-comment.talk-stream-comment-level-0 .talk-stream-comment-footer .talk-plugin-respect-button',
|
||||
restrictedMessageBox: '.talk-restricted-message-box',
|
||||
suspendedAccountInput: '.talk-suspended-account-username-input',
|
||||
suspendedAccountSubmitButton: '.talk-suspended-account-submit-button',
|
||||
},
|
||||
sections: {
|
||||
profile: {
|
||||
selector: '.talk-embed-stream-profile-tab-pane',
|
||||
flag: {
|
||||
selector: '.talk-plugin-flags-popup',
|
||||
elements: {
|
||||
notLoggedIn: '.talk-embed-stream-not-logged-in',
|
||||
myCommentHistory: '.talk-my-profile-comment-history',
|
||||
myCommentHistoryReactions: '.talk-my-profile-comment-history .comment-summary .comment-summary-reactions',
|
||||
myCommentHistoryReactionCount: '.talk-my-profile-comment-history .comment-summary .comment-summary-reactions .comment-summary-reaction-count',
|
||||
myCommentHistoryComment: '.talk-my-profile-comment-history .my-comment-body'
|
||||
offensiveUsernameRadio: '.talk-plugin-flags-popup-radio#USERNAME_OFFENSIVE',
|
||||
flagUsernameRadio: '.talk-plugin-flags-popup-radio#USERS',
|
||||
flagCommentRadio: '.talk-plugin-flags-popup-radio#COMMENTS',
|
||||
continueButton: '.talk-plugin-flags-popup-button',
|
||||
popUpText: '.talk-plugin-flags-popup-text',
|
||||
spamCommentRadio: '.talk-plugin-flags-popup-radio#COMMENT_SPAM',
|
||||
}
|
||||
},
|
||||
mod: {
|
||||
selector: '.talk-plugin-moderation-actions',
|
||||
elements: {
|
||||
arrow: '.talk-plugin-moderation-actions-arrow',
|
||||
menu: '.talk-plugin-modetarion-actions-menu',
|
||||
banButton: '.talk-plugin-moderation-actions-ban',
|
||||
},
|
||||
},
|
||||
comments: {
|
||||
selector: '.talk-embed-stream-comments-tab-pane',
|
||||
elements: {},
|
||||
},
|
||||
},
|
||||
},
|
||||
profile: {
|
||||
selector: '.talk-embed-stream-profile-tab-pane',
|
||||
elements: {
|
||||
notLoggedIn: '.talk-embed-stream-not-logged-in',
|
||||
myCommentHistory: '.talk-my-profile-comment-history',
|
||||
myCommentHistoryReactions: '.talk-my-profile-comment-history .comment-summary .comment-summary-reactions',
|
||||
myCommentHistoryReactionCount: '.talk-my-profile-comment-history .comment-summary .comment-summary-reactions .comment-summary-reaction-count',
|
||||
myCommentHistoryComment: '.talk-my-profile-comment-history .my-comment-body',
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
@@ -1,13 +0,0 @@
|
||||
module.exports = {
|
||||
elements: {
|
||||
registerButton: '#coralRegister',
|
||||
signInButton: '#coralSignInButton',
|
||||
emailInput: '#email',
|
||||
usernameInput: '#username',
|
||||
passwordInput: '#password',
|
||||
confirmPasswordInput: '#confirmPassword',
|
||||
signUpButton: '#coralSignUpButton',
|
||||
signIn: '.coral-sign-in',
|
||||
loginButton: '#coralLogInButton'
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,41 @@
|
||||
module.exports = {
|
||||
commands: [{
|
||||
ready() {
|
||||
return this
|
||||
.waitForElementVisible('body');
|
||||
},
|
||||
login(user) {
|
||||
return this
|
||||
.setValue('@emailInput', user.email)
|
||||
.setValue('@passwordInput', user.password)
|
||||
.waitForElementVisible('@signIn')
|
||||
.waitForElementVisible('@loginButton')
|
||||
.click('@loginButton');
|
||||
},
|
||||
register(user) {
|
||||
return this
|
||||
.waitForElementVisible('@registerButton')
|
||||
.click('@registerButton')
|
||||
.setValue('@emailInput', user.email)
|
||||
.setValue('@usernameInput', user.username)
|
||||
.setValue('@passwordInput', user.password)
|
||||
.setValue('@confirmPasswordInput', user.password)
|
||||
.waitForElementVisible('@signUpButton')
|
||||
.click('@signUpButton')
|
||||
.waitForElementVisible('@signIn')
|
||||
.waitForElementVisible('@loginButton')
|
||||
.click('@loginButton');
|
||||
},
|
||||
}],
|
||||
elements: {
|
||||
registerButton: '#coralRegister',
|
||||
signInButton: '#coralSignInButton',
|
||||
emailInput: '#email',
|
||||
usernameInput: '#username',
|
||||
passwordInput: '#password',
|
||||
confirmPasswordInput: '#confirmPassword',
|
||||
signUpButton: '#coralSignUpButton',
|
||||
signIn: '.coral-sign-in',
|
||||
loginButton: '#coralLogInButton'
|
||||
},
|
||||
};
|
||||
@@ -1,12 +1,27 @@
|
||||
module.exports = {
|
||||
'@tags': ['install'],
|
||||
|
||||
before: (client) => {
|
||||
client.resizeWindow(1600, 1200);
|
||||
},
|
||||
|
||||
afterEach: (client, done) => {
|
||||
if (client.currentTest.results.failed) {
|
||||
throw new Error('Test Case failed, skipping all the rest');
|
||||
}
|
||||
done();
|
||||
},
|
||||
|
||||
after: (client) => {
|
||||
client.end();
|
||||
},
|
||||
|
||||
'User goes to install': (client) => {
|
||||
const install = client.page.install();
|
||||
|
||||
install
|
||||
.navigate()
|
||||
.expect.section('@step1').to.be.present;
|
||||
.expect.section('@step1').to.be.visible;
|
||||
|
||||
},
|
||||
'User clicks get started button': (client) => {
|
||||
@@ -20,7 +35,7 @@ module.exports = {
|
||||
const install = client.page.install();
|
||||
|
||||
install
|
||||
.expect.section('@step2').to.be.present;
|
||||
.expect.section('@step2').to.be.visible;
|
||||
},
|
||||
'User fills step 2': (client) => {
|
||||
const step2 = client.page.install().section.step2;
|
||||
@@ -36,7 +51,7 @@ module.exports = {
|
||||
const install = client.page.install();
|
||||
|
||||
install
|
||||
.expect.section('@step3').to.be.present;
|
||||
.expect.section('@step3').to.be.visible;
|
||||
},
|
||||
'User fills step 3': (client) => {
|
||||
const step3 = client.page.install().section.step3;
|
||||
@@ -54,7 +69,7 @@ module.exports = {
|
||||
const install = client.page.install();
|
||||
|
||||
install
|
||||
.expect.section('@step4').to.be.present;
|
||||
.expect.section('@step4').to.be.visible;
|
||||
},
|
||||
'User fills step 4': (client) => {
|
||||
const step4 = client.page.install().section.step4;
|
||||
@@ -73,9 +88,6 @@ module.exports = {
|
||||
const install = client.page.install();
|
||||
|
||||
install
|
||||
.expect.section('@step5').to.be.present;
|
||||
.expect.section('@step5').to.be.visible;
|
||||
},
|
||||
after: (client) => {
|
||||
client.end();
|
||||
}
|
||||
};
|
||||
|
||||
+19
-27
@@ -1,49 +1,41 @@
|
||||
module.exports = {
|
||||
'@tags': ['admin', 'login'],
|
||||
beforeEach: (client) => {
|
||||
|
||||
// Testing Desktop
|
||||
client.resizeWindow(1280, 800);
|
||||
before: (client) => {
|
||||
client.resizeWindow(1024, 800);
|
||||
},
|
||||
|
||||
afterEach: (client, done) => {
|
||||
if (client.currentTest.results.failed) {
|
||||
throw new Error('Test Case failed, skipping all the rest');
|
||||
}
|
||||
done();
|
||||
},
|
||||
|
||||
after: (client) => {
|
||||
client.end();
|
||||
},
|
||||
|
||||
'Admin logs in': (client) => {
|
||||
const adminPage = client.page.admin();
|
||||
const {testData: {admin}} = client.globals;
|
||||
|
||||
adminPage
|
||||
.navigate()
|
||||
.waitForElementVisible('@loginLayout')
|
||||
.waitForElementVisible('@signInForm')
|
||||
.setValue('@emailInput', admin.email)
|
||||
.setValue('@passwordInput', admin.password)
|
||||
.waitForElementVisible('@signInButton')
|
||||
.click('@signInButton');
|
||||
|
||||
client.pause(3000);
|
||||
|
||||
adminPage
|
||||
.waitForElementVisible('@moderationContainer');
|
||||
adminPage.navigateAndLogin(admin);
|
||||
},
|
||||
|
||||
'Admin goes to Stories': (client) => {
|
||||
const adminPage = client.page.admin();
|
||||
|
||||
adminPage
|
||||
.navigate()
|
||||
.waitForElementVisible('@storiesNav')
|
||||
.click('@storiesNav')
|
||||
.waitForElementVisible('@storiesSection');
|
||||
.openDrawer()
|
||||
.goToStories();
|
||||
},
|
||||
|
||||
'Admin goes to Community': (client) => {
|
||||
const adminPage = client.page.admin();
|
||||
|
||||
adminPage
|
||||
.navigate()
|
||||
.waitForElementVisible('@communityNav')
|
||||
.click('@communityNav')
|
||||
.waitForElementVisible('@communitySection');
|
||||
.openDrawer()
|
||||
.goToCommunity();
|
||||
},
|
||||
after: (client) => {
|
||||
client.end();
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1,5 +1,21 @@
|
||||
module.exports = {
|
||||
'@tags': ['embedStream', 'login'],
|
||||
|
||||
before: (client) => {
|
||||
client.resizeWindow(1600, 1200);
|
||||
},
|
||||
|
||||
afterEach: (client, done) => {
|
||||
if (client.currentTest.results.failed) {
|
||||
throw new Error('Test Case failed, skipping all the rest');
|
||||
}
|
||||
done();
|
||||
},
|
||||
|
||||
after: (client) => {
|
||||
client.end();
|
||||
},
|
||||
|
||||
'creates a new asset': (client) => {
|
||||
const asset = 'newAssetTest';
|
||||
const embedStream = client.page.embedStream();
|
||||
@@ -7,80 +23,43 @@ module.exports = {
|
||||
embedStream
|
||||
.navigateToAsset(asset)
|
||||
.assert.title(asset)
|
||||
.getEmbedSection();
|
||||
.ready();
|
||||
},
|
||||
|
||||
'creates an user and user logs in': (client) => {
|
||||
const {testData: {user}} = client.globals;
|
||||
const embedStream = client.page.embedStream();
|
||||
|
||||
const embed = embedStream
|
||||
// Go back to default asset.
|
||||
const comments =
|
||||
embedStream
|
||||
.navigate()
|
||||
.getEmbedSection();
|
||||
.ready();
|
||||
|
||||
embed
|
||||
.waitForElementVisible('@signInButton')
|
||||
.click('@signInButton');
|
||||
|
||||
client.pause(3000);
|
||||
|
||||
// Focusing on the Login PopUp
|
||||
client.windowHandles((result) => {
|
||||
const handle = result.value[1];
|
||||
client.switchWindow(handle);
|
||||
});
|
||||
|
||||
const login = client.page.login();
|
||||
|
||||
login
|
||||
.waitForElementVisible('@registerButton')
|
||||
.click('@registerButton')
|
||||
.setValue('@emailInput', user.email)
|
||||
.setValue('@usernameInput', user.username)
|
||||
.setValue('@passwordInput', user.password)
|
||||
.setValue('@confirmPasswordInput', user.password)
|
||||
.waitForElementVisible('@signUpButton')
|
||||
.click('@signUpButton')
|
||||
.waitForElementVisible('@signIn')
|
||||
.waitForElementVisible('@loginButton')
|
||||
.click('@loginButton');
|
||||
|
||||
// Focusing on the Embed Window
|
||||
client.windowHandles((result) => {
|
||||
const handle = result.value[0];
|
||||
client.switchWindow(handle);
|
||||
});
|
||||
comments
|
||||
.openLoginPopup((popup) => {
|
||||
popup.register(user);
|
||||
});
|
||||
},
|
||||
'user posts a comment': (client) => {
|
||||
const embedStream = client.page.embedStream();
|
||||
const comments = client.page.embedStream().section.comments;
|
||||
const {testData: {comment}} = client.globals;
|
||||
|
||||
const embed = embedStream
|
||||
.navigate()
|
||||
.getEmbedSection();
|
||||
|
||||
embed
|
||||
comments
|
||||
.waitForElementVisible('@commentBoxTextarea')
|
||||
.setValue('@commentBoxTextarea', comment.body)
|
||||
.waitForElementVisible('@commentBoxPostButton')
|
||||
.click('@commentBoxPostButton')
|
||||
.waitForElementVisible('@firstCommentContent')
|
||||
.getText('@firstCommentContent', (result) => {
|
||||
embed.assert.equal(result.value, comment.body);
|
||||
comments.assert.equal(result.value, comment.body);
|
||||
});
|
||||
},
|
||||
|
||||
'signed in user sees comment history': (client) => {
|
||||
const embedStream = client.page.embedStream();
|
||||
const profile = client.page.embedStream().goToProfileSection();
|
||||
const {testData: {comment}} = client.globals;
|
||||
|
||||
const embed = embedStream
|
||||
.navigate()
|
||||
.getEmbedSection();
|
||||
|
||||
const profile = embed
|
||||
.getProfileSection();
|
||||
|
||||
profile
|
||||
.waitForElementVisible('@myCommentHistory')
|
||||
.waitForElementVisible('@myCommentHistoryComment')
|
||||
@@ -89,14 +68,7 @@ module.exports = {
|
||||
});
|
||||
},
|
||||
'user sees replies and reactions to comments': (client) => {
|
||||
const embedStream = client.page.embedStream();
|
||||
|
||||
const embed = embedStream
|
||||
.navigate()
|
||||
.getEmbedSection();
|
||||
|
||||
const profile = embed
|
||||
.getProfileSection();
|
||||
const profile = client.page.embedStream().section.profile;
|
||||
|
||||
profile
|
||||
.waitForElementVisible('@myCommentHistory')
|
||||
@@ -108,17 +80,12 @@ module.exports = {
|
||||
},
|
||||
'user goes to the stream and replies and reacts to comment': (client) => {
|
||||
const embedStream = client.page.embedStream();
|
||||
|
||||
const embed = embedStream
|
||||
.navigate()
|
||||
.getEmbedSection();
|
||||
|
||||
embed
|
||||
const comments = embedStream.goToCommentsSection();
|
||||
comments
|
||||
.waitForElementVisible('@respectButton')
|
||||
.click('@respectButton');
|
||||
|
||||
const profile = embed
|
||||
.getProfileSection();
|
||||
const profile = embedStream.goToProfileSection();
|
||||
|
||||
profile
|
||||
.waitForElementVisible('@myCommentHistory')
|
||||
@@ -130,31 +97,16 @@ module.exports = {
|
||||
},
|
||||
'user logs out': (client) => {
|
||||
const embedStream = client.page.embedStream();
|
||||
const comments = embedStream.goToCommentsSection();
|
||||
|
||||
const embed = embedStream
|
||||
.navigate()
|
||||
.getEmbedSection();
|
||||
|
||||
embed
|
||||
.waitForElementVisible('@commentsTabButton')
|
||||
.click('@commentsTabButton')
|
||||
.waitForElementVisible('@logoutButton')
|
||||
.click('@logoutButton');
|
||||
comments
|
||||
.logout();
|
||||
},
|
||||
'not logged in user clicks my profile tab': (client) => {
|
||||
const embedStream = client.page.embedStream();
|
||||
|
||||
const embed = embedStream
|
||||
.navigate()
|
||||
.getEmbedSection();
|
||||
|
||||
const profile = embed
|
||||
.getProfileSection();
|
||||
const profile = embedStream.goToProfileSection();
|
||||
|
||||
profile
|
||||
.assert.visible('@notLoggedIn');
|
||||
},
|
||||
after: (client) => {
|
||||
client.end();
|
||||
}
|
||||
};
|
||||
|
||||
@@ -0,0 +1,117 @@
|
||||
module.exports = {
|
||||
|
||||
before: (client) => {
|
||||
client.resizeWindow(1600, 1200);
|
||||
},
|
||||
|
||||
afterEach: (client, done) => {
|
||||
if (client.currentTest.results.failed) {
|
||||
throw new Error('Test Case failed, skipping all the rest');
|
||||
}
|
||||
done();
|
||||
},
|
||||
|
||||
after: (client) => {
|
||||
client.end();
|
||||
},
|
||||
|
||||
'admin logs in': (client) => {
|
||||
const adminPage = client.page.admin();
|
||||
const {testData: {admin}} = client.globals;
|
||||
|
||||
adminPage.navigateAndLogin(admin);
|
||||
},
|
||||
'admin flags user\'s username as offensive': (client) => {
|
||||
const embedStream = client.page.embedStream();
|
||||
|
||||
const comments = embedStream
|
||||
.navigate()
|
||||
.ready();
|
||||
|
||||
comments
|
||||
.waitForElementVisible('@firstComment')
|
||||
.waitForElementVisible('@flagButton')
|
||||
.click('@flagButton');
|
||||
|
||||
comments.section.flag
|
||||
.waitForElementVisible('@flagUsernameRadio')
|
||||
.click('@flagUsernameRadio')
|
||||
.waitForElementVisible('@continueButton')
|
||||
.click('@continueButton')
|
||||
.waitForElementVisible('@offensiveUsernameRadio')
|
||||
.click('@offensiveUsernameRadio')
|
||||
.click('@continueButton')
|
||||
.waitForElementVisible('@popUpText')
|
||||
.click('@continueButton');
|
||||
},
|
||||
'admin goes to Reported Usernames': (client) => {
|
||||
const adminPage = client.page.admin();
|
||||
|
||||
const community = adminPage
|
||||
.navigate()
|
||||
.ready()
|
||||
.goToCommunity();
|
||||
|
||||
community
|
||||
.waitForElementVisible('@flaggedAccountsContainer')
|
||||
.waitForElementVisible('@flaggedUser');
|
||||
},
|
||||
'admin rejects the user flag': (client) => {
|
||||
const community = client.page.admin().section.community;
|
||||
|
||||
community
|
||||
.waitForElementVisible('@flaggedUserRejectButton')
|
||||
.click('@flaggedUserRejectButton');
|
||||
},
|
||||
'admin suspends the user': (client) => {
|
||||
const community = client.page.admin().section.community;
|
||||
|
||||
community
|
||||
.waitForElementVisible('@usernameDialog')
|
||||
.waitForElementVisible('@usernameDialogButtons')
|
||||
.waitForElementVisible('@usernameDialogSuspend')
|
||||
.click('@usernameDialogSuspend')
|
||||
.waitForElementVisible('@usernameDialogSuspensionMessage')
|
||||
.click('@usernameDialogSuspend')
|
||||
.waitForElementNotPresent('@flaggedUser');
|
||||
},
|
||||
'admin logs out': (client) => {
|
||||
client.page.admin().logout();
|
||||
},
|
||||
'user logs in': (client) => {
|
||||
const {testData: {user}} = client.globals;
|
||||
const embedStream = client.page.embedStream();
|
||||
|
||||
embedStream
|
||||
.navigate()
|
||||
.ready()
|
||||
.openLoginPopup((popup) => popup.login(user));
|
||||
},
|
||||
'user account is suspended, should see restricted message box': (client) => {
|
||||
const embedStream = client.page.embedStream();
|
||||
const comments = embedStream.section.comments;
|
||||
|
||||
comments
|
||||
.waitForElementVisible('@restrictedMessageBox');
|
||||
},
|
||||
'user picks another username': (client) => {
|
||||
const embedStream = client.page.embedStream();
|
||||
const comments = embedStream.section.comments;
|
||||
const {testData: {user}} = client.globals;
|
||||
|
||||
comments
|
||||
.waitForElementVisible('@suspendedAccountInput')
|
||||
.setValue('@suspendedAccountInput', `${user.username}_alternative`)
|
||||
.waitForElementVisible('@suspendedAccountSubmitButton')
|
||||
.click('@suspendedAccountSubmitButton')
|
||||
.waitForElementNotPresent('@suspendedAccountInput');
|
||||
},
|
||||
'user should not be able to comment': (client) => {
|
||||
const embedStream = client.page.embedStream();
|
||||
const comments = embedStream.section.comments;
|
||||
|
||||
comments
|
||||
.waitForElementNotPresent('@commentBoxTextarea')
|
||||
.waitForElementNotPresent('@commentBoxPostButton');
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,125 @@
|
||||
module.exports = {
|
||||
|
||||
before: (client) => {
|
||||
client.resizeWindow(1600, 1200);
|
||||
},
|
||||
|
||||
afterEach: (client, done) => {
|
||||
if (client.currentTest.results.failed) {
|
||||
throw new Error('Test Case failed, skipping all the rest');
|
||||
}
|
||||
done();
|
||||
},
|
||||
|
||||
after: (client) => {
|
||||
client.end();
|
||||
},
|
||||
|
||||
'admin logs in': (client) => {
|
||||
const adminPage = client.page.admin();
|
||||
const {testData: {admin}} = client.globals;
|
||||
|
||||
adminPage.navigateAndLogin(admin);
|
||||
},
|
||||
'navigate to the embed stream': (client) => {
|
||||
const embedStream = client.page.embedStream();
|
||||
|
||||
embedStream
|
||||
.navigate()
|
||||
.ready();
|
||||
},
|
||||
'admin bans user': (client) => {
|
||||
const embedStream = client.page.embedStream();
|
||||
const comments = embedStream.section.comments;
|
||||
|
||||
comments.section.mod
|
||||
.waitForElementVisible('@arrow')
|
||||
.click('@arrow')
|
||||
.waitForElementVisible('@menu')
|
||||
.waitForElementVisible('@banButton')
|
||||
.click('@banButton');
|
||||
|
||||
embedStream
|
||||
.waitForElementVisible('@banDialog')
|
||||
.waitForElementVisible('@banDialogConfirmButton')
|
||||
.click('@banDialogConfirmButton')
|
||||
.waitForElementNotVisible('@banDialog');
|
||||
},
|
||||
'admin logs out': (client) => {
|
||||
const comments = client.page.embedStream().section.comments;
|
||||
|
||||
comments
|
||||
.logout();
|
||||
},
|
||||
'user logs in': (client) => {
|
||||
const {testData: {user}} = client.globals;
|
||||
const comments = client.page.embedStream().section.comments;
|
||||
|
||||
comments
|
||||
.openLoginPopup((popup) => popup.login(user));
|
||||
},
|
||||
'user account is banned, should see restricted message box': (client) => {
|
||||
const embedStream = client.page.embedStream();
|
||||
const comments = embedStream.section.comments;
|
||||
|
||||
comments
|
||||
.waitForElementVisible('@restrictedMessageBox');
|
||||
},
|
||||
'user logs out': (client) => {
|
||||
const embedStream = client.page.embedStream();
|
||||
const comments = embedStream.section.comments;
|
||||
|
||||
comments
|
||||
.logout();
|
||||
},
|
||||
'admin logs in (2)': (client) => {
|
||||
const adminPage = client.page.admin();
|
||||
const {testData: {admin}} = client.globals;
|
||||
|
||||
adminPage.navigateAndLogin(admin);
|
||||
},
|
||||
'admin goes to community': (client) => {
|
||||
const adminPage = client.page.admin();
|
||||
|
||||
adminPage
|
||||
.goToCommunity()
|
||||
.goToPeople();
|
||||
},
|
||||
'admin removes ban from user': (client) => {
|
||||
const people = client.page.admin()
|
||||
.section.community
|
||||
.section.people;
|
||||
|
||||
people
|
||||
.waitForElementVisible('@firstRow')
|
||||
.waitForElementVisible('@dropdownStatus')
|
||||
.click('@dropdownStatus')
|
||||
.waitForElementVisible('@dropdownStatusActive')
|
||||
.click('@optionActive');
|
||||
},
|
||||
'admin logs out 2': (client) => {
|
||||
client.page.admin().logout();
|
||||
},
|
||||
'navigate to the embed stream 2': (client) => {
|
||||
const embedStream = client.page.embedStream();
|
||||
|
||||
embedStream
|
||||
.navigate()
|
||||
.ready();
|
||||
},
|
||||
'user logs in 2': (client) => {
|
||||
const {testData: {user}} = client.globals;
|
||||
const comments = client.page.embedStream().section.comments;
|
||||
|
||||
comments
|
||||
.openLoginPopup((popup) => popup.login(user));
|
||||
},
|
||||
'user should be able to comment': (client) => {
|
||||
const embedStream = client.page.embedStream();
|
||||
const comments = embedStream.section.comments;
|
||||
|
||||
comments
|
||||
.waitForElementVisible('@commentBoxTextarea')
|
||||
.waitForElementVisible('@commentBoxPostButton');
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,138 @@
|
||||
module.exports = {
|
||||
|
||||
before: (client) => {
|
||||
client.resizeWindow(1600, 1200);
|
||||
},
|
||||
|
||||
afterEach: (client, done) => {
|
||||
if (client.currentTest.results.failed) {
|
||||
throw new Error('Test Case failed, skipping all the rest');
|
||||
}
|
||||
done();
|
||||
},
|
||||
|
||||
after: (client) => {
|
||||
client.end();
|
||||
},
|
||||
'user logs in': (client) => {
|
||||
const {testData: {user}} = client.globals;
|
||||
const embedStream = client.page.embedStream();
|
||||
const comments = client.page.embedStream().section.comments;
|
||||
|
||||
embedStream
|
||||
.navigate()
|
||||
.ready();
|
||||
|
||||
comments
|
||||
.openLoginPopup((popup) => popup.login(user));
|
||||
},
|
||||
'user posts comment': (client) => {
|
||||
const comments = client.page.embedStream().section.comments;
|
||||
const {testData: {comment}} = client.globals;
|
||||
|
||||
comments
|
||||
.waitForElementVisible('@commentBoxTextarea')
|
||||
.setValue('@commentBoxTextarea', comment.body)
|
||||
.waitForElementVisible('@commentBoxPostButton')
|
||||
.click('@commentBoxPostButton')
|
||||
.waitForElementVisible('@firstCommentContent')
|
||||
.getText('@firstCommentContent', (result) => {
|
||||
comments.assert.equal(result.value, comment.body);
|
||||
});
|
||||
},
|
||||
'user logs out': (client) => {
|
||||
const embedStream = client.page.embedStream();
|
||||
const comments = embedStream.section.comments;
|
||||
|
||||
comments
|
||||
.logout();
|
||||
},
|
||||
'admin logs in': (client) => {
|
||||
const adminPage = client.page.admin();
|
||||
const {testData: {admin}} = client.globals;
|
||||
|
||||
adminPage.navigateAndLogin(admin);
|
||||
},
|
||||
'navigate to the embed stream': (client) => {
|
||||
const embedStream = client.page.embedStream();
|
||||
|
||||
embedStream
|
||||
.navigate()
|
||||
.ready();
|
||||
},
|
||||
'admin reports comment': (client) => {
|
||||
const embedStream = client.page.embedStream();
|
||||
const comments = embedStream.section.comments;
|
||||
|
||||
comments
|
||||
.waitForElementVisible('@firstComment')
|
||||
.waitForElementVisible('@flagButton')
|
||||
.click('@flagButton');
|
||||
|
||||
comments.section.flag
|
||||
.waitForElementVisible('@flagCommentRadio')
|
||||
.click('@flagCommentRadio')
|
||||
.waitForElementVisible('@continueButton')
|
||||
.click('@continueButton')
|
||||
.waitForElementVisible('@spamCommentRadio')
|
||||
.click('@spamCommentRadio')
|
||||
.click('@continueButton')
|
||||
.waitForElementVisible('@popUpText')
|
||||
.click('@continueButton');
|
||||
},
|
||||
'admin suspends user': (client) => {
|
||||
const adminPage = client.page.admin();
|
||||
const moderate = adminPage.section.moderate;
|
||||
|
||||
adminPage
|
||||
.navigate()
|
||||
.ready()
|
||||
.goToModerate();
|
||||
|
||||
moderate
|
||||
.waitForElementVisible('@comment')
|
||||
.waitForElementVisible('@commentActionMenu')
|
||||
.waitForElementVisible('@actionMenuButton')
|
||||
.click('@actionMenuButton')
|
||||
.waitForElementVisible('@actionItemSuspendUser')
|
||||
.click('@actionItemSuspendUser');
|
||||
|
||||
adminPage
|
||||
.waitForElementVisible('@suspendUserDialog')
|
||||
.waitForElementVisible('@suspendUserConfirmButton')
|
||||
.click('@suspendUserConfirmButton')
|
||||
.waitForElementVisible('@supendUserSendButton')
|
||||
.click('@supendUserSendButton');
|
||||
|
||||
adminPage
|
||||
.waitForElementVisible('@toast')
|
||||
.waitForElementVisible('@toastClose')
|
||||
.click('@toastClose');
|
||||
|
||||
},
|
||||
'admin logs out': (client) => {
|
||||
const adminPage = client.page.admin();
|
||||
|
||||
adminPage
|
||||
.logout();
|
||||
},
|
||||
'user logs in (2)': (client) => {
|
||||
const {testData: {user}} = client.globals;
|
||||
const embedStream = client.page.embedStream();
|
||||
const comments = client.page.embedStream().section.comments;
|
||||
|
||||
embedStream
|
||||
.navigate()
|
||||
.ready();
|
||||
|
||||
comments
|
||||
.openLoginPopup((popup) => popup.login(user));
|
||||
},
|
||||
'user account is suspended, should see restricted message box': (client) => {
|
||||
const embedStream = client.page.embedStream();
|
||||
const comments = embedStream.section.comments;
|
||||
|
||||
comments
|
||||
.waitForElementVisible('@restrictedMessageBox');
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,41 @@
|
||||
|
||||
/**
|
||||
* SortedWindowHandler assists in making e2e tests more robust by returning
|
||||
* deterministic window handles. An instance must be created before new windows
|
||||
* are created and windowHandles must be called each time a window was created or
|
||||
* closed.
|
||||
*/
|
||||
class SortedWindowHandler {
|
||||
|
||||
/**
|
||||
* Constructor, must be called before new windows were created.
|
||||
*/
|
||||
constructor(client) {
|
||||
this.client = client;
|
||||
this.client.windowHandles((result) => {
|
||||
this.handles = result.value;
|
||||
if (this.handles.length > 2) {
|
||||
throw new Error('SortedWindowHandler must be created before new windows were created.');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* windowHandles will call given `callback` with an array of window handles.
|
||||
*/
|
||||
windowHandles(callback) {
|
||||
this.client.windowHandles((result) => {
|
||||
this.handles = this.handles.filter((handle) => result.value.includes(handle));
|
||||
const remaining = result.value.filter((handle) => !this.handles.includes(handle));
|
||||
if (remaining.length === 1) {
|
||||
this.handles.push(remaining[0]);
|
||||
}
|
||||
if (remaining.length > 1) {
|
||||
throw new Error('Cannot detect new window handle, because more than one windows was created.');
|
||||
}
|
||||
callback(this.handles);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = SortedWindowHandler;
|
||||
@@ -1,8 +1,11 @@
|
||||
const UsersService = require('../../../services/users');
|
||||
const SettingsService = require('../../../services/settings');
|
||||
const MailerService = require('../../../services/mailer');
|
||||
|
||||
const chai = require('chai');
|
||||
chai.use(require('chai-as-promised'));
|
||||
const sinon = require('sinon');
|
||||
chai.use(require('sinon-chai'));
|
||||
const expect = chai.expect;
|
||||
|
||||
describe('services.UsersService', () => {
|
||||
@@ -15,7 +18,7 @@ describe('services.UsersService', () => {
|
||||
mockUsers = await UsersService.createLocalUsers([{
|
||||
email: 'stampi@gmail.com',
|
||||
username: 'Stampi',
|
||||
password: '1Coral!-'
|
||||
password: '1Coral!-',
|
||||
}, {
|
||||
email: 'sockmonster@gmail.com',
|
||||
username: 'Sockmonster',
|
||||
@@ -25,6 +28,12 @@ describe('services.UsersService', () => {
|
||||
username: 'Marvel',
|
||||
password: '3Coral!3'
|
||||
}]);
|
||||
|
||||
sinon.spy(MailerService, 'sendSimple');
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
MailerService.sendSimple.restore();
|
||||
});
|
||||
|
||||
describe('#findById()', () => {
|
||||
@@ -149,7 +158,11 @@ describe('services.UsersService', () => {
|
||||
.then(() => UsersService.findById(mockUsers[0].id))
|
||||
.then((user) => {
|
||||
expect(user).to.have.property('status', 'ACTIVE');
|
||||
})
|
||||
.then(() => {
|
||||
expect(MailerService.sendSimple).to.not.have.been.called;
|
||||
});
|
||||
|
||||
});
|
||||
});
|
||||
|
||||
@@ -188,6 +201,12 @@ describe('services.UsersService', () => {
|
||||
.then(() => UsersService.findById(mockUsers[0].id))
|
||||
.then((user) => {
|
||||
expect(user).to.have.property('status', 'BANNED');
|
||||
})
|
||||
.then(() => {
|
||||
expect(MailerService.sendSimple).to.have.been.calledWithMatch({
|
||||
template: 'banned',
|
||||
to: mockUsers[0].profiles[0].id
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -7953,9 +7953,9 @@ select@^1.1.2:
|
||||
version "1.1.2"
|
||||
resolved "https://registry.yarnpkg.com/select/-/select-1.1.2.tgz#0e7350acdec80b1108528786ec1d4418d11b396d"
|
||||
|
||||
selenium-standalone@6.9.0:
|
||||
version "6.9.0"
|
||||
resolved "https://registry.yarnpkg.com/selenium-standalone/-/selenium-standalone-6.9.0.tgz#5805f403c82b48b4e136d8a97f1832aad7852bd5"
|
||||
selenium-standalone@^6.11.0:
|
||||
version "6.11.0"
|
||||
resolved "https://registry.yarnpkg.com/selenium-standalone/-/selenium-standalone-6.11.0.tgz#eb21ff65f3b2ece75061b35d4f9e7572ac0280c2"
|
||||
dependencies:
|
||||
async "^2.1.4"
|
||||
commander "^2.9.0"
|
||||
|
||||
Reference in New Issue
Block a user