|
- {row.displayName}
+ {row.username}
{row.profiles.map(({id}) => id)}
|
diff --git a/client/coral-admin/src/containers/Install/components/Steps/CreateYourAccount.js b/client/coral-admin/src/containers/Install/components/Steps/CreateYourAccount.js
index 91c664c37..90deff640 100644
--- a/client/coral-admin/src/containers/Install/components/Steps/CreateYourAccount.js
+++ b/client/coral-admin/src/containers/Install/components/Steps/CreateYourAccount.js
@@ -21,12 +21,12 @@ const InitialStep = props => {
this.setState({singleView: !this.state.singleView}));
- key('shift+/', () => this.setState({modalOpen: true}));
- key('esc', () => this.setState({modalOpen: false}));
+
+ key('s', () => singleView());
+ key('shift+/', () => toggleModal(true));
+ key('esc', () => toggleModal(false));
}
componentWillUnmount() {
@@ -45,90 +36,85 @@ class ModerationContainer extends React.Component {
key.unbind('esc');
}
- componentDidMount() {
-
- // Hack for dynamic mdl tabs
- if (typeof componentHandler !== 'undefined') {
-
- // FIXME: fix this hack
- componentHandler.upgradeAllRegistered(); // eslint-disable-line no-undef
+ componentWillReceiveProps(nextProps) {
+ const {updateAssets} = this.props;
+ if(!isEqual(nextProps.data.assets, this.props.data.assets)) {
+ updateAssets(nextProps.data.assets);
}
}
- onTabClick(activeTab) {
- this.setState({activeTab});
-
- if (activeTab === 'premod') {
- this.props.fetchPremodQueue();
- } else if (activeTab === 'rejected') {
- this.props.fetchRejectedQueue();
- } else if (activeTab === 'flagged') {
- this.props.fetchFlaggedQueue();
- } else {
- this.props.fetchModerationQueueComments();
- }
- }
-
- onClose() {
- this.setState({modalOpen: false});
- }
-
render () {
- const {comments, actions, settings} = this.props;
- const premodIds = comments.ids.filter(id => comments.byId[id].status === 'PREMOD');
- const rejectedIds = comments.ids.filter(id => comments.byId[id].status === 'REJECTED');
- const flaggedIds = comments.ids.filter(id =>
- comments.byId[id].flagged === true &&
- comments.byId[id].status !== 'REJECTED' &&
- comments.byId[id].status !== 'ACCEPTED'
- );
- const userActionIds = actions.ids.filter(id => actions.byId[id].item_type === 'USERS');
+ const {data, moderation, settings, assets, ...props} = this.props;
+ const providedAssetId = this.props.params.id;
+ let asset;
- // show the Pre-Mod tab if premod is enabled globally OR there are pre-mod comments in the db.
- let enablePremodTab = (settings.settings && settings.settings.moderation === 'PRE') || premodIds.length;
+ if (data.loading) {
+ return ;
+ }
+ if (data.error) {
+ console.log(data);
+ return Error ;
+ }
+
+ if (providedAssetId) {
+ asset = assets.find(asset => asset.id === this.props.params.id);
+
+ if (!asset) {
+ return ;
+ }
+ }
+
+ const enablePremodTab = !!data.premod.length;
return (
-
+
+
+
+
+
+
);
}
}
const mapStateToProps = state => ({
- comments: state.comments.toJS(),
+ moderation: state.moderation.toJS(),
settings: state.settings.toJS(),
- users: state.users.toJS(),
- actions: state.actions.toJS(),
+ assets: state.assets.get('assets')
});
-const mapDispatchToProps = dispatch => {
- return {
- fetchSettings: () => dispatch(fetchSettings()),
- fetchModerationQueueComments: () => dispatch(fetchModerationQueueComments()),
- fetchPremodQueue: () => dispatch(fetchPremodQueue()),
- fetchRejectedQueue: () => dispatch(fetchRejectedQueue()),
- fetchFlaggedQueue: () => dispatch(fetchFlaggedQueue()),
- showBanUserDialog: (userId, userName, commentId) => dispatch(showBanUserDialog(userId, userName, commentId)),
- hideBanUserDialog: () => dispatch(hideBanUserDialog(false)),
- userStatusUpdate: (status, userId, commentId) => dispatch(userStatusUpdate(status, userId, commentId)).then(() => {
- dispatch(fetchModerationQueueComments());
- }),
- suspendUser: (userId, subject, text) => dispatch(userStatusUpdate('BANNED', userId))
- .then(() => dispatch(enableUsernameEdit(userId)))
- .then(() => dispatch(sendNotificationEmail(userId, subject, text)))
- .then(() => dispatch(fetchModerationQueueComments()))
- ,
- updateStatus: (action, comment) => dispatch(updateStatus(action, comment))
- };
-};
+const mapDispatchToProps = dispatch => ({
+ onTabClick: activeTab => dispatch(setActiveTab(activeTab)),
+ toggleModal: toggle => dispatch(toggleModal(toggle)),
+ onClose: () => dispatch(toggleModal(false)),
+ singleView: () => dispatch(singleView()),
+ updateAssets: assets => dispatch(updateAssets(assets)),
+ fetchSettings: () => dispatch(fetchSettings()),
+ showBanUserDialog: (user, commentId) => dispatch(showBanUserDialog(user, commentId)),
+ hideBanUserDialog: () => dispatch(hideBanUserDialog(false)),
+});
-export default connect(mapStateToProps, mapDispatchToProps)(ModerationContainer);
+export default compose(
+ connect(mapStateToProps, mapDispatchToProps),
+ setCommentStatus,
+ modQueueQuery,
+ banUser
+)(ModerationContainer);
diff --git a/client/coral-admin/src/containers/ModerationQueue/ModerationQueue.css b/client/coral-admin/src/containers/ModerationQueue/ModerationQueue.css
deleted file mode 100644
index 84ee8b923..000000000
--- a/client/coral-admin/src/containers/ModerationQueue/ModerationQueue.css
+++ /dev/null
@@ -1,53 +0,0 @@
-@custom-media --big-viewport (min-width: 780px);
-
-.listContainer {
- max-width: 860px;
- margin: 0 auto;
-}
-
-.tabBar {
- background: #262626;
- z-index: 5;
-}
-
-.tab {
- flex: 1;
- color: white;
- text-transform: capitalize;
- font-weight: 500;
- font-size: 15px;
- letter-spacing: 1px;
- transition: border-bottom 200ms;
-}
-
-.active {
- color: white;
- box-sizing: border-box;
- border-bottom: solid 5px #F36451;
-}
-
-.active > span {
- color: white;
-}
-
-.active:after {
- background: transparent !important;
-}
-
-.showShortcuts {
- position: absolute;
- right: 130px;
- display: flex;
- align-items: center;
- font-size: 13px;
-
- span {
- margin-left: 7px;
- }
-}
-
-@media (--big-viewport) {
- .tab {
- flex: none;
- }
-}
diff --git a/client/coral-admin/src/containers/ModerationQueue/ModerationQueue.js b/client/coral-admin/src/containers/ModerationQueue/ModerationQueue.js
index 09b02daed..052138590 100644
--- a/client/coral-admin/src/containers/ModerationQueue/ModerationQueue.js
+++ b/client/coral-admin/src/containers/ModerationQueue/ModerationQueue.js
@@ -1,212 +1,34 @@
import React, {PropTypes} from 'react';
-import styles from './ModerationQueue.css';
-import ModerationKeysModal from 'components/ModerationKeysModal';
-import ModerationList from 'components/ModerationList';
-import BanUserDialog from 'components/BanUserDialog';
+import Comment from './components/Comment';
+import {actionsMap} from './helpers/moderationQueueActionsMap';
-import I18n from 'coral-framework/modules/i18n/i18n';
-import translations from '../../translations.json';
-
-const lang = new I18n(translations);
-
-const ModerationQueue = (props) => (
-
-
-
-
- {
- props.activeTab === 'all' &&
-
-
-
-
- }
-
+const ModerationQueue = props => {
+ return (
+
+
{
- props.enablePremodTab
- ?
- {
- props.activeTab === 'premod' &&
-
-
-
-
- }
-
- : null
+ props.data[props.activeTab].map((comment, i) => {
+ return ;
+ })
}
-
-
- {
- props.activeTab === 'account' &&
-
-
-
-
- }
-
-
- {
- props.activeTab === 'flagged' &&
-
-
-
-
- }
-
-
- {
- props.activeTab === 'rejected' &&
-
-
-
-
- }
-
-
-
+
-
-);
+ );
+};
ModerationQueue.propTypes = {
- enablePremodTab: PropTypes.bool.isRequired
+ data: PropTypes.object.isRequired
};
export default ModerationQueue;
diff --git a/client/coral-admin/src/containers/ModerationQueue/components/Comment.js b/client/coral-admin/src/containers/ModerationQueue/components/Comment.js
new file mode 100644
index 000000000..ae70b303a
--- /dev/null
+++ b/client/coral-admin/src/containers/ModerationQueue/components/Comment.js
@@ -0,0 +1,79 @@
+import React from 'react';
+import timeago from 'timeago.js';
+import Linkify from 'react-linkify';
+import Highlighter from 'react-highlight-words';
+import {Link} from 'react-router';
+
+import styles from './styles.css';
+import {Icon} from 'coral-ui';
+import ActionButton from '../../../components/ActionButton';
+import FlagBox from './FlagBox';
+
+const linkify = new Linkify();
+
+import I18n from 'coral-framework/modules/i18n/i18n';
+import translations from 'coral-admin/src/translations.json';
+const lang = new I18n(translations);
+
+const Comment = ({actions = [], ...props}) => {
+ const links = linkify.getMatches(props.comment.body);
+ const actionSumaries = props.comment.action_summaries;
+ return (
+
+
+
+ {props.comment.user.name}
+
+ {timeago().format(props.comment.created_at || (Date.now() - props.index * 60 * 1000), lang.getLocale().replace('-', '_'))}
+
+ {props.comment.action_summaries ? {lang.t('comment.flagged')} : null}
+
+
+ {links ? Contains Link : null}
+
+ {actions.map((action, i) =>
+ props.acceptComment({commentId: props.comment.id})}
+ rejectComment={() => props.rejectComment({commentId: props.comment.id})}
+ showBanUserDialog={() => props.showBanUserDialog(props.comment.user, props.comment.id)}
+ />
+ )}
+
+ {props.comment.user.banned === 'banned' ?
+
+
+ {lang.t('comment.banned_user')}
+
+ : null}
+
+
+ {!props.currentAsset && (
+
+ Article: {props.comment.asset.title} Moderate Article
+
+ )}
+
+ {actionSumaries && }
+
+ {/* */}
+ {/* View context*/}
+ {/* */}
+
+ );
+};
+
+const linkStyles = {
+ backgroundColor: 'rgb(255, 219, 135)',
+ padding: '1px 2px'
+};
+
+export default Comment;
diff --git a/client/coral-admin/src/containers/ModerationQueue/components/FlagBox.js b/client/coral-admin/src/containers/ModerationQueue/components/FlagBox.js
new file mode 100644
index 000000000..bf5a34f29
--- /dev/null
+++ b/client/coral-admin/src/containers/ModerationQueue/components/FlagBox.js
@@ -0,0 +1,19 @@
+import React, {PropTypes} from 'react';
+import styles from './styles.css';
+
+const FlagBox = props => (
+
+ Flags:
+
+ {props.actionSumaries.map((action, i) =>
+ - {!action.reason ? No reason provided : action.reason} ({action.count})
+ )}
+
+
+);
+
+FlagBox.propTypes = {
+ actionSumaries: PropTypes.array.isRequired
+};
+
+export default FlagBox;
diff --git a/client/coral-admin/src/containers/ModerationQueue/components/ModerationHeader.js b/client/coral-admin/src/containers/ModerationQueue/components/ModerationHeader.js
new file mode 100644
index 000000000..474d8fd27
--- /dev/null
+++ b/client/coral-admin/src/containers/ModerationQueue/components/ModerationHeader.js
@@ -0,0 +1,25 @@
+import React from 'react';
+import {Link} from 'react-router';
+import styles from './styles.css';
+
+const ModerationHeader = props => (
+
+
+ {
+ props.asset ?
+
+ :
+
+ }
+
+
+);
+export default ModerationHeader;
diff --git a/client/coral-admin/src/containers/ModerationQueue/components/ModerationMenu.js b/client/coral-admin/src/containers/ModerationQueue/components/ModerationMenu.js
new file mode 100644
index 000000000..51605c443
--- /dev/null
+++ b/client/coral-admin/src/containers/ModerationQueue/components/ModerationMenu.js
@@ -0,0 +1,59 @@
+import React, {PropTypes} from 'react';
+import styles from './styles.css';
+import I18n from 'coral-framework/modules/i18n/i18n';
+import translations from 'coral-admin/src/translations.json';
+
+const lang = new I18n(translations);
+
+const ModerationMenu = (props) => (
+
+);
+
+ModerationMenu.propTypes = {
+ activeTab: PropTypes.string.isRequired,
+ enablePremodTab: PropTypes.bool
+};
+
+export default ModerationMenu;
diff --git a/client/coral-admin/src/containers/ModerationQueue/components/NotFoundAsset.js b/client/coral-admin/src/containers/ModerationQueue/components/NotFoundAsset.js
new file mode 100644
index 000000000..ffa1adfcd
--- /dev/null
+++ b/client/coral-admin/src/containers/ModerationQueue/components/NotFoundAsset.js
@@ -0,0 +1,14 @@
+import React from 'react';
+import {Link} from 'react-router';
+import styles from './styles.css';
+
+const NotFound = props => (
+
+
+ The provided asset id {props.assetId} does not exist.
+ Go to Streams
+
+
+);
+
+export default NotFound;
diff --git a/client/coral-admin/src/containers/ModerationQueue/components/styles.css b/client/coral-admin/src/containers/ModerationQueue/components/styles.css
new file mode 100644
index 000000000..658c9a059
--- /dev/null
+++ b/client/coral-admin/src/containers/ModerationQueue/components/styles.css
@@ -0,0 +1,327 @@
+@custom-media --big-viewport (min-width: 780px);
+
+.listContainer {
+ max-width: 860px;
+ margin: 0 auto;
+}
+
+.tabBar {
+ background-color: rgba(44, 44, 44, 0.89);
+ z-index: 5;
+}
+
+.tab {
+ flex: 1;
+ color: white;
+ text-transform: capitalize;
+ font-weight: 500;
+ font-size: 15px;
+ letter-spacing: 1px;
+ transition: border-bottom 200ms;
+}
+
+.active {
+ color: white;
+ box-sizing: border-box;
+ border-bottom: solid 5px #F36451;
+}
+
+.active > span {
+ color: white;
+}
+
+.active:after {
+ background: transparent !important;
+}
+
+.showShortcuts {
+ position: absolute;
+ right: 130px;
+ display: flex;
+ align-items: center;
+ font-size: 13px;
+
+span {
+ margin-left: 7px;
+}
+}
+
+@media (--big-viewport) {
+ .tab {
+ flex: none;
+ }
+}
+
+.notFound {
+ position: relative;
+ margin: 20px auto;
+ text-align: center;
+ padding: 68px 45px;
+ vertical-align: middle;
+ min-width: 500px;
+
+ a {
+ color: rgb(244, 126, 107);
+ font-weight: 500;
+
+ &.goToStreams {
+ position: absolute;
+ right: 10px;
+ bottom: 10px;
+ }
+ }
+}
+
+.header {
+ background-color: #2c2c2c;
+ color: white;
+ margin-bottom: -1px;
+
+ .moderateAsset {
+ a {
+ -webkit-box-flex: 1;
+ -ms-flex: 1;
+ flex: 1;
+ color: white;
+ text-transform: capitalize;
+ font-weight: 500;
+ font-size: 15px;
+ letter-spacing: 1px;
+ transition: opacity 200ms;
+ opacity: 1;
+
+ &:hover {
+ opacity: .8;
+ cursor: pointer;
+ }
+
+ &:first-child {
+ text-align: left;
+ }
+
+ &:nth-child(2) {
+ text-align: center;
+ }
+
+ &:last-child {
+ text-align: right;
+ }
+ }
+ }
+}
+
+
+@custom-media --big-viewport (min-width: 780px);
+
+.list {
+ padding: 8px 0;
+ list-style: none;
+ display: block;
+
+ &.singleView .listItem {
+ display: none;
+ }
+
+ &.singleView .listItem.activeItem {
+ display: block;
+ height: 100%;
+ font-size: 1.5em;
+ line-height: 1.5em;
+ border: none;
+
+ .actions {
+ position: fixed;
+ bottom: 60px;
+ left: 25%;
+ margin: 0 auto;
+ display: flex;
+ justify-content: space-around;
+ width: 50%;
+ margin: 0;
+ }
+
+ .actionButton {
+ transform: scale(1.4);
+ }
+ }
+}
+
+.listItem {
+ border-bottom: 1px solid #e0e0e0;
+ font-size: 16px;
+ width: 100%;
+ max-width: 660px;
+ min-width: 400px;
+ margin: 0 auto;
+ padding: 16px 14px;
+ position: relative;
+ transition: box-shadow 200ms;
+ margin-top: 0;
+
+
+ &:hover {
+ box-shadow: 0 3px 6px rgba(0,0,0,0.16), 0 3px 6px rgba(0,0,0,0.23);
+ }
+
+ &:last-child {
+ border-bottom: none;
+ }
+
+ .context {
+ a {
+ color: #f36451;
+ text-decoration: underline;
+ float: right;
+ }
+ }
+
+ .sideActions {
+ position: absolute;
+ right: 0;
+ height: 100%;
+ top: 0;
+ padding: 40px 18px;
+ box-sizing: border-box;
+ }
+
+ .itemHeader {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+
+ .author {
+ min-width: 230px;
+ display: flex;
+ align-items: center;
+ }
+ }
+
+ .itemBody {
+ display: flex;
+ justify-content: space-between;
+ }
+
+ .avatar {
+ margin-right: 16px;
+ height: 40px;
+ width: 40px;
+ border-radius: 50%;
+ background-color: #757575;
+ font-size: 40px;
+ color: #fff;
+ }
+
+ .created {
+ color: #666;
+ font-size: 13px;
+ margin-left: 40px;
+ }
+
+ .actionButton {
+ transform: scale(.8);
+ margin: 0;
+ }
+
+ .body {
+ margin-top: 0px;
+ flex: 1;
+ font-size: 0.88em;
+ color: black;
+ max-width: 500px;
+ word-wrap: break-word;
+ }
+
+ .flagged {
+ color: rgba(255, 0, 0, .5);
+ padding-top: 15px;
+ padding-left: 10px;
+ }
+
+ .flagCount{
+ font-size: 12px;
+ color: #d32f2f;
+ }
+
+}
+
+.empty {
+ color: #444;
+ margin-top: 50px;
+ text-align: center;
+}
+
+
+@media (--big-viewport) {
+ .listItem {
+ border: 1px solid #e0e0e0;
+ margin-bottom: 30px;
+
+ &:last-child {
+ border-bottom: 1px solid #e0e0e0;
+ }
+
+ &.activeItem {
+ border: 2px solid #333;
+ }
+ }
+
+}
+
+.hasLinks {
+ color: #f00;
+ text-align: right;
+ display: flex;
+ align-items: center;
+
+ i {
+ margin-right: 5px;
+ }
+}
+
+.banned {
+ color: #f00;
+ text-align: left;
+ display: flex;
+ align-items: center;
+
+ i {
+ margin-right: 5px;
+ }
+}
+
+.ban {
+ display: block;
+ text-align: center;
+ margin-top: 5px;
+}
+
+.Comment {
+ .moderateArticle {
+ font-size: 12px;
+ a {
+ display: inline-block;
+ color: #679af3;
+ text-decoration: none;
+ font-size: 1em;
+ font-weight: 400;
+ letter-spacing: .5px;
+ font-size: 12px;
+ margin-left: 10px;
+
+ &:hover {
+ text-decoration: underline;
+ opacity: .9;
+ cursor: pointer;
+ }
+ }
+ }
+}
+
+.flagBox {
+ border-top: 1px solid rgba(66, 66, 66, 0.12);
+ h3 {
+ font-size: 14px;
+ margin: 0;
+ font-weight: 500;
+ }
+}
diff --git a/client/coral-admin/src/containers/ModerationQueue/helpers/moderationQueueActionsMap.js b/client/coral-admin/src/containers/ModerationQueue/helpers/moderationQueueActionsMap.js
new file mode 100644
index 000000000..0da93e898
--- /dev/null
+++ b/client/coral-admin/src/containers/ModerationQueue/helpers/moderationQueueActionsMap.js
@@ -0,0 +1,13 @@
+export const actionsMap = {
+ PREMOD: ['REJECT', 'APPROVE', 'BAN'],
+ FLAGGED: ['REJECT', 'APPROVE'],
+ REJECTED: ['APPROVE']
+};
+
+export const menuActionsMap = {
+ 'REJECT': {status: 'REJECTED', icon: 'close', key: 'r'},
+ 'APPROVE': {status: 'ACCEPTED', icon: 'done', key: 't'},
+ 'FLAGGED': {status: 'FLAGGED', icon: 'flag', filter: 'Untouched'},
+ 'BAN': {status: 'BANNED', icon: 'not interested'},
+ '': {icon: 'done'}
+};
diff --git a/client/coral-admin/src/containers/Streams/Streams.css b/client/coral-admin/src/containers/Streams/Streams.css
index 26fb4f74f..d8a75a4ce 100644
--- a/client/coral-admin/src/containers/Streams/Streams.css
+++ b/client/coral-admin/src/containers/Streams/Streams.css
@@ -55,6 +55,12 @@
border-left: none;
border-right: none;
+ a {
+ color: rgb(44, 44, 44);
+ font-weight: 500;
+ text-decoration: none;
+ }
+
th {
font-size: 1.1em;
}
diff --git a/client/coral-admin/src/containers/Streams/Streams.js b/client/coral-admin/src/containers/Streams/Streams.js
index 1937f82c0..62f2f009e 100644
--- a/client/coral-admin/src/containers/Streams/Streams.js
+++ b/client/coral-admin/src/containers/Streams/Streams.js
@@ -4,14 +4,10 @@ import {connect} from 'react-redux';
import I18n from 'coral-framework/modules/i18n/i18n';
import {fetchAssets, updateAssetState} from '../../actions/assets';
import translations from '../../translations.json';
-import {
- RadioGroup,
- Radio,
- Icon,
- DataTable,
- TableHeader
-} from 'react-mdl';
-import Pager from 'coral-ui/components/Pager';
+import {Link} from 'react-router';
+
+import {Pager, Icon} from 'coral-ui';
+import {DataTable, TableHeader, RadioGroup, Radio} from 'react-mdl';
const limit = 25;
@@ -74,6 +70,8 @@ class Streams extends Component {
}
}
+ renderTitle = (title, {id}) => {title}
+
renderStatus = (closedAt, {id}) => {
const closed = closedAt && new Date(closedAt).getTime() < Date.now();
const statusMenuOpen = this.state.statusMenus[id];
@@ -104,6 +102,9 @@ class Streams extends Component {
render () {
const {search, sort, filter} = this.state;
const {assets} = this.props;
+
+ const assetsIds = assets.ids.map((id) => assets.byId[id]);
+
return (
@@ -142,16 +143,14 @@ class Streams extends Component {
- assets.byId[id])}>
- {lang.t('streams.article')}
-
- {lang.t('streams.pubdate')}
-
-
- {lang.t('streams.status')}
-
+
+ {lang.t('streams.article')}
+
+ {lang.t('streams.pubdate')}
+
+
+ {lang.t('streams.status')}
+
{
assets: assets.toJS()
};
};
+
const mapDispatchToProps = (dispatch) => {
return {
fetchAssets: (...args) => {
diff --git a/client/coral-admin/src/graphql/fragments/commentView.graphql b/client/coral-admin/src/graphql/fragments/commentView.graphql
new file mode 100644
index 000000000..e78c28a28
--- /dev/null
+++ b/client/coral-admin/src/graphql/fragments/commentView.graphql
@@ -0,0 +1,15 @@
+fragment commentView on Comment {
+ id
+ body
+ created_at
+ status
+ user {
+ id
+ name: username
+ status
+ }
+ asset {
+ id
+ title
+ }
+}
diff --git a/client/coral-admin/src/graphql/mutations/index.js b/client/coral-admin/src/graphql/mutations/index.js
new file mode 100644
index 000000000..fe3a1faf9
--- /dev/null
+++ b/client/coral-admin/src/graphql/mutations/index.js
@@ -0,0 +1,38 @@
+import {graphql} from 'react-apollo';
+import SET_USER_STATUS from './setUserStatus.graphql';
+import SET_COMMENT_STATUS from './setCommentStatus.graphql';
+
+export const banUser = graphql(SET_USER_STATUS, {
+ props: ({mutate}) => ({
+ banUser: ({userId}) => {
+ return mutate({
+ variables: {
+ userId,
+ status: 'BANNED'
+ }
+ });
+ }}),
+});
+
+export const setCommentStatus = graphql(SET_COMMENT_STATUS, {
+ props: ({mutate}) => ({
+ acceptComment: ({commentId}) => {
+ return mutate({
+ variables: {
+ commentId,
+ status: 'ACCEPTED'
+ },
+ refetchQueries: ['ModQueue']
+ });
+ },
+ rejectComment: ({commentId}) => {
+ return mutate({
+ variables: {
+ commentId,
+ status: 'REJECTED'
+ },
+ refetchQueries: ['ModQueue']
+ });
+ }
+ })
+});
diff --git a/client/coral-admin/src/graphql/mutations/setCommentStatus.graphql b/client/coral-admin/src/graphql/mutations/setCommentStatus.graphql
new file mode 100644
index 000000000..7ff6173a8
--- /dev/null
+++ b/client/coral-admin/src/graphql/mutations/setCommentStatus.graphql
@@ -0,0 +1,7 @@
+mutation setCommentStatus($commentId: ID!, $status: COMMENT_STATUS!){
+ setCommentStatus(id: $commentId, status: $status) {
+ errors {
+ translation_key
+ }
+ }
+}
diff --git a/client/coral-admin/src/graphql/mutations/setUserStatus.graphql b/client/coral-admin/src/graphql/mutations/setUserStatus.graphql
new file mode 100644
index 000000000..32fcf7e20
--- /dev/null
+++ b/client/coral-admin/src/graphql/mutations/setUserStatus.graphql
@@ -0,0 +1,7 @@
+mutation setUserStatus($userId: ID!, $status: USER_STATUS!) {
+ setUserStatus(id: $userId, status: $status) {
+ errors {
+ translation_key
+ }
+ }
+}
diff --git a/client/coral-admin/src/graphql/queries/assetsQuery.graphql b/client/coral-admin/src/graphql/queries/assetsQuery.graphql
new file mode 100644
index 000000000..37950692d
--- /dev/null
+++ b/client/coral-admin/src/graphql/queries/assetsQuery.graphql
@@ -0,0 +1,6 @@
+query Assets {
+ assets {
+ id
+ title
+ }
+}
diff --git a/client/coral-admin/src/graphql/queries/index.js b/client/coral-admin/src/graphql/queries/index.js
new file mode 100644
index 000000000..fc59f8f84
--- /dev/null
+++ b/client/coral-admin/src/graphql/queries/index.js
@@ -0,0 +1,12 @@
+import {graphql} from 'react-apollo';
+import MOD_QUEUE_QUERY from './modQueueQuery.graphql';
+
+export const modQueueQuery = graphql(MOD_QUEUE_QUERY, {
+ options: ({params: {id = ''}}) => {
+ return {
+ variables: {
+ asset_id: id
+ }
+ };
+ }
+});
diff --git a/client/coral-admin/src/graphql/queries/modQueueQuery.graphql b/client/coral-admin/src/graphql/queries/modQueueQuery.graphql
new file mode 100644
index 000000000..9eba7a971
--- /dev/null
+++ b/client/coral-admin/src/graphql/queries/modQueueQuery.graphql
@@ -0,0 +1,38 @@
+#import "../fragments/commentView.graphql"
+
+query ModQueue ($asset_id: ID!) {
+ all: comments(query: {
+ statuses: [REJECTED, PREMOD],
+ asset_id: $asset_id
+ }) {
+ ...commentView
+ }
+ premod: comments(query: {
+ statuses: [PREMOD],
+ asset_id: $asset_id
+ }) {
+ ...commentView
+ }
+ flagged: comments(query: {
+ action_type: FLAG,
+ asset_id: $asset_id
+ }) {
+ ...commentView
+ action_summaries {
+ count
+ ... on FlagActionSummary {
+ reason
+ }
+ }
+ }
+ rejected: comments(query: {
+ statuses: [REJECTED],
+ asset_id: $asset_id
+ }) {
+ ...commentView
+ }
+ assets: assets {
+ id
+ title
+ }
+}
diff --git a/client/coral-admin/src/reducers/actions.js b/client/coral-admin/src/reducers/actions.js
deleted file mode 100644
index 284e41c72..000000000
--- a/client/coral-admin/src/reducers/actions.js
+++ /dev/null
@@ -1,27 +0,0 @@
-import {Map, Set, fromJS} from 'immutable';
-import * as types from '../constants/actions';
-
-const initialState = Map({
- ids: Set(),
- byId: Map()
-});
-
-export default (state = initialState, action) => {
- switch (action.type) {
- case types.ACTIONS_MODERATION_QUEUE_FETCH_SUCCESS: return addActions(state, action);
- default:
- return state;
- }
-};
-
-const addActions = (state, action) => {
-
- // Make ids that are unique by item_id and by action type
- const typeId = (action) => `${action.action_type}_${action.item_id}`;
- const ids = action.actions.map(action => typeId(action));
- const map = action.actions.reduce((memo, action) => {
- memo[typeId(action)] = action;
- return memo;
- }, {});
- return state.set('byId', fromJS(map)).set('ids', new Set(ids));
-};
diff --git a/client/coral-admin/src/reducers/assets.js b/client/coral-admin/src/reducers/assets.js
index 77d0bf081..c9a82f1c5 100644
--- a/client/coral-admin/src/reducers/assets.js
+++ b/client/coral-admin/src/reducers/assets.js
@@ -1,20 +1,26 @@
import {Map, List, fromJS} from 'immutable';
-import {FETCH_ASSETS_SUCCESS, UPDATE_ASSET_STATE_REQUEST} from '../constants/assets';
+import * as actions from '../constants/assets';
const initialState = Map({
byId: Map(),
- ids: List()
+ ids: List(),
+ assets: List()
});
-export default (state = initialState, action) => {
+export default function assets (state = initialState, action) {
switch (action.type) {
- case FETCH_ASSETS_SUCCESS:
+ case actions.FETCH_ASSETS_SUCCESS:
return replaceAssets(action, state);
- case UPDATE_ASSET_STATE_REQUEST:
- return state.setIn(['byId', action.id, 'closedAt'], action.closedAt);
- default: return state;
+ case actions.UPDATE_ASSET_STATE_REQUEST:
+ return state
+ .setIn(['byId', action.id, 'closedAt'], action.closedAt);
+ case actions.UPDATE_ASSETS:
+ return state
+ .set('assets', List(action.assets));
+ default:
+ return state;
}
-};
+}
const replaceAssets = (action, state) => {
const assets = fromJS(action.assets.reduce((prev, curr) => { prev[curr.id] = curr; return prev; }, {}));
diff --git a/client/coral-admin/src/reducers/comments.js b/client/coral-admin/src/reducers/comments.js
deleted file mode 100644
index 034016088..000000000
--- a/client/coral-admin/src/reducers/comments.js
+++ /dev/null
@@ -1,77 +0,0 @@
-import * as actions from '../constants/comments';
-import * as userActions from '../constants/users';
-import {Map, List, fromJS} from 'immutable';
-
-/**
- * Comments state is stored using 2 structures:
- * - byId is a Map holding the comments using the item_id property as keys
- * - ids is a List of item_id, this allows us to order and iterate easily
- * since maps are unordered and some times we just need a list of things
- */
-
-const initialState = Map({
- byId: Map(),
- ids: List(),
- loading: false,
- showBanUserDialog: false,
- banUser: {
- 'userName': '',
- 'userId': '',
- 'commentId': ''
- }
-});
-
-// Handle the comment actions
-export default (state = initialState, action) => {
- switch (action.type) {
- case actions.COMMENTS_MODERATION_QUEUE_FETCH_REQUEST: return state.set('loading', true);
- case actions.COMMENTS_MODERATION_QUEUE_FETCH_SUCCESS: return replaceComments(action, state);
- case actions.COMMENTS_MODERATION_QUEUE_FAILED: return state.set('loading', false);
- case actions.COMMENT_STATUS_UPDATE_REQUEST: return updateStatus(state, action);
- case actions.COMMENT_FLAG: return flag(state, action);
- case actions.COMMENT_CREATE_SUCCESS: return addComment(state, action);
- case actions.COMMENT_STREAM_FETCH_SUCCESS: return replaceComments(action, state);
- case actions.SHOW_BANUSER_DIALOG: return setBanUser(state, true, action);
- case actions.HIDE_BANUSER_DIALOG: return setBanUser(state, false, action);
- case actions.USER_BAN_SUCCESS: return setBanUser(state, false, action);
- case userActions.UPDATE_STATUS_SUCCESS: return setBanUser(state, false, action);
- default: return state;
- }
-};
-
-// hide or show the UI for the dialog confirming the ban
-// set the user that is going to set and the comment that is the reason
-const setBanUser = (state, showBanUser, action) => {
- const banUser = {'userName': action.userName, 'userId': action.userId, 'commentId': action.commentId};
- return state.set('showBanUserDialog', showBanUser)
- .set('banUser', banUser);
-};
-
-// Update a comment status
-const updateStatus = (state, action) => {
- const byId = state.get('byId');
- const data = byId.get(action.id).set('status', action.status.toLowerCase());
- return state.set('byId', byId.set(action.id, data));
-};
-
-// Flag a comment
-const flag = (state, action) => {
- const byId = state.get('byId');
- const data = byId.get(action.id).set('flagged', true);
- const comment = byId.get(action.id).set('data', data);
- return state.set('byId', byId.set(action.id, comment));
-};
-
-// Replace the comment list with a new one
-const replaceComments = (action, state) => {
- const comments = fromJS(action.comments.reduce((prev, curr) => { prev[curr.id] = curr; return prev; }, {}));
- return state.set('byId', comments).set('loading', false)
- .set('ids', List(comments.keys()));
-};
-
-// Add a new comment
-const addComment = (state, action) => {
- const comment = fromJS(action.comment);
- return state.set('byId', state.get('byId').set(comment.get('item_id'), comment))
- .set('ids', state.get('ids').unshift(comment.get('item_id')));
-};
diff --git a/client/coral-admin/src/reducers/index.js b/client/coral-admin/src/reducers/index.js
index f12de96ba..e58f5bf5a 100644
--- a/client/coral-admin/src/reducers/index.js
+++ b/client/coral-admin/src/reducers/index.js
@@ -1,19 +1,13 @@
-import auth from 'reducers/auth';
-import users from 'reducers/users';
-import assets from 'reducers/assets';
-import actions from 'reducers/actions';
-import install from 'reducers/install';
-import comments from 'reducers/comments';
-import settings from 'reducers/settings';
-import community from 'reducers/community';
+import auth from './auth';
+import assets from './assets';
+import settings from './settings';
+import community from './community';
+import moderation from './moderation';
export default {
- settings,
- comments,
- community,
auth,
- actions,
assets,
- users,
- install
+ settings,
+ community,
+ moderation
};
diff --git a/client/coral-admin/src/reducers/install.js b/client/coral-admin/src/reducers/install.js
index a49f76b5e..596fd16cd 100644
--- a/client/coral-admin/src/reducers/install.js
+++ b/client/coral-admin/src/reducers/install.js
@@ -9,7 +9,7 @@ const initialState = Map({
organizationName: ''
}),
user: Map({
- displayName: '',
+ username: '',
email: '',
password: '',
confirmPassword: ''
@@ -17,7 +17,7 @@ const initialState = Map({
}),
errors: Map({
organizationName: '',
- displayName: '',
+ username: '',
email: '',
password: '',
confirmPassword: ''
diff --git a/client/coral-admin/src/reducers/moderation.js b/client/coral-admin/src/reducers/moderation.js
new file mode 100644
index 000000000..3131f6096
--- /dev/null
+++ b/client/coral-admin/src/reducers/moderation.js
@@ -0,0 +1,37 @@
+import {Map} from 'immutable';
+import * as actions from '../constants/moderation';
+
+const initialState = Map({
+ activeTab: 'all',
+ singleView: false,
+ modalOpen: false,
+ user: Map({}),
+ commentId: null,
+ banDialog: false
+});
+
+export default function moderation (state = initialState, action) {
+ switch (action.type) {
+ case actions.HIDE_BANUSER_DIALOG:
+ return state
+ .set('banDialog', false);
+ case actions.SHOW_BANUSER_DIALOG:
+ return state
+ .merge({
+ user: Map(action.user),
+ commentId: action.commentId,
+ banDialog: true
+ });
+ case actions.SET_ACTIVE_TAB:
+ return state
+ .set('activeTab', action.activeTab);
+ case actions.TOGGLE_MODAL:
+ return state
+ .set('modalOpen', action.open);
+ case actions.SINGLE_VIEW:
+ return state
+ .set('singleView', !state.get('singleView'));
+ default :
+ return state;
+ }
+}
diff --git a/client/coral-admin/src/reducers/settings.js b/client/coral-admin/src/reducers/settings.js
index 4f743bc0a..70c52f028 100644
--- a/client/coral-admin/src/reducers/settings.js
+++ b/client/coral-admin/src/reducers/settings.js
@@ -1,5 +1,5 @@
import {Map, List} from 'immutable';
-import * as types from '../actions/settings';
+import * as actions from '../actions/settings';
const initialState = Map({
settings: Map({
@@ -16,48 +16,49 @@ const initialState = Map({
fetchingSettings: false
});
-// Handle the comment actions
-export default (state = initialState, action) => {
+export default function settings (state = initialState, action) {
switch (action.type) {
- case types.SETTINGS_LOADING: return state.set('fetchingSettings', true).set('fetchSettingsError', null);
- case types.SETTINGS_RECEIVED: return updateSettings(state, action);
- case types.SETTINGS_FETCH_ERROR: return settingsFetchFailed(state, action);
- case types.SETTINGS_UPDATED: return updateSettings(state, action);
- case types.SAVE_SETTINGS_LOADING: return state.set('fetchingSettings', true).set('saveSettingsError', null);
- case types.SAVE_SETTINGS_SUCCESS: return saveComplete(state, action);
- case types.SAVE_SETTINGS_FAILED: return settingsSaveFailed(state, action);
- case types.WORDLIST_UPDATED: return updateWordlist(state, action);
- case types.DOMAINLIST_UPDATED: return updateDomainlist(state, action);
- default: return state;
+ case actions.SETTINGS_LOADING:
+ return state
+ .set('fetchingSettings', true)
+ .set('fetchSettingsError', null);
+ case actions.SETTINGS_RECEIVED:
+ return state.merge({
+ fetchingSettings: null,
+ fetchSettingsError: null,
+ ...action.settings
+ });
+ case actions.SETTINGS_FETCH_ERROR:
+ return state
+ .set('fetchingSettings', false)
+ .set('fetchSettingsError', action.error);
+ case actions.SETTINGS_UPDATED:
+ return state.merge({
+ fetchingSettings: null,
+ fetchSettingsError: null,
+ ...action.settings
+ });
+ case actions.SAVE_SETTINGS_LOADING:
+ return state
+ .set('fetchingSettings', true)
+ .set('saveSettingsError', null);
+ case actions.SAVE_SETTINGS_SUCCESS:
+ return state.merge({
+ fetchingSettings: false,
+ fetchSettingsError: null,
+ ...action.settings
+ });
+ case actions.SAVE_SETTINGS_FAILED:
+ return state
+ .set('fetchingSettings', false)
+ .set('fetchSettingsError', action.error);
+ case actions.WORDLIST_UPDATED:
+ return state
+ .setIn(['settings', 'wordlist', action.listName], action.list);
+ case actions.DOMAINLIST_UPDATED:
+ return state
+ .setIn(['settings', 'domains', action.listName], action.list);
+ default:
+ return state;
}
-};
-
-// only for updating top-level settings
-const updateSettings = (state, action) => {
- const s = state.set('fetchingSettings', false).set('fetchSettingsError', null);
- const settings = s.get('settings').merge(action.settings);
- return s.set('settings', settings);
-};
-
-// any nested settings must have a specialized setter
-const updateWordlist = (state, action) => {
- return state.setIn(['settings', 'wordlist', action.listName], action.list);
-};
-
-const updateDomainlist = (state, action) => {
- return state.setIn(['settings', 'domains', action.listName], action.list);
-};
-
-const saveComplete = (state, action) => {
- const s = state.set('fetchingSettings', false).set('saveSettingsError', null);
- const settings = s.get('settings').merge(action.settings);
- return s.set('settings', settings);
-};
-
-const settingsFetchFailed = (state, action) => {
- return state.set('fetchingSettings', false).set('fetchSettingsError', action.error);
-};
-
-const settingsSaveFailed = (state, action) => {
- return state.set('fetchingSettings', false).set('fetchSettingsError', action.error);
-};
+}
diff --git a/client/coral-admin/src/reducers/users.js b/client/coral-admin/src/reducers/users.js
deleted file mode 100644
index ef589c155..000000000
--- a/client/coral-admin/src/reducers/users.js
+++ /dev/null
@@ -1,28 +0,0 @@
-import {Map, List, fromJS} from 'immutable';
-
-const initialState = Map({
- byId: Map(),
- ids: List()
-});
-
-export default (state = initialState, action) => {
- switch (action.type) {
- case 'USERS_MODERATION_QUEUE_FETCH_SUCCESS': return replaceUsers(action, state);
- case 'USER_STATUS_UPDATE': return updateUserStatus(state, action);
- default: return state;
- }
-};
-
-// Replace the comment list with a new one
-const replaceUsers = (action, state) => {
- const users = fromJS(action.users.reduce((prev, curr) => { prev[curr.id] = curr; return prev; }, {}));
- return state.set('byId', users)
- .set('ids', List(users.keys()));
-};
-
-// Update a user status
-const updateUserStatus = (state, action) => {
- const byId = state.get('byId');
- const data = byId.get(action.author_id).set('status', action.status.toLowerCase());
- return state.set('byId', byId.set(action.author_id, data));
-};
diff --git a/client/coral-admin/src/services/PymConnection.js b/client/coral-admin/src/services/PymConnection.js
new file mode 100644
index 000000000..ca592b824
--- /dev/null
+++ b/client/coral-admin/src/services/PymConnection.js
@@ -0,0 +1,9 @@
+import Pym from '../../node_modules/pym.js';
+
+const pym = new Pym.Child({polling: 100});
+export default pym;
+
+export const link = (url) => (e) => {
+ e.preventDefault();
+ pym.sendMessage('navigate', url);
+};
diff --git a/client/coral-admin/src/services/client.js b/client/coral-admin/src/services/client.js
index b4a7a38df..40a539634 100644
--- a/client/coral-admin/src/services/client.js
+++ b/client/coral-admin/src/services/client.js
@@ -2,7 +2,6 @@ import ApolloClient, {addTypename} from 'apollo-client';
import getNetworkInterface from './transport';
export const client = new ApolloClient({
- connectToDevTools: true,
queryTransformer: addTypename,
dataIdFromObject: (result) => {
if (result.id && result.__typename) { // eslint-disable-line no-underscore-dangle
diff --git a/client/coral-admin/src/translations.json b/client/coral-admin/src/translations.json
index 0eb1ab98d..42028c3a2 100644
--- a/client/coral-admin/src/translations.json
+++ b/client/coral-admin/src/translations.json
@@ -198,7 +198,7 @@
},
"bandialog": {
"ban_user": "Quieres suspender el Usuario?",
- "are_you_sure": "Estas segura que quieres suspender a {props.author.displayName}?",
+ "are_you_sure": "Estas segura que quieres suspender a {props.author.username}?",
"note": "Nota: Suspender este usuario también va a colocar este comentario en la cola de Rechazados.",
"cancel": "Cancelar",
"yes_ban_user": "Si, Suspendan el usuario"
diff --git a/client/coral-embed-stream/src/Embed.js b/client/coral-embed-stream/src/Embed.js
index 116c8d34e..e5eec37cb 100644
--- a/client/coral-embed-stream/src/Embed.js
+++ b/client/coral-embed-stream/src/Embed.js
@@ -16,6 +16,7 @@ import {Notification, notificationActions, authActions, assetActions, pym} from
import Stream from './Stream';
import InfoBox from 'coral-plugin-infobox/InfoBox';
+import {ModerationLink} from 'coral-plugin-moderation';
import Count from 'coral-plugin-comment-count/CommentCount';
import CommentBox from 'coral-plugin-commentbox/CommentBox';
import UserBox from 'coral-sign-in/components/UserBox';
@@ -135,6 +136,7 @@ class Embed extends Component {
charCount={asset.settings.charCountEnable && asset.settings.charCount} />
: null
}
+
: {asset.settings.closedMessage}
@@ -207,7 +209,7 @@ const mapDispatchToProps = dispatch => ({
});
},
clearNotification: () => dispatch(clearNotification()),
- editName: (displayName) => dispatch(editName(displayName)),
+ editName: (username) => dispatch(editName(username)),
showSignInDialog: (offset) => dispatch(showSignInDialog(offset)),
logout: () => dispatch(logout()),
dispatch: d => dispatch(d)
diff --git a/client/coral-embed-stream/src/Stream.js b/client/coral-embed-stream/src/Stream.js
index 1695cd578..6119cfc66 100644
--- a/client/coral-embed-stream/src/Stream.js
+++ b/client/coral-embed-stream/src/Stream.js
@@ -10,7 +10,7 @@ class Stream extends React.Component {
asset: PropTypes.object.isRequired,
comments: PropTypes.array.isRequired,
currentUser: PropTypes.shape({
- displayName: PropTypes.string,
+ username: PropTypes.string,
id: PropTypes.string
})
}
diff --git a/client/coral-framework/actions/auth.js b/client/coral-framework/actions/auth.js
index bda46ae42..a2206fc09 100644
--- a/client/coral-framework/actions/auth.js
+++ b/client/coral-framework/actions/auth.js
@@ -15,11 +15,11 @@ export const hideCreateDisplayNameDialog = () => ({type: actions.HIDE_CREATEDISP
const createDisplayNameSuccess = () => ({type: actions.CREATEDISPLAYNAME_SUCCESS});
const createDisplayNameFailure = error => ({type: actions.CREATEDISPLAYNAME_FAILURE, error});
-export const updateDisplayName = ({displayName}) => ({type: actions.UPDATE_DISPLAYNAME, displayName});
+export const updateDisplayName = ({username}) => ({type: actions.UPDATE_DISPLAYNAME, username});
export const createDisplayName = (userId, formData) => dispatch => {
dispatch(createDisplayNameRequest());
- coralApi('/account/displayname', {method: 'PUT', body: formData})
+ coralApi('/account/username', {method: 'PUT', body: formData})
.then(() => {
dispatch(createDisplayNameSuccess());
dispatch(hideCreateDisplayNameDialog());
diff --git a/client/coral-framework/actions/user.js b/client/coral-framework/actions/user.js
index 42d2e0398..bf2a77402 100644
--- a/client/coral-framework/actions/user.js
+++ b/client/coral-framework/actions/user.js
@@ -5,8 +5,8 @@ import I18n from 'coral-framework/modules/i18n/i18n';
import translations from './../translations';
const lang = new I18n(translations);
-export const editName = (displayName) => (dispatch) => {
- return coralApi('/account/displayname', {method: 'PUT', body: {displayName}})
+export const editName = (username) => (dispatch) => {
+ return coralApi('/account/username', {method: 'PUT', body: {username}})
.then(() => {
dispatch(addNotification('success', lang.t('successNameUpdate')));
});
diff --git a/client/coral-framework/components/SuspendedAccount.js b/client/coral-framework/components/SuspendedAccount.js
index 4c221aa81..00adc9952 100644
--- a/client/coral-framework/components/SuspendedAccount.js
+++ b/client/coral-framework/components/SuspendedAccount.js
@@ -14,16 +14,16 @@ class SuspendedAccount extends Component {
}
state = {
- displayName: '',
+ username: '',
alert: ''
}
onSubmitClick = (e) => {
const {editName} = this.props;
- const {displayName} = this.state;
+ const {username} = this.state;
e.preventDefault();
- if (validate.displayName(displayName)) {
- editName(displayName)
+ if (validate.username(username)) {
+ editName(username)
.then(() => location.reload())
.catch((error) => {
this.setState({alert: lang.t(`error.${error.message}`)});
@@ -36,7 +36,7 @@ class SuspendedAccount extends Component {
render () {
const {canEditName} = this.props;
- const {displayName, alert} = this.state;
+ const {username, alert} = this.state;
return
{
@@ -51,7 +51,7 @@ class SuspendedAccount extends Component {
{alert}
|