summaryrefslogtreecommitdiffstats
path: root/web/react
diff options
context:
space:
mode:
Diffstat (limited to 'web/react')
-rw-r--r--web/react/.eslintrc.json2
-rw-r--r--web/react/components/admin_console/admin_controller.jsx4
-rw-r--r--web/react/components/admin_console/analytics.jsx489
-rw-r--r--web/react/components/admin_console/line_chart.jsx50
-rw-r--r--web/react/components/admin_console/service_settings.jsx35
-rw-r--r--web/react/components/admin_console/system_analytics.jsx216
-rw-r--r--web/react/components/admin_console/team_analytics.jsx253
-rw-r--r--web/react/components/admin_console/user_item.jsx61
-rw-r--r--web/react/components/analytics/doughnut_chart.jsx (renamed from web/react/components/admin_console/doughnut_chart.jsx)4
-rw-r--r--web/react/components/analytics/line_chart.jsx90
-rw-r--r--web/react/components/analytics/statistic_count.jsx (renamed from web/react/components/admin_console/statistic_count.jsx)4
-rw-r--r--web/react/components/analytics/system_analytics.jsx346
-rw-r--r--web/react/components/analytics/table_chart.jsx60
-rw-r--r--web/react/components/analytics/team_analytics.jsx235
-rw-r--r--web/react/components/channel_invite_modal.jsx65
-rw-r--r--web/react/components/channel_members_modal.jsx113
-rw-r--r--web/react/components/confirm_modal.jsx10
-rw-r--r--web/react/components/filtered_user_list.jsx136
-rw-r--r--web/react/components/member_list.jsx56
-rw-r--r--web/react/components/member_list_item.jsx142
-rw-r--r--web/react/components/member_list_team.jsx26
-rw-r--r--web/react/components/more_direct_channels.jsx220
-rw-r--r--web/react/components/team_members_dropdown.jsx (renamed from web/react/components/member_list_team_item.jsx)141
-rw-r--r--web/react/components/team_members_modal.jsx41
-rw-r--r--web/react/components/user_list.jsx53
-rw-r--r--web/react/components/user_list_row.jsx65
-rw-r--r--web/react/components/user_settings/manage_languages.jsx36
-rw-r--r--web/react/components/user_settings/user_settings_display.jsx159
-rw-r--r--web/react/stores/analytics_store.jsx85
-rw-r--r--web/react/utils/async_client.jsx270
-rw-r--r--web/react/utils/client.jsx23
-rw-r--r--web/react/utils/constants.jsx22
-rw-r--r--web/react/utils/utils.jsx4
33 files changed, 1741 insertions, 1775 deletions
diff --git a/web/react/.eslintrc.json b/web/react/.eslintrc.json
index 51345113b..bc06643c1 100644
--- a/web/react/.eslintrc.json
+++ b/web/react/.eslintrc.json
@@ -192,7 +192,7 @@
"react/display-name": [2, { "ignoreTranspilerName": false }],
"react/no-deprecated": 2,
"react/no-is-mounted": 2,
- "react/no-string-refs": 1,
+ "react/no-string-refs": 0,
"react/jsx-pascal-case": 2,
"react/jsx-indent": [1, 4],
"react/jsx-equals-spacing": [2, "never"],
diff --git a/web/react/components/admin_console/admin_controller.jsx b/web/react/components/admin_console/admin_controller.jsx
index de0b085bc..32ed70a99 100644
--- a/web/react/components/admin_console/admin_controller.jsx
+++ b/web/react/components/admin_console/admin_controller.jsx
@@ -21,10 +21,10 @@ import TeamSettingsTab from './team_settings.jsx';
import ServiceSettingsTab from './service_settings.jsx';
import LegalAndSupportSettingsTab from './legal_and_support_settings.jsx';
import TeamUsersTab from './team_users.jsx';
-import TeamAnalyticsTab from './team_analytics.jsx';
+import TeamAnalyticsTab from '../analytics/team_analytics.jsx';
import LdapSettingsTab from './ldap_settings.jsx';
import LicenseSettingsTab from './license_settings.jsx';
-import SystemAnalyticsTab from './system_analytics.jsx';
+import SystemAnalyticsTab from '../analytics/system_analytics.jsx';
export default class AdminController extends React.Component {
constructor(props) {
diff --git a/web/react/components/admin_console/analytics.jsx b/web/react/components/admin_console/analytics.jsx
deleted file mode 100644
index ec9ad4da0..000000000
--- a/web/react/components/admin_console/analytics.jsx
+++ /dev/null
@@ -1,489 +0,0 @@
-// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved.
-// See License.txt for license information.
-
-import Constants from '../../utils/constants.jsx';
-import LineChart from './line_chart.jsx';
-import DoughnutChart from './doughnut_chart.jsx';
-import StatisticCount from './statistic_count.jsx';
-
-var Tooltip = ReactBootstrap.Tooltip;
-var OverlayTrigger = ReactBootstrap.OverlayTrigger;
-
-import {injectIntl, intlShape, defineMessages, FormattedMessage, FormattedDate} from 'mm-intl';
-
-const holders = defineMessages({
- analyticsTotalUsers: {
- id: 'admin.analytics.totalUsers',
- defaultMessage: 'Total Users'
- },
- analyticsPublicChannels: {
- id: 'admin.analytics.publicChannels',
- defaultMessage: 'Public Channels'
- },
- analyticsPrivateGroups: {
- id: 'admin.analytics.privateGroups',
- defaultMessage: 'Private Groups'
- },
- analyticsTotalPosts: {
- id: 'admin.analytics.totalPosts',
- defaultMessage: 'Total Posts'
- },
- analyticsFilePosts: {
- id: 'admin.analytics.totalFilePosts',
- defaultMessage: 'Posts with Files'
- },
- analyticsHashtagPosts: {
- id: 'admin.analytics.totalHashtagPosts',
- defaultMessage: 'Posts with Hashtags'
- },
- analyticsIncomingHooks: {
- id: 'admin.analytics.totalIncomingWebhooks',
- defaultMessage: 'Incoming Webhooks'
- },
- analyticsOutgoingHooks: {
- id: 'admin.analytics.totalOutgoingWebhooks',
- defaultMessage: 'Outgoing Webhooks'
- },
- analyticsChannelTypes: {
- id: 'admin.analytics.channelTypes',
- defaultMessage: 'Channel Types'
- },
- analyticsTextPosts: {
- id: 'admin.analytics.textPosts',
- defaultMessage: 'Posts with Text-only'
- },
- analyticsPostTypes: {
- id: 'admin.analytics.postTypes',
- defaultMessage: 'Posts, Files and Hashtags'
- }
-});
-
-export default class Analytics extends React.Component {
- constructor(props) {
- super(props);
-
- this.state = {};
- }
-
- render() { // in the future, break down these into smaller components
- const {formatMessage} = this.props.intl;
-
- var serverError = '';
- if (this.props.serverError) {
- serverError = <div className='form-group has-error'><label className='control-label'>{this.props.serverError}</label></div>;
- }
-
- let loading = (
- <h5>
- <FormattedMessage
- id='admin.analytics.loading'
- defaultMessage='Loading...'
- />
- </h5>
- );
-
- let firstRow;
- let extraGraphs;
- if (this.props.showAdvanced) {
- firstRow = (
- <div className='row'>
- <StatisticCount
- title={formatMessage(holders.analyticsTotalUsers)}
- icon='fa-users'
- count={this.props.uniqueUserCount}
- />
- <StatisticCount
- title={formatMessage(holders.analyticsTotalPosts)}
- icon='fa-comment'
- count={this.props.postCount}
- />
- <StatisticCount
- title={formatMessage(holders.analyticsIncomingHooks)}
- icon='fa-arrow-down'
- count={this.props.incomingWebhookCount}
- />
- <StatisticCount
- title={formatMessage(holders.analyticsOutgoingHooks)}
- icon='fa-arrow-up'
- count={this.props.outgoingWebhookCount}
- />
- </div>
- );
-
- const channelTypeData = [
- {
- value: this.props.channelOpenCount,
- color: '#46BFBD',
- highlight: '#5AD3D1',
- label: formatMessage(holders.analyticsPublicChannels)
- },
- {
- value: this.props.channelPrivateCount,
- color: '#FDB45C',
- highlight: '#FFC870',
- label: formatMessage(holders.analyticsPrivateGroups)
- }
- ];
-
- const postTypeData = [
- {
- value: this.props.filePostCount,
- color: '#46BFBD',
- highlight: '#5AD3D1',
- label: formatMessage(holders.analyticsFilePosts)
- },
- {
- value: this.props.filePostCount,
- color: '#F7464A',
- highlight: '#FF5A5E',
- label: formatMessage(holders.analyticsHashtagPosts)
- },
- {
- value: this.props.postCount - this.props.filePostCount - this.props.hashtagPostCount,
- color: '#FDB45C',
- highlight: '#FFC870',
- label: formatMessage(holders.analyticsTextPosts)
- }
- ];
-
- extraGraphs = (
- <div className='row'>
- <DoughnutChart
- title={formatMessage(holders.analyticsChannelTypes)}
- data={channelTypeData}
- width='300'
- height='225'
- />
- <DoughnutChart
- title={formatMessage(holders.analyticsPostTypes)}
- data={postTypeData}
- width='300'
- height='225'
- />
- </div>
- );
- } else {
- firstRow = (
- <div className='row'>
- <StatisticCount
- title={formatMessage(holders.analyticsTotalUsers)}
- icon='fa-users'
- count={this.props.uniqueUserCount}
- />
- <StatisticCount
- title={formatMessage(holders.analyticsPublicChannels)}
- icon='fa-globe'
- count={this.props.channelOpenCount}
- />
- <StatisticCount
- title={formatMessage(holders.analyticsPrivateGroups)}
- icon='fa-lock'
- count={this.props.channelPrivateCount}
- />
- <StatisticCount
- title={formatMessage(holders.analyticsTotalPosts)}
- icon='fa-comment'
- count={this.props.postCount}
- />
- </div>
- );
- }
-
- let postCountsByDay;
- if (this.props.postCountsDay == null) {
- postCountsByDay = (
- <div className='col-sm-12'>
- <div className='total-count by-day'>
- <div className='title'>
- <FormattedMessage
- id='admin.analytics.totalPosts'
- defaultMessage='Total Posts'
- />
- </div>
- <div className='content'>{loading}</div>
- </div>
- </div>
- );
- } else {
- let content;
- if (this.props.postCountsDay.labels.length === 0) {
- content = (
- <h5>
- <FormattedMessage
- id='admin.analytics.meaningful'
- defaultMessage='Not enough data for a meaningful representation.'
- />
- </h5>
- );
- } 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'>
- <FormattedMessage
- id='admin.analytics.totalPosts'
- defaultMessage='Total Posts'
- />
- </div>
- <div className='content'>
- {content}
- </div>
- </div>
- </div>
- );
- }
-
- let usersWithPostsByDay;
- if (this.props.userCountsWithPostsDay == null) {
- usersWithPostsByDay = (
- <div className='col-sm-12'>
- <div className='total-count by-day'>
- <div className='title'>
- <FormattedMessage
- id='admin.analytics.activeUsers'
- defaultMessage='Active Users With Posts'
- />
- </div>
- <div className='content'>{loading}</div>
- </div>
- </div>
- );
- } else {
- let content;
- if (this.props.userCountsWithPostsDay.labels.length === 0) {
- content = (
- <h5>
- <FormattedMessage
- id='admin.analytics.meaningful'
- defaultMessage='Not enough data for a meaningful representation.'
- />
- </h5>
- );
- } 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'>
- <FormattedMessage
- id='admin.analytics.activeUsers'
- defaultMessage='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>
- <FormattedDate
- value={user.last_activity_at}
- day='numeric'
- month='long'
- year='numeric'
- hour12={true}
- hour='2-digit'
- minute='2-digit'
- />
- </td>
- </tr>
- );
- })
- }
- </tbody>
- </table>
- );
- }
- recentActiveUser = (
- <div className='col-sm-6'>
- <div className='total-count recent-active-users'>
- <div className='title'>
- <FormattedMessage
- id='admin.analytics.recentActive'
- defaultMessage='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>
- <FormattedDate
- value={user.create_at}
- day='numeric'
- month='long'
- year='numeric'
- hour12={true}
- hour='2-digit'
- minute='2-digit'
- />
- </td>
- </tr>
- );
- })
- }
- </tbody>
- </table>
- );
- }
- newUsers = (
- <div className='col-sm-6'>
- <div className='total-count recent-active-users'>
- <div className='title'>
- <FormattedMessage
- id='admin.analytics.newlyCreated'
- defaultMessage='Newly Created Users'
- />
- </div>
- <div className='content'>
- {content}
- </div>
- </div>
- </div>
- );
- }
-
- return (
- <div className='wrapper--fixed team_statistics'>
- <h3>
- <FormattedMessage
- id='admin.analytics.title'
- defaultMessage='Statistics for {title}'
- values={{
- title: this.props.title
- }}
- />
- </h3>
- {serverError}
- {firstRow}
- {extraGraphs}
- <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 = {
- intl: intlShape.isRequired,
- title: React.PropTypes.string,
- channelOpenCount: React.PropTypes.number,
- channelPrivateCount: React.PropTypes.number,
- postCount: React.PropTypes.number,
- showAdvanced: React.PropTypes.bool,
- filePostCount: React.PropTypes.number,
- hashtagPostCount: React.PropTypes.number,
- incomingWebhookCount: React.PropTypes.number,
- outgoingWebhookCount: 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
-};
-
-export default injectIntl(Analytics);
diff --git a/web/react/components/admin_console/line_chart.jsx b/web/react/components/admin_console/line_chart.jsx
deleted file mode 100644
index 7e2f95c84..000000000
--- a/web/react/components/admin_console/line_chart.jsx
+++ /dev/null
@@ -1,50 +0,0 @@
-// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
-// See License.txt for license information.
-
-export default class LineChart extends React.Component {
- constructor(props) {
- super(props);
-
- this.initChart = this.initChart.bind(this);
- this.chart = null;
- }
-
- componentDidMount() {
- this.initChart(this.props);
- }
-
- componentWillReceiveProps(nextProps) {
- if (this.chart) {
- this.chart.destroy();
- this.initChart(nextProps);
- }
- }
-
- componentWillUnmount() {
- if (this.chart) {
- this.chart.destroy();
- }
- }
-
- initChart(props) {
- var el = ReactDOM.findDOMNode(this);
- var ctx = el.getContext('2d');
- this.chart = new Chart(ctx).Line(props.data, props.options || {}); //eslint-disable-line new-cap
- }
-
- render() {
- return (
- <canvas
- width={this.props.width}
- height={this.props.height}
- />
- );
- }
-}
-
-LineChart.propTypes = {
- width: React.PropTypes.string,
- height: React.PropTypes.string,
- data: React.PropTypes.object,
- options: React.PropTypes.object
-};
diff --git a/web/react/components/admin_console/service_settings.jsx b/web/react/components/admin_console/service_settings.jsx
index 047c7eb8d..9ed81b6a3 100644
--- a/web/react/components/admin_console/service_settings.jsx
+++ b/web/react/components/admin_console/service_settings.jsx
@@ -31,6 +31,10 @@ var holders = defineMessages({
id: 'admin.service.sessionDaysEx',
defaultMessage: 'Ex "30"'
},
+ corsExample: {
+ id: 'admin.service.corsEx',
+ defaultMessage: 'http://example.com'
+ },
saving: {
id: 'admin.service.saving',
defaultMessage: 'Saving Config...'
@@ -131,6 +135,8 @@ class ServiceSettings extends React.Component {
config.ServiceSettings.SessionCacheInMinutes = SessionCacheInMinutes;
ReactDOM.findDOMNode(this.refs.SessionCacheInMinutes).value = SessionCacheInMinutes;
+ config.ServiceSettings.AllowCorsFrom = ReactDOM.findDOMNode(this.refs.AllowCorsFrom).value.trim();
+
Client.saveConfig(
config,
() => {
@@ -766,6 +772,35 @@ class ServiceSettings extends React.Component {
<div className='form-group'>
<label
className='control-label col-sm-4'
+ htmlFor='AllowCorsFrom'
+ >
+ <FormattedMessage
+ id='admin.service.corsTitle'
+ defaultMessage='Allow Cross-origin Requests from:'
+ />
+ </label>
+ <div className='col-sm-8'>
+ <input
+ type='text'
+ className='form-control'
+ id='AllowCorsFrom'
+ ref='AllowCorsFrom'
+ placeholder={formatMessage(holders.corsExample)}
+ defaultValue={this.props.config.ServiceSettings.AllowCorsFrom}
+ onChange={this.handleChange}
+ />
+ <p className='help-text'>
+ <FormattedMessage
+ id='admin.service.corsDescription'
+ defaultMessage='Enable HTTP Cross origin request from a specific domain. Use "*" if you want to allow CORS from any domain or leave it blank to disable it.'
+ />
+ </p>
+ </div>
+ </div>
+
+ <div className='form-group'>
+ <label
+ className='control-label col-sm-4'
htmlFor='SessionLengthWebInDays'
>
<FormattedMessage
diff --git a/web/react/components/admin_console/system_analytics.jsx b/web/react/components/admin_console/system_analytics.jsx
deleted file mode 100644
index f983db177..000000000
--- a/web/react/components/admin_console/system_analytics.jsx
+++ /dev/null
@@ -1,216 +0,0 @@
-// 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 {injectIntl, intlShape, defineMessages} from 'mm-intl';
-
-const labels = defineMessages({
- totalPosts: {
- id: 'admin.system_analytics.totalPosts',
- defaultMessage: 'Total Posts'
- },
- activeUsers: {
- id: 'admin.system_analytics.activeUsers',
- defaultMessage: 'Active Users With Posts'
- },
- title: {
- id: 'admin.system_analytics.title',
- defaultMessage: 'the System'
- }
-});
-
-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
- const {formatMessage} = this.props.intl;
- 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: formatMessage(labels.totalPosts),
- 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: formatMessage(labels.activeUsers),
- 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});
- }
- );
-
- if (global.window.mm_license.IsLicensed === 'true') {
- Client.getSystemAnalytics(
- 'extra_counts',
- (data) => {
- for (var index in data) {
- if (data[index].name === 'file_post_count') {
- this.setState({file_post_count: data[index].value});
- }
-
- if (data[index].name === 'hashtag_post_count') {
- this.setState({hashtag_post_count: data[index].value});
- }
-
- if (data[index].name === 'incoming_webhook_count') {
- this.setState({incoming_webhook_count: data[index].value});
- }
-
- if (data[index].name === 'outgoing_webhook_count') {
- this.setState({outgoing_webhook_count: data[index].value});
- }
- }
- },
- (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
- intl={this.props.intl}
- title={this.props.intl.formatMessage(labels.title)}
- channelOpenCount={this.state.channel_open_count}
- channelPrivateCount={this.state.channel_private_count}
- postCount={this.state.post_count}
- showAdvanced={global.window.mm_license.IsLicensed === 'true'}
- filePostCount={this.state.file_post_count}
- hashtagPostCount={this.state.hashtag_post_count}
- incomingWebhookCount={this.state.incoming_webhook_count}
- outgoingWebhookCount={this.state.outgoing_webhook_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 = {
- intl: intlShape.isRequired,
- team: React.PropTypes.object
-};
-
-export default injectIntl(SystemAnalytics);
diff --git a/web/react/components/admin_console/team_analytics.jsx b/web/react/components/admin_console/team_analytics.jsx
deleted file mode 100644
index 808d8046d..000000000
--- a/web/react/components/admin_console/team_analytics.jsx
+++ /dev/null
@@ -1,253 +0,0 @@
-// 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 {injectIntl, intlShape, defineMessages} from 'mm-intl';
-
-const labels = defineMessages({
- totalPosts: {
- id: 'admin.team_analytics.totalPosts',
- defaultMessage: 'Total Posts'
- },
- activeUsers: {
- id: 'admin.team_analytics.activeUsers',
- defaultMessage: 'Active Users With Posts'
- }
-});
-
-class TeamAnalytics 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(this.props.team.id);
- }
-
- getData(teamId) { // should be moved to an action creator eventually
- const {formatMessage} = this.props.intl;
- Client.getTeamAnalytics(
- teamId,
- '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.getTeamAnalytics(
- teamId,
- 'post_counts_day',
- (data) => {
- data.reverse();
-
- var chartData = {
- labels: [],
- datasets: [{
- label: formatMessage(labels.totalPosts),
- 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.getTeamAnalytics(
- teamId,
- 'user_counts_with_posts_day',
- (data) => {
- data.reverse();
-
- var chartData = {
- labels: [],
- datasets: [{
- label: formatMessage(labels.activeUsers),
- 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});
- }
- );
-
- Client.getProfilesForTeam(
- teamId,
- (users) => {
- this.setState({users});
-
- var usersList = [];
- for (var id in users) {
- if (users.hasOwnProperty(id)) {
- usersList.push(users[id]);
- }
- }
-
- usersList.sort((a, b) => {
- if (a.last_activity_at < b.last_activity_at) {
- return 1;
- }
-
- if (a.last_activity_at > b.last_activity_at) {
- return -1;
- }
-
- return 0;
- });
-
- 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;
- }
- }
-
- this.setState({recent_active_users: recentActive});
-
- usersList.sort((a, b) => {
- if (a.create_at < b.create_at) {
- return 1;
- }
-
- if (a.create_at > b.create_at) {
- return -1;
- }
-
- return 0;
- });
-
- var newlyCreated = [];
- for (let i = 0; i < usersList.length; i++) {
- newlyCreated.push(usersList[i]);
- if (i > 19) {
- break;
- }
- }
-
- this.setState({newly_created_users: newlyCreated});
- },
- (err) => {
- this.setState({serverError: err.message});
- }
- );
- }
-
- componentWillReceiveProps(newProps) {
- this.setState({
- 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
- });
-
- this.getData(newProps.team.id);
- }
-
- render() {
- return (
- <div>
- <Analytics
- intl={this.props.intl}
- 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>
- );
- }
-}
-
-TeamAnalytics.propTypes = {
- intl: intlShape.isRequired,
- team: React.PropTypes.object
-};
-
-export default injectIntl(TeamAnalytics);
diff --git a/web/react/components/admin_console/user_item.jsx b/web/react/components/admin_console/user_item.jsx
index 009a9f004..4af350bcd 100644
--- a/web/react/components/admin_console/user_item.jsx
+++ b/web/react/components/admin_console/user_item.jsx
@@ -7,26 +7,7 @@ import UserStore from '../../stores/user_store.jsx';
import ConfirmModal from '../confirm_modal.jsx';
import TeamStore from '../../stores/team_store.jsx';
-import {injectIntl, intlShape, defineMessages, FormattedMessage} from 'mm-intl';
-
-var holders = defineMessages({
- confirmDemoteRoleTitle: {
- id: 'admin.user_item.confirmDemoteRoleTitle',
- defaultMessage: 'Confirm demotion from System Admin role'
- },
- confirmDemotion: {
- id: 'admin.user_item.confirmDemotion',
- defaultMessage: 'Confirm Demotion'
- },
- confirmDemoteDescription: {
- id: 'admin.user_item.confirmDemoteDescription',
- defaultMessage: 'If you demote yourself from the System Admin role and there is not another user with System Admin privileges, you\'ll need to re-assign a System Admin by accessing the Mattermost server through a terminal and running the following command.'
- },
- confirmDemotionCmd: {
- id: 'admin.user_item.confirmDemotionCmd',
- defaultMessage: 'platform -assign_role -team_name="yourteam" -email="name@yourcompany.com" -role="system_admin"'
- }
-});
+import {FormattedMessage} from 'mm-intl';
export default class UserItem extends React.Component {
constructor(props) {
@@ -336,15 +317,44 @@ export default class UserItem extends React.Component {
);
}
const me = UserStore.getCurrentUser();
- const {formatMessage} = this.props.intl;
let makeDemoteModal = null;
if (this.props.user.id === me.id) {
+ const title = (
+ <FormattedMessage
+ id='admin.user_item.confirmDemoteRoleTitle'
+ defaultMessage='Confirm demotion from System Admin role'
+ />
+ );
+
+ const message = (
+ <div>
+ <FormattedMessage
+ id='admin.user_item.confirmDemoteDescription'
+ defaultMessage="If you demote yourself from the System Admin role and there is not another user with System Admin privileges, you\'ll need to re-assign a System Admin by accessing the Mattermost server through a terminal and running the following command."
+ />
+ <br/>
+ <br/>
+ <FormattedMessage
+ id='admin.user_item.confirmDemotionCmd'
+ defaultMessage='platform -assign_role -team_name="yourteam" -email="name@yourcompany.com" -role="system_admin"'
+ />
+ {serverError}
+ </div>
+ );
+
+ const confirmButton = (
+ <FormattedMessage
+ id='admin.user_item.confirmDemotion'
+ defaultMessage='Confirm Demotion'
+ />
+ );
+
makeDemoteModal = (
<ConfirmModal
show={this.state.showDemoteModal}
- title={formatMessage(holders.confirmDemoteRoleTitle)}
- message={[formatMessage(holders.confirmDemoteDescription), React.createElement('br'), React.createElement('br'), formatMessage(holders.confirmDemotionCmd), serverError]}
- confirm_button={formatMessage(holders.confirmDemotion)}
+ title={title}
+ message={message}
+ confirmButton={confirmButton}
onConfirm={this.handleDemoteSubmit}
onCancel={this.handleDemoteCancel}
/>
@@ -405,10 +415,7 @@ export default class UserItem extends React.Component {
}
UserItem.propTypes = {
- intl: intlShape.isRequired,
user: React.PropTypes.object.isRequired,
refreshProfiles: React.PropTypes.func.isRequired,
doPasswordReset: React.PropTypes.func.isRequired
};
-
-export default injectIntl(UserItem);
diff --git a/web/react/components/admin_console/doughnut_chart.jsx b/web/react/components/analytics/doughnut_chart.jsx
index e2dc01528..00bb66f0a 100644
--- a/web/react/components/admin_console/doughnut_chart.jsx
+++ b/web/react/components/analytics/doughnut_chart.jsx
@@ -39,7 +39,7 @@ export default class DoughnutChart extends React.Component {
if (this.props.data == null) {
content = (
<FormattedMessage
- id='admin.analytics.loading'
+ id='analytics.chart.loading'
defaultMessage='Loading...'
/>
);
@@ -69,7 +69,7 @@ export default class DoughnutChart extends React.Component {
}
DoughnutChart.propTypes = {
- title: React.PropTypes.string,
+ title: React.PropTypes.node,
width: React.PropTypes.string,
height: React.PropTypes.string,
data: React.PropTypes.array,
diff --git a/web/react/components/analytics/line_chart.jsx b/web/react/components/analytics/line_chart.jsx
new file mode 100644
index 000000000..d1bb6b9cb
--- /dev/null
+++ b/web/react/components/analytics/line_chart.jsx
@@ -0,0 +1,90 @@
+// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+import {FormattedMessage} from 'mm-intl';
+
+export default class LineChart extends React.Component {
+ constructor(props) {
+ super(props);
+
+ this.initChart = this.initChart.bind(this);
+ this.chart = null;
+ }
+
+ componentDidMount() {
+ this.initChart();
+ }
+
+ componentDidUpdate() {
+ if (this.chart) {
+ this.chart.destroy();
+ }
+ this.initChart();
+ }
+
+ componentWillUnmount() {
+ if (this.chart) {
+ this.chart.destroy();
+ }
+ }
+
+ initChart() {
+ if (!this.refs.canvas) {
+ return;
+ }
+ var el = ReactDOM.findDOMNode(this.refs.canvas);
+ var ctx = el.getContext('2d');
+ this.chart = new Chart(ctx).Line(this.props.data, this.props.options || {}); //eslint-disable-line new-cap
+ }
+
+ render() {
+ let content;
+ if (this.props.data == null) {
+ content = (
+ <FormattedMessage
+ id='analytics.chart.loading'
+ defaultMessage='Loading...'
+ />
+ );
+ } else if (this.props.data.labels.length === 0) {
+ content = (
+ <h5>
+ <FormattedMessage
+ id='analytics.chart.meaningful'
+ defaultMessage='Not enough data for a meaningful representation.'
+ />
+ </h5>
+ );
+ } else {
+ content = (
+ <canvas
+ ref='canvas'
+ width={this.props.width}
+ height={this.props.height}
+ />
+ );
+ }
+
+ return (
+ <div className='col-sm-12'>
+ <div className='total-count by-day'>
+ <div className='title'>
+ {this.props.title}
+ </div>
+ <div className='content'>
+ {content}
+ </div>
+ </div>
+ </div>
+ );
+ }
+}
+
+LineChart.propTypes = {
+ title: React.PropTypes.node.isRequired,
+ width: React.PropTypes.string.isRequired,
+ height: React.PropTypes.string.isRequired,
+ data: React.PropTypes.object,
+ options: React.PropTypes.object
+};
+
diff --git a/web/react/components/admin_console/statistic_count.jsx b/web/react/components/analytics/statistic_count.jsx
index 118a0ad31..cf457310f 100644
--- a/web/react/components/admin_console/statistic_count.jsx
+++ b/web/react/components/analytics/statistic_count.jsx
@@ -7,7 +7,7 @@ export default class StatisticCount extends React.Component {
render() {
let loading = (
<FormattedMessage
- id='admin.analytics.loading'
+ id='analytics.chart.loading'
defaultMessage='Loading...'
/>
);
@@ -27,7 +27,7 @@ export default class StatisticCount extends React.Component {
}
StatisticCount.propTypes = {
- title: React.PropTypes.string.isRequired,
+ title: React.PropTypes.node.isRequired,
icon: React.PropTypes.string.isRequired,
count: React.PropTypes.number
};
diff --git a/web/react/components/analytics/system_analytics.jsx b/web/react/components/analytics/system_analytics.jsx
new file mode 100644
index 000000000..a2b783a79
--- /dev/null
+++ b/web/react/components/analytics/system_analytics.jsx
@@ -0,0 +1,346 @@
+// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+import LineChart from './line_chart.jsx';
+import DoughnutChart from './doughnut_chart.jsx';
+import StatisticCount from './statistic_count.jsx';
+
+import AnalyticsStore from '../../stores/analytics_store.jsx';
+
+import * as Utils from '../../utils/utils.jsx';
+import * as AsyncClient from '../../utils/async_client.jsx';
+import Constants from '../../utils/constants.jsx';
+const StatTypes = Constants.StatTypes;
+
+import {injectIntl, intlShape, defineMessages, FormattedMessage} from 'mm-intl';
+
+const holders = defineMessages({
+ analyticsPublicChannels: {
+ id: 'analytics.system.publicChannels',
+ defaultMessage: 'Public Channels'
+ },
+ analyticsPrivateGroups: {
+ id: 'analytics.system.privateGroups',
+ defaultMessage: 'Private Groups'
+ },
+ analyticsFilePosts: {
+ id: 'analytics.system.totalFilePosts',
+ defaultMessage: 'Posts with Files'
+ },
+ analyticsHashtagPosts: {
+ id: 'analytics.system.totalHashtagPosts',
+ defaultMessage: 'Posts with Hashtags'
+ },
+ analyticsTextPosts: {
+ id: 'analytics.system.textPosts',
+ defaultMessage: 'Posts with Text-only'
+ }
+});
+
+class SystemAnalytics extends React.Component {
+ constructor(props) {
+ super(props);
+
+ this.onChange = this.onChange.bind(this);
+
+ this.state = {stats: AnalyticsStore.getAllSystem()};
+ }
+
+ componentDidMount() {
+ AnalyticsStore.addChangeListener(this.onChange);
+
+ AsyncClient.getStandardAnalytics();
+ AsyncClient.getPostsPerDayAnalytics();
+ AsyncClient.getUsersPerDayAnalytics();
+
+ if (global.window.mm_license.IsLicensed === 'true') {
+ AsyncClient.getAdvancedAnalytics();
+ }
+ }
+
+ componentWillUnmount() {
+ AnalyticsStore.removeChangeListener(this.onChange);
+ }
+
+ shouldComponentUpdate(nextProps, nextState) {
+ if (!Utils.areObjectsEqual(nextState.stats, this.state.stats)) {
+ return true;
+ }
+
+ return false;
+ }
+
+ onChange() {
+ this.setState({stats: AnalyticsStore.getAllSystem()});
+ }
+
+ render() {
+ const stats = this.state.stats;
+
+ let advancedCounts;
+ let advancedGraphs;
+ if (global.window.mm_license.IsLicensed === 'true') {
+ advancedCounts = (
+ <div className='row'>
+ <StatisticCount
+ title={
+ <FormattedMessage
+ id='analytics.system.totalSessions'
+ defaultMessage='Total Sessions'
+ />
+ }
+ icon='fa-signal'
+ count={stats[StatTypes.TOTAL_SESSIONS]}
+ />
+ <StatisticCount
+ title={
+ <FormattedMessage
+ id='analytics.system.totalCommands'
+ defaultMessage='Total Commands'
+ />
+ }
+ icon='fa-terminal'
+ count={stats[StatTypes.TOTAL_COMMANDS]}
+ />
+ <StatisticCount
+ title={
+ <FormattedMessage
+ id='analytics.system.totalIncomingWebhooks'
+ defaultMessage='Incoming Webhooks'
+ />
+ }
+ icon='fa-arrow-down'
+ count={stats[StatTypes.TOTAL_IHOOKS]}
+ />
+ <StatisticCount
+ title={
+ <FormattedMessage
+ id='analytics.system.totalOutgoingWebhooks'
+ defaultMessage='Outgoing Webhooks'
+ />
+ }
+ icon='fa-arrow-up'
+ count={stats[StatTypes.TOTAL_OHOOKS]}
+ />
+ </div>
+ );
+
+ const channelTypeData = formatChannelDoughtnutData(stats[StatTypes.TOTAL_PUBLIC_CHANNELS], stats[StatTypes.TOTAL_PRIVATE_GROUPS], this.props.intl);
+ const postTypeData = formatPostDoughtnutData(stats[StatTypes.TOTAL_FILE_POSTS], stats[StatTypes.TOTAL_HASHTAG_POSTS], stats[StatTypes.TOTAL_POSTS], this.props.intl);
+
+ advancedGraphs = (
+ <div className='row'>
+ <DoughnutChart
+ title={
+ <FormattedMessage
+ id='analytics.system.channelTypes'
+ defaultMessage='Channel Types'
+ />
+ }
+ data={channelTypeData}
+ width='300'
+ height='225'
+ />
+ <DoughnutChart
+ title={
+ <FormattedMessage
+ id='analytics.system.postTypes'
+ defaultMessage='Posts, Files and Hashtags'
+ />
+ }
+ data={postTypeData}
+ width='300'
+ height='225'
+ />
+ </div>
+ );
+ }
+
+ const postCountsDay = formatPostsPerDayData(stats[StatTypes.POST_PER_DAY]);
+ const userCountsWithPostsDay = formatUsersWithPostsPerDayData(stats[StatTypes.USERS_WITH_POSTS_PER_DAY]);
+
+ return (
+ <div className='wrapper--fixed team_statistics'>
+ <h3>
+ <FormattedMessage
+ id='analytics.system.title'
+ defaultMessage='System Statistics'
+ />
+ </h3>
+ <div className='row'>
+ <StatisticCount
+ title={
+ <FormattedMessage
+ id='analytics.system.totalUsers'
+ defaultMessage='Total Users'
+ />
+ }
+ icon='fa-user'
+ count={stats[StatTypes.TOTAL_USERS]}
+ />
+ <StatisticCount
+ title={
+ <FormattedMessage
+ id='analytics.system.totalTeams'
+ defaultMessage='Total Teams'
+ />
+ }
+ icon='fa-users'
+ count={stats[StatTypes.TOTAL_TEAMS]}
+ />
+ <StatisticCount
+ title={
+ <FormattedMessage
+ id='analytics.system.totalPosts'
+ defaultMessage='Total Posts'
+ />
+ }
+ icon='fa-comment'
+ count={stats[StatTypes.TOTAL_POSTS]}
+ />
+ <StatisticCount
+ title={
+ <FormattedMessage
+ id='analytics.system.totalChannels'
+ defaultMessage='Total Channels'
+ />
+ }
+ icon='fa-globe'
+ count={stats[StatTypes.TOTAL_PUBLIC_CHANNELS] + stats[StatTypes.TOTAL_PRIVATE_GROUPS]}
+ />
+ </div>
+ {advancedCounts}
+ {advancedGraphs}
+ <div className='row'>
+ <LineChart
+ title={
+ <FormattedMessage
+ id='analytics.system.totalPosts'
+ defaultMessage='Total Posts'
+ />
+ }
+ data={postCountsDay}
+ width='740'
+ height='225'
+ />
+ </div>
+ <div className='row'>
+ <LineChart
+ title={
+ <FormattedMessage
+ id='analytics.system.activeUsers'
+ defaultMessage='Active Users With Posts'
+ />
+ }
+ data={userCountsWithPostsDay}
+ width='740'
+ height='225'
+ />
+ </div>
+ </div>
+ );
+ }
+}
+
+SystemAnalytics.propTypes = {
+ intl: intlShape.isRequired,
+ team: React.PropTypes.object
+};
+
+export default injectIntl(SystemAnalytics);
+
+export function formatChannelDoughtnutData(totalPublic, totalPrivate, intl) {
+ const {formatMessage} = intl;
+ const channelTypeData = [
+ {
+ value: totalPublic,
+ color: '#46BFBD',
+ highlight: '#5AD3D1',
+ label: formatMessage(holders.analyticsPublicChannels)
+ },
+ {
+ value: totalPrivate,
+ color: '#FDB45C',
+ highlight: '#FFC870',
+ label: formatMessage(holders.analyticsPrivateGroups)
+ }
+ ];
+
+ return channelTypeData;
+}
+
+export function formatPostDoughtnutData(filePosts, hashtagPosts, totalPosts, intl) {
+ const {formatMessage} = intl;
+ const postTypeData = [
+ {
+ value: filePosts,
+ color: '#46BFBD',
+ highlight: '#5AD3D1',
+ label: formatMessage(holders.analyticsFilePosts)
+ },
+ {
+ value: hashtagPosts,
+ color: '#F7464A',
+ highlight: '#FF5A5E',
+ label: formatMessage(holders.analyticsHashtagPosts)
+ },
+ {
+ value: totalPosts - filePosts - hashtagPosts,
+ color: '#FDB45C',
+ highlight: '#FFC870',
+ label: formatMessage(holders.analyticsTextPosts)
+ }
+ ];
+
+ return postTypeData;
+}
+
+export function formatPostsPerDayData(data) {
+ var chartData = {
+ labels: [],
+ datasets: [{
+ 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);
+ }
+ }
+
+ return chartData;
+}
+
+export function formatUsersWithPostsPerDayData(data) {
+ var chartData = {
+ labels: [],
+ datasets: [{
+ 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);
+ }
+ }
+
+ return chartData;
+}
diff --git a/web/react/components/analytics/table_chart.jsx b/web/react/components/analytics/table_chart.jsx
new file mode 100644
index 000000000..c94fa300b
--- /dev/null
+++ b/web/react/components/analytics/table_chart.jsx
@@ -0,0 +1,60 @@
+// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+import Constants from '../../utils/constants.jsx';
+
+const Tooltip = ReactBootstrap.Tooltip;
+const OverlayTrigger = ReactBootstrap.OverlayTrigger;
+
+export default class TableChart extends React.Component {
+ render() {
+ return (
+ <div className='col-sm-6'>
+ <div className='total-count recent-active-users'>
+ <div className='title'>
+ {this.props.title}
+ </div>
+ <div className='content'>
+ <table>
+ <tbody>
+ {
+ this.props.data.map((item) => {
+ const tooltip = (
+ <Tooltip id={'tip-table-entry-' + item.name}>
+ {item.tip}
+ </Tooltip>
+ );
+
+ return (
+ <tr key={'table-entry-' + item.name}>
+ <td>
+ <OverlayTrigger
+ delayShow={Constants.OVERLAY_TIME_DELAY}
+ placement='top'
+ overlay={tooltip}
+ >
+ <time>
+ {item.name}
+ </time>
+ </OverlayTrigger>
+ </td>
+ <td>
+ {item.value}
+ </td>
+ </tr>
+ );
+ })
+ }
+ </tbody>
+ </table>
+ </div>
+ </div>
+ </div>
+ );
+ }
+}
+
+TableChart.propTypes = {
+ title: React.PropTypes.node,
+ data: React.PropTypes.array
+};
diff --git a/web/react/components/analytics/team_analytics.jsx b/web/react/components/analytics/team_analytics.jsx
new file mode 100644
index 000000000..1236c070b
--- /dev/null
+++ b/web/react/components/analytics/team_analytics.jsx
@@ -0,0 +1,235 @@
+// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+import LineChart from './line_chart.jsx';
+import StatisticCount from './statistic_count.jsx';
+import TableChart from './table_chart.jsx';
+
+import AnalyticsStore from '../../stores/analytics_store.jsx';
+
+import * as Utils from '../../utils/utils.jsx';
+import * as AsyncClient from '../../utils/async_client.jsx';
+import Constants from '../../utils/constants.jsx';
+const StatTypes = Constants.StatTypes;
+
+import {formatPostsPerDayData, formatUsersWithPostsPerDayData} from './system_analytics.jsx';
+import {injectIntl, intlShape, FormattedMessage, FormattedDate} from 'mm-intl';
+
+class TeamAnalytics extends React.Component {
+ constructor(props) {
+ super(props);
+
+ this.onChange = this.onChange.bind(this);
+
+ this.state = {stats: AnalyticsStore.getAllTeam(this.props.team.id)};
+ }
+
+ componentDidMount() {
+ AnalyticsStore.addChangeListener(this.onChange);
+
+ this.getData(this.props.team.id);
+ }
+
+ getData(id) {
+ AsyncClient.getStandardAnalytics(id);
+ AsyncClient.getPostsPerDayAnalytics(id);
+ AsyncClient.getUsersPerDayAnalytics(id);
+ AsyncClient.getRecentAndNewUsersAnalytics(id);
+ }
+
+ componentWillUnmount() {
+ AnalyticsStore.removeChangeListener(this.onChange);
+ }
+
+ componentWillReceiveProps(nextProps) {
+ this.getData(nextProps.team.id);
+ this.setState({stats: AnalyticsStore.getAllTeam(nextProps.team.id)});
+ }
+
+ shouldComponentUpdate(nextProps, nextState) {
+ if (!Utils.areObjectsEqual(nextState.stats, this.state.stats)) {
+ return true;
+ }
+
+ if (!Utils.areObjectsEqual(nextProps.team, this.props.team)) {
+ return true;
+ }
+
+ return false;
+ }
+
+ onChange() {
+ this.setState({stats: AnalyticsStore.getAllTeam(this.props.team.id)});
+ }
+
+ render() {
+ const stats = this.state.stats;
+ const postCountsDay = formatPostsPerDayData(stats[StatTypes.POST_PER_DAY]);
+ const userCountsWithPostsDay = formatUsersWithPostsPerDayData(stats[StatTypes.USERS_WITH_POSTS_PER_DAY]);
+ const recentActiveUsers = formatRecentUsersData(stats[StatTypes.RECENTLY_ACTIVE_USERS]);
+ const newlyCreatedUsers = formatNewUsersData(stats[StatTypes.NEWLY_CREATED_USERS]);
+
+ return (
+ <div className='wrapper--fixed team_statistics'>
+ <h3>
+ <FormattedMessage
+ id='analytics.team.title'
+ defaultMessage='Team Statistics for {team}'
+ values={{
+ team: this.props.team.name
+ }}
+ />
+ </h3>
+ <div className='row'>
+ <StatisticCount
+ title={
+ <FormattedMessage
+ id='analytics.team.totalUsers'
+ defaultMessage='Total Users'
+ />
+ }
+ icon='fa-user'
+ count={stats[StatTypes.TOTAL_USERS]}
+ />
+ <StatisticCount
+ title={
+ <FormattedMessage
+ id='analytics.team.publicChannels'
+ defaultMessage='Public Channels'
+ />
+ }
+ icon='fa-users'
+ count={stats[StatTypes.TOTAL_PUBLIC_CHANNELS]}
+ />
+ <StatisticCount
+ title={
+ <FormattedMessage
+ id='analytics.team.privateGroups'
+ defaultMessage='Private Groups'
+ />
+ }
+ icon='fa-globe'
+ count={stats[StatTypes.TOTAL_PRIVATE_GROUPS]}
+ />
+ <StatisticCount
+ title={
+ <FormattedMessage
+ id='analytics.team.totalPosts'
+ defaultMessage='Total Posts'
+ />
+ }
+ icon='fa-comment'
+ count={stats[StatTypes.TOTAL_POSTS]}
+ />
+ </div>
+ <div className='row'>
+ <LineChart
+ title={
+ <FormattedMessage
+ id='analytics.team.totalPosts'
+ defaultMessage='Total Posts'
+ />
+ }
+ data={postCountsDay}
+ width='740'
+ height='225'
+ />
+ </div>
+ <div className='row'>
+ <LineChart
+ title={
+ <FormattedMessage
+ id='analytics.team.activeUsers'
+ defaultMessage='Active Users With Posts'
+ />
+ }
+ data={userCountsWithPostsDay}
+ width='740'
+ height='225'
+ />
+ </div>
+ <div className='row'>
+ <TableChart
+ title={
+ <FormattedMessage
+ id='analytics.team.activeUsers'
+ defaultMessage='Recent Active Users'
+ />
+ }
+ data={recentActiveUsers}
+ />
+ <TableChart
+ title={
+ <FormattedMessage
+ id='analytics.team.newlyCreated'
+ defaultMessage='Newly Created Users'
+ />
+ }
+ data={newlyCreatedUsers}
+ />
+ </div>
+ </div>
+ );
+ }
+}
+
+TeamAnalytics.propTypes = {
+ intl: intlShape.isRequired,
+ team: React.PropTypes.object.isRequired
+};
+
+export default injectIntl(TeamAnalytics);
+
+export function formatRecentUsersData(data) {
+ if (data == null) {
+ return [];
+ }
+
+ const formattedData = data.map((user) => {
+ const item = {};
+ item.name = user.username;
+ item.value = (
+ <FormattedDate
+ value={user.last_activity_at}
+ day='numeric'
+ month='long'
+ year='numeric'
+ hour12={true}
+ hour='2-digit'
+ minute='2-digit'
+ />
+ );
+ item.tip = user.email;
+
+ return item;
+ });
+
+ return formattedData;
+}
+
+export function formatNewUsersData(data) {
+ if (data == null) {
+ return [];
+ }
+
+ const formattedData = data.map((user) => {
+ const item = {};
+ item.name = user.username;
+ item.value = (
+ <FormattedDate
+ value={user.create_at}
+ day='numeric'
+ month='long'
+ year='numeric'
+ hour12={true}
+ hour='2-digit'
+ minute='2-digit'
+ />
+ );
+ item.tip = user.email;
+
+ return item;
+ });
+
+ return formattedData;
+}
diff --git a/web/react/components/channel_invite_modal.jsx b/web/react/components/channel_invite_modal.jsx
index c9fe871d0..6c8d51abb 100644
--- a/web/react/components/channel_invite_modal.jsx
+++ b/web/react/components/channel_invite_modal.jsx
@@ -1,7 +1,7 @@
// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
-import MemberList from './member_list.jsx';
+import FilteredUserList from './filtered_user_list.jsx';
import LoadingScreen from './loading_screen.jsx';
import UserStore from '../stores/user_store.jsx';
@@ -22,8 +22,12 @@ export default class ChannelInviteModal extends React.Component {
this.onListenerChange = this.onListenerChange.bind(this);
this.handleInvite = this.handleInvite.bind(this);
+ this.createInviteButton = this.createInviteButton.bind(this);
+
// the state gets populated when the modal is shown
- this.state = {};
+ this.state = {
+ loading: true
+ };
}
shouldComponentUpdate(nextProps, nextState) {
if (!this.props.show && !nextProps.show) {
@@ -77,19 +81,6 @@ export default class ChannelInviteModal extends React.Component {
loading: false
};
}
- onShow() {
- if ($(window).width() > 768) {
- $(ReactDOM.findDOMNode(this.refs.modalBody)).perfectScrollbar();
- $(ReactDOM.findDOMNode(this.refs.modalBody)).css('max-height', $(window).height() - 200);
- } else {
- $(ReactDOM.findDOMNode(this.refs.modalBody)).css('max-height', $(window).height() - 150);
- }
- }
- componentDidUpdate(prevProps) {
- if (this.props.show && !prevProps.show) {
- this.onShow();
- }
- }
componentWillReceiveProps(nextProps) {
if (!this.props.show && nextProps.show) {
ChannelStore.addExtraInfoChangeListener(this.onListenerChange);
@@ -108,9 +99,10 @@ export default class ChannelInviteModal extends React.Component {
this.setState(newState);
}
}
- handleInvite(userId) {
- var data = {};
- data.user_id = userId;
+ handleInvite(user) {
+ const data = {
+ user_id: user.id
+ };
Client.addChannelMember(
this.props.channel.id,
@@ -124,27 +116,40 @@ export default class ChannelInviteModal extends React.Component {
}
);
}
+ createInviteButton({user}) {
+ return (
+ <a
+ onClick={this.handleInvite.bind(this, user)}
+ className='btn btn-sm btn-primary'
+ >
+ <i className='glyphicon glyphicon-envelope'/>
+ <FormattedMessage
+ id='channel_invite.add'
+ defaultMessage=' Add'
+ />
+ </a>
+ );
+ }
render() {
var inviteError = null;
if (this.state.inviteError) {
inviteError = (<label className='has-error control-label'>{this.state.inviteError}</label>);
}
- var currentMember = ChannelStore.getCurrentMember();
- var isAdmin = false;
- if (currentMember) {
- isAdmin = Utils.isAdmin(currentMember.roles) || Utils.isAdmin(UserStore.getCurrentUser().roles);
- }
-
var content;
if (this.state.loading) {
content = (<LoadingScreen/>);
} else {
+ let maxHeight = 1000;
+ if (Utils.windowHeight() <= 1200) {
+ maxHeight = Utils.windowHeight() - 300;
+ }
+
content = (
- <MemberList
- memberList={this.state.nonmembers}
- isAdmin={isAdmin}
- handleInvite={this.handleInvite}
+ <FilteredUserList
+ style={{maxHeight}}
+ users={this.state.nonmembers}
+ actions={[this.createInviteButton]}
/>
);
}
@@ -164,9 +169,7 @@ export default class ChannelInviteModal extends React.Component {
<span className='name'>{this.props.channel.display_name}</span>
</Modal.Title>
</Modal.Header>
- <Modal.Body
- ref='modalBody'
- >
+ <Modal.Body>
{inviteError}
{content}
</Modal.Body>
diff --git a/web/react/components/channel_members_modal.jsx b/web/react/components/channel_members_modal.jsx
index fd452f206..688ab7dd2 100644
--- a/web/react/components/channel_members_modal.jsx
+++ b/web/react/components/channel_members_modal.jsx
@@ -1,8 +1,8 @@
// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
+import FilteredUserList from './filtered_user_list.jsx';
import LoadingScreen from './loading_screen.jsx';
-import MemberList from './member_list.jsx';
import ChannelInviteModal from './channel_invite_modal.jsx';
import UserStore from '../stores/user_store.jsx';
@@ -24,6 +24,8 @@ export default class ChannelMembersModal extends React.Component {
this.onChange = this.onChange.bind(this);
this.handleRemove = this.handleRemove.bind(this);
+ this.createRemoveMemberButton = this.createRemoveMemberButton.bind(this);
+
// the rest of the state gets populated when the modal is shown
this.state = {
showInviteModal: false
@@ -51,24 +53,10 @@ export default class ChannelMembersModal extends React.Component {
};
}
- const users = UserStore.getActiveOnlyProfiles();
- const memberList = extraInfo.members;
-
- const nonmemberList = [];
- for (const id in users) {
- if (users.hasOwnProperty(id)) {
- let found = false;
- for (let i = 0; i < memberList.length; i++) {
- if (memberList[i].id === id) {
- found = true;
- break;
- }
- }
- if (!found) {
- nonmemberList.push(users[id]);
- }
- }
- }
+ // clone the member list since we mutate it later on
+ const memberList = extraInfo.members.map((member) => {
+ return Object.assign({}, member);
+ });
function compareByUsername(a, b) {
if (a.username < b.username) {
@@ -81,24 +69,12 @@ export default class ChannelMembersModal extends React.Component {
}
memberList.sort(compareByUsername);
- nonmemberList.sort(compareByUsername);
return {
- nonmemberList,
memberList,
loading: false
};
}
- onShow() {
- if ($(window).width() > 768) {
- $(ReactDOM.findDOMNode(this.refs.modalBody)).perfectScrollbar();
- }
- }
- componentDidUpdate(prevProps) {
- if (this.props.show && !prevProps.show) {
- this.onShow();
- }
- }
componentWillReceiveProps(nextProps) {
if (!this.props.show && nextProps.show) {
ChannelStore.addExtraInfoChangeListener(this.onChange);
@@ -116,41 +92,25 @@ export default class ChannelMembersModal extends React.Component {
this.setState(newState);
}
}
- handleRemove(userId) {
- // Make sure the user is a member of the channel
- const memberList = this.state.memberList;
- let found = false;
- for (let i = 0; i < memberList.length; i++) {
- if (memberList[i].id === userId) {
- found = true;
- break;
- }
- }
-
- if (!found) {
- return;
- }
+ handleRemove(user) {
+ const userId = user.id;
const data = {};
data.user_id = userId;
- Client.removeChannelMember(ChannelStore.getCurrentId(), data,
+ Client.removeChannelMember(
+ ChannelStore.getCurrentId(),
+ data,
() => {
- let oldMember;
+ const memberList = this.state.memberList.slice();
for (let i = 0; i < memberList.length; i++) {
if (userId === memberList[i].id) {
- oldMember = memberList[i];
memberList.splice(i, 1);
break;
}
}
- const nonmemberList = this.state.nonmemberList;
- if (oldMember) {
- nonmemberList.push(oldMember);
- }
-
- this.setState({memberList, nonmemberList});
+ this.setState({memberList});
AsyncClient.getChannelExtraInfo();
},
(err) => {
@@ -158,30 +118,40 @@ export default class ChannelMembersModal extends React.Component {
}
);
}
- render() {
- var maxHeight = 1000;
- if (Utils.windowHeight() <= 1200) {
- maxHeight = Utils.windowHeight() - 300;
- }
-
- const currentMember = ChannelStore.getCurrentMember();
- let isAdmin = false;
- if (currentMember) {
- isAdmin = Utils.isAdmin(currentMember.roles) || Utils.isAdmin(UserStore.getCurrentUser().roles);
+ createRemoveMemberButton({user}) {
+ if (user.id === UserStore.getCurrentId()) {
+ return null;
}
+ return (
+ <button
+ type='button'
+ className='btn btn-primary btn-message'
+ onClick={this.handleRemove.bind(this, user)}
+ >
+ <FormattedMessage
+ id='channel_members_modal.removeMember'
+ defaultMessage='Remove Member'
+ />
+ </button>
+ );
+ }
+ render() {
let content;
if (this.state.loading) {
content = (<LoadingScreen/>);
} else {
+ let maxHeight = 1000;
+ if (Utils.windowHeight() <= 1200) {
+ maxHeight = Utils.windowHeight() - 300;
+ }
+
content = (
- <div className='team-member-list'>
- <MemberList
- memberList={this.state.memberList}
- isAdmin={isAdmin}
- handleRemove={this.handleRemove}
- />
- </div>
+ <FilteredUserList
+ style={{maxHeight}}
+ users={this.state.memberList}
+ actions={[this.createRemoveMemberButton]}
+ />
);
}
@@ -217,7 +187,6 @@ export default class ChannelMembersModal extends React.Component {
</Modal.Header>
<Modal.Body
ref='modalBody'
- style={{maxHeight}}
>
{content}
</Modal.Body>
diff --git a/web/react/components/confirm_modal.jsx b/web/react/components/confirm_modal.jsx
index 987649f38..bb3576684 100644
--- a/web/react/components/confirm_modal.jsx
+++ b/web/react/components/confirm_modal.jsx
@@ -44,7 +44,7 @@ export default class ConfirmModal extends React.Component {
className='btn btn-primary'
onClick={this.props.onConfirm}
>
- {this.props.confirm_button}
+ {this.props.confirmButton}
</button>
</Modal.Footer>
</Modal>
@@ -55,13 +55,13 @@ export default class ConfirmModal extends React.Component {
ConfirmModal.defaultProps = {
title: '',
message: '',
- confirm_button: ''
+ confirmButton: ''
};
ConfirmModal.propTypes = {
show: React.PropTypes.bool.isRequired,
- title: React.PropTypes.string,
- message: React.PropTypes.string,
- confirm_button: React.PropTypes.string,
+ title: React.PropTypes.node,
+ message: React.PropTypes.node,
+ confirmButton: React.PropTypes.node,
onConfirm: React.PropTypes.func.isRequired,
onCancel: React.PropTypes.func.isRequired
};
diff --git a/web/react/components/filtered_user_list.jsx b/web/react/components/filtered_user_list.jsx
new file mode 100644
index 000000000..ffd6ebf53
--- /dev/null
+++ b/web/react/components/filtered_user_list.jsx
@@ -0,0 +1,136 @@
+// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+import UserList from './user_list.jsx';
+
+import {intlShape, injectIntl, defineMessages, FormattedMessage} from 'mm-intl';
+
+const holders = defineMessages({
+ member: {
+ id: 'filtered_user_list.member',
+ defaultMessage: 'Member'
+ },
+ search: {
+ id: 'filtered_user_list.search',
+ defaultMessage: 'Search members'
+ }
+});
+
+class FilteredUserList extends React.Component {
+ constructor(props) {
+ super(props);
+
+ this.handleFilterChange = this.handleFilterChange.bind(this);
+
+ this.state = {
+ filter: ''
+ };
+ }
+
+ componentDidMount() {
+ $(ReactDOM.findDOMNode(this.refs.userList)).perfectScrollbar();
+ }
+
+ componentDidUpdate(prevProps, prevState) {
+ if (prevState.filter !== this.state.filter) {
+ $(ReactDOM.findDOMNode(this.refs.userList)).scrollTop(0);
+ }
+ }
+
+ handleFilterChange(e) {
+ this.setState({
+ filter: e.target.value
+ });
+ }
+
+ render() {
+ const {formatMessage} = this.props.intl;
+
+ let users = this.props.users;
+
+ if (this.state.filter) {
+ const filter = this.state.filter.toLowerCase();
+
+ users = users.filter((user) => {
+ return user.username.toLowerCase().indexOf(filter) !== -1 ||
+ (user.first_name && user.first_name.toLowerCase().indexOf(filter) !== -1) ||
+ (user.last_name && user.last_name.toLowerCase().indexOf(filter) !== -1) ||
+ (user.nickname && user.nickname.toLowerCase().indexOf(filter) !== -1);
+ });
+ }
+
+ let count;
+ if (users.length === this.props.users.length) {
+ count = (
+ <FormattedMessage
+ id='filtered_user_list.count'
+ defaultMessage='{count} {count, plural,
+ one {member}
+ other {members}
+ }'
+ values={{
+ count: users.length
+ }}
+ />
+ );
+ } else {
+ count = (
+ <FormattedMessage
+ id='filtered_user_list.countTotal'
+ defaultMessage='{count} {count, plural,
+ one {member}
+ other {members}
+ } of {total} Total'
+ values={{
+ count: users.length,
+ total: this.props.users.length
+ }}
+ />
+ );
+ }
+
+ return (
+ <div
+ className='filtered-user-list'
+ style={this.props.style}
+ >
+ <div className='filter-row'>
+ <div className='col-sm-6'>
+ <input
+ ref='filter'
+ className='form-control filter-textbox'
+ placeholder={formatMessage(holders.search)}
+ onInput={this.handleFilterChange}
+ />
+ </div>
+ <div className='col-sm-6'>
+ <span className='member-count'>{count}</span>
+ </div>
+ </div>
+ <div
+ ref='userList'
+ className='user-list'
+ >
+ <UserList
+ users={users}
+ actions={this.props.actions}
+ />
+ </div>
+ </div>
+ );
+ }
+}
+
+FilteredUserList.defaultProps = {
+ users: [],
+ actions: []
+};
+
+FilteredUserList.propTypes = {
+ intl: intlShape.isRequired,
+ users: React.PropTypes.arrayOf(React.PropTypes.object),
+ actions: React.PropTypes.arrayOf(React.PropTypes.func),
+ style: React.PropTypes.object
+};
+
+export default injectIntl(FilteredUserList);
diff --git a/web/react/components/member_list.jsx b/web/react/components/member_list.jsx
deleted file mode 100644
index 62d600279..000000000
--- a/web/react/components/member_list.jsx
+++ /dev/null
@@ -1,56 +0,0 @@
-// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
-// See License.txt for license information.
-
-import MemberListItem from './member_list_item.jsx';
-
-import {FormattedMessage} from 'mm-intl';
-
-export default class MemberList extends React.Component {
- render() {
- var members = [];
-
- if (this.props.memberList !== null) {
- members = this.props.memberList;
- }
-
- var message = null;
- if (members.length === 0) {
- message = (
- <tr><td>
- <FormattedMessage
- id='member_list.noUsersAdd'
- defaultMessage='No users to add.'
- />
- </td></tr>
- );
- }
-
- return (
- <table className='table more-table member-list-holder'>
- <tbody>
- {members.map(function mymembers(member) {
- return (
- <MemberListItem
- key={member.id}
- member={member}
- isAdmin={this.props.isAdmin}
- handleInvite={this.props.handleInvite}
- handleRemove={this.props.handleRemove}
- handleMakeAdmin={this.props.handleMakeAdmin}
- />
- );
- }, this)}
- {message}
- </tbody>
- </table>
- );
- }
-}
-
-MemberList.propTypes = {
- memberList: React.PropTypes.array,
- isAdmin: React.PropTypes.bool,
- handleInvite: React.PropTypes.func,
- handleRemove: React.PropTypes.func,
- handleMakeAdmin: React.PropTypes.func
-};
diff --git a/web/react/components/member_list_item.jsx b/web/react/components/member_list_item.jsx
deleted file mode 100644
index 88b98738d..000000000
--- a/web/react/components/member_list_item.jsx
+++ /dev/null
@@ -1,142 +0,0 @@
-// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
-// See License.txt for license information.
-
-import UserStore from '../stores/user_store.jsx';
-import * as Utils from '../utils/utils.jsx';
-
-import {FormattedMessage} from 'mm-intl';
-
-export default class MemberListItem extends React.Component {
- constructor(props) {
- super(props);
-
- this.handleInvite = this.handleInvite.bind(this);
- this.handleRemove = this.handleRemove.bind(this);
- this.handleMakeAdmin = this.handleMakeAdmin.bind(this);
- }
- handleInvite(e) {
- e.preventDefault();
- this.props.handleInvite(this.props.member.id);
- }
- handleRemove(e) {
- e.preventDefault();
- this.props.handleRemove(this.props.member.id);
- }
- handleMakeAdmin(e) {
- e.preventDefault();
- this.props.handleMakeAdmin(this.props.member.id);
- }
- render() {
- var member = this.props.member;
- var isAdmin = this.props.isAdmin;
- var isMemberAdmin = Utils.isAdmin(member.roles);
- var timestamp = UserStore.getCurrentUser().update_at;
-
- var invite;
- if (this.props.handleInvite) {
- invite = (
- <a
- onClick={this.handleInvite}
- className='btn btn-sm btn-primary'
- >
- <i className='glyphicon glyphicon-envelope'/>
- <FormattedMessage
- id='member_item.add'
- defaultMessage=' Add'
- />
- </a>
- );
- } else if (isAdmin && !isMemberAdmin && (member.id !== UserStore.getCurrentId())) {
- var self = this;
-
- let makeAdminOption = null;
- if (this.props.handleMakeAdmin) {
- makeAdminOption = (
- <li role='presentation'>
- <a
- href=''
- role='menuitem'
- onClick={self.handleMakeAdmin}
- >
- <FormattedMessage
- id='member_item.makeAdmin'
- defaultMessage='Make Admin'
- />
- </a>
- </li>);
- }
-
- let handleRemoveOption = null;
- if (this.props.handleRemove) {
- handleRemoveOption = (
- <li role='presentation'>
- <a
- href=''
- role='menuitem'
- onClick={self.handleRemove}
- >
- <FormattedMessage
- id='member_item.removeMember'
- defaultMessage='Remove Member'
- />
- </a>
- </li>);
- }
-
- invite = (
- <div className='dropdown member-drop'>
- <a
- href='#'
- className='dropdown-toggle theme'
- type='button'
- data-toggle='dropdown'
- aria-expanded='true'
- >
- <span className='fa fa-pencil'></span>
- <span className='text-capitalize'>
- {member.roles ||
- <FormattedMessage
- id='member_item.member'
- defaultMessage='Member'
- />
- }
- </span>
- </a>
- <ul
- className='dropdown-menu member-menu'
- role='menu'
- >
- {makeAdminOption}
- {handleRemoveOption}
- </ul>
- </div>
- );
- } else {
- invite = (<div className='member-role text-capitalize'><span className='fa fa-pencil hidden'></span>{member.roles || <FormattedMessage id='member_item.member'/>}</div>);
- }
-
- return (
- <tr>
- <td className='direct-channel'>
- <img
- className='profile-img pull-left'
- src={'/api/v1/users/' + member.id + '/image?time=' + timestamp + '&' + Utils.getSessionIndex()}
- height='36'
- width='36'
- />
- <div className='more-name'>{Utils.displayUsername(member.id)}</div>
- <div className='more-description'>{member.email}</div>
- </td>
- <td className='td--action lg'>{invite}</td>
- </tr>
- );
- }
-}
-
-MemberListItem.propTypes = {
- handleInvite: React.PropTypes.func,
- handleRemove: React.PropTypes.func,
- handleMakeAdmin: React.PropTypes.func,
- member: React.PropTypes.object,
- isAdmin: React.PropTypes.bool
-};
diff --git a/web/react/components/member_list_team.jsx b/web/react/components/member_list_team.jsx
index f1c31131f..cfd5359b7 100644
--- a/web/react/components/member_list_team.jsx
+++ b/web/react/components/member_list_team.jsx
@@ -1,7 +1,8 @@
// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
-import MemberListTeamItem from './member_list_team_item.jsx';
+import FilteredUserList from './filtered_user_list.jsx';
+import TeamMembersDropdown from './team_members_dropdown.jsx';
import UserStore from '../stores/user_store.jsx';
export default class MemberListTeam extends React.Component {
@@ -44,21 +45,16 @@ export default class MemberListTeam extends React.Component {
}
render() {
- const memberList = this.state.users.map((user) => {
- return (
- <MemberListTeamItem
- key={user.id}
- user={user}
- />
- );
- });
-
return (
- <table className='table more-table member-list-holder'>
- <tbody>
- {memberList}
- </tbody>
- </table>
+ <FilteredUserList
+ style={this.props.style}
+ users={this.state.users}
+ actions={[TeamMembersDropdown]}
+ />
);
}
}
+
+MemberListTeam.propTypes = {
+ style: React.PropTypes.object
+};
diff --git a/web/react/components/more_direct_channels.jsx b/web/react/components/more_direct_channels.jsx
index 3b72b251c..0814ac1b3 100644
--- a/web/react/components/more_direct_channels.jsx
+++ b/web/react/components/more_direct_channels.jsx
@@ -2,36 +2,24 @@
// See License.txt for license information.
const Modal = ReactBootstrap.Modal;
+import FilteredUserList from './filtered_user_list.jsx';
import UserStore from '../stores/user_store.jsx';
import * as Utils from '../utils/utils.jsx';
-import {intlShape, injectIntl, defineMessages, FormattedMessage} from 'mm-intl';
+import {FormattedMessage} from 'mm-intl';
-const holders = defineMessages({
- member: {
- id: 'more_direct_channels.member',
- defaultMessage: 'Member'
- },
- search: {
- id: 'more_direct_channels.search',
- defaultMessage: 'Search members'
- }
-});
-
-class MoreDirectChannels extends React.Component {
+export default class MoreDirectChannels extends React.Component {
constructor(props) {
super(props);
- this.handleFilterChange = this.handleFilterChange.bind(this);
this.handleHide = this.handleHide.bind(this);
this.handleShowDirectChannel = this.handleShowDirectChannel.bind(this);
this.handleUserChange = this.handleUserChange.bind(this);
- this.createRowForUser = this.createRowForUser.bind(this);
+ this.createJoinDirectChannelButton = this.createJoinDirectChannelButton.bind(this);
this.state = {
users: this.getUsersFromStore(),
- filter: '',
loadingDMChannel: -1
};
}
@@ -60,39 +48,10 @@ class MoreDirectChannels extends React.Component {
UserStore.removeChangeListener(this.handleUserChange);
}
- componentDidUpdate(prevProps) {
- if (!prevProps.show && this.props.show) {
- this.onShow();
- }
- }
-
- onShow() {
- if (Utils.isMobile()) {
- $(ReactDOM.findDOMNode(this.refs.userList)).css('max-height', $(window).height() - 250);
- } else {
- $(ReactDOM.findDOMNode(this.refs.userList)).perfectScrollbar();
- $(ReactDOM.findDOMNode(this.refs.userList)).css('max-height', $(window).height() - 300);
- }
- }
-
- handleFilterChange() {
- const filter = ReactDOM.findDOMNode(this.refs.filter).value;
-
- if ($(window).width() > 768) {
- $(ReactDOM.findDOMNode(this.refs.userList)).scrollTop(0);
- }
-
- if (filter !== this.state.filter) {
- this.setState({filter});
- }
- }
-
handleHide() {
if (this.props.onModalDismissed) {
this.props.onModalDismissed();
}
-
- this.setState({filter: ''});
}
handleShowDirectChannel(teammate, e) {
@@ -120,145 +79,39 @@ class MoreDirectChannels extends React.Component {
this.setState({users: this.getUsersFromStore()});
}
- createRowForUser(user) {
- const details = [];
-
- const fullName = Utils.getFullName(user);
- if (fullName) {
- details.push(
- <span
- key={`${user.id}__full-name`}
- className='full-name'
- >
- {fullName}
- </span>
- );
- }
-
- if (user.nickname) {
- const separator = fullName ? ' - ' : '';
- details.push(
- <span
- key={`${user.nickname}__nickname`}
- >
- {separator + user.nickname}
- </span>
- );
- }
-
- let joinButton;
+ createJoinDirectChannelButton({user}) {
if (this.state.loadingDMChannel === user.id) {
- joinButton = (
+ return (
<img
className='channel-loading-gif'
src='/static/images/load.gif'
/>
);
- } else {
- joinButton = (
- <button
- type='button'
- className='btn btn-primary btn-message'
- onClick={this.handleShowDirectChannel.bind(this, user)}
- >
- <FormattedMessage
- id='more_direct_channels.message'
- defaultMessage='Message'
- />
- </button>
- );
}
return (
- <tr key={'direct-channel-row-user' + user.id}>
- <td
- key={user.id}
- className='direct-channel'
- >
- <img
- className='profile-img pull-left'
- width='38'
- height='38'
- src={`/api/v1/users/${user.id}/image?time=${user.update_at}&${Utils.getSessionIndex()}`}
- />
- <div className='more-name'>
- {user.username}
- </div>
- <div className='more-description'>
- {details}
- </div>
- </td>
- <td className='td--action lg'>
- {joinButton}
- </td>
- </tr>
+ <button
+ type='button'
+ className='btn btn-primary btn-message'
+ onClick={this.handleShowDirectChannel.bind(this, user)}
+ >
+ <FormattedMessage
+ id='more_direct_channels.message'
+ defaultMessage='Message'
+ />
+ </button>
);
}
render() {
- const {formatMessage} = this.props.intl;
- if (!this.props.show) {
- return null;
- }
-
- let users = this.state.users;
- if (this.state.filter) {
- const filter = this.state.filter.toLowerCase();
-
- users = users.filter((user) => {
- return user.username.toLowerCase().indexOf(filter) !== -1 ||
- user.first_name.toLowerCase().indexOf(filter) !== -1 ||
- user.last_name.toLowerCase().indexOf(filter) !== -1 ||
- user.nickname.toLowerCase().indexOf(filter) !== -1;
- });
- }
-
- const userEntries = users.map(this.createRowForUser);
-
- if (userEntries.length === 0) {
- userEntries.push(
- <tr key='no-users-found'><td>
- <FormattedMessage
- id='more_direct_channels.notFound'
- defaultMessage='No users found :('
- />
- </td></tr>);
- }
-
- let memberString = formatMessage(holders.member);
- if (users.length !== 1) {
- memberString += 's';
- }
-
- let count;
- if (users.length === this.state.users.length) {
- count = (
- <FormattedMessage
- id='more_direct_channels.count'
- defaultMessage='{count} {member}'
- values={{
- count: users.length,
- member: memberString
- }}
- />
- );
- } else {
- count = (
- <FormattedMessage
- id='more_direct_channels.countTotal'
- defaultMessage='{count} {member} of {total} Total'
- values={{
- count: users.length,
- member: memberString,
- total: this.state.users.length
- }}
- />
- );
+ let maxHeight = 1000;
+ if (Utils.windowHeight() <= 1200) {
+ maxHeight = Utils.windowHeight() - 300;
}
return (
<Modal
- dialogClassName='more-modal'
+ dialogClassName='more-modal more-direct-channels'
show={this.props.show}
onHide={this.handleHide}
>
@@ -270,30 +123,12 @@ class MoreDirectChannels extends React.Component {
/>
</Modal.Title>
</Modal.Header>
- <Modal.Body ref='modalBody'>
- <div className='filter-row'>
- <div className='col-sm-6'>
- <input
- ref='filter'
- className='form-control filter-textbox'
- placeholder={formatMessage(holders.search)}
- onInput={this.handleFilterChange}
- />
- </div>
- <div className='col-sm-6'>
- <span className='member-count'>{count}</span>
- </div>
- </div>
- <div
- ref='userList'
- className='user-list'
- >
- <table className='more-table table'>
- <tbody>
- {userEntries}
- </tbody>
- </table>
- </div>
+ <Modal.Body>
+ <FilteredUserList
+ style={{maxHeight}}
+ users={this.state.users}
+ actions={[this.createJoinDirectChannelButton]}
+ />
</Modal.Body>
<Modal.Footer>
<button
@@ -313,9 +148,6 @@ class MoreDirectChannels extends React.Component {
}
MoreDirectChannels.propTypes = {
- intl: intlShape.isRequired,
show: React.PropTypes.bool.isRequired,
onModalDismissed: React.PropTypes.func
};
-
-export default injectIntl(MoreDirectChannels);
diff --git a/web/react/components/member_list_team_item.jsx b/web/react/components/team_members_dropdown.jsx
index 23bc10781..8aacba8ca 100644
--- a/web/react/components/member_list_team_item.jsx
+++ b/web/react/components/team_members_dropdown.jsx
@@ -9,28 +9,9 @@ import * as Utils from '../utils/utils.jsx';
import ConfirmModal from './confirm_modal.jsx';
import TeamStore from '../stores/team_store.jsx';
-import {injectIntl, intlShape, defineMessages, FormattedMessage} from 'mm-intl';
+import {FormattedMessage} from 'mm-intl';
-var holders = defineMessages({
- confirmDemoteRoleTitle: {
- id: 'member_team_item.confirmDemoteRoleTitle',
- defaultMessage: 'Confirm demotion from System Admin role'
- },
- confirmDemotion: {
- id: 'member_team_item.confirmDemotion',
- defaultMessage: 'Confirm Demotion'
- },
- confirmDemoteDescription: {
- id: 'member_team_item.confirmDemoteDescription',
- defaultMessage: 'If you demote yourself from the System Admin role and there is not another user with System Admin privileges, you\'ll need to re-assign a System Admin by accessing the Mattermost server through a terminal and running the following command.'
- },
- confirmDemotionCmd: {
- id: 'member_team_item.confirmDemotionCmd',
- defaultMessage: 'platform -assign_role -team_name="yourteam" -email="name@yourcompany.com" -role="system_admin"'
- }
-});
-
-export default class MemberListTeamItem extends React.Component {
+export default class TeamMembersDropdown extends React.Component {
constructor(props) {
super(props);
@@ -159,24 +140,23 @@ export default class MemberListTeamItem extends React.Component {
const user = this.props.user;
let currentRoles = (
<FormattedMessage
- id='member_team_item.member'
+ id='team_members_dropdown.member'
defaultMessage='Member'
/>
);
- const timestamp = UserStore.getCurrentUser().update_at;
if (user.roles.length > 0) {
if (Utils.isSystemAdmin(user.roles)) {
currentRoles = (
<FormattedMessage
- id='member_team_item.systemAdmin'
+ id='team_members_dropdown.systemAdmin'
defaultMessage='System Admin'
/>
);
} else if (Utils.isAdmin(user.roles)) {
currentRoles = (
<FormattedMessage
- id='member_team_item.teamAdmin'
+ id='team_members_dropdown.teamAdmin'
defaultMessage='Team Admin'
/>
);
@@ -185,7 +165,6 @@ export default class MemberListTeamItem extends React.Component {
}
}
- const email = user.email;
let showMakeMember = user.roles === 'admin' || user.roles === 'system_admin';
let showMakeAdmin = user.roles === '' || user.roles === 'system_admin';
let showMakeActive = false;
@@ -194,7 +173,7 @@ export default class MemberListTeamItem extends React.Component {
if (user.delete_at > 0) {
currentRoles = (
<FormattedMessage
- id='member_team_item.inactive'
+ id='team_members_dropdown.inactive'
defaultMessage='Inactive'
/>
);
@@ -214,7 +193,7 @@ export default class MemberListTeamItem extends React.Component {
onClick={this.handleMakeAdmin}
>
<FormattedMessage
- id='member_team_item.makeAdmin'
+ id='team_members_dropdown.makeAdmin'
defaultMessage='Make Team Admin'
/>
</a>
@@ -232,7 +211,7 @@ export default class MemberListTeamItem extends React.Component {
onClick={this.handleMakeMember}
>
<FormattedMessage
- id='member_team_item.makeMember'
+ id='team_members_dropdown.makeMember'
defaultMessage='Make Member'
/>
</a>
@@ -250,7 +229,7 @@ export default class MemberListTeamItem extends React.Component {
onClick={this.handleMakeActive}
>
<FormattedMessage
- id='member_team_item.makeActive'
+ id='team_members_dropdown.makeActive'
defaultMessage='Make Active'
/>
</a>
@@ -268,7 +247,7 @@ export default class MemberListTeamItem extends React.Component {
onClick={this.handleMakeNotActive}
>
<FormattedMessage
- id='member_team_item.makeInactive'
+ id='team_members_dropdown.makeInactive'
defaultMessage='Make Inactive'
/>
</a>
@@ -276,15 +255,44 @@ export default class MemberListTeamItem extends React.Component {
);
}
const me = UserStore.getCurrentUser();
- const {formatMessage} = this.props.intl;
let makeDemoteModal = null;
if (this.props.user.id === me.id) {
+ const title = (
+ <FormattedMessage
+ id='team_members_dropdown.confirmDemoteRoleTitle'
+ defaultMessage='Confirm demotion from System Admin role'
+ />
+ );
+
+ const message = (
+ <div>
+ <FormattedMessage
+ id='team_members_dropdown.confirmDemoteDescription'
+ defaultMessage="If you demote yourself from the System Admin role and there is not another user with System Admin privileges, you'll need to re-assign a System Admin by accessing the Mattermost server through a terminal and running the following command."
+ />
+ <br/>
+ <br/>
+ <FormattedMessage
+ id='team_members_dropdown.confirmDemotionCmd'
+ defaultMessage='platform -assign_role -team_name="yourteam" -email="name@yourcompany.com" -role="system_admin"'
+ />
+ {serverError}
+ </div>
+ );
+
+ const confirmButton = (
+ <FormattedMessage
+ id='team_members_dropdown.confirmDemotion'
+ defaultMessage='Confirm Demotion'
+ />
+ );
+
makeDemoteModal = (
<ConfirmModal
show={this.state.showDemoteModal}
- title={formatMessage(holders.confirmDemoteRoleTitle)}
- message={[formatMessage(holders.confirmDemoteDescription), React.createElement('br'), React.createElement('br'), formatMessage(holders.confirmDemotionCmd), serverError]}
- confirm_button={formatMessage(holders.confirmDemotion)}
+ title={title}
+ message={message}
+ confirmButton={confirmButton}
onConfirm={this.handleDemoteSubmit}
onCancel={this.handleDemoteCancel}
/>
@@ -292,48 +300,33 @@ export default class MemberListTeamItem extends React.Component {
}
return (
- <tr>
- <td className='row member-div'>
- <img
- className='post-profile-img pull-left'
- src={`/api/v1/users/${user.id}/image?time=${timestamp}&${Utils.getSessionIndex()}`}
- height='36'
- width='36'
- />
- <span className='more-name'>{Utils.displayUsername(user.id)}</span>
- <span className='more-description'>{email}</span>
- <div className='dropdown member-drop'>
- <a
- href='#'
- className='dropdown-toggle theme'
- type='button'
- data-toggle='dropdown'
- aria-expanded='true'
- >
- <span className='fa fa-pencil'></span>
- <span>{currentRoles} </span>
- </a>
- <ul
- className='dropdown-menu member-menu'
- role='menu'
- >
- {makeAdmin}
- {makeMember}
- {makeActive}
- {makeNotActive}
- </ul>
- </div>
- {makeDemoteModal}
- {serverError}
- </td>
- </tr>
+ <div className='dropdown member-drop'>
+ <a
+ href='#'
+ className='dropdown-toggle theme'
+ type='button'
+ data-toggle='dropdown'
+ aria-expanded='true'
+ >
+ <span className='fa fa-pencil'></span>
+ <span>{currentRoles} </span>
+ </a>
+ <ul
+ className='dropdown-menu member-menu'
+ role='menu'
+ >
+ {makeAdmin}
+ {makeMember}
+ {makeActive}
+ {makeNotActive}
+ </ul>
+ {makeDemoteModal}
+ {serverError}
+ </div>
);
}
}
-MemberListTeamItem.propTypes = {
- intl: intlShape.isRequired,
+TeamMembersDropdown.propTypes = {
user: React.PropTypes.object.isRequired
};
-
-export default injectIntl(MemberListTeamItem);
diff --git a/web/react/components/team_members_modal.jsx b/web/react/components/team_members_modal.jsx
index 8ac435742..9bdb16438 100644
--- a/web/react/components/team_members_modal.jsx
+++ b/web/react/components/team_members_modal.jsx
@@ -3,45 +3,24 @@
import MemberListTeam from './member_list_team.jsx';
import TeamStore from '../stores/team_store.jsx';
+import * as Utils from '../utils/utils.jsx';
import {FormattedMessage} from 'mm-intl';
const Modal = ReactBootstrap.Modal;
export default class TeamMembersModal extends React.Component {
- constructor(props) {
- super(props);
-
- this.onShow = this.onShow.bind(this);
- }
-
- componentDidMount() {
- if (this.props.show) {
- this.onShow();
- }
- }
-
- componentDidUpdate(prevProps) {
- if (this.props.show && !prevProps.show) {
- this.onShow();
- }
- }
-
- onShow() {
- if ($(window).width() > 768) {
- $(ReactDOM.findDOMNode(this.refs.modalBody)).perfectScrollbar();
- $(ReactDOM.findDOMNode(this.refs.modalBody)).css('max-height', $(window).height() - 200);
- } else {
- $(ReactDOM.findDOMNode(this.refs.modalBody)).css('max-height', $(window).height() - 150);
- }
- }
-
render() {
const team = TeamStore.getCurrent();
+ let maxHeight = 1000;
+ if (Utils.windowHeight() <= 1200) {
+ maxHeight = Utils.windowHeight() - 300;
+ }
+
return (
<Modal
- dialogClassName='team-members-modal'
+ dialogClassName='more-modal'
show={this.props.show}
onHide={this.props.onHide}
>
@@ -54,10 +33,8 @@ export default class TeamMembersModal extends React.Component {
}}
/>
</Modal.Header>
- <Modal.Body ref='modalBody'>
- <div className='team-member-list'>
- <MemberListTeam/>
- </div>
+ <Modal.Body>
+ <MemberListTeam style={{maxHeight}}/>
</Modal.Body>
<Modal.Footer>
<button
diff --git a/web/react/components/user_list.jsx b/web/react/components/user_list.jsx
new file mode 100644
index 000000000..39453a827
--- /dev/null
+++ b/web/react/components/user_list.jsx
@@ -0,0 +1,53 @@
+// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+import {FormattedMessage} from 'mm-intl';
+import UserListRow from './user_list_row.jsx';
+
+export default class UserList extends React.Component {
+ render() {
+ const users = this.props.users;
+
+ let content;
+ if (users.length > 0) {
+ content = users.map((user) => {
+ return (
+ <UserListRow
+ key={user.id}
+ user={user}
+ actions={this.props.actions}
+ />
+ );
+ });
+ } else {
+ content = (
+ <tr key='no-users-found'>
+ <td>
+ <FormattedMessage
+ id='user_list.notFound'
+ defaultMessage='No users found :('
+ />
+ </td>
+ </tr>
+ );
+ }
+
+ return (
+ <table className='more-table table'>
+ <tbody>
+ {content}
+ </tbody>
+ </table>
+ );
+ }
+}
+
+UserList.defaultProps = {
+ users: [],
+ actions: []
+};
+
+UserList.propTypes = {
+ users: React.PropTypes.arrayOf(React.PropTypes.object),
+ actions: React.PropTypes.arrayOf(React.PropTypes.func)
+};
diff --git a/web/react/components/user_list_row.jsx b/web/react/components/user_list_row.jsx
new file mode 100644
index 000000000..2aeca7d47
--- /dev/null
+++ b/web/react/components/user_list_row.jsx
@@ -0,0 +1,65 @@
+// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+import Constants from '../utils/constants.jsx';
+import PreferenceStore from '../stores/preference_store.jsx';
+import * as Utils from '../utils/utils.jsx';
+
+export default function UserListRow({user, actions}) {
+ const nameFormat = PreferenceStore.get(Constants.Preferences.CATEGORY_DISPLAY_SETTINGS, 'name_format', '');
+
+ let name = user.username;
+ if (user.nickname && nameFormat === Constants.Preferences.DISPLAY_PREFER_NICKNAME) {
+ name = `${user.nickname} (${user.username})`;
+ } else if ((user.first_name || user.last_name) && (nameFormat === Constants.Preferences.DISPLAY_PREFER_NICKNAME || nameFormat === Constants.Preferences.DISPLAY_PREFER_FULL_NAME)) {
+ name = `${Utils.getFullName(user)} (${user.username})`;
+ }
+
+ const buttons = actions.map((Action, index) => {
+ return (
+ <Action
+ key={index.toString()}
+ user={user}
+ />
+ );
+ });
+
+ return (
+ <tr>
+ <td
+ key={user.id}
+ className='direct-channel'
+ style={{display: 'flex'}}
+ >
+ <img
+ className='profile-img'
+ src={`/api/v1/users/${user.id}/image?time=${user.update_at}&${Utils.getSessionIndex()}`}
+ />
+ <div
+ className='user-list-item__details'
+ >
+ <div className='more-name'>
+ {name}
+ </div>
+ <div className='more-description'>
+ {user.email}
+ </div>
+ </div>
+ <div
+ className='user-list-item__actions'
+ >
+ {buttons}
+ </div>
+ </td>
+ </tr>
+ );
+}
+
+UserListRow.defaultProps = {
+ actions: []
+};
+
+UserListRow.propTypes = {
+ user: React.PropTypes.object.isRequired,
+ actions: React.PropTypes.arrayOf(React.PropTypes.func)
+};
diff --git a/web/react/components/user_settings/manage_languages.jsx b/web/react/components/user_settings/manage_languages.jsx
index fee6d9da2..2d1c74717 100644
--- a/web/react/components/user_settings/manage_languages.jsx
+++ b/web/react/components/user_settings/manage_languages.jsx
@@ -1,6 +1,8 @@
// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved.
// See License.txt for license information.
+import SettingItemMax from '../setting_item_max.jsx';
+
import * as Client from '../../utils/client.jsx';
import * as Utils from '../../utils/utils.jsx';
@@ -69,7 +71,7 @@ export default class ManageLanguage extends React.Component {
</option>);
});
- return (
+ const input = (
<div key='changeLanguage'>
<br/>
<label className='control-label'>
@@ -88,24 +90,28 @@ export default class ManageLanguage extends React.Component {
{options}
</select>
{serverError}
- <div className='padding-top'>
- <a
- className={'btn btn-sm btn-primary'}
- href='#'
- onClick={this.changeLanguage}
- >
- <FormattedMessage
- id='user.settings.languages'
- defaultMessage='Set language'
- />
- </a>
- </div>
</div>
</div>
);
+
+ return (
+ <SettingItemMax
+ title={
+ <FormattedMessage
+ id='user.settings.display.language'
+ defaultMessage='Language'
+ />
+ }
+ width='medium'
+ submit={this.changeLanguage}
+ inputs={[input]}
+ updateSection={this.props.updateSection}
+ />
+ );
}
}
ManageLanguage.propTypes = {
- user: React.PropTypes.object
-}; \ No newline at end of file
+ user: React.PropTypes.object.isRequired,
+ updateSection: React.PropTypes.func.isRequired
+};
diff --git a/web/react/components/user_settings/user_settings_display.jsx b/web/react/components/user_settings/user_settings_display.jsx
index 5f23a8995..3e468e08f 100644
--- a/web/react/components/user_settings/user_settings_display.jsx
+++ b/web/react/components/user_settings/user_settings_display.jsx
@@ -12,46 +12,7 @@ import * as Utils from '../../utils/utils.jsx';
import Constants from '../../utils/constants.jsx';
import {savePreferences} from '../../utils/client.jsx';
-import {intlShape, injectIntl, defineMessages, FormattedMessage} from 'mm-intl';
-
-const holders = defineMessages({
- normalClock: {
- id: 'user.settings.display.normalClock',
- defaultMessage: '12-hour clock (example: 4:00 PM)'
- },
- militaryClock: {
- id: 'user.settings.display.militaryClock',
- defaultMessage: '24-hour clock (example: 16:00)'
- },
- clockDisplay: {
- id: 'user.settings.display.clockDisplay',
- defaultMessage: 'Clock Display'
- },
- teammateDisplay: {
- id: 'user.settings.display.teammateDisplay',
- defaultMessage: 'Teammate Name Display'
- },
- showNickname: {
- id: 'user.settings.display.showNickname',
- defaultMessage: 'Show nickname if one exists, otherwise show first and last name'
- },
- showUsername: {
- id: 'user.settings.display.showUsername',
- defaultMessage: 'Show username (team default)'
- },
- showFullname: {
- id: 'user.settings.display.showFullname',
- defaultMessage: 'Show first and last name'
- },
- fontTitle: {
- id: 'user.settings.display.fontTitle',
- defaultMessage: 'Display Font'
- },
- language: {
- id: 'user.settings.display.language',
- defaultMessage: 'Language'
- }
-});
+import {FormattedMessage} from 'mm-intl';
function getDisplayStateFromStores() {
const militaryTime = PreferenceStore.getPreference(Constants.Preferences.CATEGORY_DISPLAY_SETTINGS, 'use_military_time', {value: 'false'});
@@ -65,7 +26,7 @@ function getDisplayStateFromStores() {
};
}
-class UserSettingsDisplay extends React.Component {
+export default class UserSettingsDisplay extends React.Component {
constructor(props) {
super(props);
@@ -119,7 +80,6 @@ class UserSettingsDisplay extends React.Component {
this.updateState();
}
render() {
- const {formatMessage} = this.props.intl;
const serverError = this.state.serverError || null;
let clockSection;
let nameFormatSection;
@@ -181,7 +141,12 @@ class UserSettingsDisplay extends React.Component {
clockSection = (
<SettingItemMax
- title={formatMessage(holders.clockDisplay)}
+ title={
+ <FormattedMessage
+ id='user.settings.display.clockDisplay'
+ defaultMessage='Clock Display'
+ />
+ }
inputs={inputs}
submit={this.handleSubmit}
server_error={serverError}
@@ -189,11 +154,21 @@ class UserSettingsDisplay extends React.Component {
/>
);
} else {
- let describe = '';
+ let describe;
if (this.state.militaryTime === 'true') {
- describe = formatMessage(holders.militaryClock);
+ describe = (
+ <FormattedMessage
+ id='user.settings.display.militaryClock'
+ defaultMessage='24-hour clock (example: 16:00)'
+ />
+ );
} else {
- describe = formatMessage(holders.normalClock);
+ describe = (
+ <FormattedMessage
+ id='user.settings.display.normalClock'
+ defaultMessage='12-hour clock (example: 4:00 PM)'
+ />
+ );
}
const handleUpdateClockSection = () => {
@@ -202,7 +177,12 @@ class UserSettingsDisplay extends React.Component {
clockSection = (
<SettingItemMin
- title={formatMessage(holders.clockDisplay)}
+ title={
+ <FormattedMessage
+ id='user.settings.display.clockDisplay'
+ defaultMessage='Clock Display'
+ />
+ }
describe={describe}
updateSection={handleUpdateClockSection}
/>
@@ -284,7 +264,12 @@ class UserSettingsDisplay extends React.Component {
nameFormatSection = (
<SettingItemMax
- title={formatMessage(holders.teammateDisplay)}
+ title={
+ <FormattedMessage
+ id='user.settings.display.teammateDisplay'
+ defaultMessage='Teammate Name Display'
+ />
+ }
inputs={inputs}
submit={this.handleSubmit}
server_error={serverError}
@@ -295,18 +280,38 @@ class UserSettingsDisplay extends React.Component {
/>
);
} else {
- let describe = '';
+ let describe;
if (this.state.nameFormat === 'username') {
- describe = formatMessage(holders.showUsername);
+ describe = (
+ <FormattedMessage
+ id='user.settings.display.showUsername'
+ defaultMessage='Show username (team default)'
+ />
+ );
} else if (this.state.nameFormat === 'full_name') {
- describe = formatMessage(holders.showFullname);
+ describe = (
+ <FormattedMessage
+ id='user.settings.display.showFullname'
+ defaultMessage='Show first and last name'
+ />
+ );
} else {
- describe = formatMessage(holders.showNickname);
+ describe = (
+ <FormattedMessage
+ id='user.settings.display.showNickname'
+ defaultMessage='Show nickname if one exists, otherwise show first and last name'
+ />
+ );
}
nameFormatSection = (
<SettingItemMin
- title={formatMessage(holders.teammateDisplay)}
+ title={
+ <FormattedMessage
+ id='user.settings.display.teammateDisplay'
+ defaultMessage='Teammate Name Display'
+ />
+ }
describe={describe}
updateSection={() => {
this.props.updateSection('name_format');
@@ -356,7 +361,12 @@ class UserSettingsDisplay extends React.Component {
fontSection = (
<SettingItemMax
- title={formatMessage(holders.fontTitle)}
+ title={
+ <FormattedMessage
+ id='user.settings.display.fontTitle'
+ defaultMessage='Display Font'
+ />
+ }
inputs={inputs}
submit={this.handleSubmit}
server_error={serverError}
@@ -369,7 +379,12 @@ class UserSettingsDisplay extends React.Component {
} else {
fontSection = (
<SettingItemMin
- title={formatMessage(holders.fontTitle)}
+ title={
+ <FormattedMessage
+ id='user.settings.display.fontTitle'
+ defaultMessage='Display Font'
+ />
+ }
describe={this.state.selectedFont}
updateSection={() => {
this.props.updateSection('font');
@@ -379,19 +394,9 @@ class UserSettingsDisplay extends React.Component {
}
if (this.props.activeSection === 'languages') {
- var inputs = [];
- inputs.push(
+ languagesSection = (
<ManageLanguages
user={this.props.user}
- key='languages-ui'
- />
- );
-
- languagesSection = (
- <SettingItemMax
- title={formatMessage(holders.language)}
- width='medium'
- inputs={inputs}
updateSection={(e) => {
this.updateSection('');
e.preventDefault();
@@ -408,7 +413,12 @@ class UserSettingsDisplay extends React.Component {
languagesSection = (
<SettingItemMin
- title={formatMessage(holders.language)}
+ title={
+ <FormattedMessage
+ id='user.settings.display.language'
+ defaultMessage='Language'
+ />
+ }
width='medium'
describe={locale}
updateSection={() => {
@@ -452,12 +462,12 @@ class UserSettingsDisplay extends React.Component {
/>
</h3>
<div className='divider-dark first'/>
- <ThemeSetting
- selected={this.props.activeSection === 'theme'}
- updateSection={this.updateSection}
- setRequireConfirm={this.props.setRequireConfirm}
- setEnforceFocus={this.props.setEnforceFocus}
- />
+ <ThemeSetting
+ selected={this.props.activeSection === 'theme'}
+ updateSection={this.updateSection}
+ setRequireConfirm={this.props.setRequireConfirm}
+ setEnforceFocus={this.props.setEnforceFocus}
+ />
<div className='divider-dark'/>
{fontSection}
<div className='divider-dark'/>
@@ -473,7 +483,6 @@ class UserSettingsDisplay extends React.Component {
}
UserSettingsDisplay.propTypes = {
- intl: intlShape.isRequired,
user: React.PropTypes.object,
updateSection: React.PropTypes.func,
updateTab: React.PropTypes.func,
@@ -483,5 +492,3 @@ UserSettingsDisplay.propTypes = {
setRequireConfirm: React.PropTypes.func.isRequired,
setEnforceFocus: React.PropTypes.func.isRequired
};
-
-export default injectIntl(UserSettingsDisplay);
diff --git a/web/react/stores/analytics_store.jsx b/web/react/stores/analytics_store.jsx
new file mode 100644
index 000000000..ec827f6d7
--- /dev/null
+++ b/web/react/stores/analytics_store.jsx
@@ -0,0 +1,85 @@
+// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+import AppDispatcher from '../dispatcher/app_dispatcher.jsx';
+import EventEmitter from 'events';
+
+import Constants from '../utils/constants.jsx';
+const ActionTypes = Constants.ActionTypes;
+
+const CHANGE_EVENT = 'change';
+
+class AnalyticsStoreClass extends EventEmitter {
+ constructor() {
+ super();
+ this.systemStats = {};
+ this.teamStats = {};
+ }
+
+ emitChange() {
+ this.emit(CHANGE_EVENT);
+ }
+
+ addChangeListener(callback) {
+ this.on(CHANGE_EVENT, callback);
+ }
+
+ removeChangeListener(callback) {
+ this.removeListener(CHANGE_EVENT, callback);
+ }
+
+ getAllSystem() {
+ return JSON.parse(JSON.stringify(this.systemStats));
+ }
+
+ getAllTeam(id) {
+ if (id in this.teamStats) {
+ return JSON.parse(JSON.stringify(this.teamStats[id]));
+ }
+
+ return {};
+ }
+
+ storeSystemStats(newStats) {
+ for (const stat in newStats) {
+ if (!newStats.hasOwnProperty(stat)) {
+ continue;
+ }
+ this.systemStats[stat] = newStats[stat];
+ }
+ }
+
+ storeTeamStats(id, newStats) {
+ if (!(id in this.teamStats)) {
+ this.teamStats[id] = {};
+ }
+
+ for (const stat in newStats) {
+ if (!newStats.hasOwnProperty(stat)) {
+ continue;
+ }
+ this.teamStats[id][stat] = newStats[stat];
+ }
+ }
+
+}
+
+var AnalyticsStore = new AnalyticsStoreClass();
+
+AnalyticsStore.dispatchToken = AppDispatcher.register((payload) => {
+ var action = payload.action;
+
+ switch (action.type) {
+ case ActionTypes.RECEIVED_ANALYTICS:
+ if (action.teamId == null) {
+ AnalyticsStore.storeSystemStats(action.stats);
+ } else {
+ AnalyticsStore.storeTeamStats(action.teamId, action.stats);
+ }
+ AnalyticsStore.emitChange();
+ break;
+ default:
+ }
+});
+
+export default AnalyticsStore;
diff --git a/web/react/utils/async_client.jsx b/web/react/utils/async_client.jsx
index ca9d81865..7d5e1bd0f 100644
--- a/web/react/utils/async_client.jsx
+++ b/web/react/utils/async_client.jsx
@@ -11,16 +11,17 @@ import UserStore from '../stores/user_store.jsx';
import * as utils from './utils.jsx';
import Constants from './constants.jsx';
-var ActionTypes = Constants.ActionTypes;
+const ActionTypes = Constants.ActionTypes;
+const StatTypes = Constants.StatTypes;
// Used to track in progress async calls
-var callTracker = {};
+const callTracker = {};
export function dispatchError(err, method) {
AppDispatcher.handleServerAction({
type: ActionTypes.RECEIVED_ERROR,
- err: err,
- method: method
+ err,
+ method
});
}
@@ -848,3 +849,264 @@ export function getFileInfo(filename) {
}
);
}
+
+export function getStandardAnalytics(teamId) {
+ const callName = 'getStandardAnaytics' + teamId;
+
+ if (isCallInProgress(callName)) {
+ return;
+ }
+
+ callTracker[callName] = utils.getTimestamp();
+
+ client.getAnalytics(
+ 'standard',
+ teamId,
+ (data) => {
+ callTracker[callName] = 0;
+
+ const stats = {};
+
+ for (const index in data) {
+ if (data[index].name === 'channel_open_count') {
+ stats[StatTypes.TOTAL_PUBLIC_CHANNELS] = data[index].value;
+ }
+
+ if (data[index].name === 'channel_private_count') {
+ stats[StatTypes.TOTAL_PRIVATE_GROUPS] = data[index].value;
+ }
+
+ if (data[index].name === 'post_count') {
+ stats[StatTypes.TOTAL_POSTS] = data[index].value;
+ }
+
+ if (data[index].name === 'unique_user_count') {
+ stats[StatTypes.TOTAL_USERS] = data[index].value;
+ }
+
+ if (data[index].name === 'team_count' && teamId == null) {
+ stats[StatTypes.TOTAL_TEAMS] = data[index].value;
+ }
+ }
+
+ AppDispatcher.handleServerAction({
+ type: ActionTypes.RECEIVED_ANALYTICS,
+ teamId,
+ stats
+ });
+ },
+ (err) => {
+ callTracker[callName] = 0;
+
+ dispatchError(err, 'getStandardAnalytics');
+ }
+ );
+}
+
+export function getAdvancedAnalytics(teamId) {
+ const callName = 'getAdvancedAnalytics' + teamId;
+
+ if (isCallInProgress(callName)) {
+ return;
+ }
+
+ callTracker[callName] = utils.getTimestamp();
+
+ client.getAnalytics(
+ 'extra_counts',
+ teamId,
+ (data) => {
+ callTracker[callName] = 0;
+
+ const stats = {};
+
+ for (const index in data) {
+ if (data[index].name === 'file_post_count') {
+ stats[StatTypes.TOTAL_FILE_POSTS] = data[index].value;
+ }
+
+ if (data[index].name === 'hashtag_post_count') {
+ stats[StatTypes.TOTAL_HASHTAG_POSTS] = data[index].value;
+ }
+
+ if (data[index].name === 'incoming_webhook_count') {
+ stats[StatTypes.TOTAL_IHOOKS] = data[index].value;
+ }
+
+ if (data[index].name === 'outgoing_webhook_count') {
+ stats[StatTypes.TOTAL_OHOOKS] = data[index].value;
+ }
+
+ if (data[index].name === 'command_count') {
+ stats[StatTypes.TOTAL_COMMANDS] = data[index].value;
+ }
+
+ if (data[index].name === 'session_count') {
+ stats[StatTypes.TOTAL_SESSIONS] = data[index].value;
+ }
+ }
+
+ AppDispatcher.handleServerAction({
+ type: ActionTypes.RECEIVED_ANALYTICS,
+ teamId,
+ stats
+ });
+ },
+ (err) => {
+ callTracker[callName] = 0;
+
+ dispatchError(err, 'getAdvancedAnalytics');
+ }
+ );
+}
+
+export function getPostsPerDayAnalytics(teamId) {
+ const callName = 'getPostsPerDayAnalytics' + teamId;
+
+ if (isCallInProgress(callName)) {
+ return;
+ }
+
+ callTracker[callName] = utils.getTimestamp();
+
+ client.getAnalytics(
+ 'post_counts_day',
+ teamId,
+ (data) => {
+ callTracker[callName] = 0;
+
+ data.reverse();
+
+ const stats = {};
+ stats[StatTypes.POST_PER_DAY] = data;
+
+ AppDispatcher.handleServerAction({
+ type: ActionTypes.RECEIVED_ANALYTICS,
+ teamId,
+ stats
+ });
+ },
+ (err) => {
+ callTracker[callName] = 0;
+
+ dispatchError(err, 'getPostsPerDayAnalytics');
+ }
+ );
+}
+
+export function getUsersPerDayAnalytics(teamId) {
+ const callName = 'getUsersPerDayAnalytics' + teamId;
+
+ if (isCallInProgress(callName)) {
+ return;
+ }
+
+ callTracker[callName] = utils.getTimestamp();
+
+ client.getAnalytics(
+ 'user_counts_with_posts_day',
+ teamId,
+ (data) => {
+ callTracker[callName] = 0;
+
+ data.reverse();
+
+ const stats = {};
+ stats[StatTypes.USERS_WITH_POSTS_PER_DAY] = data;
+
+ AppDispatcher.handleServerAction({
+ type: ActionTypes.RECEIVED_ANALYTICS,
+ teamId,
+ stats
+ });
+ },
+ (err) => {
+ callTracker[callName] = 0;
+
+ dispatchError(err, 'getUsersPerDayAnalytics');
+ }
+ );
+}
+
+export function getRecentAndNewUsersAnalytics(teamId) {
+ const callName = 'getRecentAndNewUsersAnalytics' + teamId;
+
+ if (isCallInProgress(callName)) {
+ return;
+ }
+
+ callTracker[callName] = utils.getTimestamp();
+
+ client.getProfilesForTeam(
+ teamId,
+ (users) => {
+ const stats = {};
+
+ const usersList = [];
+ for (const id in users) {
+ if (users.hasOwnProperty(id)) {
+ usersList.push(users[id]);
+ }
+ }
+
+ usersList.sort((a, b) => {
+ if (a.last_activity_at < b.last_activity_at) {
+ return 1;
+ }
+
+ if (a.last_activity_at > b.last_activity_at) {
+ return -1;
+ }
+
+ return 0;
+ });
+
+ const recentActive = [];
+ for (let i = 0; i < usersList.length; i++) {
+ if (usersList[i].last_activity_at == null) {
+ continue;
+ }
+
+ recentActive.push(usersList[i]);
+ if (i >= Constants.STAT_MAX_ACTIVE_USERS) {
+ break;
+ }
+ }
+
+ stats[StatTypes.RECENTLY_ACTIVE_USERS] = recentActive;
+
+ usersList.sort((a, b) => {
+ if (a.create_at < b.create_at) {
+ return 1;
+ }
+
+ if (a.create_at > b.create_at) {
+ return -1;
+ }
+
+ return 0;
+ });
+
+ var newlyCreated = [];
+ for (let i = 0; i < usersList.length; i++) {
+ newlyCreated.push(usersList[i]);
+ if (i >= Constants.STAT_MAX_NEW_USERS) {
+ break;
+ }
+ }
+
+ stats[StatTypes.NEWLY_CREATED_USERS] = newlyCreated;
+
+ AppDispatcher.handleServerAction({
+ type: ActionTypes.RECEIVED_ANALYTICS,
+ teamId,
+ stats
+ });
+ },
+ (err) => {
+ callTracker[callName] = 0;
+
+ dispatchError(err, 'getRecentAndNewUsersAnalytics');
+ }
+ );
+}
diff --git a/web/react/utils/client.jsx b/web/react/utils/client.jsx
index f647e2296..1a002bc8c 100644
--- a/web/react/utils/client.jsx
+++ b/web/react/utils/client.jsx
@@ -435,23 +435,16 @@ export function getConfig(success, error) {
});
}
-export function getTeamAnalytics(teamId, name, success, error) {
- $.ajax({
- url: '/api/v1/admin/analytics/' + teamId + '/' + name,
- dataType: 'json',
- contentType: 'application/json',
- type: 'GET',
- success,
- error: (xhr, status, err) => {
- var e = handleError('getTeamAnalytics', xhr, status, err);
- error(e);
- }
- });
-}
+export function getAnalytics(name, teamId, success, error) {
+ let url = '/api/v1/admin/analytics/';
+ if (teamId == null) {
+ url += name;
+ } else {
+ url += teamId + '/' + name;
+ }
-export function getSystemAnalytics(name, success, error) {
$.ajax({
- url: '/api/v1/admin/analytics/' + name,
+ url,
dataType: 'json',
contentType: 'application/json',
type: 'GET',
diff --git a/web/react/utils/constants.jsx b/web/react/utils/constants.jsx
index 0a4944708..daea9f43e 100644
--- a/web/react/utils/constants.jsx
+++ b/web/react/utils/constants.jsx
@@ -71,6 +71,26 @@ export default {
VIEW_ACTION: null
}),
+ StatTypes: keyMirror({
+ TOTAL_USERS: null,
+ TOTAL_PUBLIC_CHANNELS: null,
+ TOTAL_PRIVATE_GROUPS: null,
+ TOTAL_POSTS: null,
+ TOTAL_TEAMS: null,
+ TOTAL_FILE_POSTS: null,
+ TOTAL_HASHTAG_POSTS: null,
+ TOTAL_IHOOKS: null,
+ TOTAL_OHOOKS: null,
+ TOTAL_COMMANDS: null,
+ TOTAL_SESSIONS: null,
+ POST_PER_DAY: null,
+ USERS_WITH_POSTS_PER_DAY: null,
+ RECENTLY_ACTIVE_USERS: null,
+ NEWLY_CREATED_USERS: null
+ }),
+ STAT_MAX_ACTIVE_USERS: 20,
+ STAT_MAX_NEW_USERS: 20,
+
SocketEvents: {
POSTED: 'posted',
POST_EDITED: 'post_edited',
@@ -417,6 +437,8 @@ export default {
Preferences: {
CATEGORY_DIRECT_CHANNEL_SHOW: 'direct_channel_show',
CATEGORY_DISPLAY_SETTINGS: 'display_settings',
+ DISPLAY_PREFER_NICKNAME: 'nickname_full_name',
+ DISPLAY_PREFER_FULL_NAME: 'full_name',
CATEGORY_ADVANCED_SETTINGS: 'advanced_settings',
TUTORIAL_STEP: 'tutorial_step'
},
diff --git a/web/react/utils/utils.jsx b/web/react/utils/utils.jsx
index 3e531c821..6ab2f64d4 100644
--- a/web/react/utils/utils.jsx
+++ b/web/react/utils/utils.jsx
@@ -1082,9 +1082,9 @@ export function displayUsername(userId) {
let username = '';
if (user) {
- if (nameFormat === 'nickname_full_name') {
+ if (nameFormat === Constants.Preferences.DISPLAY_PREFER_NICKNAME) {
username = user.nickname || getFullName(user);
- } else if (nameFormat === 'full_name') {
+ } else if (nameFormat === Constants.Preferences.DISPLAY_PREFER_FULL_NAME) {
username = getFullName(user);
}
if (!username.trim().length) {