summaryrefslogtreecommitdiffstats
path: root/web/react/components
diff options
context:
space:
mode:
Diffstat (limited to 'web/react/components')
-rw-r--r--web/react/components/admin_console/admin_controller.jsx5
-rw-r--r--web/react/components/admin_console/admin_sidebar.jsx11
-rw-r--r--web/react/components/admin_console/line_chart.jsx50
-rw-r--r--web/react/components/admin_console/team_analytics.jsx357
-rw-r--r--web/react/components/admin_console/team_users.jsx10
-rw-r--r--web/react/components/create_comment.jsx33
-rw-r--r--web/react/components/create_post.jsx33
-rw-r--r--web/react/components/file_attachment.jsx2
-rw-r--r--web/react/components/get_link_modal.jsx2
-rw-r--r--web/react/components/navbar_dropdown.jsx1
-rw-r--r--web/react/components/register_app_modal.jsx135
-rw-r--r--web/react/components/search_autocomplete.jsx23
-rw-r--r--web/react/components/setting_item_max.jsx4
-rw-r--r--web/react/components/setting_picture.jsx4
-rw-r--r--web/react/components/sidebar_header.jsx11
-rw-r--r--web/react/components/team_signup_with_email.jsx2
-rw-r--r--web/react/components/user_settings/code_theme_chooser.jsx55
-rw-r--r--web/react/components/user_settings/manage_outgoing_hooks.jsx1
-rw-r--r--web/react/components/user_settings/user_settings.jsx12
-rw-r--r--web/react/components/user_settings/user_settings_advanced.jsx169
-rw-r--r--web/react/components/user_settings/user_settings_appearance.jsx23
-rw-r--r--web/react/components/user_settings/user_settings_general.jsx1
-rw-r--r--web/react/components/user_settings/user_settings_integrations.jsx4
-rw-r--r--web/react/components/user_settings/user_settings_modal.jsx1
24 files changed, 764 insertions, 185 deletions
diff --git a/web/react/components/admin_console/admin_controller.jsx b/web/react/components/admin_console/admin_controller.jsx
index f770d166c..d309ced2e 100644
--- a/web/react/components/admin_console/admin_controller.jsx
+++ b/web/react/components/admin_console/admin_controller.jsx
@@ -18,6 +18,7 @@ var SqlSettingsTab = require('./sql_settings.jsx');
var TeamSettingsTab = require('./team_settings.jsx');
var ServiceSettingsTab = require('./service_settings.jsx');
var TeamUsersTab = require('./team_users.jsx');
+var TeamAnalyticsTab = require('./team_analytics.jsx');
export default class AdminController extends React.Component {
constructor(props) {
@@ -149,6 +150,10 @@ export default class AdminController extends React.Component {
if (this.state.teams) {
tab = <TeamUsersTab team={this.state.teams[this.state.selectedTeam]} />;
}
+ } else if (this.state.selected === 'team_analytics') {
+ if (this.state.teams) {
+ tab = <TeamAnalyticsTab team={this.state.teams[this.state.selectedTeam]} />;
+ }
}
}
diff --git a/web/react/components/admin_console/admin_sidebar.jsx b/web/react/components/admin_console/admin_sidebar.jsx
index b0e01ff17..f2fb1c96d 100644
--- a/web/react/components/admin_console/admin_sidebar.jsx
+++ b/web/react/components/admin_console/admin_sidebar.jsx
@@ -24,7 +24,7 @@ export default class AdminSidebar extends React.Component {
handleClick(name, teamId, e) {
e.preventDefault();
this.props.selectTab(name, teamId);
- history.pushState({name: name, teamId: teamId}, null, `/admin_console/${name}/${teamId || ''}`);
+ history.pushState({name, teamId}, null, `/admin_console/${name}/${teamId || ''}`);
}
isSelected(name, teamId) {
@@ -121,6 +121,15 @@ export default class AdminSidebar extends React.Component {
{'- Users'}
</a>
</li>
+ <li>
+ <a
+ href='#'
+ className={this.isSelected('team_analytics', team.id)}
+ onClick={this.handleClick.bind(this, 'team_analytics', team.id)}
+ >
+ {'- Statistics'}
+ </a>
+ </li>
</ul>
</li>
</ul>
diff --git a/web/react/components/admin_console/line_chart.jsx b/web/react/components/admin_console/line_chart.jsx
new file mode 100644
index 000000000..7e2f95c84
--- /dev/null
+++ b/web/react/components/admin_console/line_chart.jsx
@@ -0,0 +1,50 @@
+// 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/team_analytics.jsx b/web/react/components/admin_console/team_analytics.jsx
new file mode 100644
index 000000000..a945a551c
--- /dev/null
+++ b/web/react/components/admin_console/team_analytics.jsx
@@ -0,0 +1,357 @@
+// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+var Client = require('../../utils/client.jsx');
+var Utils = require('../../utils/utils.jsx');
+var LineChart = require('./line_chart.jsx');
+
+export default class TeamAnalytics extends React.Component {
+ constructor(props) {
+ super(props);
+
+ this.getData = this.getData.bind(this);
+
+ this.state = {
+ 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
+ };
+ }
+
+ componentDidMount() {
+ this.getData(this.props.team.id);
+ }
+
+ getData(teamId) {
+ Client.getAnalytics(
+ 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});
+ }
+ }
+ },
+ (err) => {
+ this.setState({serverError: err.message});
+ }
+ );
+
+ Client.getAnalytics(
+ teamId,
+ 'post_counts_day',
+ (data) => {
+ data.reverse();
+
+ var chartData = {
+ labels: [],
+ datasets: [{
+ label: 'Total Posts',
+ fillColor: 'rgba(151,187,205,0.2)',
+ strokeColor: 'rgba(151,187,205,1)',
+ pointColor: 'rgba(151,187,205,1)',
+ pointStrokeColor: '#fff',
+ pointHighlightFill: '#fff',
+ pointHighlightStroke: 'rgba(151,187,205,1)',
+ data: []
+ }]
+ };
+
+ for (var index in data) {
+ if (data[index]) {
+ var row = data[index];
+ chartData.labels.push(row.name);
+ chartData.datasets[0].data.push(row.value);
+ }
+ }
+
+ this.setState({post_counts_day: chartData});
+ },
+ (err) => {
+ this.setState({serverError: err.message});
+ }
+ );
+
+ Client.getAnalytics(
+ teamId,
+ 'user_counts_with_posts_day',
+ (data) => {
+ data.reverse();
+
+ var chartData = {
+ labels: [],
+ datasets: [{
+ label: 'Active Users With Posts',
+ fillColor: 'rgba(151,187,205,0.2)',
+ strokeColor: 'rgba(151,187,205,1)',
+ pointColor: 'rgba(151,187,205,1)',
+ pointStrokeColor: '#fff',
+ pointHighlightFill: '#fff',
+ pointHighlightStroke: 'rgba(151,187,205,1)',
+ data: []
+ }]
+ };
+
+ for (var index in data) {
+ if (data[index]) {
+ var row = data[index];
+ chartData.labels.push(row.name);
+ chartData.datasets[0].data.push(row.value);
+ }
+ }
+
+ this.setState({user_counts_with_posts_day: chartData});
+ },
+ (err) => {
+ this.setState({serverError: err.message});
+ }
+ );
+
+ 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++) {
+ 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
+ });
+
+ this.getData(newProps.team.id);
+ }
+
+ componentWillUnmount() {
+ }
+
+ render() {
+ var serverError = '';
+ if (this.state.serverError) {
+ serverError = <div className='form-group has-error'><label className='control-label'>{this.state.serverError}</label></div>;
+ }
+
+ var totalCount = (
+ <div className='total-count text-center'>
+ <div>{'Total Users'}</div>
+ <div>{this.state.users == null ? 'Loading...' : Object.keys(this.state.users).length}</div>
+ </div>
+ );
+
+ var openChannelCount = (
+ <div className='total-count text-center'>
+ <div>{'Public Groups'}</div>
+ <div>{this.state.channel_open_count == null ? 'Loading...' : this.state.channel_open_count}</div>
+ </div>
+ );
+
+ var openPrivateCount = (
+ <div className='total-count text-center'>
+ <div>{'Private Groups'}</div>
+ <div>{this.state.channel_private_count == null ? 'Loading...' : this.state.channel_private_count}</div>
+ </div>
+ );
+
+ var postCount = (
+ <div className='total-count text-center'>
+ <div>{'Total Posts'}</div>
+ <div>{this.state.post_count == null ? 'Loading...' : this.state.post_count}</div>
+ </div>
+ );
+
+ var postCountsByDay = (
+ <div className='total-count-by-day'>
+ <div>{'Total Posts'}</div>
+ <div>{'Loading...'}</div>
+ </div>
+ );
+
+ if (this.state.post_counts_day != null) {
+ postCountsByDay = (
+ <div className='total-count-by-day'>
+ <div>{'Total Posts'}</div>
+ <LineChart
+ data={this.state.post_counts_day}
+ width='740'
+ height='225'
+ />
+ </div>
+ );
+ }
+
+ var usersWithPostsByDay = (
+ <div className='total-count-by-day'>
+ <div>{'Total Posts'}</div>
+ <div>{'Loading...'}</div>
+ </div>
+ );
+
+ if (this.state.user_counts_with_posts_day != null) {
+ usersWithPostsByDay = (
+ <div className='total-count-by-day'>
+ <div>{'Active Users With Posts'}</div>
+ <LineChart
+ data={this.state.user_counts_with_posts_day}
+ width='740'
+ height='225'
+ />
+ </div>
+ );
+ }
+
+ var recentActiveUser = (
+ <div className='recent-active-users'>
+ <div>{'Recent Active Users'}</div>
+ <div>{'Loading...'}</div>
+ </div>
+ );
+
+ if (this.state.recent_active_users != null) {
+ recentActiveUser = (
+ <div className='recent-active-users'>
+ <div>{'Recent Active Users'}</div>
+ <table width='90%'>
+ <tbody>
+ {
+ this.state.recent_active_users.map((user) => {
+ return (
+ <tr key={user.id}>
+ <td className='recent-active-users-td'>{user.email}</td>
+ <td className='recent-active-users-td'>{Utils.displayDateTime(user.last_activity_at)}</td>
+ </tr>
+ );
+ })
+ }
+ </tbody>
+ </table>
+ </div>
+ );
+ }
+
+ var newUsers = (
+ <div className='recent-active-users'>
+ <div>{'Newly Created Users'}</div>
+ <div>{'Loading...'}</div>
+ </div>
+ );
+
+ if (this.state.newly_created_users != null) {
+ newUsers = (
+ <div className='recent-active-users'>
+ <div>{'Newly Created Users'}</div>
+ <table width='90%'>
+ <tbody>
+ {
+ this.state.newly_created_users.map((user) => {
+ return (
+ <tr key={user.id}>
+ <td className='recent-active-users-td'>{user.email}</td>
+ <td className='recent-active-users-td'>{Utils.displayDateTime(user.create_at)}</td>
+ </tr>
+ );
+ })
+ }
+ </tbody>
+ </table>
+ </div>
+ );
+ }
+
+ return (
+ <div className='wrapper--fixed'>
+ <h2>{'Statistics for ' + this.props.team.name}</h2>
+ {serverError}
+ {totalCount}
+ {postCount}
+ {openChannelCount}
+ {openPrivateCount}
+ {postCountsByDay}
+ {usersWithPostsByDay}
+ {recentActiveUser}
+ {newUsers}
+ </div>
+ );
+ }
+}
+
+TeamAnalytics.propTypes = {
+ team: React.PropTypes.object
+};
diff --git a/web/react/components/admin_console/team_users.jsx b/web/react/components/admin_console/team_users.jsx
index ffb412159..b44aba56e 100644
--- a/web/react/components/admin_console/team_users.jsx
+++ b/web/react/components/admin_console/team_users.jsx
@@ -33,14 +33,6 @@ export default class UserList extends React.Component {
this.getTeamProfiles(this.props.team.id);
}
- // this.setState({
- // teamId: this.state.teamId,
- // users: this.state.users,
- // serverError: this.state.serverError,
- // showPasswordModal: this.state.showPasswordModal,
- // user: this.state.user
- // });
-
getTeamProfiles(teamId) {
Client.getProfilesForTeam(
teamId,
@@ -95,8 +87,6 @@ export default class UserList extends React.Component {
}
doPasswordResetDismiss() {
- this.state.showPasswordModal = false;
- this.state.user = null;
this.setState({
teamId: this.state.teamId,
users: this.state.users,
diff --git a/web/react/components/create_comment.jsx b/web/react/components/create_comment.jsx
index 18936e808..058594165 100644
--- a/web/react/components/create_comment.jsx
+++ b/web/react/components/create_comment.jsx
@@ -8,6 +8,7 @@ const SocketStore = require('../stores/socket_store.jsx');
const ChannelStore = require('../stores/channel_store.jsx');
const UserStore = require('../stores/user_store.jsx');
const PostStore = require('../stores/post_store.jsx');
+const PreferenceStore = require('../stores/preference_store.jsx');
const Textbox = require('./textbox.jsx');
const MsgTyping = require('./msg_typing.jsx');
const FileUpload = require('./file_upload.jsx');
@@ -27,7 +28,7 @@ export default class CreateComment extends React.Component {
this.handleSubmit = this.handleSubmit.bind(this);
this.commentMsgKeyPress = this.commentMsgKeyPress.bind(this);
this.handleUserInput = this.handleUserInput.bind(this);
- this.handleArrowUp = this.handleArrowUp.bind(this);
+ this.handleKeyDown = this.handleKeyDown.bind(this);
this.handleUploadStart = this.handleUploadStart.bind(this);
this.handleFileUploadComplete = this.handleFileUploadComplete.bind(this);
this.handleUploadError = this.handleUploadError.bind(this);
@@ -36,6 +37,7 @@ export default class CreateComment extends React.Component {
this.handleSubmit = this.handleSubmit.bind(this);
this.getFileCount = this.getFileCount.bind(this);
this.handleResize = this.handleResize.bind(this);
+ this.onPreferenceChange = this.onPreferenceChange.bind(this);
PostStore.clearCommentDraftUploads();
@@ -45,15 +47,23 @@ export default class CreateComment extends React.Component {
uploadsInProgress: draft.uploadsInProgress,
previews: draft.previews,
submitting: false,
- windowWidth: Utils.windowWidth()
+ windowWidth: Utils.windowWidth(),
+ ctrlSend: PreferenceStore.getPreference(Constants.Preferences.CATEGORY_ADVANCED_SETTINGS, 'send_on_ctrl_enter', {value: 'false'}).value
};
}
componentDidMount() {
+ PreferenceStore.addChangeListener(this.onPreferenceChange);
window.addEventListener('resize', this.handleResize);
}
componentWillUnmount() {
+ PreferenceStore.removeChangeListener(this.onPreferenceChange);
window.removeEventListener('resize', this.handleResize);
}
+ onPreferenceChange() {
+ this.setState({
+ ctrlSend: PreferenceStore.getPreference(Constants.Preferences.CATEGORY_ADVANCED_SETTINGS, 'send_on_ctrl_enter', {value: 'false'}).value
+ });
+ }
handleResize() {
this.setState({windowWidth: Utils.windowWidth()});
}
@@ -140,10 +150,12 @@ export default class CreateComment extends React.Component {
this.setState({messageText: '', submitting: false, postError: null, previews: [], serverError: null});
}
commentMsgKeyPress(e) {
- if (e.which === 13 && !e.shiftKey && !e.altKey) {
- e.preventDefault();
- ReactDOM.findDOMNode(this.refs.textbox).blur();
- this.handleSubmit(e);
+ if (this.state.ctrlSend === 'true' && e.ctrlKey || this.state.ctrlSend === 'false') {
+ if (e.which === KeyCodes.ENTER && !e.shiftKey && !e.altKey) {
+ e.preventDefault();
+ ReactDOM.findDOMNode(this.refs.textbox).blur();
+ this.handleSubmit(e);
+ }
}
const t = Date.now();
@@ -161,7 +173,12 @@ export default class CreateComment extends React.Component {
$('.post-right__scroll').perfectScrollbar('update');
this.setState({messageText: messageText});
}
- handleArrowUp(e) {
+ handleKeyDown(e) {
+ if (this.state.ctrlSend === 'true' && e.keyCode === KeyCodes.ENTER && e.ctrlKey === true) {
+ this.commentMsgKeyPress(e);
+ return;
+ }
+
if (e.keyCode === KeyCodes.UP && this.state.messageText === '') {
e.preventDefault();
@@ -313,7 +330,7 @@ export default class CreateComment extends React.Component {
<Textbox
onUserInput={this.handleUserInput}
onKeyPress={this.commentMsgKeyPress}
- onKeyDown={this.handleArrowUp}
+ onKeyDown={this.handleKeyDown}
messageText={this.state.messageText}
createMessage='Add a comment...'
initialText=''
diff --git a/web/react/components/create_post.jsx b/web/react/components/create_post.jsx
index 32ee31efe..cdbc3bc6d 100644
--- a/web/react/components/create_post.jsx
+++ b/web/react/components/create_post.jsx
@@ -8,6 +8,7 @@ const ChannelStore = require('../stores/channel_store.jsx');
const PostStore = require('../stores/post_store.jsx');
const UserStore = require('../stores/user_store.jsx');
const SocketStore = require('../stores/socket_store.jsx');
+const PreferenceStore = require('../stores/preference_store.jsx');
const MsgTyping = require('./msg_typing.jsx');
const Textbox = require('./textbox.jsx');
const FileUpload = require('./file_upload.jsx');
@@ -36,9 +37,10 @@ export default class CreatePost extends React.Component {
this.removePreview = this.removePreview.bind(this);
this.onChange = this.onChange.bind(this);
this.getFileCount = this.getFileCount.bind(this);
- this.handleArrowUp = this.handleArrowUp.bind(this);
+ this.handleKeyDown = this.handleKeyDown.bind(this);
this.handleResize = this.handleResize.bind(this);
this.sendMessage = this.sendMessage.bind(this);
+ this.onPreferenceChange = this.onPreferenceChange.bind(this);
PostStore.clearDraftUploads();
@@ -52,8 +54,16 @@ export default class CreatePost extends React.Component {
submitting: false,
initialText: draft.messageText,
windowWidth: Utils.windowWidth(),
- windowHeight: Utils.windowHeight()
+ windowHeight: Utils.windowHeight(),
+ ctrlSend: PreferenceStore.getPreference(Constants.Preferences.CATEGORY_ADVANCED_SETTINGS, 'send_on_ctrl_enter', {value: 'false'}).value
};
+
+ PreferenceStore.addChangeListener(this.onPreferenceChange);
+ }
+ onPreferenceChange() {
+ this.setState({
+ ctrlSend: PreferenceStore.getPreference(Constants.Preferences.CATEGORY_ADVANCED_SETTINGS, 'send_on_ctrl_enter', {value: 'false'}).value
+ });
}
handleResize() {
this.setState({
@@ -201,10 +211,12 @@ export default class CreatePost extends React.Component {
);
}
postMsgKeyPress(e) {
- if (e.which === KeyCodes.ENTER && !e.shiftKey && !e.altKey) {
- e.preventDefault();
- ReactDOM.findDOMNode(this.refs.textbox).blur();
- this.handleSubmit(e);
+ if (this.state.ctrlSend === 'true' && e.ctrlKey || this.state.ctrlSend === 'false') {
+ if (e.which === KeyCodes.ENTER && !e.shiftKey && !e.altKey) {
+ e.preventDefault();
+ ReactDOM.findDOMNode(this.refs.textbox).blur();
+ this.handleSubmit(e);
+ }
}
const t = Date.now();
@@ -328,7 +340,12 @@ export default class CreatePost extends React.Component {
const draft = PostStore.getDraft(channelId);
return draft.previews.length + draft.uploadsInProgress.length;
}
- handleArrowUp(e) {
+ handleKeyDown(e) {
+ if (this.state.ctrlSend === 'true' && e.keyCode === KeyCodes.ENTER && e.ctrlKey === true) {
+ this.postMsgKeyPress(e);
+ return;
+ }
+
if (e.keyCode === KeyCodes.UP && this.state.messageText === '') {
e.preventDefault();
@@ -393,7 +410,7 @@ export default class CreatePost extends React.Component {
<Textbox
onUserInput={this.handleUserInput}
onKeyPress={this.postMsgKeyPress}
- onKeyDown={this.handleArrowUp}
+ onKeyDown={this.handleKeyDown}
onHeightChange={this.resizePostHolder}
messageText={this.state.messageText}
createMessage='Write a message...'
diff --git a/web/react/components/file_attachment.jsx b/web/react/components/file_attachment.jsx
index 4d4e8390c..e707e32f5 100644
--- a/web/react/components/file_attachment.jsx
+++ b/web/react/components/file_attachment.jsx
@@ -270,7 +270,7 @@ export default class FileAttachment extends React.Component {
href={fileUrl}
download={filenameString}
data-toggle='tooltip'
- title={filenameString}
+ title={'Download ' + filenameString}
className='post-image__name'
>
{trimmedFilename}
diff --git a/web/react/components/get_link_modal.jsx b/web/react/components/get_link_modal.jsx
index 325e86f3d..8839bc3c7 100644
--- a/web/react/components/get_link_modal.jsx
+++ b/web/react/components/get_link_modal.jsx
@@ -98,7 +98,7 @@ export default class GetLinkModal extends React.Component {
<br /><br />
</p>
<textarea
- className='form-control no-resize'
+ className='form-control no-resize min-height'
readOnly='true'
ref='textarea'
value={this.state.value}
diff --git a/web/react/components/navbar_dropdown.jsx b/web/react/components/navbar_dropdown.jsx
index 2b68645e5..2b0f3c40e 100644
--- a/web/react/components/navbar_dropdown.jsx
+++ b/web/react/components/navbar_dropdown.jsx
@@ -58,6 +58,7 @@ export default class NavbarDropdown extends React.Component {
TeamStore.addChangeListener(this.onListenerChange);
$(ReactDOM.findDOMNode(this.refs.dropdown)).on('hide.bs.dropdown', () => {
+ $('.sidebar--left .dropdown-menu').scrollTop(0);
this.blockToggle = true;
setTimeout(() => {
this.blockToggle = false;
diff --git a/web/react/components/register_app_modal.jsx b/web/react/components/register_app_modal.jsx
index 3d4d9bf45..c40409dcc 100644
--- a/web/react/components/register_app_modal.jsx
+++ b/web/react/components/register_app_modal.jsx
@@ -96,75 +96,74 @@ export default class RegisterAppModal extends React.Component {
var body = '';
if (this.state.clientId === '') {
body = (
- <div className='form-group user-settings'>
- <h3>{'Register a New Application'}</h3>
- <br/>
- <label className='col-sm-4 control-label'>{'Application Name'}</label>
- <div className='col-sm-7'>
- <input
- ref='name'
- className='form-control'
- type='text'
- placeholder='Required'
- />
- {nameError}
- </div>
- <br/>
- <br/>
- <label className='col-sm-4 control-label'>{'Homepage URL'}</label>
- <div className='col-sm-7'>
- <input
- ref='homepage'
- className='form-control'
- type='text'
- placeholder='Required'
- />
- {homepageError}
- </div>
- <br/>
- <br/>
- <label className='col-sm-4 control-label'>{'Description'}</label>
- <div className='col-sm-7'>
- <input
- ref='desc'
- className='form-control'
- type='text'
- placeholder='Optional'
- />
- </div>
- <br/>
- <br/>
- <label className='col-sm-4 control-label'>{'Callback URL'}</label>
- <div className='col-sm-7'>
- <textarea
- ref='callback'
- className='form-control'
- type='text'
- placeholder='Required'
- rows='5'
- />
- {callbackError}
+ <div className='settings-modal'>
+ <div className='form-horizontal user-settings'>
+ <h4 className='padding-bottom x3'>{'Register a New Application'}</h4>
+ <div className='row'>
+ <label className='col-sm-4 control-label'>{'Application Name'}</label>
+ <div className='col-sm-7'>
+ <input
+ ref='name'
+ className='form-control'
+ type='text'
+ placeholder='Required'
+ />
+ {nameError}
+ </div>
+ </div>
+ <div className='row padding-top x2'>
+ <label className='col-sm-4 control-label'>{'Homepage URL'}</label>
+ <div className='col-sm-7'>
+ <input
+ ref='homepage'
+ className='form-control'
+ type='text'
+ placeholder='Required'
+ />
+ {homepageError}
+ </div>
+ </div>
+ <div className='row padding-top x2'>
+ <label className='col-sm-4 control-label'>{'Description'}</label>
+ <div className='col-sm-7'>
+ <input
+ ref='desc'
+ className='form-control'
+ type='text'
+ placeholder='Optional'
+ />
+ </div>
+ </div>
+ <div className='row padding-top padding-bottom x2'>
+ <label className='col-sm-4 control-label'>{'Callback URL'}</label>
+ <div className='col-sm-7'>
+ <textarea
+ ref='callback'
+ className='form-control'
+ type='text'
+ placeholder='Required'
+ rows='5'
+ />
+ {callbackError}
+ </div>
+ </div>
+ {serverError}
+ <hr />
+ <a
+ className='btn btn-sm theme pull-right'
+ href='#'
+ data-dismiss='modal'
+ aria-label='Close'
+ >
+ {'Cancel'}
+ </a>
+ <a
+ className='btn btn-sm btn-primary pull-right'
+ onClick={this.register}
+ >
+ {'Register'}
+ </a>
</div>
- <br/>
- <br/>
- <br/>
- <br/>
- <br/>
- {serverError}
- <a
- className='btn btn-sm theme pull-right'
- href='#'
- data-dismiss='modal'
- aria-label='Close'
- >
- {'Cancel'}
- </a>
- <a
- className='btn btn-sm btn-primary pull-right'
- onClick={this.register}
- >
- {'Register'}
- </a>
</div>
);
} else {
diff --git a/web/react/components/search_autocomplete.jsx b/web/react/components/search_autocomplete.jsx
index 03c7b894c..f7d772677 100644
--- a/web/react/components/search_autocomplete.jsx
+++ b/web/react/components/search_autocomplete.jsx
@@ -10,6 +10,7 @@ const patterns = new Map([
['channels', /\b(?:in|channel):\s*(\S*)$/i],
['users', /\bfrom:\s*(\S*)$/i]
]);
+const Popover = ReactBootstrap.Popover;
export default class SearchAutocomplete extends React.Component {
constructor(props) {
@@ -36,6 +37,11 @@ export default class SearchAutocomplete extends React.Component {
$(document).on('click', this.handleDocumentClick);
}
+ componentDidUpdate() {
+ $(ReactDOM.findDOMNode(this.refs.searchPopover)).find('.popover-content').perfectScrollbar();
+ $(ReactDOM.findDOMNode(this.refs.searchPopover)).find('.popover-content').css('max-height', $(window).height() - 200);
+ }
+
componentWillUnmount() {
$(document).off('click', this.handleDocumentClick);
}
@@ -193,7 +199,7 @@ export default class SearchAutocomplete extends React.Component {
if (this.state.mode === 'channels') {
suggestions = this.state.suggestions.map((channel, index) => {
- let className = 'search-autocomplete__channel';
+ let className = 'search-autocomplete__item';
if (this.state.selection === index) {
className += ' selected';
}
@@ -211,7 +217,7 @@ export default class SearchAutocomplete extends React.Component {
});
} else if (this.state.mode === 'users') {
suggestions = this.state.suggestions.map((user, index) => {
- let className = 'search-autocomplete__user';
+ let className = 'search-autocomplete__item';
if (this.state.selection === index) {
className += ' selected';
}
@@ -224,7 +230,7 @@ export default class SearchAutocomplete extends React.Component {
className={className}
>
<img
- className='profile-img'
+ className='profile-img rounded'
src={'/api/v1/users/' + user.id + '/image?time=' + user.update_at}
/>
{user.username}
@@ -234,12 +240,15 @@ export default class SearchAutocomplete extends React.Component {
}
return (
- <div
- ref='container'
- className='search-autocomplete'
+ <Popover
+ ref='searchPopover'
+ onShow={this.componentDidMount}
+ id='search-autocomplete__popover'
+ className='search-help-popover autocomplete visible'
+ placement='bottom'
>
{suggestions}
- </div>
+ </Popover>
);
}
}
diff --git a/web/react/components/setting_item_max.jsx b/web/react/components/setting_item_max.jsx
index 774f98a43..d6c4b0d4b 100644
--- a/web/react/components/setting_item_max.jsx
+++ b/web/react/components/setting_item_max.jsx
@@ -35,8 +35,10 @@ export default class SettingItemMax extends React.Component {
var widthClass;
if (this.props.width === 'full') {
widthClass = 'col-sm-12';
- } else {
+ } else if (this.props.width === 'medium') {
widthClass = 'col-sm-10 col-sm-offset-2';
+ } else {
+ widthClass = 'col-sm-9 col-sm-offset-3';
}
return (
diff --git a/web/react/components/setting_picture.jsx b/web/react/components/setting_picture.jsx
index b6bcb13a6..e69412cca 100644
--- a/web/react/components/setting_picture.jsx
+++ b/web/react/components/setting_picture.jsx
@@ -42,7 +42,7 @@ export default class SettingPicture extends React.Component {
img = (
<img
ref='image'
- className='profile-img'
+ className='profile-img rounded'
src=''
/>
);
@@ -50,7 +50,7 @@ export default class SettingPicture extends React.Component {
img = (
<img
ref='image'
- className='profile-img'
+ className='profile-img rounded'
src={this.props.src}
/>
);
diff --git a/web/react/components/sidebar_header.jsx b/web/react/components/sidebar_header.jsx
index de28a8374..65e4c6d7e 100644
--- a/web/react/components/sidebar_header.jsx
+++ b/web/react/components/sidebar_header.jsx
@@ -5,6 +5,9 @@ var NavbarDropdown = require('./navbar_dropdown.jsx');
var UserStore = require('../stores/user_store.jsx');
const Utils = require('../utils/utils.jsx');
+const Tooltip = ReactBootstrap.Tooltip;
+const OverlayTrigger = ReactBootstrap.OverlayTrigger;
+
export default class SidebarHeader extends React.Component {
constructor(props) {
super(props);
@@ -47,7 +50,15 @@ export default class SidebarHeader extends React.Component {
{profilePicture}
<div className='header__info'>
<div className='user__name'>{'@' + me.username}</div>
+ <OverlayTrigger
+ trigger={['hover', 'focus']}
+ delayShow={1000}
+ placement='bottom'
+ overlay={<Tooltip id='team-name__tooltip'>{this.props.teamDisplayName}</Tooltip>}
+ ref='descriptionOverlay'
+ >
<div className='team__name'>{this.props.teamDisplayName}</div>
+ </OverlayTrigger>
</div>
</a>
<NavbarDropdown
diff --git a/web/react/components/team_signup_with_email.jsx b/web/react/components/team_signup_with_email.jsx
index ff4ccd4d8..021713f04 100644
--- a/web/react/components/team_signup_with_email.jsx
+++ b/web/react/components/team_signup_with_email.jsx
@@ -71,7 +71,7 @@ export default class EmailSignUpPage extends React.Component {
className='btn btn-md btn-primary'
type='submit'
>
- {'Sign up'}
+ {'Create Team'}
</button>
{serverError}
</div>
diff --git a/web/react/components/user_settings/code_theme_chooser.jsx b/web/react/components/user_settings/code_theme_chooser.jsx
deleted file mode 100644
index eef4b24ba..000000000
--- a/web/react/components/user_settings/code_theme_chooser.jsx
+++ /dev/null
@@ -1,55 +0,0 @@
-// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
-// See License.txt for license information.
-
-var Constants = require('../../utils/constants.jsx');
-
-export default class CodeThemeChooser extends React.Component {
- constructor(props) {
- super(props);
- this.state = {};
- }
- render() {
- const theme = this.props.theme;
-
- const premadeThemes = [];
- for (const k in Constants.CODE_THEMES) {
- if (Constants.CODE_THEMES.hasOwnProperty(k)) {
- let activeClass = '';
- if (k === theme.codeTheme) {
- activeClass = 'active';
- }
-
- premadeThemes.push(
- <div
- className='col-xs-6 col-sm-3 premade-themes'
- key={'premade-theme-key' + k}
- >
- <div
- className={activeClass}
- onClick={() => this.props.updateTheme(k)}
- >
- <label>
- <img
- className='img-responsive'
- src={'/static/images/themes/code_themes/' + k + '.png'}
- />
- <div className='theme-label'>{Constants.CODE_THEMES[k]}</div>
- </label>
- </div>
- </div>
- );
- }
- }
-
- return (
- <div className='row'>
- {premadeThemes}
- </div>
- );
- }
-}
-
-CodeThemeChooser.propTypes = {
- theme: React.PropTypes.object.isRequired,
- updateTheme: React.PropTypes.func.isRequired
-};
diff --git a/web/react/components/user_settings/manage_outgoing_hooks.jsx b/web/react/components/user_settings/manage_outgoing_hooks.jsx
index 6e9b2205d..4c56db0a1 100644
--- a/web/react/components/user_settings/manage_outgoing_hooks.jsx
+++ b/web/react/components/user_settings/manage_outgoing_hooks.jsx
@@ -236,6 +236,7 @@ export default class ManageOutgoingHooks extends React.Component {
return (
<div key='addOutgoingHook'>
<label className='control-label'>{'Add a new outgoing webhook'}</label>
+ <div className='padding-top divider-light'></div>
<div className='padding-top'>
<div>
<label className='control-label'>{'Channel'}</label>
diff --git a/web/react/components/user_settings/user_settings.jsx b/web/react/components/user_settings/user_settings.jsx
index 15bf961d6..546e26ca3 100644
--- a/web/react/components/user_settings/user_settings.jsx
+++ b/web/react/components/user_settings/user_settings.jsx
@@ -10,6 +10,7 @@ var AppearanceTab = require('./user_settings_appearance.jsx');
var DeveloperTab = require('./user_settings_developer.jsx');
var IntegrationsTab = require('./user_settings_integrations.jsx');
var DisplayTab = require('./user_settings_display.jsx');
+var AdvancedTab = require('./user_settings_advanced.jsx');
export default class UserSettings extends React.Component {
constructor(props) {
@@ -110,6 +111,17 @@ export default class UserSettings extends React.Component {
/>
</div>
);
+ } else if (this.props.activeTab === 'advanced') {
+ return (
+ <div>
+ <AdvancedTab
+ user={this.state.user}
+ activeSection={this.props.activeSection}
+ updateSection={this.props.updateSection}
+ updateTab={this.props.updateTab}
+ />
+ </div>
+ );
}
return <div/>;
diff --git a/web/react/components/user_settings/user_settings_advanced.jsx b/web/react/components/user_settings/user_settings_advanced.jsx
new file mode 100644
index 000000000..910444735
--- /dev/null
+++ b/web/react/components/user_settings/user_settings_advanced.jsx
@@ -0,0 +1,169 @@
+// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+const Client = require('../../utils/client.jsx');
+const SettingItemMin = require('../setting_item_min.jsx');
+const SettingItemMax = require('../setting_item_max.jsx');
+const Constants = require('../../utils/constants.jsx');
+const PreferenceStore = require('../../stores/preference_store.jsx');
+
+export default class AdvancedSettingsDisplay extends React.Component {
+ constructor(props) {
+ super(props);
+
+ this.updateSection = this.updateSection.bind(this);
+ this.updateSetting = this.updateSetting.bind(this);
+ this.handleClose = this.handleClose.bind(this);
+ this.setupInitialState = this.setupInitialState.bind(this);
+
+ this.state = this.setupInitialState();
+ }
+
+ setupInitialState() {
+ const sendOnCtrlEnter = PreferenceStore.getPreference(
+ Constants.Preferences.CATEGORY_ADVANCED_SETTINGS,
+ 'send_on_ctrl_enter',
+ {value: 'false'}
+ ).value;
+
+ return {
+ settings: {send_on_ctrl_enter: sendOnCtrlEnter}
+ };
+ }
+
+ updateSetting(setting, value) {
+ const settings = this.state.settings;
+ settings[setting] = value;
+ this.setState(settings);
+ }
+
+ handleSubmit(setting) {
+ const preference = PreferenceStore.setPreference(
+ Constants.Preferences.CATEGORY_ADVANCED_SETTINGS,
+ setting,
+ this.state.settings[setting]
+ );
+
+ Client.savePreferences([preference],
+ () => {
+ PreferenceStore.emitChange();
+ this.updateSection('');
+ },
+ (err) => {
+ this.setState({serverError: err.message});
+ }
+ );
+ }
+
+ updateSection(section) {
+ this.props.updateSection(section);
+ }
+
+ handleClose() {
+ this.updateSection('');
+ }
+
+ componentDidMount() {
+ $('#user_settings').on('hidden.bs.modal', this.handleClose);
+ }
+
+ componentWillUnmount() {
+ $('#user_settings').off('hidden.bs.modal', this.handleClose);
+ }
+
+ render() {
+ const serverError = this.state.serverError || null;
+ let ctrlSendSection;
+
+ if (this.props.activeSection === 'advancedCtrlSend') {
+ const ctrlSendActive = [
+ this.state.settings.send_on_ctrl_enter === 'true',
+ this.state.settings.send_on_ctrl_enter === 'false'
+ ];
+
+ const inputs = [
+ <div key='ctrlSendSetting'>
+ <div className='radio'>
+ <label>
+ <input
+ type='radio'
+ checked={ctrlSendActive[0]}
+ onChange={this.updateSetting.bind(this, 'send_on_ctrl_enter', 'true')}
+ />
+ {'On'}
+ </label>
+ <br/>
+ </div>
+ <div className='radio'>
+ <label>
+ <input
+ type='radio'
+ checked={ctrlSendActive[1]}
+ onChange={this.updateSetting.bind(this, 'send_on_ctrl_enter', 'false')}
+ />
+ {'Off'}
+ </label>
+ <br/>
+ </div>
+ <div><br/>{'If enabled \'Enter\' inserts a new line and \'Ctrl + Enter\' submits the message.'}</div>
+ </div>
+ ];
+
+ ctrlSendSection = (
+ <SettingItemMax
+ title='Send messages on Ctrl + Enter'
+ inputs={inputs}
+ submit={() => this.handleSubmit('send_on_ctrl_enter')}
+ server_error={serverError}
+ updateSection={(e) => {
+ this.updateSection('');
+ e.preventDefault();
+ }}
+ />
+ );
+ } else {
+ ctrlSendSection = (
+ <SettingItemMin
+ title='Send messages on Ctrl + Enter'
+ describe={this.state.settings.send_on_ctrl_enter === 'true' ? 'On' : 'Off'}
+ updateSection={() => this.props.updateSection('advancedCtrlSend')}
+ />
+ );
+ }
+
+ return (
+ <div>
+ <div className='modal-header'>
+ <button
+ type='button'
+ className='close'
+ data-dismiss='modal'
+ aria-label='Close'
+ >
+ <span aria-hidden='true'>{'×'}</span>
+ </button>
+ <h4
+ className='modal-title'
+ ref='title'
+ >
+ <i className='modal-back'></i>
+ {'Advanced Settings'}
+ </h4>
+ </div>
+ <div className='user-settings'>
+ <h3 className='tab-header'>{'Advanced Settings'}</h3>
+ <div className='divider-dark first'/>
+ {ctrlSendSection}
+ <div className='divider-dark'/>
+ </div>
+ </div>
+ );
+ }
+}
+
+AdvancedSettingsDisplay.propTypes = {
+ user: React.PropTypes.object,
+ updateSection: React.PropTypes.func,
+ updateTab: React.PropTypes.func,
+ activeSection: React.PropTypes.string
+};
diff --git a/web/react/components/user_settings/user_settings_appearance.jsx b/web/react/components/user_settings/user_settings_appearance.jsx
index e94894a1d..8c62a189d 100644
--- a/web/react/components/user_settings/user_settings_appearance.jsx
+++ b/web/react/components/user_settings/user_settings_appearance.jsx
@@ -7,7 +7,6 @@ var Utils = require('../../utils/utils.jsx');
const CustomThemeChooser = require('./custom_theme_chooser.jsx');
const PremadeThemeChooser = require('./premade_theme_chooser.jsx');
-const CodeThemeChooser = require('./code_theme_chooser.jsx');
const AppDispatcher = require('../../dispatcher/app_dispatcher.jsx');
const Constants = require('../../utils/constants.jsx');
const ActionTypes = Constants.ActionTypes;
@@ -19,14 +18,12 @@ export default class UserSettingsAppearance extends React.Component {
this.onChange = this.onChange.bind(this);
this.submitTheme = this.submitTheme.bind(this);
this.updateTheme = this.updateTheme.bind(this);
- this.updateCodeTheme = this.updateCodeTheme.bind(this);
this.handleClose = this.handleClose.bind(this);
this.handleImportModal = this.handleImportModal.bind(this);
this.state = this.getStateFromStores();
this.originalTheme = this.state.theme;
- this.originalCodeTheme = this.state.theme.codeTheme;
}
componentDidMount() {
UserStore.addChangeListener(this.onChange);
@@ -61,10 +58,6 @@ export default class UserSettingsAppearance extends React.Component {
type = 'custom';
}
- if (!theme.codeTheme) {
- theme.codeTheme = Constants.DEFAULT_CODE_THEME;
- }
-
return {theme, type};
}
onChange() {
@@ -100,13 +93,6 @@ export default class UserSettingsAppearance extends React.Component {
);
}
updateTheme(theme) {
- theme.codeTheme = this.state.theme.codeTheme;
- this.setState({theme});
- Utils.applyTheme(theme);
- }
- updateCodeTheme(codeTheme) {
- var theme = this.state.theme;
- theme.codeTheme = codeTheme;
this.setState({theme});
Utils.applyTheme(theme);
}
@@ -116,7 +102,6 @@ export default class UserSettingsAppearance extends React.Component {
handleClose() {
const state = this.getStateFromStores();
state.serverError = null;
- state.theme.codeTheme = this.originalCodeTheme;
Utils.applyTheme(state.theme);
@@ -185,13 +170,7 @@ export default class UserSettingsAppearance extends React.Component {
</div>
{custom}
<hr />
- <strong className='radio'>{'Code Theme'}</strong>
- <CodeThemeChooser
- theme={this.state.theme}
- updateTheme={this.updateCodeTheme}
- />
- <hr />
- {serverError}
+ {serverError}
<a
className='btn btn-sm btn-primary'
href='#'
diff --git a/web/react/components/user_settings/user_settings_general.jsx b/web/react/components/user_settings/user_settings_general.jsx
index 1c8ce3c79..3adac197a 100644
--- a/web/react/components/user_settings/user_settings_general.jsx
+++ b/web/react/components/user_settings/user_settings_general.jsx
@@ -570,6 +570,7 @@ export default class UserSettingsGeneralTab extends React.Component {
/>
);
}
+
return (
<div>
<div className='modal-header'>
diff --git a/web/react/components/user_settings/user_settings_integrations.jsx b/web/react/components/user_settings/user_settings_integrations.jsx
index 4b1e5e532..9bee74343 100644
--- a/web/react/components/user_settings/user_settings_integrations.jsx
+++ b/web/react/components/user_settings/user_settings_integrations.jsx
@@ -43,6 +43,7 @@ export default class UserSettingsIntegrationsTab extends React.Component {
incomingHooksSection = (
<SettingItemMax
title='Incoming Webhooks'
+ width='medium'
inputs={inputs}
updateSection={(e) => {
this.updateSection('');
@@ -54,6 +55,7 @@ export default class UserSettingsIntegrationsTab extends React.Component {
incomingHooksSection = (
<SettingItemMin
title='Incoming Webhooks'
+ width='medium'
describe='Manage your incoming webhooks (Developer feature)'
updateSection={() => {
this.updateSection('incoming-hooks');
@@ -72,6 +74,7 @@ export default class UserSettingsIntegrationsTab extends React.Component {
outgoingHooksSection = (
<SettingItemMax
title='Outgoing Webhooks'
+ width='medium'
inputs={inputs}
updateSection={(e) => {
this.updateSection('');
@@ -83,6 +86,7 @@ export default class UserSettingsIntegrationsTab extends React.Component {
outgoingHooksSection = (
<SettingItemMin
title='Outgoing Webhooks'
+ width='medium'
describe='Manage your outgoing webhooks'
updateSection={() => {
this.updateSection('outgoing-hooks');
diff --git a/web/react/components/user_settings/user_settings_modal.jsx b/web/react/components/user_settings/user_settings_modal.jsx
index 5449ae91e..18dd490e7 100644
--- a/web/react/components/user_settings/user_settings_modal.jsx
+++ b/web/react/components/user_settings/user_settings_modal.jsx
@@ -43,6 +43,7 @@ export default class UserSettingsModal extends React.Component {
tabs.push({name: 'integrations', uiName: 'Integrations', icon: 'glyphicon glyphicon-transfer'});
}
tabs.push({name: 'display', uiName: 'Display', icon: 'glyphicon glyphicon-eye-open'});
+ tabs.push({name: 'advanced', uiName: 'Advanced', icon: 'glyphicon glyphicon-list-alt'});
return (
<div