diff --git a/client/coral-admin/src/routes/Configure/components/OrganizationSettings.js b/client/coral-admin/src/routes/Configure/components/OrganizationSettings.js index 2bbe1a230..f3daeffc2 100644 --- a/client/coral-admin/src/routes/Configure/components/OrganizationSettings.js +++ b/client/coral-admin/src/routes/Configure/components/OrganizationSettings.js @@ -84,7 +84,6 @@ class OrganizationSettings extends React.Component { render() { const { settings, slotPassthrough, canSave } = this.props; const hasErrors = this.state.errors.length; - return (

{t('configure.organization_info_copy')}

@@ -125,7 +124,7 @@ class OrganizationSettings extends React.Component { [styles.editable]: this.state.editing, })} onChange={this.updateEmail} - value={settings.organizationContactEmail} + value={settings.organizationContactEmail || ''} id={t('configure.organization_contact_email')} readOnly={!this.state.editing} /> diff --git a/client/coral-admin/src/routes/Configure/containers/Configure.js b/client/coral-admin/src/routes/Configure/containers/Configure.js index 7f0951154..3d2789974 100644 --- a/client/coral-admin/src/routes/Configure/containers/Configure.js +++ b/client/coral-admin/src/routes/Configure/containers/Configure.js @@ -20,7 +20,8 @@ import OrganizationSettings from './OrganizationSettings'; import { withRouter } from 'react-router'; class ConfigureContainer extends React.Component { - state = { nextRoute: '' }; + nextRoute = ''; + unregisterLeaveHook = null; savePending = async () => { await this.props.updateSettings(this.props.pending); @@ -40,18 +41,16 @@ class ConfigureContainer extends React.Component { }; gotoNextRoute = () => { - const { nextRoute } = this.state; - if (nextRoute) { - this.props.router.push(nextRoute); - this.setState({ nextRoute: '' }); + if (this.nextRoute) { + this.props.router.push(this.nextRoute); + this.nextRoute = ''; } }; handleSectionChange = async section => { const nextRoute = `/admin/configure/${section}`; - - if (this.shouldShowSaveDialog()) { - await this.setState({ nextRoute }); + if (this.hasPendingData()) { + this.nextRoute = nextRoute; this.props.showSaveDialog(); } else { // Just go to the section @@ -59,20 +58,37 @@ class ConfigureContainer extends React.Component { } }; - shouldShowSaveDialog = () => { + navigationPrompt = e => { + if (this.hasPendingData()) { + const confirmationMessage = 'Changes that you made may not be saved.'; + e.returnValue = confirmationMessage; // Gecko, Trident, Chrome 34+ + return confirmationMessage; // Gecko, WebKit, Chrome <34 + } + }; + + hasPendingData = () => { return !!Object.keys(this.props.pending).length; }; routeLeave = ({ pathname }) => { - if (this.shouldShowSaveDialog()) { - this.setState({ nextRoute: pathname }); + if (this.hasPendingData()) { + this.nextRoute = pathname; this.props.showSaveDialog(); return false; } }; componentDidMount() { - this.props.router.setRouteLeaveHook(this.props.route, this.routeLeave); + this.unregisterLeaveHook = this.props.router.setRouteLeaveHook( + this.props.route, + this.routeLeave + ); + window.addEventListener('beforeunload', this.navigationPrompt); + } + + componentWillUnmount() { + this.unregisterLeaveHook(); + window.removeEventListener('beforeunload', this.navigationPrompt); } render() { diff --git a/client/coral-framework/utils/index.js b/client/coral-framework/utils/index.js index 66792b9b3..8dc8fe113 100644 --- a/client/coral-framework/utils/index.js +++ b/client/coral-framework/utils/index.js @@ -237,7 +237,11 @@ export function getTotalReactionsCount(actionSummaries) { // Like lodash merge but does not recurse into arrays. export function mergeExcludingArrays(objValue, srcValue) { - if (typeof srcValue === 'object' && !Array.isArray(srcValue)) { + if ( + typeof srcValue === 'object' && + !Array.isArray(srcValue) && + srcValue !== null + ) { return assignWith({}, objValue, srcValue, mergeExcludingArrays); } return srcValue; diff --git a/locales/en.yml b/locales/en.yml index ff0d7e487..393d01e23 100644 --- a/locales/en.yml +++ b/locales/en.yml @@ -250,7 +250,7 @@ en: ALREADY_EXISTS: "Resource already exists" INVALID_ASSET_URL: "Assert URL is invalid" CANNOT_IGNORE_STAFF: "Cannot ignore Staff members." - email: "Not a valid E-Mail" + email: "Please enter a valid email." confirm_password: "Passwords don't match. Please check again" network_error: "Failed to connect to server. Check your internet connection and try again." email_not_verified: "Email address {0} not verified." diff --git a/models/schema/index.js b/models/schema/index.js index 976b437c0..c33402078 100644 --- a/models/schema/index.js +++ b/models/schema/index.js @@ -1,5 +1,3 @@ -const { CREATE_MONGO_INDEXES } = require('../../config'); - const Action = require('./action'); const Asset = require('./asset'); const Comment = require('./comment'); @@ -7,15 +5,4 @@ const Migration = require('./migration'); const Setting = require('./setting'); const User = require('./user'); -const schema = { Action, Asset, Comment, Migration, Setting, User }; - -// Provide the schema to each of the plugins so that they can add in indexes if -// it is enabled. -if (CREATE_MONGO_INDEXES) { - const plugins = require('../../services/plugins'); - plugins.get('server', 'indexes').map(({ indexes }) => { - indexes(schema); - }); -} - -module.exports = schema; +module.exports = { Action, Asset, Comment, Migration, Setting, User }; diff --git a/plugin-api/beta/server/getReactionConfig.js b/plugin-api/beta/server/getReactionConfig.js index da075d76c..e90a06122 100644 --- a/plugin-api/beta/server/getReactionConfig.js +++ b/plugin-api/beta/server/getReactionConfig.js @@ -2,23 +2,24 @@ const { SEARCH_OTHER_USERS } = require('../../../perms/constants'); const { ErrNotFound, ErrAlreadyExists } = require('../../../errors'); const pluralize = require('pluralize'); const sc = require('snake-case'); -// const { CREATE_MONGO_INDEXES } = require('../../../config'); +const { CREATE_MONGO_INDEXES } = require('../../../config'); +const Comment = require('models/comment'); function getReactionConfig(reaction) { reaction = reaction.toLowerCase(); - // if (CREATE_MONGO_INDEXES) { - // // Create the index on the comment model based on the reaction config. - // CommentModel.collection.createIndex( - // { - // created_at: 1, - // [`action_counts.${sc(reaction)}`]: 1, - // }, - // { - // background: true, - // } - // ); - // } + if (CREATE_MONGO_INDEXES) { + // Create the index on the comment model based on the reaction config. + Comment.collection.createIndex( + { + created_at: 1, + [`action_counts.${sc(reaction)}`]: 1, + }, + { + background: true, + } + ); + } const reactionPlural = pluralize(reaction); const Reaction = reaction.charAt(0).toUpperCase() + reaction.slice(1); @@ -127,17 +128,6 @@ function getReactionConfig(reaction) { return { typeDefs, - indexes: ({ Comment }) => { - Comment.index( - { - created_at: 1, - [`action_counts.${sc(reaction)}`]: 1, - }, - { - background: true, - } - ); - }, context: { Sort: () => ({ Comments: { diff --git a/plugins/talk-plugin-auth/client/index.js b/plugins/talk-plugin-auth/client/index.js index 6341dc7c9..aa591dd68 100644 --- a/plugins/talk-plugin-auth/client/index.js +++ b/plugins/talk-plugin-auth/client/index.js @@ -6,6 +6,7 @@ import Login from './login/containers/Main'; import reducer from './login/reducer'; import ChangePassword from './profile-settings/containers/ChangePassword'; import Profile from './profile-settings/containers/Profile'; +import ChangeUsername from './profile-settings/containers/ChangeUsername'; export default { reducer, @@ -14,6 +15,7 @@ export default { stream: [UserBox, SignInButton, SetUsernameDialog], login: [Login], profileHeader: [Profile], + profileHeader: [ChangeUsername], profileSettings: [ChangePassword], }, }; diff --git a/plugins/talk-plugin-auth/client/profile-settings/components/ChangeUsername.css b/plugins/talk-plugin-auth/client/profile-settings/components/ChangeUsername.css new file mode 100644 index 000000000..5b4631ecf --- /dev/null +++ b/plugins/talk-plugin-auth/client/profile-settings/components/ChangeUsername.css @@ -0,0 +1,122 @@ +.container { + margin-bottom: 20px; + display: flex; + position: relative; + color: #202020; + padding: 10px; + border-radius: 2px; + box-sizing: border-box; + justify-content: space-between; + + &.editing { + background-color: #EDEDED; + } +} + +.content { + flex-grow: 1; +} + +.actions { + flex-grow: 0; + display: flex; + flex-direction: column; + align-items: center; +} + +.email { + margin: 0; +} + +.username { + margin-bottom: 4px; +} + +.button { + border: 1px solid #787d80; + background-color: transparent; + height: 30px; + font-size: 0.9em; + line-height: normal; +} + +.saveButton { + background-color: #3498DB; + border-color: #3498DB; + color: white; + + > i { + font-size: 17px; + } + + &:hover { + background-color: #399ee2; + color: white; + } + + &:disabled { + border-color: #e0e0e0; + + &:hover { + background-color: #e0e0e0; + color: #4f5c67; + cursor: default; + } + } +} + +.cancelButton { + color:#787D80; + margin-top: 6px; + font-size: 0.9em; + + &:hover { + cursor: pointer; + } +} + +.detailLabel { + border: solid 1px #787D80; + border-radius: 2px; + background-color: white; + height: 30px; + display: inline-block; + width: 230px; + display: flex; + + > .detailLabelIcon { + font-size: 1.2em; + padding: 0 5px; + color: #787D80; + line-height: 30px; + } + + &.disabled { + background-color: #E0E0E0; + } +} + +.detailValue { + background: transparent; + border: none; + font-size: 1em; + color: #000; + height: 30px; + outline: none; + flex: 1; +} + +.bottomText { + color: #474747; + font-size: 0.9em; +} + +.detailList { + list-style: none; + margin: 0; + padding: 0; +} + +.detailItem { + margin-bottom: 12px; +} diff --git a/plugins/talk-plugin-auth/client/profile-settings/components/ChangeUsername.js b/plugins/talk-plugin-auth/client/profile-settings/components/ChangeUsername.js new file mode 100644 index 000000000..8188bdfbe --- /dev/null +++ b/plugins/talk-plugin-auth/client/profile-settings/components/ChangeUsername.js @@ -0,0 +1,188 @@ +import React from 'react'; +import cn from 'classnames'; +import PropTypes from 'prop-types'; +import styles from './ChangeUsername.css'; +import { Button } from 'plugin-api/beta/client/components/ui'; +import ChangeUsernameDialog from './ChangeUsernameDialog'; +import { t } from 'plugin-api/beta/client/services'; +import InputField from './InputField'; +import { getErrorMessages } from 'coral-framework/utils'; +import { canUsernameBeUpdated } from 'coral-framework/utils/user'; + +const initialState = { + editing: false, + showDialog: false, + formData: {}, +}; + +class ChangeUsername extends React.Component { + state = initialState; + + clearForm = () => { + this.setState(initialState); + }; + + enableEditing = () => { + this.setState({ + editing: true, + }); + }; + + disableEditing = () => { + this.setState({ + editing: false, + }); + }; + + cancel = () => { + this.clearForm(); + this.disableEditing(); + }; + + showDialog = () => { + this.setState({ + showDialog: true, + }); + }; + + onSave = async () => { + this.showDialog(); + }; + + saveChanges = async () => { + const { newUsername } = this.state.formData; + const { changeUsername } = this.props; + + try { + await changeUsername(newUsername); + this.props.notify( + 'success', + t('talk-plugin-auth.change_username.changed_username_success_msg') + ); + } catch (err) { + this.props.notify('error', getErrorMessages(err)); + } + + this.clearForm(); + this.disableEditing(); + }; + + onChange = e => { + const { name, value } = e.target; + + this.setState(state => ({ + formData: { + ...state.formData, + [name]: value, + }, + })); + }; + + closeDialog = () => { + this.setState({ + showDialog: false, + }); + }; + + render() { + const { + username, + emailAddress, + root: { me: { state: { status } } }, + notify, + } = this.props; + const { editing, formData, showDialog } = this.state; + + return ( +
+ + + {editing ? ( +
+
+ + + {t('talk-plugin-auth.change_username.change_username_note')} + + + +
+
+ ) : ( +
+

{username}

+ {emailAddress ? ( +

{emailAddress}

+ ) : null} +
+ )} + {editing ? ( +
+ + + {t('talk-plugin-auth.change_username.cancel')} + +
+ ) : ( +
+ +
+ )} +
+ ); + } +} + +ChangeUsername.propTypes = { + root: PropTypes.object.isRequired, + changeUsername: PropTypes.func.isRequired, + notify: PropTypes.func.isRequired, + username: PropTypes.string, + emailAddress: PropTypes.string, +}; + +export default ChangeUsername; diff --git a/plugins/talk-plugin-auth/client/profile-settings/components/ChangeUsernameDialog.css b/plugins/talk-plugin-auth/client/profile-settings/components/ChangeUsernameDialog.css new file mode 100644 index 000000000..af681d596 --- /dev/null +++ b/plugins/talk-plugin-auth/client/profile-settings/components/ChangeUsernameDialog.css @@ -0,0 +1,84 @@ +.dialog { + border: none; + box-shadow: 0 9px 46px 8px rgba(0, 0, 0, 0.14), 0 11px 15px -7px rgba(0, 0, 0, 0.12), 0 24px 38px 3px rgba(0, 0, 0, 0.2); + width: 320px; + top: 10px; + font-family: Helvetica, 'Helvetica Neue', Verdana, sans-serif; + font-size: 14px; + border-radius: 4px; + padding: 12px 20px; +} + +.close { + font-size: 20px; + line-height: 14px; + top: 10px; + right: 10px; + position: absolute; + display: block; + font-weight: bold; + color: #363636; + cursor: pointer; + + &:hover { + color: #6b6b6b; + } +} + +.title { + font-size: 1.3em; + margin-bottom: 8px; +} + +.description { + font-size: 1em; + line-height: 20px; + margin: 0; +} + +.item { + display: block; + color: #4C4C4D; + font-size: 1em; + margin-bottom: 2px; +} + +.bottomNote { + font-size: 0.9em; + line-height: 20px; + padding-top: 10px; + display: block; +} + +.bottomActions { + text-align: right; +} + +.usernamesChange { + margin: 18px 0; +} + +.cancel { + border: 1px solid #787d80; + background-color: transparent; + height: 30px; + font-size: 0.9em; + line-height: normal; + + &:hover { + background-color: #eaeaea; + } +} + +.confirmChanges { + background-color: #3498DB; + border-color: #3498DB; + color: white; + height: 30px; + font-size: 0.9em; + + &:hover { + background-color: #3ba3ec; + color: white; + } +} \ No newline at end of file diff --git a/plugins/talk-plugin-auth/client/profile-settings/components/ChangeUsernameDialog.js b/plugins/talk-plugin-auth/client/profile-settings/components/ChangeUsernameDialog.js new file mode 100644 index 000000000..321b36926 --- /dev/null +++ b/plugins/talk-plugin-auth/client/profile-settings/components/ChangeUsernameDialog.js @@ -0,0 +1,117 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import cn from 'classnames'; +import styles from './ChangeUsernameDialog.css'; +import InputField from './InputField'; +import { Button, Dialog } from 'plugin-api/beta/client/components/ui'; +import { t } from 'plugin-api/beta/client/services'; + +class ChangeUsernameDialog extends React.Component { + state = { + showError: false, + }; + + showError = () => { + this.setState({ + showError: true, + }); + }; + + confirmChanges = async () => { + if (this.formHasError()) { + this.showError(); + return; + } + + if (!this.props.canUsernameBeUpdated) { + this.props.notify( + 'error', + t('talk-plugin-auth.change_username.change_username_attempt') + ); + return; + } + + await this.props.saveChanges(); + this.props.closeDialog(); + }; + + formHasError = () => + this.props.formData.confirmNewUsername !== this.props.formData.newUsername; + + render() { + return ( + + + × + +

+ {t('talk-plugin-auth.change_username.confirm_username_change')} +

+
+

+ {t('talk-plugin-auth.change_username.description')} +

+
+ + {t('talk-plugin-auth.change_username.old_username')}:{' '} + {this.props.username} + + + {t('talk-plugin-auth.change_username.new_username')}:{' '} + {this.props.formData.newUsername} + +
+
+ + + {t('talk-plugin-auth.change_username.bottom_note')} + + +
+
+ + +
+
+
+ ); + } +} + +ChangeUsernameDialog.propTypes = { + saveChanges: PropTypes.func, + closeDialog: PropTypes.func, + showDialog: PropTypes.bool, + onChange: PropTypes.func, + username: PropTypes.string, + formData: PropTypes.object, + canUsernameBeUpdated: PropTypes.bool.isRequired, + notify: PropTypes.func.isRequired, +}; + +export default ChangeUsernameDialog; diff --git a/plugins/talk-plugin-auth/client/profile-settings/containers/ChangeUsername.js b/plugins/talk-plugin-auth/client/profile-settings/containers/ChangeUsername.js new file mode 100644 index 000000000..87e1e18b5 --- /dev/null +++ b/plugins/talk-plugin-auth/client/profile-settings/containers/ChangeUsername.js @@ -0,0 +1,12 @@ +import { compose } from 'react-apollo'; +import { bindActionCreators } from 'redux'; +import { connect } from 'plugin-api/beta/client/hocs'; +import ChangeUsername from '../components/ChangeUsername'; +import { notify } from 'coral-framework/actions/notification'; +import { withChangeUsername } from 'plugin-api/beta/client/hocs'; + +const mapDispatchToProps = dispatch => bindActionCreators({ notify }, dispatch); + +export default compose(connect(null, mapDispatchToProps), withChangeUsername)( + ChangeUsername +); diff --git a/plugins/talk-plugin-auth/client/translations.yml b/plugins/talk-plugin-auth/client/translations.yml index 17fba29c8..aa7579d74 100644 --- a/plugins/talk-plugin-auth/client/translations.yml +++ b/plugins/talk-plugin-auth/client/translations.yml @@ -165,8 +165,9 @@ en: incorrect_password: "Incorrect Password" confirm_change: "Confirm Change" cancel: "Cancel" - change_email_msg: "Email Address Changed - Your email address has been successfully changed. This email address will now be used for signing in and email notifications" + change_email_msg: "Email Address Changed - Your email address has been successfully changed. This email address will now be used for signing in and email notifications." changed_username_success_msg: "Username Changed - Your username has been successfully changed. You will not be able to change your user name for 14 days." + change_username_attempt: "Username can't be updated. Usernames can only be changed every 14 days." de: talk-plugin-auth: login: diff --git a/services/mongoose.js b/services/mongoose.js index 4e242cf47..167b0d45b 100644 --- a/services/mongoose.js +++ b/services/mongoose.js @@ -66,6 +66,7 @@ if (CREATE_MONGO_INDEXES) { require('../models/action'); require('../models/asset'); require('../models/comment'); + require('../models/migration'); require('../models/setting'); require('../models/user'); require('./migration');