diff options
Diffstat (limited to 'web/react/components')
-rw-r--r-- | web/react/components/admin_console/admin_controller.jsx | 7 | ||||
-rw-r--r-- | web/react/components/admin_console/admin_sidebar.jsx | 19 | ||||
-rw-r--r-- | web/react/components/admin_console/analytics.jsx | 279 | ||||
-rw-r--r-- | web/react/components/admin_console/system_analytics.jsx | 161 | ||||
-rw-r--r-- | web/react/components/admin_console/team_analytics.jsx | 260 | ||||
-rw-r--r-- | web/react/components/create_comment.jsx | 2 | ||||
-rw-r--r-- | web/react/components/create_post.jsx | 2 | ||||
-rw-r--r-- | web/react/components/login.jsx | 17 | ||||
-rw-r--r-- | web/react/components/sidebar.jsx | 4 | ||||
-rw-r--r-- | web/react/components/suggestion/at_mention_provider.jsx | 6 | ||||
-rw-r--r-- | web/react/components/team_general_tab.jsx | 2 | ||||
-rw-r--r-- | web/react/components/team_signup_welcome_page.jsx | 8 | ||||
-rw-r--r-- | web/react/components/user_settings/manage_languages.jsx | 101 | ||||
-rw-r--r-- | web/react/components/user_settings/user_settings_display.jsx | 46 | ||||
-rw-r--r-- | web/react/components/view_image.jsx | 3 |
15 files changed, 679 insertions, 238 deletions
diff --git a/web/react/components/admin_console/admin_controller.jsx b/web/react/components/admin_console/admin_controller.jsx index 0f85c238d..efd163017 100644 --- a/web/react/components/admin_console/admin_controller.jsx +++ b/web/react/components/admin_console/admin_controller.jsx @@ -1,4 +1,4 @@ -// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved. +// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved. // See License.txt for license information. import AdminSidebar from './admin_sidebar.jsx'; @@ -23,6 +23,7 @@ import TeamUsersTab from './team_users.jsx'; import TeamAnalyticsTab from './team_analytics.jsx'; import LdapSettingsTab from './ldap_settings.jsx'; import LicenseSettingsTab from './license_settings.jsx'; +import SystemAnalyticsTab from './system_analytics.jsx'; export default class AdminController extends React.Component { constructor(props) { @@ -45,7 +46,7 @@ export default class AdminController extends React.Component { config: AdminStore.getConfig(), teams: AdminStore.getAllTeams(), selectedTeams, - selected: props.tab || 'service_settings', + selected: props.tab || 'system_analytics', selectedTeam: props.teamId || null }; @@ -165,6 +166,8 @@ export default class AdminController extends React.Component { if (this.state.teams) { tab = <TeamAnalyticsTab team={this.state.teams[this.state.selectedTeam]} />; } + } else if (this.state.selected === 'system_analytics') { + tab = <SystemAnalyticsTab />; } } diff --git a/web/react/components/admin_console/admin_sidebar.jsx b/web/react/components/admin_console/admin_sidebar.jsx index 5a5eaa055..66f82c55b 100644 --- a/web/react/components/admin_console/admin_sidebar.jsx +++ b/web/react/components/admin_console/admin_sidebar.jsx @@ -196,6 +196,25 @@ export default class AdminSidebar extends React.Component { <li> <h4> <span className='icon fa fa-gear'></span> + <span>{'SITE REPORTS'}</span> + </h4> + </li> + </ul> + <ul className='nav nav__sub-menu padded'> + <li> + <a + href='#' + className={this.isSelected('system_analytics')} + onClick={this.handleClick.bind(this, 'system_analytics', null)} + > + {'View Statistics'} + </a> + </li> + </ul> + <ul className='nav nav__sub-menu'> + <li> + <h4> + <span className='icon fa fa-gear'></span> <span>{'SETTINGS'}</span> </h4> </li> diff --git a/web/react/components/admin_console/analytics.jsx b/web/react/components/admin_console/analytics.jsx new file mode 100644 index 000000000..70ef1ecab --- /dev/null +++ b/web/react/components/admin_console/analytics.jsx @@ -0,0 +1,279 @@ +// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +import * as Utils from '../../utils/utils.jsx'; +import Constants from '../../utils/constants.jsx'; +import LineChart from './line_chart.jsx'; + +var Tooltip = ReactBootstrap.Tooltip; +var OverlayTrigger = ReactBootstrap.OverlayTrigger; + +export default class Analytics extends React.Component { + constructor(props) { + super(props); + + this.state = {}; + } + + render() { // in the future, break down these into smaller components + var serverError = ''; + if (this.props.serverError) { + serverError = <div className='form-group has-error'><label className='control-label'>{this.props.serverError}</label></div>; + } + + var totalCount = ( + <div className='col-sm-3'> + <div className='total-count'> + <div className='title'>{'Total Users'}<i className='fa fa-users'/></div> + <div className='content'>{this.props.uniqueUserCount == null ? 'Loading...' : this.props.uniqueUserCount}</div> + </div> + </div> + ); + + var openChannelCount = ( + <div className='col-sm-3'> + <div className='total-count'> + <div className='title'>{'Public Channels'}<i className='fa fa-globe'/></div> + <div className='content'>{this.props.channelOpenCount == null ? 'Loading...' : this.props.channelOpenCount}</div> + </div> + </div> + ); + + var openPrivateCount = ( + <div className='col-sm-3'> + <div className='total-count'> + <div className='title'>{'Private Groups'}<i className='fa fa-lock'/></div> + <div className='content'>{this.props.channelPrivateCount == null ? 'Loading...' : this.props.channelPrivateCount}</div> + </div> + </div> + ); + + var postCount = ( + <div className='col-sm-3'> + <div className='total-count'> + <div className='title'>{'Total Posts'}<i className='fa fa-comment'/></div> + <div className='content'>{this.props.postCount == null ? 'Loading...' : this.props.postCount}</div> + </div> + </div> + ); + + var postCountsByDay = ( + <div className='col-sm-12'> + <div className='total-count by-day'> + <div className='title'>{'Total Posts'}</div> + <div className='content'>{'Loading...'}</div> + </div> + </div> + ); + + if (this.props.postCountsDay != null) { + let content; + if (this.props.postCountsDay.labels.length === 0) { + content = 'Not enough data for a meaningful representation.'; + } else { + content = ( + <LineChart + data={this.props.postCountsDay} + width='740' + height='225' + /> + ); + } + postCountsByDay = ( + <div className='col-sm-12'> + <div className='total-count by-day'> + <div className='title'>{'Total Posts'}</div> + <div className='content'> + {content} + </div> + </div> + </div> + ); + } + + var usersWithPostsByDay = ( + <div className='col-sm-12'> + <div className='total-count by-day'> + <div className='title'>{'Active Users With Posts'}</div> + <div className='content'>{'Loading...'}</div> + </div> + </div> + ); + + if (this.props.userCountsWithPostsDay != null) { + let content; + if (this.props.userCountsWithPostsDay.labels.length === 0) { + content = 'Not enough data for a meaningful representation.'; + } else { + content = ( + <LineChart + data={this.props.userCountsWithPostsDay} + width='740' + height='225' + /> + ); + } + usersWithPostsByDay = ( + <div className='col-sm-12'> + <div className='total-count by-day'> + <div className='title'>{'Active Users With Posts'}</div> + <div className='content'> + {content} + </div> + </div> + </div> + ); + } + + let recentActiveUser; + if (this.props.recentActiveUsers != null) { + let content; + if (this.props.recentActiveUsers.length === 0) { + content = 'Loading...'; + } else { + content = ( + <table> + <tbody> + { + this.props.recentActiveUsers.map((user) => { + const tooltip = ( + <Tooltip id={'recent-user-email-tooltip-' + user.id}> + {user.email} + </Tooltip> + ); + + return ( + <tr key={'recent-user-table-entry-' + user.id}> + <td> + <OverlayTrigger + delayShow={Constants.OVERLAY_TIME_DELAY} + placement='top' + overlay={tooltip} + > + <time> + {user.username} + </time> + </OverlayTrigger> + </td> + <td>{Utils.displayDateTime(user.last_activity_at)}</td> + </tr> + ); + }) + } + </tbody> + </table> + ); + } + recentActiveUser = ( + <div className='col-sm-6'> + <div className='total-count recent-active-users'> + <div className='title'>{'Recent Active Users'}</div> + <div className='content'> + {content} + </div> + </div> + </div> + ); + } + + let newUsers; + if (this.props.newlyCreatedUsers != null) { + let content; + if (this.props.newlyCreatedUsers.length === 0) { + content = 'Loading...'; + } else { + content = ( + <table> + <tbody> + { + this.props.newlyCreatedUsers.map((user) => { + const tooltip = ( + <Tooltip id={'new-user-email-tooltip-' + user.id}> + {user.email} + </Tooltip> + ); + + return ( + <tr key={'new-user-table-entry-' + user.id}> + <td> + <OverlayTrigger + delayShow={Constants.OVERLAY_TIME_DELAY} + placement='top' + overlay={tooltip} + > + <time> + {user.username} + </time> + </OverlayTrigger> + </td> + <td>{Utils.displayDateTime(user.create_at)}</td> + </tr> + ); + }) + } + </tbody> + </table> + ); + } + newUsers = ( + <div className='col-sm-6'> + <div className='total-count recent-active-users'> + <div className='title'>{'Newly Created Users'}</div> + <div className='content'> + {content} + </div> + </div> + </div> + ); + } + + return ( + <div className='wrapper--fixed team_statistics'> + <h3>{'Statistics for ' + this.props.title}</h3> + {serverError} + <div className='row'> + {totalCount} + {postCount} + {openChannelCount} + {openPrivateCount} + </div> + <div className='row'> + {postCountsByDay} + </div> + <div className='row'> + {usersWithPostsByDay} + </div> + <div className='row'> + {recentActiveUser} + {newUsers} + </div> + </div> + ); + } +} + +Analytics.defaultProps = { + title: null, + channelOpenCount: null, + channelPrivateCount: null, + postCount: null, + postCountsDay: null, + userCountsWithPostsDay: null, + recentActiveUsers: null, + newlyCreatedUsers: null, + uniqueUserCount: null, + serverError: null +}; + +Analytics.propTypes = { + title: React.PropTypes.string, + channelOpenCount: React.PropTypes.number, + channelPrivateCount: React.PropTypes.number, + postCount: React.PropTypes.number, + postCountsDay: React.PropTypes.object, + userCountsWithPostsDay: React.PropTypes.object, + recentActiveUsers: React.PropTypes.array, + newlyCreatedUsers: React.PropTypes.array, + uniqueUserCount: React.PropTypes.number, + serverError: React.PropTypes.string +}; diff --git a/web/react/components/admin_console/system_analytics.jsx b/web/react/components/admin_console/system_analytics.jsx new file mode 100644 index 000000000..f54813a94 --- /dev/null +++ b/web/react/components/admin_console/system_analytics.jsx @@ -0,0 +1,161 @@ +// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +import Analytics from './analytics.jsx'; +import * as Client from '../../utils/client.jsx'; + +export default class SystemAnalytics extends React.Component { + constructor(props) { + super(props); + + this.getData = this.getData.bind(this); + + this.state = { // most of this state should be from a store in the future + users: null, + serverError: null, + channel_open_count: null, + channel_private_count: null, + post_count: null, + post_counts_day: null, + user_counts_with_posts_day: null, + recent_active_users: null, + newly_created_users: null, + unique_user_count: null + }; + } + + componentDidMount() { + this.getData(); + } + + getData() { // should be moved to an action creator eventually + Client.getSystemAnalytics( + 'standard', + (data) => { + for (var index in data) { + if (data[index].name === 'channel_open_count') { + this.setState({channel_open_count: data[index].value}); + } + + if (data[index].name === 'channel_private_count') { + this.setState({channel_private_count: data[index].value}); + } + + if (data[index].name === 'post_count') { + this.setState({post_count: data[index].value}); + } + + if (data[index].name === 'unique_user_count') { + this.setState({unique_user_count: data[index].value}); + } + } + }, + (err) => { + this.setState({serverError: err.message}); + } + ); + + Client.getSystemAnalytics( + 'post_counts_day', + (data) => { + data.reverse(); + + var chartData = { + labels: [], + datasets: [{ + label: 'Total Posts', + fillColor: 'rgba(151,187,205,0.2)', + strokeColor: 'rgba(151,187,205,1)', + pointColor: 'rgba(151,187,205,1)', + pointStrokeColor: '#fff', + pointHighlightFill: '#fff', + pointHighlightStroke: 'rgba(151,187,205,1)', + data: [] + }] + }; + + for (var index in data) { + if (data[index]) { + var row = data[index]; + chartData.labels.push(row.name); + chartData.datasets[0].data.push(row.value); + } + } + + this.setState({post_counts_day: chartData}); + }, + (err) => { + this.setState({serverError: err.message}); + } + ); + + Client.getSystemAnalytics( + 'user_counts_with_posts_day', + (data) => { + data.reverse(); + + var chartData = { + labels: [], + datasets: [{ + label: 'Active Users With Posts', + fillColor: 'rgba(151,187,205,0.2)', + strokeColor: 'rgba(151,187,205,1)', + pointColor: 'rgba(151,187,205,1)', + pointStrokeColor: '#fff', + pointHighlightFill: '#fff', + pointHighlightStroke: 'rgba(151,187,205,1)', + data: [] + }] + }; + + for (var index in data) { + if (data[index]) { + var row = data[index]; + chartData.labels.push(row.name); + chartData.datasets[0].data.push(row.value); + } + } + + this.setState({user_counts_with_posts_day: chartData}); + }, + (err) => { + this.setState({serverError: err.message}); + } + ); + } + + componentWillReceiveProps() { + this.setState({ + serverError: null, + channel_open_count: null, + channel_private_count: null, + post_count: null, + post_counts_day: null, + user_counts_with_posts_day: null, + unique_user_count: null + }); + + this.getData(); + } + + render() { + return ( + <div> + <Analytics + title={'the System'} + channelOpenCount={this.state.channel_open_count} + channelPrivateCount={this.state.channel_private_count} + postCount={this.state.post_count} + postCountsDay={this.state.post_counts_day} + userCountsWithPostsDay={this.state.user_counts_with_posts_day} + uniqueUserCount={this.state.unique_user_count} + serverError={this.state.serverError} + /> + </div> + ); + } +} + +SystemAnalytics.propTypes = { + team: React.PropTypes.object +}; diff --git a/web/react/components/admin_console/team_analytics.jsx b/web/react/components/admin_console/team_analytics.jsx index fe7230946..c164dd98c 100644 --- a/web/react/components/admin_console/team_analytics.jsx +++ b/web/react/components/admin_console/team_analytics.jsx @@ -1,13 +1,8 @@ -// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved. +// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved. // See License.txt for license information. +import Analytics from './analytics.jsx'; import * as Client from '../../utils/client.jsx'; -import * as Utils from '../../utils/utils.jsx'; -import Constants from '../../utils/constants.jsx'; -import LineChart from './line_chart.jsx'; - -var Tooltip = ReactBootstrap.Tooltip; -var OverlayTrigger = ReactBootstrap.OverlayTrigger; export default class TeamAnalytics extends React.Component { constructor(props) { @@ -15,7 +10,7 @@ export default class TeamAnalytics extends React.Component { this.getData = this.getData.bind(this); - this.state = { + this.state = { // most of this state should be from a store in the future users: null, serverError: null, channel_open_count: null, @@ -24,7 +19,8 @@ export default class TeamAnalytics extends React.Component { post_counts_day: null, user_counts_with_posts_day: null, recent_active_users: null, - newly_created_users: null + newly_created_users: null, + unique_user_count: null }; } @@ -32,8 +28,8 @@ export default class TeamAnalytics extends React.Component { this.getData(this.props.team.id); } - getData(teamId) { - Client.getAnalytics( + getData(teamId) { // should be moved to an action creator eventually + Client.getTeamAnalytics( teamId, 'standard', (data) => { @@ -49,6 +45,10 @@ export default class TeamAnalytics extends React.Component { if (data[index].name === 'post_count') { this.setState({post_count: data[index].value}); } + + if (data[index].name === 'unique_user_count') { + this.setState({unique_user_count: data[index].value}); + } } }, (err) => { @@ -56,7 +56,7 @@ export default class TeamAnalytics extends React.Component { } ); - Client.getAnalytics( + Client.getTeamAnalytics( teamId, 'post_counts_day', (data) => { @@ -91,7 +91,7 @@ export default class TeamAnalytics extends React.Component { } ); - Client.getAnalytics( + Client.getTeamAnalytics( teamId, 'user_counts_with_posts_day', (data) => { @@ -152,6 +152,10 @@ export default class TeamAnalytics extends React.Component { var recentActive = []; for (let i = 0; i < usersList.length; i++) { + if (usersList[i].last_activity_at == null) { + continue; + } + recentActive.push(usersList[i]); if (i > 19) { break; @@ -198,227 +202,29 @@ export default class TeamAnalytics extends React.Component { post_counts_day: null, user_counts_with_posts_day: null, recent_active_users: null, - newly_created_users: null + newly_created_users: null, + unique_user_count: null }); this.getData(newProps.team.id); } - componentWillUnmount() { - } - render() { - var serverError = ''; - if (this.state.serverError) { - serverError = <div className='form-group has-error'><label className='control-label'>{this.state.serverError}</label></div>; - } - - var totalCount = ( - <div className='col-sm-3'> - <div className='total-count'> - <div className='title'>{'Total Users'}<i className='fa fa-users'/></div> - <div className='content'>{this.state.users == null ? 'Loading...' : Object.keys(this.state.users).length}</div> - </div> - </div> - ); - - var openChannelCount = ( - <div className='col-sm-3'> - <div className='total-count'> - <div className='title'>{'Public Channels'}<i className='fa fa-globe'/></div> - <div className='content'>{this.state.channel_open_count == null ? 'Loading...' : this.state.channel_open_count}</div> - </div> - </div> - ); - - var openPrivateCount = ( - <div className='col-sm-3'> - <div className='total-count'> - <div className='title'>{'Private Groups'}<i className='fa fa-lock'/></div> - <div className='content'>{this.state.channel_private_count == null ? 'Loading...' : this.state.channel_private_count}</div> - </div> - </div> - ); - - var postCount = ( - <div className='col-sm-3'> - <div className='total-count'> - <div className='title'>{'Total Posts'}<i className='fa fa-comment'/></div> - <div className='content'>{this.state.post_count == null ? 'Loading...' : this.state.post_count}</div> - </div> - </div> - ); - - var postCountsByDay = ( - <div className='col-sm-12'> - <div className='total-count by-day'> - <div className='title'>{'Total Posts'}</div> - <div className='content'>{'Loading...'}</div> - </div> - </div> - ); - - if (this.state.post_counts_day != null) { - postCountsByDay = ( - <div className='col-sm-12'> - <div className='total-count by-day'> - <div className='title'>{'Total Posts'}</div> - <div className='content'> - <LineChart - data={this.state.post_counts_day} - width='740' - height='225' - /> - </div> - </div> - </div> - ); - } - - var usersWithPostsByDay = ( - <div className='col-sm-12'> - <div className='total-count by-day'> - <div className='title'>{'Total Posts'}</div> - <div>{'Loading...'}</div> - </div> - </div> - ); - - if (this.state.user_counts_with_posts_day != null) { - usersWithPostsByDay = ( - <div className='col-sm-12'> - <div className='total-count by-day'> - <div className='title'>{'Active Users With Posts'}</div> - <div className='content'> - <LineChart - data={this.state.user_counts_with_posts_day} - width='740' - height='225' - /> - </div> - </div> - </div> - ); - } - - var recentActiveUser = ( - <div className='recent-active-users'> - <div>{'Recent Active Users'}</div> - <div>{'Loading...'}</div> - </div> - ); - - if (this.state.recent_active_users != null) { - recentActiveUser = ( - <div className='col-sm-6'> - <div className='total-count recent-active-users'> - <div className='title'>{'Recent Active Users'}</div> - <div className='content'> - <table> - <tbody> - { - this.state.recent_active_users.map((user) => { - const tooltip = ( - <Tooltip id={'recent-user-email-tooltip-' + user.id}> - {user.email} - </Tooltip> - ); - - return ( - <tr key={'recent-user-table-entry-' + user.id}> - <td> - <OverlayTrigger - delayShow={Constants.OVERLAY_TIME_DELAY} - placement='top' - overlay={tooltip} - > - <time> - {user.username} - </time> - </OverlayTrigger> - </td> - <td>{Utils.displayDateTime(user.last_activity_at)}</td> - </tr> - ); - }) - } - </tbody> - </table> - </div> - </div> - </div> - ); - } - - var newUsers = ( - <div className='recent-active-users'> - <div>{'Newly Created Users'}</div> - <div>{'Loading...'}</div> - </div> - ); - - if (this.state.newly_created_users != null) { - newUsers = ( - <div className='col-sm-6'> - <div className='total-count recent-active-users'> - <div className='title'>{'Newly Created Users'}</div> - <div className='content'> - <table> - <tbody> - { - this.state.newly_created_users.map((user) => { - const tooltip = ( - <Tooltip id={'new-user-email-tooltip-' + user.id}> - {user.email} - </Tooltip> - ); - - return ( - <tr key={'new-user-table-entry-' + user.id}> - <td> - <OverlayTrigger - delayShow={Constants.OVERLAY_TIME_DELAY} - placement='top' - overlay={tooltip} - > - <time> - {user.username} - </time> - </OverlayTrigger> - </td> - <td>{Utils.displayDateTime(user.create_at)}</td> - </tr> - ); - }) - } - </tbody> - </table> - </div> - </div> - </div> - ); - } - return ( - <div className='wrapper--fixed team_statistics'> - <h3>{'Statistics for ' + this.props.team.name}</h3> - {serverError} - <div className='row'> - {totalCount} - {postCount} - {openChannelCount} - {openPrivateCount} - </div> - <div className='row'> - {postCountsByDay} - </div> - <div className='row'> - {usersWithPostsByDay} - </div> - <div className='row'> - {recentActiveUser} - {newUsers} - </div> + <div> + <Analytics + title={this.props.team.name} + users={this.state.users} + channelOpenCount={this.state.channel_open_count} + channelPrivateCount={this.state.channel_private_count} + postCount={this.state.post_count} + postCountsDay={this.state.post_counts_day} + userCountsWithPostsDay={this.state.user_counts_with_posts_day} + recentActiveUsers={this.state.recent_active_users} + newlyCreatedUsers={this.state.newly_created_users} + uniqueUserCount={this.state.unique_user_count} + serverError={this.state.serverError} + /> </div> ); } diff --git a/web/react/components/create_comment.jsx b/web/react/components/create_comment.jsx index cae94429c..aa7ab6a7b 100644 --- a/web/react/components/create_comment.jsx +++ b/web/react/components/create_comment.jsx @@ -129,7 +129,7 @@ export default class CreateComment extends React.Component { function handlePostError(err) { let state = {}; - if (err.message === 'Invalid RootId parameter') { + if (err.id === 'api.post.create_post.root_id.app_error') { PostStore.removePendingPost(post.channel_id, post.pending_post_id); if ($('#post_deleted').length > 0) { diff --git a/web/react/components/create_post.jsx b/web/react/components/create_post.jsx index a476863a3..de971c43f 100644 --- a/web/react/components/create_post.jsx +++ b/web/react/components/create_post.jsx @@ -191,7 +191,7 @@ export default class CreatePost extends React.Component { (err) => { const state = {}; - if (err.message === 'Invalid RootId parameter') { + if (err.id === 'api.post.create_post.root_id.app_error') { if ($('#post_deleted').length > 0) { $('#post_deleted').modal('show'); } diff --git a/web/react/components/login.jsx b/web/react/components/login.jsx index 1d9b3e906..6887489a7 100644 --- a/web/react/components/login.jsx +++ b/web/react/components/login.jsx @@ -7,6 +7,8 @@ import LoginLdap from './login_ldap.jsx'; import * as Utils from '../utils/utils.jsx'; import Constants from '../utils/constants.jsx'; +var FormattedMessage = ReactIntl.FormattedMessage; + export default class Login extends React.Component { constructor(props) { super(props); @@ -86,7 +88,12 @@ export default class Login extends React.Component { if (emailSignup) { forgotPassword = ( <div className='form-group'> - <a href={'/' + teamName + '/reset_password'}>{'I forgot my password'}</a> + <a href={'/' + teamName + '/reset_password'}> + <FormattedMessage + id='login.forgot_password' + defaultMessage='I forgot my password' + /> + </a> </div> ); } @@ -141,7 +148,13 @@ export default class Login extends React.Component { {ldapLogin} {userSignUp} <div className='form-group margin--extra form-group--small'> - <span><a href='/find_team'>{'Find your other teams'}</a></span> + <span> + <a href='/find_team'> + <FormattedMessage + id='login.find_teams' + defaultMessage='Find your other teams' + /> + </a></span> </div> {forgotPassword} {teamSignUp} diff --git a/web/react/components/sidebar.jsx b/web/react/components/sidebar.jsx index eaeb7bb91..c902731c9 100644 --- a/web/react/components/sidebar.jsx +++ b/web/react/components/sidebar.jsx @@ -506,9 +506,9 @@ export default class Sidebar extends React.Component { link.rel = 'shortcut icon'; link.id = 'favicon'; if (this.badgesActive) { - link.href = '/static/images/redfavicon.ico'; + link.href = '/static/images/favicon/redfavicon-16x16.png'; } else { - link.href = '/static/images/favicon.ico'; + link.href = '/static/images/favicon/favicon-16x16.png'; } var head = document.getElementsByTagName('head')[0]; var oldLink = document.getElementById('favicon'); diff --git a/web/react/components/suggestion/at_mention_provider.jsx b/web/react/components/suggestion/at_mention_provider.jsx index 8c2893448..e502c981d 100644 --- a/web/react/components/suggestion/at_mention_provider.jsx +++ b/web/react/components/suggestion/at_mention_provider.jsx @@ -5,6 +5,8 @@ import SuggestionStore from '../../stores/suggestion_store.jsx'; import UserStore from '../../stores/user_store.jsx'; import * as Utils from '../../utils/utils.jsx'; +const MaxUserSuggestions = 40; + class AtMentionSuggestion extends React.Component { render() { const {item, isSelection, onClick} = this.props; @@ -78,6 +80,10 @@ export default class AtMentionProvider { if (user.username.startsWith(usernamePrefix)) { filtered.push(user); } + + if (filtered.length >= MaxUserSuggestions) { + break; + } } // add dummy users to represent the @all and @channel special mentions diff --git a/web/react/components/team_general_tab.jsx b/web/react/components/team_general_tab.jsx index cc06a940e..b6fb3389f 100644 --- a/web/react/components/team_general_tab.jsx +++ b/web/react/components/team_general_tab.jsx @@ -66,7 +66,7 @@ export default class GeneralTab extends React.Component { handleTeamListingRadio(listing) { if (global.window.mm_config.EnableTeamListing !== 'true' && listing) { - this.setState({clientError: 'Team directory has been disabled. Please ask a system admin to enable it.'}); + this.setState({clientError: 'Team Directory has been disabled. Please ask a System Admin to enable the Team Directory in the System Console team settings.'}); } else { this.setState({allow_team_listing: listing}); } diff --git a/web/react/components/team_signup_welcome_page.jsx b/web/react/components/team_signup_welcome_page.jsx index aa91a1329..a374dd363 100644 --- a/web/react/components/team_signup_welcome_page.jsx +++ b/web/react/components/team_signup_welcome_page.jsx @@ -59,7 +59,13 @@ export default class TeamSignupWelcomePage extends React.Component { } }.bind(this), function error(err) { - this.setState({serverError: err.message}); + let errorMsg = err.message; + + if (err.detailed_error.indexOf('Invalid RCPT TO address provided') >= 0) { + errorMsg = 'Please enter a valid email address'; + } + + this.setState({emailError: '', serverError: errorMsg}); }.bind(this) ); } diff --git a/web/react/components/user_settings/manage_languages.jsx b/web/react/components/user_settings/manage_languages.jsx new file mode 100644 index 000000000..123165b76 --- /dev/null +++ b/web/react/components/user_settings/manage_languages.jsx @@ -0,0 +1,101 @@ +// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved. +// See License.txt for license information. + +import * as Client from '../../utils/client.jsx'; +import * as Utils from '../../utils/utils.jsx'; + +export default class ManageLanguage extends React.Component { + constructor(props) { + super(props); + + this.setupInitialState = this.setupInitialState.bind(this); + this.setLanguage = this.setLanguage.bind(this); + this.changeLanguage = this.changeLanguage.bind(this); + this.submitUser = this.submitUser.bind(this); + this.state = this.setupInitialState(props); + } + setupInitialState(props) { + var user = props.user; + return { + languages: Utils.languages(), + locale: user.locale + }; + } + setLanguage(e) { + this.setState({locale: e.target.value}); + } + changeLanguage(e) { + e.preventDefault(); + + var user = this.props.user; + var locale = this.state.locale; + + user.locale = locale; + + this.submitUser(user); + } + submitUser(user) { + Client.updateUser(user, + () => { + window.location.reload(true); + }, + (err) => { + let serverError; + if (err.message) { + serverError = err.message; + } else { + serverError = err; + } + this.setState({serverError}); + } + ); + } + render() { + let serverError; + if (this.state.serverError) { + serverError = <label className='has-error'>{this.state.serverError}</label>; + } + + const options = []; + this.state.languages.forEach((lang) => { + options.push( + <option + key={lang.value} + value={lang.value} + > + {lang.name} + </option>); + }); + + return ( + <div key='changeLanguage'> + <br/> + <label className='control-label'>{'Change interface language'}</label> + <div className='padding-top'> + <select + ref='language' + className='form-control' + value={this.state.locale} + onChange={this.setLanguage} + > + {options} + </select> + {serverError} + <div className='padding-top'> + <a + className={'btn btn-sm btn-primary'} + href='#' + onClick={this.changeLanguage} + > + {'Set language'} + </a> + </div> + </div> + </div> + ); + } +} + +ManageLanguage.propTypes = { + user: React.PropTypes.object +};
\ No newline at end of file diff --git a/web/react/components/user_settings/user_settings_display.jsx b/web/react/components/user_settings/user_settings_display.jsx index 1ff0a2913..f2c2502fb 100644 --- a/web/react/components/user_settings/user_settings_display.jsx +++ b/web/react/components/user_settings/user_settings_display.jsx @@ -5,7 +5,9 @@ import {savePreferences} from '../../utils/client.jsx'; import SettingItemMin from '../setting_item_min.jsx'; import SettingItemMax from '../setting_item_max.jsx'; import Constants from '../../utils/constants.jsx'; +const PreReleaseFeatures = Constants.PRE_RELEASE_FEATURES; import PreferenceStore from '../../stores/preference_store.jsx'; +import ManageLanguages from './manage_languages.jsx'; import * as Utils from '../../utils/utils.jsx'; function getDisplayStateFromStores() { @@ -78,6 +80,7 @@ export default class UserSettingsDisplay extends React.Component { let clockSection; let nameFormatSection; let fontSection; + let languagesSection; if (this.props.activeSection === 'clock') { const clockFormat = [false, false]; @@ -292,6 +295,48 @@ export default class UserSettingsDisplay extends React.Component { ); } + if (Utils.isFeatureEnabled(PreReleaseFeatures.LOC_PREVIEW)) { + if (this.props.activeSection === 'languages') { + var inputs = []; + inputs.push( + <ManageLanguages + user={this.props.user} + key='languages-ui' + /> + ); + + languagesSection = ( + <SettingItemMax + title={'Language'} + width='medium' + inputs={inputs} + updateSection={(e) => { + this.updateSection(''); + e.preventDefault(); + }} + /> + ); + } else { + var locale = 'English'; + Utils.languages().forEach((l) => { + if (l.value === this.props.user.locale) { + locale = l.name; + } + }); + + languagesSection = ( + <SettingItemMin + title={'Language'} + width='medium' + describe={locale} + updateSection={() => { + this.updateSection('languages'); + }} + /> + ); + } + } + return ( <div> <div className='modal-header'> @@ -324,6 +369,7 @@ export default class UserSettingsDisplay extends React.Component { <div className='divider-dark'/> {nameFormatSection} <div className='divider-dark'/> + {languagesSection} </div> </div> ); diff --git a/web/react/components/view_image.jsx b/web/react/components/view_image.jsx index 31ec91248..d11f8a21c 100644 --- a/web/react/components/view_image.jsx +++ b/web/react/components/view_image.jsx @@ -211,7 +211,7 @@ export default class ViewImageModal extends React.Component { } const filename = this.props.filenames[this.state.imgId]; - const fileUrl = Utils.getFileUrl(filename); + const fileUrl = Utils.getFileUrl(filename, true); var content; if (this.state.loaded[this.state.imgId]) { @@ -377,6 +377,7 @@ function ImagePreview({filename, fileUrl, fileInfo, maxHeight}) { <a href={fileUrl} target='_blank' + download={true} > <img style={{maxHeight}} |