diff options
Diffstat (limited to 'web/react/components/signup_team_complete')
9 files changed, 1344 insertions, 0 deletions
diff --git a/web/react/components/signup_team_complete/components/signup_team_complete.jsx b/web/react/components/signup_team_complete/components/signup_team_complete.jsx new file mode 100644 index 000000000..5ad21e941 --- /dev/null +++ b/web/react/components/signup_team_complete/components/signup_team_complete.jsx @@ -0,0 +1,79 @@ +// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +import BrowserStore from '../../../stores/browser_store.jsx'; + +import {FormattedMessage} from 'mm-intl'; + +import {browserHistory} from 'react-router'; + +export default class SignupTeamComplete extends React.Component { + constructor(props) { + super(props); + + this.updateParent = this.updateParent.bind(this); + } + componentWillMount() { + const data = JSON.parse(this.props.location.query.d); + this.hash = this.props.location.query.h; + + var initialState = BrowserStore.getGlobalItem(this.hash); + + if (!initialState) { + initialState = {}; + initialState.wizard = 'welcome'; + initialState.team = {}; + initialState.team.email = data.email; + initialState.team.allowed_domains = ''; + initialState.invites = []; + initialState.invites.push(''); + initialState.invites.push(''); + initialState.invites.push(''); + initialState.user = {}; + initialState.hash = this.hash; + initialState.data = this.props.location.query.d; + } + + this.setState(initialState); + } + componentDidMount() { + browserHistory.push('/signup_team_complete/welcome'); + } + updateParent(state, skipSet) { + BrowserStore.setGlobalItem(this.hash, state); + + if (!skipSet) { + this.setState(state); + browserHistory.push('/signup_team_complete/' + state.wizard); + } + } + render() { + return ( + <div> + <div className='signup-header'> + <a href='/'> + <span classNameName='fa fa-chevron-left'/> + <FormattedMessage id='web.header.back'/> + </a> + </div> + <div className='col-sm-12'> + <div className='signup-team__container'> + <div id='signup-team-complete'> + {React.cloneElement(this.props.children, { + state: this.state, + updateParent: this.updateParent + })} + </div> + </div> + </div> + </div> + ); + } +} + +SignupTeamComplete.defaultProps = { +}; +SignupTeamComplete.propTypes = { + location: React.PropTypes.object, + children: React.PropTypes.node +}; diff --git a/web/react/components/signup_team_complete/components/team_signup_display_name_page.jsx b/web/react/components/signup_team_complete/components/team_signup_display_name_page.jsx new file mode 100644 index 000000000..280e53ce4 --- /dev/null +++ b/web/react/components/signup_team_complete/components/team_signup_display_name_page.jsx @@ -0,0 +1,136 @@ +// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +import * as utils from '../../../utils/utils.jsx'; +import * as client from '../../../utils/client.jsx'; + +import {injectIntl, intlShape, defineMessages, FormattedMessage} from 'mm-intl'; + +const holders = defineMessages({ + required: { + id: 'team_signup_display_name.required', + defaultMessage: 'This field is required' + }, + charLength: { + id: 'team_signup_display_name.charLength', + defaultMessage: 'Name must be 4 or more characters up to a maximum of 15' + } +}); + +class TeamSignupDisplayNamePage extends React.Component { + constructor(props) { + super(props); + + this.submitBack = this.submitBack.bind(this); + this.submitNext = this.submitNext.bind(this); + + this.state = {}; + } + submitBack(e) { + e.preventDefault(); + this.props.state.wizard = 'welcome'; + this.props.updateParent(this.props.state); + } + submitNext(e) { + e.preventDefault(); + + const {formatMessage} = this.props.intl; + var displayName = ReactDOM.findDOMNode(this.refs.name).value.trim(); + if (!displayName) { + this.setState({nameError: formatMessage(holders.required)}); + return; + } else if (displayName.length < 4 || displayName.length > 15) { + this.setState({nameError: formatMessage(holders.charLength)}); + return; + } + + this.props.state.wizard = 'team_url'; + this.props.state.team.display_name = displayName; + this.props.state.team.name = utils.cleanUpUrlable(displayName); + this.props.updateParent(this.props.state); + } + handleFocus(e) { + e.preventDefault(); + e.currentTarget.select(); + } + render() { + client.track('signup', 'signup_team_02_name'); + + var nameError = null; + var nameDivClass = 'form-group'; + if (this.state.nameError) { + nameError = <label className='control-label'>{this.state.nameError}</label>; + nameDivClass += ' has-error'; + } + + return ( + <div> + <form> + <img + className='signup-team-logo' + src='/static/images/logo.png' + /> + <h2> + <FormattedMessage + id='team_signup_display_name.teamName' + defaultMessage='Team Name' + /> + </h2> + <div className={nameDivClass}> + <div className='row'> + <div className='col-sm-9'> + <input + type='text' + ref='name' + className='form-control' + placeholder='' + maxLength='128' + defaultValue={this.props.state.team.display_name} + autoFocus={true} + onFocus={this.handleFocus} + spellCheck='false' + /> + </div> + </div> + {nameError} + </div> + <div> + <FormattedMessage + id='team_signup_display_name.nameHelp' + defaultMessage='Name your team in any language. Your team name shows in menus and headings.' + /> + </div> + <button + type='submit' + className='btn btn-primary margin--extra' + onClick={this.submitNext} + > + <FormattedMessage + id='team_signup_display_name.next' + defaultMessage='Next' + /><i className='glyphicon glyphicon-chevron-right'></i> + </button> + <div className='margin--extra'> + <a + href='#' + onClick={this.submitBack} + > + <FormattedMessage + id='team_signup_display_name.back' + defaultMessage='Back to previous step' + /> + </a> + </div> + </form> + </div> + ); + } +} + +TeamSignupDisplayNamePage.propTypes = { + intl: intlShape.isRequired, + state: React.PropTypes.object, + updateParent: React.PropTypes.func +}; + +export default injectIntl(TeamSignupDisplayNamePage); diff --git a/web/react/components/signup_team_complete/components/team_signup_email_item.jsx b/web/react/components/signup_team_complete/components/team_signup_email_item.jsx new file mode 100644 index 000000000..c87d6ec07 --- /dev/null +++ b/web/react/components/signup_team_complete/components/team_signup_email_item.jsx @@ -0,0 +1,86 @@ +// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +import * as Utils from '../../../utils/utils.jsx'; + +import {intlShape, injectIntl, defineMessages} from 'mm-intl'; + +const holders = defineMessages({ + validEmail: { + id: 'team_signup_email.validEmail', + defaultMessage: 'Please enter a valid email address' + }, + different: { + id: 'team_signup_email.different', + defaultMessage: 'Please use a different email than the one used at signup' + }, + address: { + id: 'team_signup_email.address', + defaultMessage: 'Email Address' + } +}); + +class TeamSignupEmailItem extends React.Component { + constructor(props) { + super(props); + + this.getValue = this.getValue.bind(this); + this.validate = this.validate.bind(this); + + this.state = {}; + } + getValue() { + return ReactDOM.findDOMNode(this.refs.email).value.trim(); + } + validate(teamEmail) { + const {formatMessage} = this.props.intl; + const email = ReactDOM.findDOMNode(this.refs.email).value.trim().toLowerCase(); + + if (!email) { + return true; + } + + if (!Utils.isEmail(email)) { + this.setState({emailError: formatMessage(holders.validEmail)}); + return false; + } else if (email === teamEmail) { + this.setState({emailError: formatMessage(holders.different)}); + return false; + } + + this.setState({emailError: ''}); + return true; + } + render() { + let emailError = null; + let emailDivClass = 'form-group'; + if (this.state.emailError) { + emailError = <label className='control-label'>{this.state.emailError}</label>; + emailDivClass += ' has-error'; + } + + return ( + <div className={emailDivClass}> + <input + autoFocus={this.props.focus} + type='email' + ref='email' + className='form-control' + placeholder={this.props.intl.formatMessage(holders.address)} + defaultValue={this.props.email} + maxLength='128' + spellCheck='false' + /> + {emailError} + </div> + ); + } +} + +TeamSignupEmailItem.propTypes = { + intl: intlShape.isRequired, + focus: React.PropTypes.bool, + email: React.PropTypes.string +}; + +export default injectIntl(TeamSignupEmailItem, {withRef: true}); diff --git a/web/react/components/signup_team_complete/components/team_signup_finished.jsx b/web/react/components/signup_team_complete/components/team_signup_finished.jsx new file mode 100644 index 000000000..fc5f756e7 --- /dev/null +++ b/web/react/components/signup_team_complete/components/team_signup_finished.jsx @@ -0,0 +1,15 @@ +// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +import {FormattedMessage} from 'mm-intl'; + +export default class FinishedPage extends React.Component { + render() { + return ( + <FormattedMessage + id='signup_team_complete.completed' + defaultMessage="You've already completed the signup process for this invitation or this invitation has expired." + /> + ); + } +} diff --git a/web/react/components/signup_team_complete/components/team_signup_password_page.jsx b/web/react/components/signup_team_complete/components/team_signup_password_page.jsx new file mode 100644 index 000000000..490a11040 --- /dev/null +++ b/web/react/components/signup_team_complete/components/team_signup_password_page.jsx @@ -0,0 +1,215 @@ +// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +import * as Client from '../../../utils/client.jsx'; +import BrowserStore from '../../../stores/browser_store.jsx'; +import UserStore from '../../../stores/user_store.jsx'; +import Constants from '../../../utils/constants.jsx'; + +import {intlShape, injectIntl, defineMessages, FormattedMessage, FormattedHTMLMessage} from 'mm-intl'; +import {browserHistory} from 'react-router'; + +const holders = defineMessages({ + passwordError: { + id: 'team_signup_password.passwordError', + defaultMessage: 'Please enter at least {chars} characters' + }, + creating: { + id: 'team_signup_password.creating', + defaultMessage: 'Creating team...' + } +}); + +class TeamSignupPasswordPage extends React.Component { + constructor(props) { + super(props); + + this.submitBack = this.submitBack.bind(this); + this.submitNext = this.submitNext.bind(this); + + this.state = {}; + } + submitBack(e) { + e.preventDefault(); + this.props.state.wizard = 'username'; + this.props.updateParent(this.props.state); + } + submitNext(e) { + e.preventDefault(); + + var password = ReactDOM.findDOMNode(this.refs.password).value.trim(); + if (!password || password.length < Constants.MIN_PASSWORD_LENGTH) { + this.setState({passwordError: this.props.intl.formatMessage(holders.passwordError, {chars: Constants.MIN_PASSWORD_LENGTH})}); + return; + } + + this.setState({passwordError: null, serverError: null}); + $('#finish-button').button('loading'); + var teamSignup = JSON.parse(JSON.stringify(this.props.state)); + teamSignup.user.password = password; + teamSignup.user.allow_marketing = true; + delete teamSignup.wizard; + + Client.createTeamFromSignup(teamSignup, + () => { + Client.track('signup', 'signup_team_08_complete'); + + var props = this.props; + + Client.loginByEmail(teamSignup.team.name, teamSignup.team.email, teamSignup.user.password, + () => { + UserStore.setLastEmail(teamSignup.team.email); + if (this.props.hash > 0) { + BrowserStore.setGlobalItem(this.props.hash, JSON.stringify({wizard: 'finished'})); + } + + $('#sign-up-button').button('reset'); + props.state.wizard = 'finished'; + props.updateParent(props.state, true); + + browserHistory.push('/' + teamSignup.team.name + '/channels/town-square'); + }, + (err) => { + if (err.id === 'api.user.login.not_verified.app_error') { + browserHistory.push('/verify_email?email=' + encodeURIComponent(teamSignup.team.email) + '&teamname=' + encodeURIComponent(teamSignup.team.name)); + } else { + this.setState({serverError: err.message}); + $('#finish-button').button('reset'); + } + } + ); + }, + (err) => { + this.setState({serverError: err.message}); + $('#finish-button').button('reset'); + } + ); + } + render() { + Client.track('signup', 'signup_team_07_password'); + + var passwordError = null; + var passwordDivStyle = 'form-group'; + if (this.state.passwordError) { + passwordError = <div className='form-group has-error'><label className='control-label'>{this.state.passwordError}</label></div>; + passwordDivStyle = ' has-error'; + } + + var serverError = null; + if (this.state.serverError) { + serverError = <div className='form-group has-error'><label className='control-label'>{this.state.serverError}</label></div>; + } + + return ( + <div> + <form> + <img + className='signup-team-logo' + src='/static/images/logo.png' + /> + <h2 className='margin--less'> + <FormattedMessage + id='team_signup_password.yourPassword' + defaultMessage='Your password' + /> + </h2> + <h5 className='color--light'> + <FormattedMessage + id='team_signup_password.selectPassword' + defaultMessage="Select a password that you'll use to login with your email address:" + /> + </h5> + <div className='inner__content margin--extra'> + <h5><strong> + <FormattedMessage + id='team_signup_password.email' + defaultMessage='Email' + /> + </strong></h5> + <div className='block--gray form-group'>{this.props.state.team.email}</div> + <div className={passwordDivStyle}> + <div className='row'> + <div className='col-sm-11'> + <h5><strong> + <FormattedMessage + id='team_signup_password.choosePwd' + defaultMessage='Choose your password' + /> + </strong></h5> + <input + autoFocus={true} + type='password' + ref='password' + className='form-control' + placeholder='' + maxLength='128' + spellCheck='false' + /> + <span className='color--light help-block'> + <FormattedMessage + id='team_signup_password.hint' + defaultMessage='Passwords must contain {min} to {max} characters. Your password will be strongest if it contains a mix of symbols, numbers, and upper and lowercase characters.' + values={{ + min: Constants.MIN_PASSWORD_LENGTH, + max: Constants.MAX_PASSWORD_LENGTH + }} + /> + </span> + </div> + </div> + {passwordError} + {serverError} + </div> + </div> + <div className='form-group'> + <button + type='submit' + className='btn btn-primary margin--extra' + id='finish-button' + data-loading-text={'<span class=\'glyphicon glyphicon-refresh glyphicon-refresh-animate\'></span> ' + this.props.intl.formatMessage(holders.creating)} + onClick={this.submitNext} + > + <FormattedMessage + id='team_signup_password.finish' + defaultMessage='Finish' + /> + </button> + </div> + <p> + <FormattedHTMLMessage + id='team_signup_password.agreement' + defaultMessage="By proceeding to create your account and use {siteName}, you agree to our <a href='/static/help/terms.html'>Terms of Service</a> and <a href='/static/help/privacy.html'>Privacy Policy</a>. If you do not agree, you cannot use {siteName}." + values={{ + siteName: global.window.mm_config.SiteName + }} + /> + </p> + <div className='margin--extra'> + <a + href='#' + onClick={this.submitBack} + > + <FormattedMessage + id='team_signup_password.back' + defaultMessage='Back to previous step' + /> + </a> + </div> + </form> + </div> + ); + } +} + +TeamSignupPasswordPage.defaultProps = { + state: {}, + hash: '' +}; +TeamSignupPasswordPage.propTypes = { + intl: intlShape.isRequired, + state: React.PropTypes.object, + hash: React.PropTypes.string, + updateParent: React.PropTypes.func +}; + +export default injectIntl(TeamSignupPasswordPage); diff --git a/web/react/components/signup_team_complete/components/team_signup_send_invites_page.jsx b/web/react/components/signup_team_complete/components/team_signup_send_invites_page.jsx new file mode 100644 index 000000000..5e987ef2c --- /dev/null +++ b/web/react/components/signup_team_complete/components/team_signup_send_invites_page.jsx @@ -0,0 +1,210 @@ +// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +import EmailItem from './team_signup_email_item.jsx'; +import * as Client from '../../../utils/client.jsx'; + +import {FormattedMessage, FormattedHTMLMessage} from 'mm-intl'; + +export default class TeamSignupSendInvitesPage extends React.Component { + constructor(props) { + super(props); + this.submitBack = this.submitBack.bind(this); + this.submitNext = this.submitNext.bind(this); + this.submitAddInvite = this.submitAddInvite.bind(this); + this.submitSkip = this.submitSkip.bind(this); + this.keySubmit = this.keySubmit.bind(this); + this.state = { + emailEnabled: global.window.mm_config.SendEmailNotifications === 'true' + }; + } + submitBack(e) { + e.preventDefault(); + this.props.state.wizard = 'team_url'; + + this.props.updateParent(this.props.state); + } + submitNext(e) { + e.preventDefault(); + + var valid = true; + + if (this.state.emailEnabled) { + var emails = []; + + for (var i = 0; i < this.props.state.invites.length; i++) { + if (this.refs['email_' + i].getWrappedInstance().validate(this.props.state.team.email)) { + emails.push(this.refs['email_' + i].getWrappedInstance().getValue()); + } else { + valid = false; + } + } + + if (valid) { + this.props.state.invites = emails; + } + } + + if (valid) { + this.props.state.wizard = 'username'; + this.props.updateParent(this.props.state); + } + } + submitAddInvite(e) { + e.preventDefault(); + this.props.state.wizard = 'send_invites'; + if (!this.props.state.invites) { + this.props.state.invites = []; + } + this.props.state.invites.push(''); + this.props.updateParent(this.props.state); + } + submitSkip(e) { + e.preventDefault(); + this.props.state.wizard = 'username'; + this.props.updateParent(this.props.state); + } + keySubmit(e) { + if (e && e.keyCode === 13) { + this.submitNext(e); + } + } + componentDidMount() { + if (!this.state.emailEnabled) { + // Must use keypress not keyup due to event chain of pressing enter + $('body').keypress(this.keySubmit); + } + } + componentWillUnmount() { + if (!this.state.emailEnabled) { + $('body').off('keypress', this.keySubmit); + } + } + render() { + Client.track('signup', 'signup_team_05_send_invites'); + + var content = null; + var bottomContent = null; + + if (this.state.emailEnabled) { + var emails = []; + + for (var i = 0; i < this.props.state.invites.length; i++) { + if (i === 0) { + emails.push( + <EmailItem + focus={true} + key={i} + ref={'email_' + i} + email={this.props.state.invites[i]} + /> + ); + } else { + emails.push( + <EmailItem + focus={false} + key={i} + ref={'email_' + i} + email={this.props.state.invites[i]} + /> + ); + } + } + + content = ( + <div> + {emails} + <div className='form-group text-right'> + <a + href='#' + onClick={this.submitAddInvite} + > + <FormattedMessage + id='team_signup_send_invites.addInvitation' + defaultMessage='Add Invitation' + /> + </a> + </div> + </div> + ); + + bottomContent = ( + <p className='color--light'> + <FormattedHTMLMessage + id='team_signup_send_invites.prefer' + defaultMessage='if you prefer, you can invite team members later<br /> and ' + /> + <a + href='#' + onClick={this.submitSkip} + > + <FormattedMessage + id='team_signup_send_invites.skip' + defaultMessage='skip this step ' + /> + </a> + <FormattedMessage + id='team_signup_send_invites.forNow' + defaultMessage='for now.' + /> + </p> + ); + } else { + content = ( + <div className='form-group color--light'> + <FormattedMessage + id='team_signup_send_invites.disabled' + defaultMessage='Email is currently disabled for your team, and emails cannot be sent. Contact your system administrator to enable email and email invitations.' + /> + </div> + ); + } + + return ( + <div> + <form> + <img + className='signup-team-logo' + src='/static/images/logo.png' + /> + <h2> + <FormattedMessage + id='team_signup_send_invites.title' + defaultMessage='Invite Team Members' + /> + </h2> + {content} + <div className='form-group'> + <button + type='submit' + className='btn-primary btn' + onClick={this.submitNext} + > + <FormattedMessage + id='team_signup_send_invites.next' + defaultMessage='Next' + /><i className='glyphicon glyphicon-chevron-right'/> + </button> + </div> + </form> + {bottomContent} + <div className='margin--extra'> + <a + href='#' + onClick={this.submitBack} + > + <FormattedMessage + id='team_signup_send_invites.back' + defaultMessage='Back to previous step' + /> + </a> + </div> + </div> + ); + } +} + +TeamSignupSendInvitesPage.propTypes = { + state: React.PropTypes.object.isRequired, + updateParent: React.PropTypes.func.isRequired +}; diff --git a/web/react/components/signup_team_complete/components/team_signup_url_page.jsx b/web/react/components/signup_team_complete/components/team_signup_url_page.jsx new file mode 100644 index 000000000..ec50e2d25 --- /dev/null +++ b/web/react/components/signup_team_complete/components/team_signup_url_page.jsx @@ -0,0 +1,205 @@ +// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +import * as Utils from '../../../utils/utils.jsx'; +import * as Client from '../../../utils/client.jsx'; +import Constants from '../../../utils/constants.jsx'; + +import {injectIntl, intlShape, defineMessages, FormattedMessage, FormattedHTMLMessage} from 'mm-intl'; + +const holders = defineMessages({ + required: { + id: 'team_signup_url.required', + defaultMessage: 'This field is required' + }, + regex: { + id: 'team_signup_url.regex', + defaultMessage: "Use only lower case letters, numbers and dashes. Must start with a letter and can't end in a dash." + }, + charLength: { + id: 'team_signup_url.charLength', + defaultMessage: 'Name must be 4 or more characters up to a maximum of 15' + }, + taken: { + id: 'team_signup_url.taken', + defaultMessage: 'URL is taken or contains a reserved word' + }, + unavailable: { + id: 'team_signup_url.unavailable', + defaultMessage: 'This URL is unavailable. Please try another.' + } +}); + +class TeamSignupUrlPage extends React.Component { + constructor(props) { + super(props); + + this.submitBack = this.submitBack.bind(this); + this.submitNext = this.submitNext.bind(this); + this.handleFocus = this.handleFocus.bind(this); + + this.state = {nameError: ''}; + } + submitBack(e) { + e.preventDefault(); + this.props.state.wizard = 'team_display_name'; + this.props.updateParent(this.props.state); + } + submitNext(e) { + e.preventDefault(); + + const {formatMessage} = this.props.intl; + const name = ReactDOM.findDOMNode(this.refs.name).value.trim(); + if (!name) { + this.setState({nameError: formatMessage(holders.required)}); + return; + } + + const cleanedName = Utils.cleanUpUrlable(name); + + const urlRegex = /^[a-z]+([a-z\-0-9]+|(__)?)[a-z0-9]+$/g; + if (cleanedName !== name || !urlRegex.test(name)) { + this.setState({nameError: formatMessage(holders.regex)}); + return; + } else if (cleanedName.length < 4 || cleanedName.length > 15) { + this.setState({nameError: formatMessage(holders.charLength)}); + return; + } + + if (global.window.mm_config.RestrictTeamNames === 'true') { + for (let index = 0; index < Constants.RESERVED_TEAM_NAMES.length; index++) { + if (cleanedName.indexOf(Constants.RESERVED_TEAM_NAMES[index]) === 0) { + this.setState({nameError: formatMessage(holders.taken)}); + return; + } + } + } + + Client.findTeamByName(name, + (data) => { + if (data) { + this.setState({nameError: formatMessage(holders.unavailable)}); + } else { + if (global.window.mm_config.SendEmailNotifications === 'true') { + this.props.state.wizard = 'send_invites'; + } else { + this.props.state.wizard = 'username'; + } + this.props.state.team.type = 'O'; + + this.props.state.team.name = name; + this.props.updateParent(this.props.state); + } + }, + (err) => { + this.setState({nameError: err.message}); + } + ); + } + handleFocus(e) { + e.preventDefault(); + + e.currentTarget.select(); + } + render() { + $('body').tooltip({selector: '[data-toggle=tooltip]', trigger: 'hover click'}); + + Client.track('signup', 'signup_team_03_url'); + + let nameError = null; + let nameDivClass = 'form-group'; + if (this.state.nameError) { + nameError = <label className='control-label'>{this.state.nameError}</label>; + nameDivClass += ' has-error'; + } + + const title = `${Utils.getWindowLocationOrigin()}/`; + + return ( + <div> + <form> + <img + className='signup-team-logo' + src='/static/images/logo.png' + /> + <h2> + <FormattedMessage + id='team_signup_url.teamUrl' + defaultMessage='Team URL' + /> + </h2> + <div className={nameDivClass}> + <div className='row'> + <div className='col-sm-11'> + <div className='input-group input-group--limit'> + <span + data-toggle='tooltip' + title={title} + className='input-group-addon' + > + {title} + </span> + <input + type='text' + ref='name' + className='form-control' + placeholder='' + maxLength='128' + defaultValue={this.props.state.team.name} + autoFocus={true} + onFocus={this.handleFocus} + spellCheck='false' + /> + </div> + </div> + </div> + {nameError} + </div> + <p> + <FormattedMessage + id='team_signup_url.webAddress' + defaultMessage='Choose the web address of your new team:' + /> + </p> + <ul className='color--light'> + <FormattedHTMLMessage + id='team_signup_url.hint' + defaultMessage="<li>Short and memorable is best</li> + <li>Use lowercase letters, numbers and dashes</li> + <li>Must start with a letter and can't end in a dash</li>" + /> + </ul> + <button + type='submit' + className='btn btn-primary margin--extra' + onClick={this.submitNext} + > + <FormattedMessage + id='team_signup_url.next' + defaultMessage='Next' + /><i className='glyphicon glyphicon-chevron-right'></i> + </button> + <div className='margin--extra'> + <a + href='#' + onClick={this.submitBack} + > + <FormattedMessage + id='team_signup_url.back' + defaultMessage='Back to previous step' + /> + </a> + </div> + </form> + </div> + ); + } +} + +TeamSignupUrlPage.propTypes = { + intl: intlShape.isRequired, + state: React.PropTypes.object, + updateParent: React.PropTypes.func +}; + +export default injectIntl(TeamSignupUrlPage); diff --git a/web/react/components/signup_team_complete/components/team_signup_username_page.jsx b/web/react/components/signup_team_complete/components/team_signup_username_page.jsx new file mode 100644 index 000000000..e56aa4cd7 --- /dev/null +++ b/web/react/components/signup_team_complete/components/team_signup_username_page.jsx @@ -0,0 +1,164 @@ +// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +import * as Utils from '../../../utils/utils.jsx'; +import * as Client from '../../../utils/client.jsx'; +import Constants from '../../../utils/constants.jsx'; + +import {intlShape, injectIntl, defineMessages, FormattedMessage} from 'mm-intl'; + +const holders = defineMessages({ + reserved: { + id: 'team_signup_username.reserved', + defaultMessage: 'This username is reserved, please choose a new one.' + }, + invalid: { + id: 'team_signup_username.invalid', + defaultMessage: 'Username must begin with a letter, and contain between {min} to {max} characters in total, which may be numbers, lowercase letters, or any of the symbols \'.\', \'-\', or \'_\'' + } +}); + +class TeamSignupUsernamePage extends React.Component { + constructor(props) { + super(props); + + this.submitBack = this.submitBack.bind(this); + this.submitNext = this.submitNext.bind(this); + + this.state = {}; + } + submitBack(e) { + e.preventDefault(); + if (global.window.mm_config.SendEmailNotifications === 'true') { + this.props.state.wizard = 'send_invites'; + } else { + this.props.state.wizard = 'team_url'; + } + + this.props.updateParent(this.props.state); + } + submitNext(e) { + e.preventDefault(); + + const {formatMessage} = this.props.intl; + var name = ReactDOM.findDOMNode(this.refs.name).value.trim().toLowerCase(); + + var usernameError = Utils.isValidUsername(name); + if (usernameError === 'Cannot use a reserved word as a username.') { //this should be change to some kind of ID + this.setState({nameError: formatMessage(holders.reserved)}); + return; + } else if (usernameError) { + this.setState({nameError: formatMessage(holders.invalid, {min: Constants.MIN_USERNAME_LENGTH, max: Constants.MAX_USERNAME_LENGTH})}); + return; + } + + this.props.state.wizard = 'password'; + this.props.state.user.username = name; + this.props.updateParent(this.props.state); + } + render() { + Client.track('signup', 'signup_team_06_username'); + + var nameError = null; + var nameHelpText = ( + <span className='color--light help-block'> + <FormattedMessage + id='team_signup_username.hint' + defaultMessage="Usernames must begin with a letter and contain between {min} to {max} characters made up of lowercase letters, numbers, and the symbols '.', '-' and '_'" + values={{ + min: Constants.MIN_USERNAME_LENGTH, + max: Constants.MAX_USERNAME_LENGTH + }} + /> + </span> + ); + var nameDivClass = 'form-group'; + if (this.state.nameError) { + nameError = <label className='control-label'>{this.state.nameError}</label>; + nameHelpText = ''; + nameDivClass += ' has-error'; + } + + return ( + <div> + <form> + <img + className='signup-team-logo' + src='/static/images/logo.png' + /> + <h2 className='margin--less'> + <FormattedMessage + id='team_signup_username.username' + defaultMessage='Your username' + /> + </h2> + <h5 className='color--light'> + <FormattedMessage + id='team_signup_username.memorable' + defaultMessage='Select a memorable username that makes it easy for teammates to identify you:' + /> + </h5> + <div className='inner__content margin--extra'> + <div className={nameDivClass}> + <div className='row'> + <div className='col-sm-11'> + <h5><strong> + <FormattedMessage + id='team_signup_username.chooseUsername' + defaultMessage='Choose your username' + /> + </strong></h5> + <input + autoFocus={true} + type='text' + ref='name' + className='form-control' + placeholder='' + defaultValue={this.props.state.user.username} + maxLength={Constants.MAX_USERNAME_LENGTH} + spellCheck='false' + /> + {nameHelpText} + </div> + </div> + {nameError} + </div> + </div> + <button + type='submit' + className='btn btn-primary margin--extra' + onClick={this.submitNext} + > + <FormattedMessage + id='team_signup_username.next' + defaultMessage='Next' + /> + <i className='glyphicon glyphicon-chevron-right'></i> + </button> + <div className='margin--extra'> + <a + href='#' + onClick={this.submitBack} + > + <FormattedMessage + id='team_signup_username.back' + defaultMessage='Back to previous step' + /> + </a> + </div> + </form> + </div> + ); + } +} + +TeamSignupUsernamePage.defaultProps = { + state: null +}; +TeamSignupUsernamePage.propTypes = { + intl: intlShape.isRequired, + state: React.PropTypes.object, + updateParent: React.PropTypes.func +}; + +export default injectIntl(TeamSignupUsernamePage); diff --git a/web/react/components/signup_team_complete/components/team_signup_welcome_page.jsx b/web/react/components/signup_team_complete/components/team_signup_welcome_page.jsx new file mode 100644 index 000000000..97782e54a --- /dev/null +++ b/web/react/components/signup_team_complete/components/team_signup_welcome_page.jsx @@ -0,0 +1,234 @@ +// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +import * as Utils from '../../../utils/utils.jsx'; +import * as Client from '../../../utils/client.jsx'; +import BrowserStore from '../../../stores/browser_store.jsx'; + +import {injectIntl, intlShape, defineMessages, FormattedMessage, FormattedHTMLMessage} from 'mm-intl'; + +import {browserHistory} from 'react-router'; + +const holders = defineMessages({ + storageError: { + id: 'team_signup_welcome.storageError', + defaultMessage: 'This service requires local storage to be enabled. Please enable it or exit private browsing.' + }, + validEmailError: { + id: 'team_signup_welcome.validEmailError', + defaultMessage: 'Please enter a valid email address' + }, + address: { + id: 'team_signup_welcome.address', + defaultMessage: 'Email Address' + } +}); + +class TeamSignupWelcomePage extends React.Component { + constructor(props) { + super(props); + + this.submitNext = this.submitNext.bind(this); + this.handleDiffEmail = this.handleDiffEmail.bind(this); + this.handleDiffSubmit = this.handleDiffSubmit.bind(this); + this.handleKeyPress = this.handleKeyPress.bind(this); + + this.state = {useDiff: false}; + + document.addEventListener('keyup', this.handleKeyPress, false); + } + submitNext(e) { + if (!BrowserStore.isLocalStorageSupported()) { + this.setState({storageError: this.props.intl.formatMessage(holders.storageError)}); + return; + } + e.preventDefault(); + this.props.state.wizard = 'team_display_name'; + this.props.updateParent(this.props.state); + } + handleDiffEmail(e) { + e.preventDefault(); + this.setState({useDiff: true}); + } + handleDiffSubmit(e) { + e.preventDefault(); + + const {formatMessage} = this.props.intl; + var state = {useDiff: true, serverError: ''}; + + var email = ReactDOM.findDOMNode(this.refs.email).value.trim().toLowerCase(); + if (!email || !Utils.isEmail(email)) { + state.emailError = formatMessage(holders.validEmailError); + this.setState(state); + return; + } else if (!BrowserStore.isLocalStorageSupported()) { + state.emailError = formatMessage(holders.storageError); + this.setState(state); + return; + } + state.emailError = ''; + + Client.signupTeam(email, + function success(data) { + if (data.follow_link) { + window.location.href = data.follow_link; + } else { + this.props.state.wizard = 'finished'; + this.props.updateParent(this.props.state); + browserHistory.push('/signup_team_confirm/?email=' + encodeURIComponent(email)); + } + }.bind(this), + function error(err) { + let errorMsg = err.message; + + if (err.detailed_error.indexOf('Invalid RCPT TO address provided') >= 0) { + errorMsg = formatMessage(holders.validEmailError); + } + + this.setState({emailError: '', serverError: errorMsg}); + }.bind(this) + ); + } + handleKeyPress(event) { + if (event.keyCode === 13) { + this.submitNext(event); + } + } + componentWillUnmount() { + document.removeEventListener('keyup', this.handleKeyPress, false); + } + render() { + Client.track('signup', 'signup_team_01_welcome'); + + var storageError = null; + if (this.state.storageError) { + storageError = <label className='control-label'>{this.state.storageError}</label>; + } + + var emailError = null; + var emailDivClass = 'form-group'; + if (this.state.emailError) { + emailError = <label className='control-label'>{this.state.emailError}</label>; + emailDivClass += ' has-error'; + } + + var serverError = null; + if (this.state.serverError) { + serverError = ( + <div className='form-group has-error'> + <label className='control-label'>{this.state.serverError}</label> + </div> + ); + } + + var differentEmailLinkClass = ''; + var emailDivContainerClass = 'hidden'; + if (this.state.useDiff) { + differentEmailLinkClass = 'hidden'; + emailDivContainerClass = ''; + } + + return ( + <div> + <img + className='signup-team-logo' + src='/static/images/logo.png' + /> + <h3 className='sub-heading'> + <FormattedMessage + id='team_signup_welcome.welcome' + defaultMessage='Welcome to:' + /> + </h3> + <h1 className='margin--top-none'>{global.window.mm_config.SiteName}</h1> + <p className='margin--less'> + <FormattedMessage + id='team_signup_welcome.lets' + defaultMessage="Let's set up your new team" + /> + </p> + <div> + <FormattedMessage + id='team_signup_welcome.confirm' + defaultMessage='Please confirm your email address:' + /> + <br/> + <div className='inner__content'> + <div className='block--gray'>{this.props.state.team.email}</div> + </div> + </div> + <p className='margin--extra color--light'> + <FormattedHTMLMessage + id='team_signup_welcome.admin' + defaultMessage='Your account will administer the new team site. <br /> + You can add other administrators later.' + /> + </p> + <div className='form-group'> + <button + className='btn-primary btn form-group' + type='submit' + onClick={this.submitNext} + > + <i className='glyphicon glyphicon-ok'></i> + <FormattedMessage + id='team_signup_welcome.yes' + defaultMessage='Yes, this address is correct' + /> + </button> + {storageError} + </div> + <hr/> + <div className={emailDivContainerClass}> + <div className={emailDivClass}> + <div className='row'> + <div className='col-sm-9'> + <input + type='email' + ref='email' + className='form-control' + placeholder={this.props.intl.formatMessage(holders.address)} + maxLength='128' + spellCheck='false' + /> + </div> + </div> + {emailError} + </div> + {serverError} + <button + className='btn btn-md btn-primary' + type='button' + onClick={this.handleDiffSubmit} + > + <FormattedMessage + id='team_signup_welcome.instead' + defaultMessage='Use this instead' + /> + </button> + </div> + <a + href='#' + onClick={this.handleDiffEmail} + className={differentEmailLinkClass} + > + <FormattedMessage + id='team_signup_welcome.different' + defaultMessage='Use a different email' + /> + </a> + </div> + ); + } +} + +TeamSignupWelcomePage.defaultProps = { + state: {} +}; +TeamSignupWelcomePage.propTypes = { + intl: intlShape.isRequired, + updateParent: React.PropTypes.func.isRequired, + state: React.PropTypes.object +}; + +export default injectIntl(TeamSignupWelcomePage); |