diff --git a/client/coral-admin/src/components/UserDetail.js b/client/coral-admin/src/components/UserDetail.js index 31ad999dc..e45e3f095 100644 --- a/client/coral-admin/src/components/UserDetail.js +++ b/client/coral-admin/src/components/UserDetail.js @@ -3,7 +3,7 @@ import cn from 'classnames'; import PropTypes from 'prop-types'; import capitalize from 'lodash/capitalize'; import styles from './UserDetail.css'; -import AccountHistory from './AccountHistory'; +import UserHistory from './UserHistory'; import { Slot } from 'coral-framework/components'; import UserDetailCommentList from '../components/UserDetailCommentList'; import { @@ -28,26 +28,6 @@ import UserInfoTooltip from './UserInfoTooltip'; import t from 'coral-framework/services/i18n'; class UserDetail extends React.Component { - rejectThenReload = async info => { - await this.props.rejectComment(info); - this.props.data.refetch(); - }; - - acceptThenReload = async info => { - await this.props.acceptComment(info); - this.props.data.refetch(); - }; - - bulkAcceptThenReload = async () => { - await this.props.bulkAccept(); - this.props.data.refetch(); - }; - - bulkRejectThenReload = async () => { - await this.props.bulkReject(); - this.props.data.refetch(); - }; - changeTab = tab => { this.props.changeTab(tab); }; @@ -110,8 +90,14 @@ class UserDetail extends React.Component { unbanUser, unsuspendUser, modal, + acceptComment, + rejectComment, + bulkAccept, + bulkReject, } = this.props; + console.log(rejectedComments, totalComments); + // if totalComments is 0, you're dividing by zero let rejectedPercent = rejectedComments / totalComments * 100; @@ -286,7 +272,7 @@ class UserDetail extends React.Component { 'talk-admin-user-detail-history-tab' )} > - {t('user_detail.account_history')} + {t('user_detail.user_history')} @@ -304,12 +290,12 @@ class UserDetail extends React.Component { loadMore={loadMore} toggleSelect={toggleSelect} viewUserDetail={viewUserDetail} - acceptComment={this.acceptThenReload} - rejectComment={this.rejectThenReload} + acceptComment={acceptComment} + rejectComment={rejectComment} selectedCommentIds={selectedCommentIds} toggleSelectAll={toggleSelectAll} - bulkAcceptThenReload={this.bulkAcceptThenReload} - bulkRejectThenReload={this.bulkRejectThenReload} + bulkAcceptThenReload={bulkAccept} + bulkRejectThenReload={bulkReject} /> - + diff --git a/client/coral-admin/src/components/AccountHistory.css b/client/coral-admin/src/components/UserHistory.css similarity index 100% rename from client/coral-admin/src/components/AccountHistory.css rename to client/coral-admin/src/components/UserHistory.css diff --git a/client/coral-admin/src/components/AccountHistory.js b/client/coral-admin/src/components/UserHistory.js similarity index 73% rename from client/coral-admin/src/components/AccountHistory.js rename to client/coral-admin/src/components/UserHistory.js index 0d85b62af..3a3472c86 100644 --- a/client/coral-admin/src/components/AccountHistory.js +++ b/client/coral-admin/src/components/UserHistory.js @@ -1,7 +1,7 @@ import React from 'react'; import PropTypes from 'prop-types'; import { murmur3 } from 'murmurhash-js'; -import styles from './AccountHistory.css'; +import styles from './UserHistory.css'; import cn from 'classnames'; import flatten from 'lodash/flatten'; import orderBy from 'lodash/orderBy'; @@ -43,15 +43,15 @@ const readableDuration = (startDate, endDate) => { const buildActionResponse = (typename, created_at, until, status) => { switch (typename) { case 'UsernameStatusHistory': - return t('account_history.username_status', status); + return t('user_history.username_status', status); case 'BannedStatusHistory': return status - ? t('account_history.user_banned') - : t('account_history.ban_removed'); + ? t('user_history.user_banned') + : t('user_history.ban_removed'); case 'SuspensionStatusHistory': return until - ? t('account_history.suspended', readableDuration(created_at, until)) - : t('account_history.suspension_removed'); + ? t('user_history.suspended', readableDuration(created_at, until)) + : t('user_history.suspension_removed'); default: return '-'; } @@ -62,43 +62,41 @@ const getModerationValue = assignedBy => assignedBy.username ) : ( - {t('account_history.system')} + {t('user_history.system')} ); -class AccountHistory extends React.Component { +class UserHistory extends React.Component { render() { const { user } = this.props; const userHistory = buildUserHistory(user.state); return (
-
+
+
{t('user_history.date')}
- {t('account_history.date')} + {t('user_history.action')}
- {t('account_history.action')} -
-
- {t('account_history.taken_by')} + {t('user_history.taken_by')}
{userHistory.map( ({ __typename, created_at, assigned_by, until, status }) => (
{moment(new Date(created_at)).format('MMM DD, YYYY')} @@ -107,7 +105,7 @@ class AccountHistory extends React.Component { className={cn( styles.item, styles.action, - 'talk-admin-account-history-row-status' + 'talk-admin-user-history-row-status' )} > {buildActionResponse(__typename, created_at, until, status)} @@ -116,7 +114,7 @@ class AccountHistory extends React.Component { className={cn( styles.item, styles.username, - 'talk-admin-account-history-row-assigned-by' + 'talk-admin-user-history-row-assigned-by' )} > {getModerationValue(assigned_by)} @@ -130,8 +128,8 @@ class AccountHistory extends React.Component { } } -AccountHistory.propTypes = { +UserHistory.propTypes = { user: PropTypes.object.isRequired, }; -export default AccountHistory; +export default UserHistory; diff --git a/client/coral-admin/src/containers/UserDetail.js b/client/coral-admin/src/containers/UserDetail.js index fb1508a2f..360819dd6 100644 --- a/client/coral-admin/src/containers/UserDetail.js +++ b/client/coral-admin/src/containers/UserDetail.js @@ -148,6 +148,7 @@ UserDetailContainer.propTypes = { selectedCommentIds: PropTypes.array, unbanUser: PropTypes.func.isRequired, unsuspendUser: PropTypes.func.isRequired, + userId: PropTypes.string, }; const LOAD_MORE_QUERY = gql` @@ -245,7 +246,6 @@ export const withUserDetailQuery = withQuery( options: ({ userId, statuses }) => { return { variables: { author_id: userId, statuses }, - fetchPolicy: 'network-only', }; }, skip: ownProps => !ownProps.userId, diff --git a/client/coral-admin/src/routes/Configure/components/OrganizationSettings.css b/client/coral-admin/src/routes/Configure/components/OrganizationSettings.css index 942307a3f..2468496b7 100644 --- a/client/coral-admin/src/routes/Configure/components/OrganizationSettings.css +++ b/client/coral-admin/src/routes/Configure/components/OrganizationSettings.css @@ -43,11 +43,7 @@ } .actionBox { - position: absolute; - right: 20px; - top: 20px; - text-align: center; - width: 100px; + flex-grow: 0; } .cancelButton { @@ -64,4 +60,28 @@ .changedSave { background-color: #00796B; color: white; +} + +.errorList { + list-style: none; + padding: 0; + margin: 0; +} + +.errorItem { + padding: 5px 10px; + margin-bottom: 20px; + color: #b71c1c; + border-radius: 2px; + display: inline-block; + background: #F9D3CE; +} + +.container { + display: flex; +} + +.content { + flex-grow: 1; + padding-right: 40px; } \ No newline at end of file diff --git a/client/coral-admin/src/routes/Configure/components/OrganizationSettings.js b/client/coral-admin/src/routes/Configure/components/OrganizationSettings.js index 06c91926c..38f211781 100644 --- a/client/coral-admin/src/routes/Configure/components/OrganizationSettings.js +++ b/client/coral-admin/src/routes/Configure/components/OrganizationSettings.js @@ -7,9 +7,25 @@ import Slot from 'coral-framework/components/Slot'; import t from 'coral-framework/services/i18n'; import ConfigurePage from './ConfigurePage'; import ConfigureCard from 'coral-framework/components/ConfigureCard'; +import validate from 'coral-framework/helpers/validate'; +import errorMsj from 'coral-framework/helpers/error'; class OrganizationSettings extends React.Component { - state = { editing: false }; + state = { editing: false, errors: [] }; + + addError = err => { + if (this.state.errors.indexOf(err) === -1) { + this.setState(({ errors }) => ({ + errors: errors.concat(err), + })); + } + }; + + removeError = err => { + this.setState(({ errors }) => ({ + errors: errors.filter(i => i !== err), + })); + }; toggleEditing = () => { this.setState(({ editing }) => ({ @@ -29,8 +45,21 @@ class OrganizationSettings extends React.Component { }; updateEmail = event => { - const updater = { organizationContactEmail: { $set: event.target.value } }; - this.props.updatePending({ updater }); + let error = null; + const email = event.target.value; + + // Add a blocker error + if (!validate.email(email)) { + error = true; + this.addError('email'); + } else { + this.removeError('email'); + } + + const updater = { organizationContactEmail: { $set: email } }; + const errorUpdater = { organizationEmail: { $set: error } }; + + this.props.updatePending({ updater, errorUpdater }); }; cancelEditing = () => { @@ -43,84 +72,101 @@ class OrganizationSettings extends React.Component { this.disableEditing(); }; + displayErrors = (errors = []) => ( +
    + {errors.map((errKey, i) => ( +
  • + {errorMsj[errKey]} +
  • + ))} +
+ ); + render() { const { settings, slotPassthrough, canSave } = this.props; + const hasErrors = this.state.errors.length; + return (

{t('configure.organization_info_copy')}

{t('configure.organization_info_copy_2')}

- {!this.state.editing ? ( -
- +
+
+ {this.displayErrors(this.state.errors)} +
    +
  • + + +
  • +
  • + + +
  • +
- ) : ( -
- {canSave ? ( + {!this.state.editing ? ( +
- ) : ( - - )} - - {t('cancel')} - -
- )} -
    -
  • - - -
  • -
  • - - -
  • -
+
+ ) : ( +
+ {canSave && !hasErrors ? ( + + ) : ( + + )} + + {t('cancel')} + +
+ )} +
diff --git a/client/coral-embed-stream/src/tabs/stream/components/Comment.js b/client/coral-embed-stream/src/tabs/stream/components/Comment.js index 5df119fbc..0983b11c4 100644 --- a/client/coral-embed-stream/src/tabs/stream/components/Comment.js +++ b/client/coral-embed-stream/src/tabs/stream/components/Comment.js @@ -574,7 +574,9 @@ export default class Comment extends React.Component { 'talk-stream-comment-header-tags-container' )} > - {isStaff(comment.tags) ? Staff : null} + {isStaff(comment.tags) ? ( + {t('community.staff')} + ) : null} { label = t('modqueue.rejected'); icon = 'close'; break; + case 'SYSTEM_WITHHELD': + label = t('modqueue.system_withheld'); + icon = 'flag'; + break; default: throw new Error(`Unknown inactive status ${status}`); } diff --git a/client/coral-framework/graphql/mutations.js b/client/coral-framework/graphql/mutations.js index 1b3b4c6fc..bd31be630 100644 --- a/client/coral-framework/graphql/mutations.js +++ b/client/coral-framework/graphql/mutations.js @@ -1,5 +1,6 @@ import { gql } from 'react-apollo'; import withMutation from '../hocs/withMutation'; +import update from 'immutability-helper'; function convertItemType(item_type) { switch (item_type) { @@ -167,9 +168,39 @@ export const withSetCommentStatus = withMutation( errors: null, }, }, + updateQueries: { + CoralAdmin_UserDetail: prev => { + const increment = { + rejectedComments: { + $apply: count => + count < prev.totalComments ? count + 1 : count, + }, + }; + + const decrement = { + rejectedComments: { + $apply: count => (count > 0 ? count - 1 : 0), + }, + }; + + // If rejected then increment rejectedComments by one + if (status === 'REJECTED') { + const updated = update(prev, increment); + return updated; + } + + // If approved then decrement rejectedComments by one + if (status === 'ACCEPTED') { + const updated = update(prev, decrement); + return updated; + } + + return prev; + }, + }, update: proxy => { const fragment = gql` - fragment Talk_SetCommentStatus on Comment { + fragment Talk_SetCommentStatus_Comment on Comment { status status_history { type @@ -182,9 +213,11 @@ 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, diff --git a/client/coral-ui/components/Icon.css b/client/coral-ui/components/Icon.css index 1a118257a..72708e863 100644 --- a/client/coral-ui/components/Icon.css +++ b/client/coral-ui/components/Icon.css @@ -1,5 +1,5 @@ .root { - vertical-align: middle; + vertical-align: sub; font-size: inherit; } diff --git a/locales/ar.yml b/locales/ar.yml index f435db811..852b9ac1b 100644 --- a/locales/ar.yml +++ b/locales/ar.yml @@ -438,8 +438,8 @@ ar: reports: "Reports" all: "All" rejected: "Rejected" - account_history: "Account History" - account_history: + user_history: "User History" + user_history: user_banned: "User banned" ban_removed: "Ban removed" username_status: "Username {0}" diff --git a/locales/da.yml b/locales/da.yml index fe5b0e917..4bcb3bb1d 100644 --- a/locales/da.yml +++ b/locales/da.yml @@ -431,8 +431,8 @@ da: reports: "Rapporter" all: "Alle" rejected: "Afvist" - account_history: "Konto historik" - account_history: + user_history: "Konto historik" + user_history: user_banned: "Bruger bannet" ban_removed: "Ban fjernet" username_status: "Brugernavn {0}" diff --git a/locales/de.yml b/locales/de.yml index 21ab5727d..d2b247fbc 100644 --- a/locales/de.yml +++ b/locales/de.yml @@ -430,8 +430,8 @@ de: reports: "Meldungen" all: "Alle" rejected: "Abgelehnte" - account_history: "Konto-Verlauf" - account_history: + user_history: "Konto-Verlauf" + user_history: user_banned: "User banned" ban_removed: "Ban removed" username_status: "Username {0}" diff --git a/locales/en.yml b/locales/en.yml index 11d55571d..f8b9b9ce2 100644 --- a/locales/en.yml +++ b/locales/en.yml @@ -358,6 +358,7 @@ en: sort: "Sort" show_shortcuts: "Show Shortcuts" singleview: "Zen mode" + system_withheld: "System Withheld" thismenu: "Open this menu" jump_to_queue: "Jump to specific queue" thousand: k @@ -460,8 +461,8 @@ en: reports: "Reports" all: "All" rejected: "Rejected" - account_history: "Account History" - account_history: + user_history: "User History" + user_history: user_banned: "User banned" ban_removed: "Ban removed" username_status: "Username {0}" diff --git a/locales/es.yml b/locales/es.yml index f68f51d0e..f5cade3f3 100644 --- a/locales/es.yml +++ b/locales/es.yml @@ -448,8 +448,8 @@ es: reports: "Reports" all: "All" rejected: "Rejected" - account_history: "Account History" - account_history: + user_history: "User History" + user_history: user_banned: "User banned" ban_removed: "Ban removed" username_status: "Username {0}" diff --git a/locales/nl_NL.yml b/locales/nl_NL.yml index aa80f58be..9d4057065 100644 --- a/locales/nl_NL.yml +++ b/locales/nl_NL.yml @@ -431,8 +431,8 @@ nl_NL: reports: "Rapportages" all: "Alle" rejected: "Afgewezen" - account_history: "Accountgeschiedenis" - account_history: + user_history: "Accountgeschiedenis" + user_history: user_banned: "User banned" ban_removed: "Ban removed" username_status: "Username {0}" diff --git a/locales/pt_BR.yml b/locales/pt_BR.yml index 4f6db11b1..c236bdb4c 100644 --- a/locales/pt_BR.yml +++ b/locales/pt_BR.yml @@ -430,8 +430,8 @@ pt_BR: reports: "Reports" all: "All" rejected: "Rejected" - account_history: "Account History" - account_history: + user_history: "User History" + user_history: user_banned: "User banned" ban_removed: "Ban removed" username_status: "Username {0}" diff --git a/locales/zh_CN.yml b/locales/zh_CN.yml index 59e028eb7..b3307a886 100644 --- a/locales/zh_CN.yml +++ b/locales/zh_CN.yml @@ -432,8 +432,8 @@ zh_CN: reports: "Reports" all: "All" rejected: "Rejected" - account_history: "Account History" - account_history: + user_history: "User History" + user_history: user_banned: "User banned" ban_removed: "Ban removed" username_status: "Username {0}" diff --git a/locales/zh_TW.yml b/locales/zh_TW.yml index 1b131f636..c186a667f 100644 --- a/locales/zh_TW.yml +++ b/locales/zh_TW.yml @@ -432,8 +432,8 @@ zh_TW: reports: "Reports" all: "All" rejected: "Rejected" - account_history: "Account History" - account_history: + user_history: "User History" + user_history: user_banned: "User banned" ban_removed: "Ban removed" username_status: "Username {0}" diff --git a/models/schema/comment.js b/models/schema/comment.js index 6ef5434d5..d9586e850 100644 --- a/models/schema/comment.js +++ b/models/schema/comment.js @@ -187,6 +187,17 @@ Comment.index( } ); +// Add an index that is optimized for finding a user's comments. +Comment.index( + { + author_id: 1, + created_at: -1, + }, + { + background: true, + } +); + // Optimize for tag searches/counts. Comment.index( { diff --git a/models/schema/setting.js b/models/schema/setting.js index 5e226e6cb..af7932910 100644 --- a/models/schema/setting.js +++ b/models/schema/setting.js @@ -49,6 +49,9 @@ const Setting = new Schema( organizationName: { type: String, }, + organizationContactEmail: { + type: String, + }, autoCloseStream: { type: Boolean, default: false, diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 000000000..d6105c594 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,27 @@ +{ + "name": "talk", + "version": "4.3.0", + "lockfileVersion": 1, + "requires": true, + "dependencies": { + "exenv": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/exenv/-/exenv-1.2.2.tgz", + "integrity": "sha1-KueOhdmJQVhnCwPUe+wfA72Ru50=" + }, + "react-side-effect": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/react-side-effect/-/react-side-effect-1.1.5.tgz", + "integrity": "sha512-Z2ZJE4p/jIfvUpiUMRydEVpQRf2f8GMHczT6qLcARmX7QRb28JDBTpnM2g/i5y/p7ZDEXYGHWg0RbhikE+hJRw==", + "requires": { + "exenv": "1.2.2", + "shallowequal": "1.0.2" + } + }, + "shallowequal": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/shallowequal/-/shallowequal-1.0.2.tgz", + "integrity": "sha512-zlVXeVUKvo+HEv1e2KQF/csyeMKx2oHvatQ9l6XjCUj3agvC8XGf6R9HvIPDSmp8FNPvx7b5kaEJTRi7CqxtEw==" + } + } +} diff --git a/package.json b/package.json index 845e2b11a..c7e9a78c1 100644 --- a/package.json +++ b/package.json @@ -79,6 +79,7 @@ "babel-polyfill": "^6.26.0", "babel-preset-es2015": "6.24.1", "babel-preset-react": "^6.23.0", + "bunyan-debug-stream": "^1.0.8", "bcryptjs": "^2.4.3", "bowser": "^1.7.2", "brotli-webpack-plugin": "^0.5.0", @@ -218,7 +219,6 @@ "babel-plugin-dynamic-import-node": "^1.1.0", "babel-plugin-transform-es2015-modules-commonjs": "^6.26.0", "browserstack-local": "^1.3.0", - "bunyan-debug-stream": "^1.0.8", "chai": "^3.5.0", "chai-as-promised": "^6.0.0", "chai-datetime": "^1.5.0", diff --git a/services/logging.js b/services/logging.js index 7ae3654b1..850283f40 100644 --- a/services/logging.js +++ b/services/logging.js @@ -2,13 +2,13 @@ const { version } = require('../package.json'); const path = require('path'); const { createLogger: createBunyanLogger, stdSerializers } = require('bunyan'); const { LOGGING_LEVEL, REVISION_HASH } = require('../config'); +const debug = require('bunyan-debug-stream'); // Streams enables the ability for development logs to be readable to a human, // but will send JSON logs in production that's parsable by a system like ELK. const streams = (() => { // In development, use the debug stream printer. if (process.env.NODE_ENV !== 'production') { - const debug = require('bunyan-debug-stream'); return [ { level: LOGGING_LEVEL, diff --git a/test/e2e/globals.js b/test/e2e/globals.js index 371fe64c5..aeecd204e 100644 --- a/test/e2e/globals.js +++ b/test/e2e/globals.js @@ -27,6 +27,6 @@ module.exports = { body: 'This is a test comment', }, organizationName: 'Coral', - organizationContactEmail: 'coral@coraltest.coral', + organizationContactEmail: 'coral@coralproject.net', }, }; diff --git a/test/e2e/page_objects/admin.js b/test/e2e/page_objects/admin.js index 218afe425..8e319e499 100644 --- a/test/e2e/page_objects/admin.js +++ b/test/e2e/page_objects/admin.js @@ -128,8 +128,8 @@ module.exports = { rejectedTab: '.talk-admin-user-detail-rejected-tab', historyTab: '.talk-admin-user-detail-history-tab', historyPane: '.talk-admin-user-detail-history-tab-pane', - accountHistory: '.talk-admin-account-history', - accountHistoryRowStatus: '.talk-admin-account-history-row-status', + UserHistory: '.talk-admin-user-history', + UserHistoryRowStatus: '.talk-admin-user-history-row-status', actionsMenu: '.talk-admin-user-detail-actions-button', actionItemSuspendUser: '.action-menu-item#suspendUser', actionMenuButton: diff --git a/test/e2e/page_objects/install.js b/test/e2e/page_objects/install.js index 26b024993..1f27eabd7 100644 --- a/test/e2e/page_objects/install.js +++ b/test/e2e/page_objects/install.js @@ -20,6 +20,8 @@ module.exports = { selector: '.talk-install-step-2', elements: { organizationNameInput: '.talk-install-step-2 #organizationName', + organizationContactEmailInput: + '.talk-install-step-2 #organizationContactEmail', saveButton: '.talk-install-step-2-save-button', }, }, diff --git a/test/e2e/specs/01_install.js b/test/e2e/specs/01_install.js index a8a88566c..75e6a60eb 100644 --- a/test/e2e/specs/01_install.js +++ b/test/e2e/specs/01_install.js @@ -38,7 +38,12 @@ module.exports = { step2 .waitForElementVisible('@organizationNameInput') + .waitForElementVisible('@organizationContactEmailInput', 5000) .setValue('@organizationNameInput', testData.organizationName) + .setValue( + '@organizationContactEmailInput', + testData.organizationContactEmail + ) .waitForElementVisible('@saveButton') .click('@saveButton'); }, diff --git a/test/e2e/specs/06_suspendUser.js b/test/e2e/specs/06_suspendUser.js index 0b89c5c01..81fde3781 100644 --- a/test/e2e/specs/06_suspendUser.js +++ b/test/e2e/specs/06_suspendUser.js @@ -125,7 +125,7 @@ module.exports = { .waitForElementVisible('@historyTab') .click('@historyTab') .waitForElementVisible('@historyPane') - .waitForElementVisible('@accountHistory') + .waitForElementVisible('@UserHistory') .click('@closeButton'); }, 'admin logs out': client => {