diff options
Diffstat (limited to 'web')
45 files changed, 892 insertions, 413 deletions
diff --git a/web/react/components/admin_console/admin_sidebar.jsx b/web/react/components/admin_console/admin_sidebar.jsx index eadd8d412..795b19eec 100644 --- a/web/react/components/admin_console/admin_sidebar.jsx +++ b/web/react/components/admin_console/admin_sidebar.jsx @@ -50,6 +50,7 @@ export default class AdminSidebar extends React.Component { removeTeam(teamId, e) { e.preventDefault(); + e.stopPropagation(); Reflect.deleteProperty(this.props.selectedTeams, teamId); this.props.removeSelectedTeam(teamId); diff --git a/web/react/components/admin_console/user_item.jsx b/web/react/components/admin_console/user_item.jsx index 0c1a55cc1..009a9f004 100644 --- a/web/react/components/admin_console/user_item.jsx +++ b/web/react/components/admin_console/user_item.jsx @@ -353,7 +353,7 @@ export default class UserItem extends React.Component { return ( <tr> - <td className='row member-div'> + <td className='row member-div padding--equal'> <img className='post-profile-img pull-left' src={`/api/v1/users/${user.id}/image?time=${user.update_at}&${Utils.getSessionIndex()}`} diff --git a/web/react/components/audit_table.jsx b/web/react/components/audit_table.jsx index 49892ff98..31d04f19b 100644 --- a/web/react/components/audit_table.jsx +++ b/web/react/components/audit_table.jsx @@ -183,6 +183,26 @@ const holders = defineMessages({ loginFailure: { id: 'audit_table.loginFailure', defaultMessage: ' (Login failure)' + }, + attemptedLicenseAdd: { + id: 'audit_table.attemptedLicenseAdd', + defaultMessage: 'Attempted to add new license' + }, + successfullLicenseAdd: { + id: 'audit_table.successfullLicenseAdd', + defaultMessage: 'Successfully added new license' + }, + failedExpiredLicenseAdd: { + id: 'audit_table.failedExpiredLicenseAdd', + defaultMessage: 'Failed to add a new license as it has either expired or not yet been started' + }, + failedInvalidLicenseAdd: { + id: 'audit_table.failedInvalidLicenseAdd', + defaultMessage: 'Failed to add an invalid license' + }, + licenseRemoved: { + id: 'audit_table.licenseRemoved', + defaultMessage: 'Successfully removed a license' } }); @@ -327,17 +347,17 @@ export function formatAuditInfo(audit, formatMessage) { switch (actionURL) { case '/channels/create': - auditDesc = formatMessage(holders.channelCreated, {channelName: channelName}); + auditDesc = formatMessage(holders.channelCreated, {channelName}); break; case '/channels/create_direct': auditDesc = formatMessage(holders.establishedDM, {username: Utils.getDirectTeammate(channelObj.id).username}); break; case '/channels/update': - auditDesc = formatMessage(holders.nameUpdated, {channelName: channelName}); + auditDesc = formatMessage(holders.nameUpdated, {channelName}); break; case '/channels/update_desc': // support the old path case '/channels/update_header': - auditDesc = formatMessage(holders.headerUpdated, {channelName: channelName}); + auditDesc = formatMessage(holders.headerUpdated, {channelName}); break; default: { let userIdField = []; @@ -356,9 +376,9 @@ export function formatAuditInfo(audit, formatMessage) { if (/\/channels\/[A-Za-z0-9]+\/delete/.test(actionURL)) { auditDesc = formatMessage(holders.channelDeleted, {url: channelURL}); } else if (/\/channels\/[A-Za-z0-9]+\/add/.test(actionURL)) { - auditDesc = formatMessage(holders.userAdded, {username: username, channelName: channelName}); + auditDesc = formatMessage(holders.userAdded, {username, channelName}); } else if (/\/channels\/[A-Za-z0-9]+\/remove/.test(actionURL)) { - auditDesc = formatMessage(holders.userRemoved, {username: username, channelName: channelName}); + auditDesc = formatMessage(holders.userRemoved, {username, channelName}); } break; @@ -495,25 +515,25 @@ export function formatAuditInfo(audit, formatMessage) { break; } } else if (actionURL.indexOf('/hooks') === 0) { - const webhookInfo = audit.extra_info.split(' '); + const webhookInfo = audit.extra_info; switch (actionURL) { case '/hooks/incoming/create': - if (webhookInfo[0] === 'attempt') { + if (webhookInfo === 'attempt') { auditDesc = formatMessage(holders.attemptedWebhookCreate); - } else if (webhookInfo[0] === 'success') { + } else if (webhookInfo === 'success') { auditDesc = formatMessage(holders.succcessfullWebhookCreate); - } else if (webhookInfo[0] === 'fail - bad channel permissions') { + } else if (webhookInfo === 'fail - bad channel permissions') { auditDesc = formatMessage(holders.failedWebhookCreate); } break; case '/hooks/incoming/delete': - if (webhookInfo[0] === 'attempt') { + if (webhookInfo === 'attempt') { auditDesc = formatMessage(holders.attemptedWebhookDelete); - } else if (webhookInfo[0] === 'success') { + } else if (webhookInfo === 'success') { auditDesc = formatMessage(holders.successfullWebhookDelete); - } else if (webhookInfo[0] === 'fail - inappropriate conditions') { + } else if (webhookInfo === 'fail - inappropriate conditions') { auditDesc = formatMessage(holders.failedWebhookDelete); } @@ -521,6 +541,28 @@ export function formatAuditInfo(audit, formatMessage) { default: break; } + } else if (actionURL.indexOf('/license') === 0) { + const licenseInfo = audit.extra_info; + + switch (actionURL) { + case '/license/add': + if (licenseInfo === 'attempt') { + auditDesc = formatMessage(holders.attemptedLicenseAdd); + } else if (licenseInfo === 'success') { + auditDesc = formatMessage(holders.successfullLicenseAdd); + } else if (licenseInfo === 'failed - expired or non-started license') { + auditDesc = formatMessage(holders.failedExpiredLicenseAdd); + } else if (licenseInfo === 'failed - invalid license') { + auditDesc = formatMessage(holders.failedInvalidLicenseAdd); + } + + break; + case '/license/remove': + auditDesc = formatMessage(holders.licenseRemoved); + break; + default: + break; + } } else { switch (actionURL) { case '/logout': diff --git a/web/react/components/channel_info_modal.jsx b/web/react/components/channel_info_modal.jsx index 5067f5913..83f5aba65 100644 --- a/web/react/components/channel_info_modal.jsx +++ b/web/react/components/channel_info_modal.jsx @@ -56,7 +56,7 @@ class ChannelInfoModal extends React.Component { </div> <div className='col-sm-9'>{channelURL}</div> </div> - <div className='row'> + <div className='row form-group'> <div className='col-sm-3 info__label'> <FormattedMessage id='channel_info.id' diff --git a/web/react/components/channel_loader.jsx b/web/react/components/channel_loader.jsx index 174c8c4e1..f3000ee05 100644 --- a/web/react/components/channel_loader.jsx +++ b/web/react/components/channel_loader.jsx @@ -95,6 +95,8 @@ class ChannelLoader extends React.Component { $(window).on('focus', function windowFocus() { AsyncClient.updateLastViewedAt(); + ChannelStore.resetCounts(ChannelStore.getCurrentId()); + ChannelStore.emitChange(); window.isActive = true; }); @@ -185,4 +187,4 @@ ChannelLoader.propTypes = { intl: intlShape.isRequired }; -export default injectIntl(ChannelLoader);
\ No newline at end of file +export default injectIntl(ChannelLoader); diff --git a/web/react/components/claim/claim_account.jsx b/web/react/components/claim/claim_account.jsx index 87026b762..5b3b584ee 100644 --- a/web/react/components/claim/claim_account.jsx +++ b/web/react/components/claim/claim_account.jsx @@ -43,11 +43,7 @@ export default class ClaimAccount extends React.Component { ); } - return ( - <div> - {content} - </div> - ); + return content; } } diff --git a/web/react/components/claim/email_to_sso.jsx b/web/react/components/claim/email_to_sso.jsx index 3d4b9e91f..87e86697c 100644 --- a/web/react/components/claim/email_to_sso.jsx +++ b/web/react/components/claim/email_to_sso.jsx @@ -70,62 +70,69 @@ class EmailToSSO extends React.Component { const uiType = Utils.toTitleCase(this.props.type) + ' SSO'; return ( - <div className='col-sm-12'> - <div className='signup-team__container'> - <h3> + <div> + <h3> + <FormattedMessage + id='claim.email_to_sso.title' + defaultMessage='Switch Email/Password Account to {uiType}' + values={{ + uiType: uiType + }} + /> + </h3> + <form onSubmit={this.submit}> + <p> <FormattedMessage - id='claim.email_to_sso.title' - defaultMessage='Switch Email/Password Account to {uiType}' + id='claim.email_to_sso.ssoType' + defaultMessage='Upon claiming your account, you will only be able to login with {type} SSO. You must already have a valid {type} account' + values={{ + type: Utils.toTitleCase(this.props.type) + }} + /> + </p> + <p> + <FormattedMessage + id='claim.email_to_sso.ssoNote' + defaultMessage='You must already have a valid {type} account' + values={{ + type: Utils.toTitleCase(this.props.type) + }} + /> + </p> + <p> + <FormattedMessage + id='claim.email_to_sso.enterPwd' + defaultMessage='Enter the password for your {team} {site} account' + values={{ + team: this.props.teamDisplayName, + site: global.window.mm_config.SiteName + }} + /> + </p> + <div className={formClass}> + <input + type='password' + className='form-control' + name='password' + ref='password' + placeholder={this.props.intl.formatMessage(holders.pwd)} + spellCheck='false' + /> + </div> + {error} + <button + type='submit' + className='btn btn-primary' + > + <FormattedMessage + id='claim.email_to_sso.switchTo' + defaultMessage='Switch account to {uiType}' values={{ uiType: uiType }} /> - </h3> - <form onSubmit={this.submit}> - <p> - <FormattedMessage - id='claim.email_to_sso.ssoType' - defaultMessage='Upon claiming your account, you will only be able to login with {type} SSO' - values={{ - type: Utils.toTitleCase(this.props.type) - }} - /> - </p> - <p> - <FormattedMessage - id='claim.email_to_sso.enterPwd' - defaultMessage='Enter the password for your {team} {site} account' - values={{ - team: this.props.teamDisplayName, - site: global.window.mm_config.SiteName - }} - /> - </p> - <div className={formClass}> - <input - type='password' - className='form-control' - name='password' - ref='password' - placeholder={this.props.intl.formatMessage(holders.pwd)} - spellCheck='false' - /> - </div> - {error} - <button - type='submit' - className='btn btn-primary' - > - <FormattedMessage - id='claim.email_to_sso.switchTo' - defaultMessage='Switch account to {uiType}' - values={{ - uiType: uiType - }} - /> - </button> - </form> - </div> + </button> + </form> </div> ); } @@ -141,4 +148,4 @@ EmailToSSO.propTypes = { teamDisplayName: React.PropTypes.string.isRequired }; -export default injectIntl(EmailToSSO);
\ No newline at end of file +export default injectIntl(EmailToSSO); diff --git a/web/react/components/claim/sso_to_email.jsx b/web/react/components/claim/sso_to_email.jsx index 73ff13cc9..74137082a 100644 --- a/web/react/components/claim/sso_to_email.jsx +++ b/web/react/components/claim/sso_to_email.jsx @@ -86,69 +86,67 @@ class SSOToEmail extends React.Component { const uiType = Utils.toTitleCase(this.props.currentType) + ' SSO'; return ( - <div className='col-sm-12'> - <div className='signup-team__container'> - <h3> + <div> + <h3> + <FormattedMessage + id='claim.sso_to_email.title' + defaultMessage='Switch {type} Account to Email' + values={{ + type: uiType + }} + /> + </h3> + <form onSubmit={this.submit}> + <p> <FormattedMessage - id='claim.sso_to_email.title' - defaultMessage='Switch {type} Account to Email' + id='claim.sso_to_email.description' + defaultMessage='Upon changing your account type, you will only be able to login with your email and password.' + /> + </p> + <p> + <FormattedMessage + id='claim.sso_to_email_newPwd' + defaultMessage='Enter a new password for your {team} {site} account' + values={{ + team: this.props.teamDisplayName, + site: global.window.mm_config.SiteName + }} + /> + </p> + <div className={formClass}> + <input + type='password' + className='form-control' + name='password' + ref='password' + placeholder={formatMessage(holders.newPwd)} + spellCheck='false' + /> + </div> + <div className={formClass}> + <input + type='password' + className='form-control' + name='passwordconfirm' + ref='passwordconfirm' + placeholder={formatMessage(holders.confirm)} + spellCheck='false' + /> + </div> + {error} + <button + type='submit' + className='btn btn-primary' + > + <FormattedMessage + id='claim.sso_to_email.switchTo' + defaultMessage='Switch {type} to email and password' values={{ type: uiType }} /> - </h3> - <form onSubmit={this.submit}> - <p> - <FormattedMessage - id='claim.sso_to_email.description' - defaultMessage='Upon changing your account type, you will only be able to login with your email and password.' - /> - </p> - <p> - <FormattedMessage - id='claim.sso_to_email_newPwd' - defaultMessage='Enter a new password for your {team} {site} account' - values={{ - team: this.props.teamDisplayName, - site: global.window.mm_config.SiteName - }} - /> - </p> - <div className={formClass}> - <input - type='password' - className='form-control' - name='password' - ref='password' - placeholder={formatMessage(holders.newPwd)} - spellCheck='false' - /> - </div> - <div className={formClass}> - <input - type='password' - className='form-control' - name='passwordconfirm' - ref='passwordconfirm' - placeholder={formatMessage(holders.confirm)} - spellCheck='false' - /> - </div> - {error} - <button - type='submit' - className='btn btn-primary' - > - <FormattedMessage - id='claim.sso_to_email.switchTo' - defaultMessage='Switch {type} to email and password' - values={{ - type: uiType - }} - /> - </button> - </form> - </div> + </button> + </form> </div> ); } @@ -164,4 +162,4 @@ SSOToEmail.propTypes = { teamDisplayName: React.PropTypes.string.isRequired }; -export default injectIntl(SSOToEmail);
\ No newline at end of file +export default injectIntl(SSOToEmail); diff --git a/web/react/components/create_comment.jsx b/web/react/components/create_comment.jsx index 55dd8276c..24e0ff6e9 100644 --- a/web/react/components/create_comment.jsx +++ b/web/react/components/create_comment.jsx @@ -7,6 +7,7 @@ import * as AsyncClient from '../utils/async_client.jsx'; import SocketStore from '../stores/socket_store.jsx'; import ChannelStore from '../stores/channel_store.jsx'; import UserStore from '../stores/user_store.jsx'; +import PostDeletedModal from './post_deleted_modal.jsx'; import PostStore from '../stores/post_store.jsx'; import PreferenceStore from '../stores/preference_store.jsx'; import Textbox from './textbox.jsx'; @@ -60,6 +61,8 @@ class CreateComment extends React.Component { this.handleResize = this.handleResize.bind(this); this.onPreferenceChange = this.onPreferenceChange.bind(this); this.focusTextbox = this.focusTextbox.bind(this); + this.showPostDeletedModal = this.showPostDeletedModal.bind(this); + this.hidePostDeletedModal = this.hidePostDeletedModal.bind(this); PostStore.clearCommentDraftUploads(); @@ -70,7 +73,8 @@ class CreateComment extends React.Component { previews: draft.previews, submitting: false, windowWidth: Utils.windowWidth(), - ctrlSend: PreferenceStore.getBool(Constants.Preferences.CATEGORY_ADVANCED_SETTINGS, 'send_on_ctrl_enter') + ctrlSend: PreferenceStore.getBool(Constants.Preferences.CATEGORY_ADVANCED_SETTINGS, 'send_on_ctrl_enter'), + showPostDeletedModal: false }; } componentDidMount() { @@ -141,8 +145,10 @@ class CreateComment extends React.Component { PostStore.storePendingPost(post); PostStore.storeCommentDraft(this.props.rootId, null); - Client.createPost(post, ChannelStore.getCurrent(), - function handlePostSuccess(data) { + Client.createPost( + post, + ChannelStore.getCurrent(), + (data) => { AsyncClient.getPosts(this.props.channelId); const channel = ChannelStore.get(this.props.channelId); @@ -155,27 +161,30 @@ class CreateComment extends React.Component { type: ActionTypes.RECEIVED_POST, post: data }); - }.bind(this), - function handlePostError(err) { - let state = {}; - + }, + (err) => { if (err.id === 'api.post.create_post.root_id.app_error') { - PostStore.removePendingPost(post.channel_id, post.pending_post_id); + this.showPostDeletedModal(); - if ($('#post_deleted').length > 0) { - $('#post_deleted').modal('show'); - } + PostStore.removePendingPost(post.channel_id, post.pending_post_id); } else { post.state = Constants.POST_FAILED; PostStore.updatePendingPost(post); } - state.submitting = false; - this.setState(state); - }.bind(this) + this.setState({ + submitting: false + }); + } ); - this.setState({messageText: '', submitting: false, postError: null, previews: [], serverError: null}); + this.setState({ + messageText: '', + submitting: false, + postError: null, + previews: [], + serverError: null + }); } commentMsgKeyPress(e) { if (this.state.ctrlSend && e.ctrlKey || !this.state.ctrlSend) { @@ -285,7 +294,7 @@ class CreateComment extends React.Component { if (index !== -1) { uploadsInProgress.splice(index, 1); - this.refs.fileUpload.cancelUpload(id); + this.refs.fileUpload.getWrappedInstance().cancelUpload(id); } } else { previews.splice(index, 1); @@ -312,6 +321,16 @@ class CreateComment extends React.Component { this.refs.textbox.focus(); } } + showPostDeletedModal() { + this.setState({ + showPostDeletedModal: true + }); + } + hidePostDeletedModal() { + this.setState({ + showPostDeletedModal: false + }); + } render() { let serverError = null; if (this.state.serverError) { @@ -411,6 +430,10 @@ class CreateComment extends React.Component { {serverError} </div> </div> + <PostDeletedModal + show={this.state.showPostDeletedModal} + onHide={this.hidePostDeletedModal} + /> </form> ); } diff --git a/web/react/components/create_post.jsx b/web/react/components/create_post.jsx index b9fbf09b5..48b6594a1 100644 --- a/web/react/components/create_post.jsx +++ b/web/react/components/create_post.jsx @@ -5,6 +5,7 @@ import MsgTyping from './msg_typing.jsx'; import Textbox from './textbox.jsx'; import FileUpload from './file_upload.jsx'; import FilePreview from './file_preview.jsx'; +import PostDeletedModal from './post_deleted_modal.jsx'; import TutorialTip from './tutorial/tutorial_tip.jsx'; import AppDispatcher from '../dispatcher/app_dispatcher.jsx'; @@ -64,6 +65,8 @@ class CreatePost extends React.Component { this.handleKeyDown = this.handleKeyDown.bind(this); this.sendMessage = this.sendMessage.bind(this); this.focusTextbox = this.focusTextbox.bind(this); + this.showPostDeletedModal = this.showPostDeletedModal.bind(this); + this.hidePostDeletedModal = this.hidePostDeletedModal.bind(this); PostStore.clearDraftUploads(); @@ -77,7 +80,8 @@ class CreatePost extends React.Component { submitting: false, initialText: draft.messageText, ctrlSend: false, - showTutorialTip: false + showTutorialTip: false, + showPostDeletedModal: false }; } getCurrentDraft() { @@ -157,7 +161,6 @@ class CreatePost extends React.Component { post.pending_post_id = `${userId}:${time}`; post.user_id = userId; post.create_at = time; - post.root_id = this.state.rootId; post.parent_id = this.state.parentId; const channel = ChannelStore.get(this.state.channelId); @@ -177,20 +180,19 @@ class CreatePost extends React.Component { EventHelpers.emitPostRecievedEvent(data); }, (err) => { - const state = {}; - if (err.id === 'api.post.create_post.root_id.app_error') { - if ($('#post_deleted').length > 0) { - $('#post_deleted').modal('show'); - } + // this should never actually happen since you can't reply from this textbox + this.showPostDeletedModal(); + PostStore.removePendingPost(post.pending_post_id); } else { post.state = Constants.POST_FAILED; PostStore.updatePendingPost(post); } - state.submitting = false; - this.setState(state); + this.setState({ + submitting: false + }); } ); } @@ -286,7 +288,7 @@ class CreatePost extends React.Component { if (index !== -1) { uploadsInProgress.splice(index, 1); - this.refs.fileUpload.cancelUpload(id); + this.refs.fileUpload.getWrappedInstance().cancelUpload(id); } } else { previews.splice(index, 1); @@ -374,6 +376,16 @@ class CreatePost extends React.Component { }); } } + showPostDeletedModal() { + this.setState({ + showPostDeletedModal: true + }); + } + hidePostDeletedModal() { + this.setState({ + showPostDeletedModal: false + }); + } createTutorialTip() { const screens = []; @@ -479,6 +491,10 @@ class CreatePost extends React.Component { {serverError} </div> </div> + <PostDeletedModal + show={this.state.showPostDeletedModal} + onHide={this.hidePostDeletedModal} + /> </form> ); } diff --git a/web/react/components/file_upload.jsx b/web/react/components/file_upload.jsx index f5c32c825..0454fe510 100644 --- a/web/react/components/file_upload.jsx +++ b/web/react/components/file_upload.jsx @@ -34,6 +34,7 @@ class FileUpload extends React.Component { this.uploadFiles = this.uploadFiles.bind(this); this.handleChange = this.handleChange.bind(this); this.handleDrop = this.handleDrop.bind(this); + this.cancelUpload = this.cancelUpload.bind(this); this.state = { requests: {} @@ -331,4 +332,4 @@ FileUpload.propTypes = { postType: React.PropTypes.string }; -export default injectIntl(FileUpload); +export default injectIntl(FileUpload, {withRef: true}); diff --git a/web/react/components/member_list_team_item.jsx b/web/react/components/member_list_team_item.jsx index 30086d1b2..7b1f6170d 100644 --- a/web/react/components/member_list_team_item.jsx +++ b/web/react/components/member_list_team_item.jsx @@ -5,8 +5,29 @@ import UserStore from '../stores/user_store.jsx'; import * as Client from '../utils/client.jsx'; import * as AsyncClient from '../utils/async_client.jsx'; import * as Utils from '../utils/utils.jsx'; +import ConfirmModal from './confirm_modal.jsx'; +import TeamStore from '../stores/team_store.jsx'; -import {FormattedMessage} from 'mm-intl'; +import {injectIntl, intlShape, defineMessages, 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 { constructor(props) { @@ -16,23 +37,35 @@ export default class MemberListTeamItem extends React.Component { this.handleMakeActive = this.handleMakeActive.bind(this); this.handleMakeNotActive = this.handleMakeNotActive.bind(this); this.handleMakeAdmin = this.handleMakeAdmin.bind(this); + this.handleDemote = this.handleDemote.bind(this); + this.handleDemoteSubmit = this.handleDemoteSubmit.bind(this); + this.handleDemoteCancel = this.handleDemoteCancel.bind(this); - this.state = {}; + this.state = { + serverError: null, + showDemoteModal: false, + user: null, + role: null + }; } handleMakeMember() { - const data = { - user_id: this.props.user.id, - new_roles: '' - }; - - Client.updateRoles(data, - () => { - AsyncClient.getProfiles(); - }, - (err) => { - this.setState({serverError: err.message}); - } - ); + const me = UserStore.getCurrentUser(); + if (this.props.user.id === me.id) { + this.handleDemote(this.props.user, ''); + } else { + const data = { + user_id: this.props.user.id, + new_roles: '' + }; + Client.updateRoles(data, + () => { + AsyncClient.getProfiles(); + }, + (err) => { + this.setState({serverError: err.message}); + } + ); + } } handleMakeActive() { Client.updateActive(this.props.user.id, true, @@ -55,14 +88,55 @@ export default class MemberListTeamItem extends React.Component { ); } handleMakeAdmin() { + const me = UserStore.getCurrentUser(); + if (this.props.user.id === me.id) { + this.handleDemote(this.props.user, 'admin'); + } else { + const data = { + user_id: this.props.user.id, + new_roles: 'admin' + }; + + Client.updateRoles(data, + () => { + AsyncClient.getProfiles(); + }, + (err) => { + this.setState({serverError: err.message}); + } + ); + } + } + handleDemote(user, role) { + this.setState({ + serverError: this.state.serverError, + showDemoteModal: true, + user, + role + }); + } + handleDemoteCancel() { + this.setState({ + serverError: null, + showDemoteModal: false, + user: null, + role: null + }); + } + handleDemoteSubmit() { const data = { user_id: this.props.user.id, - new_roles: 'admin' + new_roles: this.state.role }; Client.updateRoles(data, () => { - AsyncClient.getProfiles(); + const teamUrl = TeamStore.getCurrentTeamUrl(); + if (teamUrl) { + window.location.href = teamUrl; + } else { + window.location.href = '/'; + } }, (err) => { this.setState({serverError: err.message}); @@ -198,6 +272,21 @@ export default class MemberListTeamItem extends React.Component { </li> ); } + const me = UserStore.getCurrentUser(); + const {formatMessage} = this.props.intl; + let makeDemoteModal = null; + if (this.props.user.id === me.id) { + 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)} + onConfirm={this.handleDemoteSubmit} + onCancel={this.handleDemoteCancel} + /> + ); + } return ( <tr> @@ -231,6 +320,7 @@ export default class MemberListTeamItem extends React.Component { {makeNotActive} </ul> </div> + {makeDemoteModal} {serverError} </td> </tr> @@ -239,5 +329,8 @@ export default class MemberListTeamItem extends React.Component { } MemberListTeamItem.propTypes = { + intl: intlShape.isRequired, user: React.PropTypes.object.isRequired }; + +export default injectIntl(MemberListTeamItem); diff --git a/web/react/components/navbar.jsx b/web/react/components/navbar.jsx index e6a9fbd25..835298635 100644 --- a/web/react/components/navbar.jsx +++ b/web/react/components/navbar.jsx @@ -25,6 +25,7 @@ const ActionTypes = Constants.ActionTypes; import AppDispatcher from '../dispatcher/app_dispatcher.jsx'; import {FormattedMessage} from 'mm-intl'; +import attachFastClick from 'fastclick'; const Popover = ReactBootstrap.Popover; const OverlayTrigger = ReactBootstrap.OverlayTrigger; @@ -59,6 +60,7 @@ export default class Navbar extends React.Component { ChannelStore.addChangeListener(this.onChange); ChannelStore.addExtraInfoChangeListener(this.onChange); $('.inner__wrap').click(this.hideSidebars); + attachFastClick(document.body); } componentWillUnmount() { ChannelStore.removeChangeListener(this.onChange); diff --git a/web/react/components/post_deleted_modal.jsx b/web/react/components/post_deleted_modal.jsx index 642befeab..be22989a6 100644 --- a/web/react/components/post_deleted_modal.jsx +++ b/web/react/components/post_deleted_modal.jsx @@ -1,7 +1,6 @@ // Copyright (c) 2015 Mattermost, Inc. All Rights Reserved. // See License.txt for license information. -import UserStore from '../stores/user_store.jsx'; import AppDispatcher from '../dispatcher/app_dispatcher.jsx'; import Constants from '../utils/constants.jsx'; @@ -9,20 +8,22 @@ import {FormattedMessage} from 'mm-intl'; var ActionTypes = Constants.ActionTypes; +const Modal = ReactBootstrap.Modal; + export default class PostDeletedModal extends React.Component { constructor(props) { super(props); - this.handleClose = this.handleClose.bind(this); - - this.state = {}; + this.handleHide = this.handleHide.bind(this); } - componentDidMount() { - $(ReactDOM.findDOMNode(this.refs.modal)).on('hidden.bs.modal', () => { - this.handleClose(); - }); + + shouldComponentUpdate(nextProps) { + return nextProps.show !== this.props.show; } - handleClose() { + + handleHide(e) { + e.preventDefault(); + AppDispatcher.handleServerAction({ type: ActionTypes.RECEIVED_SEARCH, results: null @@ -39,67 +40,50 @@ export default class PostDeletedModal extends React.Component { type: ActionTypes.RECEIVED_POST_SELECTED, results: null }); - } - render() { - var currentUser = UserStore.getCurrentUser(); - if (currentUser != null) { - return ( - <div - className='modal fade' - ref='modal' - id='post_deleted' - tabIndex='-1' - role='dialog' - aria-hidden='true' - > - <div className='modal-dialog'> - <div className='modal-content'> - <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' - id='myModalLabel' - > - <FormattedMessage - id='post_delete.notPosted' - defaultMessage='Comment could not be posted' - /> - </h4> - </div> - <div className='modal-body'> - <p> - <FormattedMessage - id='post_delete.someone' - defaultMessage='Someone deleted the message on which you tried to post a comment.' - /> - </p> - </div> - <div className='modal-footer'> - <button - type='button' - className='btn btn-primary' - data-dismiss='modal' - > - <FormattedMessage - id='post_delete.okay' - defaultMessage='Okay' - /> - </button> - </div> - </div> - </div> - </div> - ); - } + this.props.onHide(); + } - return <div/>; + render() { + return ( + <Modal + show={this.props.show} + onHide={this.handleHide} + > + <Modal.Header closeButton={true}> + <Modal.Title> + <FormattedMessage + id='post_delete.notPosted' + defaultMessage='Comment could not be posted' + /> + </Modal.Title> + </Modal.Header> + <Modal.Body> + <p> + <FormattedMessage + id='post_delete.someone' + defaultMessage='Someone deleted the message on which you tried to post a comment.' + /> + </p> + </Modal.Body> + <Modal.Footer> + <button + type='button' + className='btn btn-primary' + onClick={this.handleHide} + > + <FormattedMessage + id='post_delete.okay' + defaultMessage='Okay' + /> + </button> + </Modal.Footer> + </Modal> + ); } } + +PostDeletedModal.propTypes = { + show: React.PropTypes.bool.isRequired, + onHide: React.PropTypes.func.isRequired +}; diff --git a/web/react/components/post_info.jsx b/web/react/components/post_info.jsx index 6d82423d5..ffac6eaef 100644 --- a/web/react/components/post_info.jsx +++ b/web/react/components/post_info.jsx @@ -14,9 +14,17 @@ export default class PostInfo extends React.Component { constructor(props) { super(props); + this.dropdownPosition = this.dropdownPosition.bind(this); this.handlePermalink = this.handlePermalink.bind(this); this.removePost = this.removePost.bind(this); } + dropdownPosition(e) { + var position = $('#post-list').height() - $(e.target).offset().top; + var dropdown = $(e.target).next('.dropdown-menu'); + if (position < dropdown.height()) { + dropdown.addClass('bottom'); + } + } createDropdown() { var post = this.props.post; var isOwner = UserStore.getCurrentId() === post.user_id; @@ -57,22 +65,24 @@ export default class PostInfo extends React.Component { ); } - dropdownContents.push( - <li - key='copyLink' - role='presentation' - > - <a - href='#' - onClick={this.handlePermalink} + if (!Utils.isMobile()) { + dropdownContents.push( + <li + key='copyLink' + role='presentation' > - <FormattedMessage - id='post_info.permalink' - defaultMessage='Permalink' - /> - </a> - </li> - ); + <a + href='#' + onClick={this.handlePermalink} + > + <FormattedMessage + id='post_info.permalink' + defaultMessage='Permalink' + /> + </a> + </li> + ); + } if (isOwner || isAdmin) { dropdownContents.push( @@ -133,6 +143,7 @@ export default class PostInfo extends React.Component { type='button' data-toggle='dropdown' aria-expanded='false' + onClick={this.dropdownPosition} /> <ul className='dropdown-menu' @@ -144,7 +155,8 @@ export default class PostInfo extends React.Component { ); } - handlePermalink() { + handlePermalink(e) { + e.preventDefault(); EventHelpers.showGetPostLinkModal(this.props.post); } diff --git a/web/react/components/rhs_comment.jsx b/web/react/components/rhs_comment.jsx index 9c85e9940..201a4c569 100644 --- a/web/react/components/rhs_comment.jsx +++ b/web/react/components/rhs_comment.jsx @@ -31,6 +31,7 @@ class RhsComment extends React.Component { this.retryComment = this.retryComment.bind(this); this.parseEmojis = this.parseEmojis.bind(this); + this.handlePermalink = this.handlePermalink.bind(this); this.state = {}; } @@ -67,6 +68,10 @@ class RhsComment extends React.Component { parseEmojis() { twemoji.parse(ReactDOM.findDOMNode(this), {size: Constants.EMOJI_SIZE}); } + handlePermalink(e) { + e.preventDefault(); + EventHelpers.showGetPostLinkModal(this.props.post); + } componentDidMount() { this.parseEmojis(); } @@ -92,6 +97,25 @@ class RhsComment extends React.Component { var dropdownContents = []; + if (!Utils.isMobile()) { + dropdownContents.push( + <li + key='rhs-root-permalink' + role='presentation' + > + <a + href='#' + onClick={this.handlePermalink} + > + <FormattedMessage + id='rhs_comment.permalink' + defaultMessage='Permalink' + /> + </a> + </li> + ); + } + if (isOwner) { dropdownContents.push( <li diff --git a/web/react/components/rhs_root_post.jsx b/web/react/components/rhs_root_post.jsx index f9f7f8f81..adf66da85 100644 --- a/web/react/components/rhs_root_post.jsx +++ b/web/react/components/rhs_root_post.jsx @@ -5,7 +5,7 @@ import ChannelStore from '../stores/channel_store.jsx'; import UserProfile from './user_profile.jsx'; import UserStore from '../stores/user_store.jsx'; import * as TextFormatting from '../utils/text_formatting.jsx'; -import * as utils from '../utils/utils.jsx'; +import * as Utils from '../utils/utils.jsx'; import * as Emoji from '../utils/emoticons.jsx'; import FileAttachmentList from './file_attachment_list.jsx'; import twemoji from 'twemoji'; @@ -21,6 +21,7 @@ export default class RhsRootPost extends React.Component { super(props); this.parseEmojis = this.parseEmojis.bind(this); + this.handlePermalink = this.handlePermalink.bind(this); this.state = {}; } @@ -31,11 +32,15 @@ export default class RhsRootPost extends React.Component { folder: Emoji.getImagePathForEmoticon() }); } + handlePermalink(e) { + e.preventDefault(); + EventHelpers.showGetPostLinkModal(this.props.post); + } componentDidMount() { this.parseEmojis(); } shouldComponentUpdate(nextProps) { - if (!utils.areObjectsEqual(nextProps.post, this.props.post)) { + if (!Utils.areObjectsEqual(nextProps.post, this.props.post)) { return true; } @@ -48,7 +53,7 @@ export default class RhsRootPost extends React.Component { var post = this.props.post; var currentUser = UserStore.getCurrentUser(); var isOwner = currentUser.id === post.user_id; - var isAdmin = utils.isAdmin(currentUser.roles); + var isAdmin = Utils.isAdmin(currentUser.roles); var timestamp = UserStore.getProfile(post.user_id).update_at; var channel = ChannelStore.get(post.channel_id); @@ -63,7 +68,7 @@ export default class RhsRootPost extends React.Component { } var systemMessageClass = ''; - if (utils.isSystemMessage(post)) { + if (Utils.isSystemMessage(post)) { systemMessageClass = 'post--system'; } @@ -83,6 +88,25 @@ export default class RhsRootPost extends React.Component { var dropdownContents = []; + if (!Utils.isMobile()) { + dropdownContents.push( + <li + key='rhs-root-permalink' + role='presentation' + > + <a + href='#' + onClick={this.handlePermalink} + > + <FormattedMessage + id='rhs_root.permalink' + defaultMessage='Permalink' + /> + </a> + </li> + ); + } + if (isOwner) { dropdownContents.push( <li @@ -176,7 +200,7 @@ export default class RhsRootPost extends React.Component { } botIndicator = <li className='col col__name bot-indicator'>{'BOT'}</li>; - } else if (utils.isSystemMessage(post)) { + } else if (Utils.isSystemMessage(post)) { userProfile = ( <UserProfile userId={''} @@ -187,12 +211,12 @@ export default class RhsRootPost extends React.Component { ); } - let src = '/api/v1/users/' + post.user_id + '/image?time=' + timestamp + '&' + utils.getSessionIndex(); + let src = '/api/v1/users/' + post.user_id + '/image?time=' + timestamp + '&' + Utils.getSessionIndex(); if (post.props && post.props.from_webhook && global.window.mm_config.EnablePostIconOverride === 'true') { if (post.props.override_icon_url) { src = post.props.override_icon_url; } - } else if (utils.isSystemMessage(post)) { + } else if (Utils.isSystemMessage(post)) { src = Constants.SYSTEM_MESSAGE_PROFILE_IMAGE; } diff --git a/web/react/components/team_general_tab.jsx b/web/react/components/team_general_tab.jsx index 0a1b02853..c1b2a2e7f 100644 --- a/web/react/components/team_general_tab.jsx +++ b/web/react/components/team_general_tab.jsx @@ -486,13 +486,9 @@ class GeneralTab extends React.Component { inputs.push( <div key='teamInviteSetting'> <div className='row'> - <label className='col-sm-5 control-label'> - <FormattedMessage - id='general_tab.codeTitle' - defaultMessage='Invite Code' - /> + <label className='col-sm-5 control-label visible-xs-block'> </label> - <div className='col-sm-7'> + <div className='col-sm-12'> <input className='form-control' type='text' diff --git a/web/react/components/team_signup_email_item.jsx b/web/react/components/team_signup_email_item.jsx index feb70dc71..790ec2e5d 100644 --- a/web/react/components/team_signup_email_item.jsx +++ b/web/react/components/team_signup_email_item.jsx @@ -83,4 +83,4 @@ TeamSignupEmailItem.propTypes = { email: React.PropTypes.string }; -export default injectIntl(TeamSignupEmailItem);
\ No newline at end of file +export default injectIntl(TeamSignupEmailItem, {withRef: true}); diff --git a/web/react/components/team_signup_send_invites_page.jsx b/web/react/components/team_signup_send_invites_page.jsx index 46a6bc68e..343db13e8 100644 --- a/web/react/components/team_signup_send_invites_page.jsx +++ b/web/react/components/team_signup_send_invites_page.jsx @@ -33,8 +33,8 @@ export default class TeamSignupSendInvitesPage extends React.Component { var emails = []; for (var i = 0; i < this.props.state.invites.length; i++) { - if (this.refs['email_' + i].validate(this.props.state.team.email)) { - emails.push(this.refs['email_' + i].getValue()); + if (this.refs['email_' + i].getWrappedInstance().validate(this.props.state.team.email)) { + emails.push(this.refs['email_' + i].getWrappedInstance().getValue()); } else { valid = false; } diff --git a/web/react/components/user_settings/custom_theme_chooser.jsx b/web/react/components/user_settings/custom_theme_chooser.jsx index 2d88a3650..1e724bb6e 100644 --- a/web/react/components/user_settings/custom_theme_chooser.jsx +++ b/web/react/components/user_settings/custom_theme_chooser.jsx @@ -102,6 +102,7 @@ class CustomThemeChooser extends React.Component { this.onPickerChange = this.onPickerChange.bind(this); this.onInputChange = this.onInputChange.bind(this); this.pasteBoxChange = this.pasteBoxChange.bind(this); + this.toggleContent = this.toggleContent.bind(this); this.state = {}; } @@ -153,11 +154,23 @@ class CustomThemeChooser extends React.Component { this.props.updateTheme(theme); } + toggleContent(e) { + e.stopPropagation(); + if ($(e.target).hasClass('theme-elements__header')) { + $(e.target).next().slideToggle(); + $(e.target).toggleClass('open'); + } else { + $(e.target).closest('.theme-elements__header').next().slideToggle(); + $(e.target).closest('.theme-elements__header').toggleClass('open'); + } + } render() { const {formatMessage} = this.props.intl; const theme = this.props.theme; - const elements = []; + const sidebarElements = []; + const centerChannelElements = []; + const linkAndButtonElements = []; let colors = ''; Constants.THEME_ELEMENTS.forEach((element, index) => { if (element.id === 'codeTheme') { @@ -187,9 +200,9 @@ class CustomThemeChooser extends React.Component { </Popover> ); - elements.push( + centerChannelElements.push( <div - className='col-sm-4 form-group' + className='col-sm-6 form-group' key={'custom-theme-key' + index} > <label className='custom-label'>{formatMessage(messages[element.id])}</label> @@ -219,10 +232,54 @@ class CustomThemeChooser extends React.Component { </div> </div> ); + } else if (element.group === 'centerChannelElements') { + centerChannelElements.push( + <div + className='col-sm-6 form-group element' + key={'custom-theme-key' + index} + > + <label className='custom-label'>{formatMessage(messages[element.id])}</label> + <div + className='input-group color-picker' + id={element.id} + > + <input + className='form-control' + type='text' + value={theme[element.id]} + onChange={this.onInputChange} + /> + <span className='input-group-addon'><i></i></span> + </div> + </div> + ); + } else if (element.group === 'sidebarElements') { + sidebarElements.push( + <div + className='col-sm-6 form-group element' + key={'custom-theme-key' + index} + > + <label className='custom-label'>{formatMessage(messages[element.id])}</label> + <div + className='input-group color-picker' + id={element.id} + > + <input + className='form-control' + type='text' + value={theme[element.id]} + onChange={this.onInputChange} + /> + <span className='input-group-addon'><i></i></span> + </div> + </div> + ); + + colors += theme[element.id] + ','; } else { - elements.push( + linkAndButtonElements.push( <div - className='col-sm-4 form-group element' + className='col-sm-6 form-group element' key={'custom-theme-key' + index} > <label className='custom-label'>{formatMessage(messages[element.id])}</label> @@ -265,9 +322,51 @@ class CustomThemeChooser extends React.Component { ); return ( - <div className='appearance-section'> + <div className='appearance-section padding-top'> + <div className='theme-elements row'> + <div + className='theme-elements__header' + onClick={this.toggleContent} + > + {'Sidebar Styles'} + <div className='header__icon'> + <i className='fa fa-plus'></i> + <i className='fa fa-minus'></i> + </div> + </div> + <div className='theme-elements__body'> + {sidebarElements} + </div> + </div> + <div className='theme-elements row'> + <div + className='theme-elements__header' + onClick={this.toggleContent} + > + {'Center Channel Styles'} + <div className='header__icon'> + <i className='fa fa-plus'></i> + <i className='fa fa-minus'></i> + </div> + </div> + <div className='theme-elements__body'> + {centerChannelElements} + </div> + </div> <div className='theme-elements row form-group'> - {elements} + <div + className='theme-elements__header' + onClick={this.toggleContent} + > + {'Link and Button Styles'} + <div className='header__icon'> + <i className='fa fa-plus'></i> + <i className='fa fa-minus'></i> + </div> + </div> + <div className='theme-elements__body'> + {linkAndButtonElements} + </div> </div> <div className='row'> {pasteBox} diff --git a/web/react/components/user_settings/manage_incoming_hooks.jsx b/web/react/components/user_settings/manage_incoming_hooks.jsx index c6532b018..68e99be7d 100644 --- a/web/react/components/user_settings/manage_incoming_hooks.jsx +++ b/web/react/components/user_settings/manage_incoming_hooks.jsx @@ -183,7 +183,7 @@ export default class ManageIncomingHooks extends React.Component { <div key='addIncomingHook'> <FormattedHTMLMessage id='user.settings.hooks_in.description' - defaultMessage='Create webhook URLs for use in external integrations. Please see<a href="http://mattermost.org/webhooks" target="_blank">http://mattermost.org/webhooks</a> to learn more.' + defaultMessage='Create webhook URLs for use in external integrations. Please see <a href="http://docs.mattermost.com/developer/webhooks-incoming.html" target="_blank">incoming webhooks documentation</a> to learn more.' /> <div><label className='control-label padding-top x2'> <FormattedMessage diff --git a/web/react/components/user_settings/manage_outgoing_hooks.jsx b/web/react/components/user_settings/manage_outgoing_hooks.jsx index 3f88e9f41..9c3a60ed5 100644 --- a/web/react/components/user_settings/manage_outgoing_hooks.jsx +++ b/web/react/components/user_settings/manage_outgoing_hooks.jsx @@ -18,6 +18,10 @@ const holders = defineMessages({ callbackHolder: { id: 'user.settings.hooks_out.callbackHolder', defaultMessage: 'Each URL must start with http:// or https://' + }, + select: { + id: 'user.settings.hooks_out.select', + defaultMessage: '--- Select a channel ---' } }); @@ -153,10 +157,7 @@ class ManageOutgoingHooks extends React.Component { key='select-channel' value='' > - <FormattedMessage - id='user.settings.hooks_out.select' - defaultMessage='--- Select a channel ---' - /> + {this.props.intl.formatMessage(holders.select)} </option> ); @@ -283,7 +284,7 @@ class ManageOutgoingHooks extends React.Component { <div key='addOutgoingHook'> <FormattedHTMLMessage id='user.settings.hooks_out.addDescription' - defaultMessage='Create webhooks to send new message events to an external integration. Please see <a href="http://mattermost.org/webhooks">http://mattermost.org/webhooks</a> to learn more.' + defaultMessage='Create webhooks to send new message events to an external integration. Please see <a href="http://docs.mattermost.com/developer/webhooks-outgoing.html" target="_blank">outgoing webhooks documentation</a> to learn more.' /> <div><label className='control-label padding-top x2'> <FormattedMessage @@ -391,4 +392,4 @@ ManageOutgoingHooks.propTypes = { intl: intlShape.isRequired }; -export default injectIntl(ManageOutgoingHooks);
\ No newline at end of file +export default injectIntl(ManageOutgoingHooks); diff --git a/web/react/components/user_settings/user_settings_modal.jsx b/web/react/components/user_settings/user_settings_modal.jsx index 7e911f09a..5442f7ac4 100644 --- a/web/react/components/user_settings/user_settings_modal.jsx +++ b/web/react/components/user_settings/user_settings_modal.jsx @@ -234,7 +234,10 @@ class UserSettingsModal extends React.Component { render() { const {formatMessage} = this.props.intl; + var currentUser = UserStore.getCurrentUser(); + var isAdmin = Utils.isAdmin(currentUser.roles); var tabs = []; + tabs.push({name: 'general', uiName: formatMessage(holders.general), icon: 'glyphicon glyphicon-cog'}); tabs.push({name: 'security', uiName: formatMessage(holders.security), icon: 'glyphicon glyphicon-lock'}); tabs.push({name: 'notifications', uiName: formatMessage(holders.notifications), icon: 'glyphicon glyphicon-exclamation-sign'}); @@ -243,8 +246,17 @@ class UserSettingsModal extends React.Component { } if (global.window.mm_config.EnableIncomingWebhooks === 'true' || global.window.mm_config.EnableOutgoingWebhooks === 'true' || global.window.mm_config.EnableCommands === 'true') { - tabs.push({name: 'integrations', uiName: formatMessage(holders.integrations), icon: 'glyphicon glyphicon-transfer'}); + var show = global.window.mm_config.EnableOnlyAdminIntegrations !== 'true'; + + if (global.window.mm_config.EnableOnlyAdminIntegrations === 'true' && isAdmin) { + show = true; + } + + if (show) { + tabs.push({name: 'integrations', uiName: formatMessage(holders.integrations), icon: 'glyphicon glyphicon-transfer'}); + } } + tabs.push({name: 'display', uiName: formatMessage(holders.display), icon: 'glyphicon glyphicon-eye-open'}); tabs.push({name: 'advanced', uiName: formatMessage(holders.advanced), icon: 'glyphicon glyphicon-list-alt'}); diff --git a/web/react/components/user_settings/user_settings_security.jsx b/web/react/components/user_settings/user_settings_security.jsx index 5693047c2..53d79906f 100644 --- a/web/react/components/user_settings/user_settings_security.jsx +++ b/web/react/components/user_settings/user_settings_security.jsx @@ -11,6 +11,7 @@ import TeamStore from '../../stores/team_store.jsx'; import * as Client from '../../utils/client.jsx'; import * as AsyncClient from '../../utils/async_client.jsx'; +import * as Utils from '../../utils/utils.jsx'; import Constants from '../../utils/constants.jsx'; import {intlShape, injectIntl, defineMessages, FormattedMessage} from 'mm-intl'; @@ -216,15 +217,12 @@ class SecurityTab extends React.Component { var describe; var d = new Date(this.props.user.last_password_update); - var timeOfDay = ' am'; - if (d.getHours() >= 12) { - timeOfDay = ' pm'; - } const locale = global.window.mm_locale; + const hours12 = !Utils.isMilitaryTime(); describe = formatMessage(holders.lastUpdated, { date: d.toLocaleDateString(locale, {month: 'short', day: '2-digit', year: 'numeric'}), - time: d.toLocaleTimeString(locale, {hours12: true, hour: '2-digit', minute: '2-digit'}) + timeOfDay + time: d.toLocaleTimeString(locale, {hour12: hours12, hour: '2-digit', minute: '2-digit'}) }); updateSectionStatus = function updateSection() { diff --git a/web/react/components/user_settings/user_settings_theme.jsx b/web/react/components/user_settings/user_settings_theme.jsx index 34c688db1..74975d115 100644 --- a/web/react/components/user_settings/user_settings_theme.jsx +++ b/web/react/components/user_settings/user_settings_theme.jsx @@ -182,7 +182,6 @@ export default class ThemeSetting extends React.Component { if (displayCustom) { custom = ( <div key='customThemeChooser'> - <br/> <CustomThemeChooser theme={this.state.theme} updateTheme={this.updateTheme} @@ -241,7 +240,6 @@ export default class ThemeSetting extends React.Component { defaultMessage='Custom Theme' /> </label> - <br/> </div> ); diff --git a/web/react/package.json b/web/react/package.json index fce3e6555..5fc2f2851 100644 --- a/web/react/package.json +++ b/web/react/package.json @@ -4,6 +4,7 @@ "private": true, "dependencies": { "autolinker": "0.22.0", + "fastclick": "^1.0.6", "flux": "2.1.1", "highlight.js": "8.9.1", "keymirror": "0.1.1", diff --git a/web/react/pages/channel.jsx b/web/react/pages/channel.jsx index 2c1d256b2..bd7c7c69b 100644 --- a/web/react/pages/channel.jsx +++ b/web/react/pages/channel.jsx @@ -12,7 +12,6 @@ import RenameChannelModal from '../components/rename_channel_modal.jsx'; import EditPostModal from '../components/edit_post_modal.jsx'; import DeletePostModal from '../components/delete_post_modal.jsx'; import MoreChannelsModal from '../components/more_channels.jsx'; -import PostDeletedModal from '../components/post_deleted_modal.jsx'; import TeamSettingsModal from '../components/team_settings_modal.jsx'; import RemovedFromChannelModal from '../components/removed_from_channel_modal.jsx'; import RegisterAppModal from '../components/register_app_modal.jsx'; @@ -79,7 +78,6 @@ class Root extends React.Component { <MoreChannelsModal /> <EditPostModal /> <DeletePostModal /> - <PostDeletedModal /> <RemovedFromChannelModal /> <RegisterAppModal /> </div> diff --git a/web/react/stores/channel_store.jsx b/web/react/stores/channel_store.jsx index ac800a988..60cb10de7 100644 --- a/web/react/stores/channel_store.jsx +++ b/web/react/stores/channel_store.jsx @@ -308,7 +308,7 @@ ChannelStore.dispatchToken = AppDispatcher.register((payload) => { ChannelStore.storeChannels(action.channels); ChannelStore.storeChannelMembers(action.members); currentId = ChannelStore.getCurrentId(); - if (currentId && !document.hidden) { + if (currentId && window.isActive) { ChannelStore.resetCounts(currentId); } ChannelStore.setUnreadCounts(); @@ -321,7 +321,7 @@ ChannelStore.dispatchToken = AppDispatcher.register((payload) => { ChannelStore.pStoreChannelMember(action.member); } currentId = ChannelStore.getCurrentId(); - if (currentId && !document.hidden) { + if (currentId && window.isActive) { ChannelStore.resetCounts(currentId); } ChannelStore.setUnreadCount(action.channel.id); diff --git a/web/react/stores/post_store.jsx b/web/react/stores/post_store.jsx index b5bb93576..f5c342163 100644 --- a/web/react/stores/post_store.jsx +++ b/web/react/stores/post_store.jsx @@ -83,8 +83,6 @@ class PostStoreClass extends EventEmitter { this.getCommentDraft = this.getCommentDraft.bind(this); this.clearDraftUploads = this.clearDraftUploads.bind(this); this.clearCommentDraftUploads = this.clearCommentDraftUploads.bind(this); - this.storeLatestUpdate = this.storeLatestUpdate.bind(this); - this.getLatestUpdate = this.getLatestUpdate.bind(this); this.getCurrentUsersLatestPost = this.getCurrentUsersLatestPost.bind(this); this.getCommentCount = this.getCommentCount.bind(this); @@ -258,7 +256,7 @@ class PostStoreClass extends EventEmitter { const np = newPosts.posts[pid]; if (np.delete_at === 0) { combinedPosts.posts[pid] = np; - if (combinedPosts.order.indexOf(pid) === -1) { + if (combinedPosts.order.indexOf(pid) === -1 && newPosts.order.indexOf(pid) !== -1) { combinedPosts.order.push(pid); } } @@ -507,19 +505,6 @@ class PostStoreClass extends EventEmitter { } }); } - storeLatestUpdate(channelId, time) { - if (!this.postsInfo.hasOwnProperty(channelId)) { - this.postsInfo[channelId] = {}; - } - this.postsInfo[channelId].latestPost = time; - } - getLatestUpdate(channelId) { - if (this.postsInfo.hasOwnProperty(channelId) && this.postsInfo[channelId].hasOwnProperty('latestPost')) { - return this.postsInfo[channelId].latestPost; - } - - return 0; - } getCommentCount(post) { const posts = this.getAllPosts(post.channel_id).posts; diff --git a/web/react/stores/socket_store.jsx b/web/react/stores/socket_store.jsx index e1b65fe14..efb57e226 100644 --- a/web/react/stores/socket_store.jsx +++ b/web/react/stores/socket_store.jsx @@ -32,6 +32,8 @@ class SocketStoreClass extends EventEmitter { this.failCount = 0; + this.translations = this.getDefaultTranslations(); + this.initialize(); } @@ -174,6 +176,18 @@ class SocketStoreClass extends EventEmitter { this.translations = messages; } + getDefaultTranslations() { + return ({ + socketError: 'Please check connection, Mattermost unreachable. If issue persists, ask administrator to check WebSocket port.', + someone: 'Someone', + posted: 'Posted', + uploadedImage: ' uploaded an image', + uploadedFile: ' uploaded a file', + something: ' did something new', + wrote: ' wrote: ' + }); + } + close() { if (conn && conn.readyState === WebSocket.OPEN) { conn.close(); @@ -188,10 +202,10 @@ function handleNewPostEvent(msg, translations) { // Update channel state if (ChannelStore.getCurrentId() === msg.channel_id) { - if (document.hidden) { - AsyncClient.getChannel(msg.channel_id); - } else { + if (window.isActive) { AsyncClient.updateLastViewedAt(); + } else { + AsyncClient.getChannel(msg.channel_id); } } else if (UserStore.getCurrentId() !== msg.user_id || post.type !== Constants.POST_TYPE_JOIN_LEAVE) { AsyncClient.getChannel(msg.channel_id); diff --git a/web/react/utils/async_client.jsx b/web/react/utils/async_client.jsx index c8676f45d..13b57092d 100644 --- a/web/react/utils/async_client.jsx +++ b/web/react/utils/async_client.jsx @@ -521,18 +521,25 @@ export function getPosts(id) { return; } - if (PostStore.getAllPosts(channelId) == null) { + const postList = PostStore.getAllPosts(channelId); + + if ($.isEmptyObject(postList) || postList.order.length < Constants.POST_CHUNK_SIZE) { getPostsPage(channelId, Constants.POST_CHUNK_SIZE); return; } - const latestUpdate = PostStore.getLatestUpdate(channelId); + const latestPost = PostStore.getLatestPost(channelId); + let latestPostTime = 0; + + if (latestPost != null && latestPost.update_at != null) { + latestPostTime = latestPost.create_at; + } callTracker['getPosts_' + channelId] = utils.getTimestamp(); client.getPosts( channelId, - latestUpdate, + latestPostTime, (data, textStatus, xhr) => { if (xhr.status === 304 || !data) { return; @@ -542,7 +549,7 @@ export function getPosts(id) { type: ActionTypes.RECEIVED_POSTS, id: channelId, before: true, - numRequested: Constants.POST_CHUNK_SIZE, + numRequested: 0, post_list: data }); diff --git a/web/react/utils/constants.jsx b/web/react/utils/constants.jsx index 428549d57..766a86686 100644 --- a/web/react/utils/constants.jsx +++ b/web/react/utils/constants.jsx @@ -189,8 +189,8 @@ export default { sidebarHeaderTextColor: '#FFFFFF', onlineIndicator: '#7DBE00', awayIndicator: '#DCBD4E', - mentionBj: '#136197', - mentionColor: '#bfcde8', + mentionBj: '#FBFBFB', + mentionColor: '#2071A7', centerChannelBg: '#f2f4f8', centerChannelColor: '#333333', newMessageSeparator: '#FF8800', @@ -276,86 +276,107 @@ export default { }, THEME_ELEMENTS: [ { + group: 'sidebarElements', id: 'sidebarBg', uiName: 'Sidebar BG' }, { + group: 'sidebarElements', id: 'sidebarText', uiName: 'Sidebar Text' }, { + group: 'sidebarElements', id: 'sidebarHeaderBg', uiName: 'Sidebar Header BG' }, { + group: 'sidebarElements', id: 'sidebarHeaderTextColor', uiName: 'Sidebar Header Text' }, { + group: 'sidebarElements', id: 'sidebarUnreadText', uiName: 'Sidebar Unread Text' }, { + group: 'sidebarElements', id: 'sidebarTextHoverBg', uiName: 'Sidebar Text Hover BG' }, { + group: 'sidebarElements', id: 'sidebarTextActiveBorder', uiName: 'Sidebar Text Active Border' }, { + group: 'sidebarElements', id: 'sidebarTextActiveColor', uiName: 'Sidebar Text Active Color' }, { + group: 'sidebarElements', id: 'onlineIndicator', uiName: 'Online Indicator' }, { + group: 'sidebarElements', id: 'awayIndicator', uiName: 'Away Indicator' }, { + group: 'sidebarElements', id: 'mentionBj', uiName: 'Mention Jewel BG' }, { + group: 'sidebarElements', id: 'mentionColor', uiName: 'Mention Jewel Text' }, { + group: 'centerChannelElements', id: 'centerChannelBg', uiName: 'Center Channel BG' }, { + group: 'centerChannelElements', id: 'centerChannelColor', uiName: 'Center Channel Text' }, { + group: 'centerChannelElements', id: 'newMessageSeparator', uiName: 'New Message Separator' }, { + group: 'linkAndButtonElements', id: 'linkColor', uiName: 'Link Color' }, { + group: 'linkAndButtonElements', id: 'buttonBg', uiName: 'Button BG' }, { + group: 'linkAndButtonElements', id: 'buttonColor', uiName: 'Button Text' }, { + group: 'centerChannelElements', id: 'mentionHighlightBg', uiName: 'Mention Highlight BG' }, { + group: 'centerChannelElements', id: 'mentionHighlightLink', uiName: 'Mention Highlight Link' }, { + group: 'centerChannelElements', id: 'codeTheme', uiName: 'Code Theme', themes: [ diff --git a/web/react/utils/markdown.jsx b/web/react/utils/markdown.jsx index 17f30a7c5..493916058 100644 --- a/web/react/utils/markdown.jsx +++ b/web/react/utils/markdown.jsx @@ -222,7 +222,7 @@ class MattermostMarkdownRenderer extends marked.Renderer { } table(header, body) { - return `<table class="markdown__table"><thead>${header}</thead><tbody>${body}</tbody></table>`; + return `<div class="table-responsive"><table class="markdown__table"><thead>${header}</thead><tbody>${body}</tbody></table></div>`; } listitem(text) { diff --git a/web/react/utils/utils.jsx b/web/react/utils/utils.jsx index e2a5b9620..2b946d81f 100644 --- a/web/react/utils/utils.jsx +++ b/web/react/utils/utils.jsx @@ -160,18 +160,22 @@ export function notifyMe(title, body, channel) { } if (permission === 'granted') { - var notification = new Notification(title, {body, tag: body, icon: '/static/images/icon50x50.png'}); - notification.onclick = () => { - window.focus(); - if (channel) { - switchChannel(channel); - } else { - window.location.href = TeamStore.getCurrentTeamUrl() + '/channels/town-square'; - } - }; - setTimeout(() => { - notification.close(); - }, 5000); + try { + var notification = new Notification(title, {body, tag: body, icon: '/static/images/icon50x50.png'}); + notification.onclick = () => { + window.focus(); + if (channel) { + switchChannel(channel); + } else { + window.location.href = TeamStore.getCurrentTeamUrl() + '/channels/town-square'; + } + }; + setTimeout(() => { + notification.close(); + }, 5000); + } catch (e) { + console.error(e); //eslint-disable-line no-console + } } }); } @@ -260,6 +264,10 @@ export function displayTimeFormatted(ticks) { ); } +export function isMilitaryTime() { + return PreferenceStore.getBool(Constants.Preferences.CATEGORY_DISPLAY_SETTINGS, 'use_military_time'); +} + export function displayDateTime(ticks) { var seconds = Math.floor((Date.now() - ticks) / 1000); @@ -739,7 +747,7 @@ export function applyTheme(theme) { changeCss('.date-separator .separator__hr, .modal-footer, .modal .custom-textarea', 'border-color:' + changeOpacity(theme.centerChannelColor, 0.2), 1); changeCss('.search-item-container, .post-right__container .post.post--root', 'border-color:' + changeOpacity(theme.centerChannelColor, 0.1), 1); changeCss('.modal .custom-textarea:focus', 'border-color:' + changeOpacity(theme.centerChannelColor, 0.3), 1); - changeCss('.channel-intro, .settings-modal .settings-table .settings-content .divider-dark, hr, .settings-modal .settings-table .settings-links', 'border-color:' + changeOpacity(theme.centerChannelColor, 0.2), 1); + changeCss('.channel-intro, .settings-modal .settings-table .settings-content .divider-dark, hr, .settings-modal .settings-table .settings-links, .settings-modal .settings-table .settings-content .appearance-section .theme-elements__header', 'border-color:' + changeOpacity(theme.centerChannelColor, 0.2), 1); changeCss('.post.current--user .post__body, .post.post--comment.other--root.current--user .post-comment, pre, .post-right__container .post.post--root', 'background:' + changeOpacity(theme.centerChannelColor, 0.07), 1); changeCss('.post.current--user .post__body, .post.post--comment.other--root.current--user .post-comment, .post.same--root.post--comment .post__body, .modal .more-table tbody>tr td, .member-div:first-child, .member-div, .access-history__table .access__report, .activity-log__table', 'border-color:' + changeOpacity(theme.centerChannelColor, 0.1), 2); changeCss('@media(max-width: 1800px){.inner__wrap.move--left .post.post--comment.same--root', 'border-color:' + changeOpacity(theme.centerChannelColor, 0.07), 2); @@ -749,7 +757,7 @@ export function applyTheme(theme) { changeCss('code', 'background:' + changeOpacity(theme.centerChannelColor, 0.1), 1); changeCss('@media(min-width: 960px){.post.current--user:hover .post__body ', 'background: none;', 1); changeCss('.sidebar--right', 'color:' + theme.centerChannelColor, 2); - changeCss('.search-help-popover .search-autocomplete__item:hover', 'background:' + changeOpacity(theme.centerChannelColor, 0.05), 1); + changeCss('.search-help-popover .search-autocomplete__item:hover, .settings-modal .settings-table .settings-content .appearance-section .theme-elements__body', 'background:' + changeOpacity(theme.centerChannelColor, 0.05), 1); changeCss('.search-help-popover .search-autocomplete__item.selected', 'background:' + changeOpacity(theme.centerChannelColor, 0.15), 1); } diff --git a/web/sass-files/sass/partials/_markdown.scss b/web/sass-files/sass/partials/_markdown.scss index 339d9f45f..f34b5ec19 100644 --- a/web/sass-files/sass/partials/_markdown.scss +++ b/web/sass-files/sass/partials/_markdown.scss @@ -31,6 +31,7 @@ } .post-body--code__language { + -webkit-transform: translate3d(0,0,0); position: absolute; top: 0; right: 0; diff --git a/web/sass-files/sass/partials/_modal.scss b/web/sass-files/sass/partials/_modal.scss index db99e840b..9d4e62bc3 100644 --- a/web/sass-files/sass/partials/_modal.scss +++ b/web/sass-files/sass/partials/_modal.scss @@ -7,6 +7,67 @@ padding: 20px 15px; overflow: auto; } +.more-table { + margin: 0; + table-layout: fixed; + p { + font-size: 0.9em; + overflow: hidden; + text-overflow: ellipsis; + @include opacity(0.8); + margin: 5px 0; + } + .profile-img { + -moz-border-radius: 50px; + -webkit-border-radius: 50px; + border-radius: 50px; + margin-right: 8px; + } + .more-name { + font-weight: 600; + font-size: 0.95em; + overflow: hidden; + text-overflow: ellipsis; + } + .more-description { + @include opacity(0.7); + display: block; + overflow: hidden; + text-overflow: ellipsis; + } + tbody { + > tr { + &:hover td { + background: #f7f7f7; + } + &:first-child { + td { + border: none; + } + } + td { + width: 100%; + white-space: nowrap; + @include legacy-pie-clearfix; + text-overflow: ellipsis; + padding: 8px 8px 8px 15px; + &.padding--equal { + padding: 8px; + } + &.td--action { + text-align: right; + padding: 8px 15px 8px 8px; + width: 80px; + vertical-align: middle; + position: relative; + &.lg { + width: 110px; + } + } + } + } + } +} .modal { width: 100%; color: #333; @@ -148,64 +209,6 @@ padding: 0; } } - .more-table { - margin: 0; - table-layout: fixed; - p { - font-size: 0.9em; - overflow: hidden; - text-overflow: ellipsis; - @include opacity(0.8); - margin: 5px 0; - } - .profile-img { - -moz-border-radius: 50px; - -webkit-border-radius: 50px; - border-radius: 50px; - margin-right: 8px; - } - .more-name { - font-weight: 600; - font-size: 0.95em; - overflow: hidden; - text-overflow: ellipsis; - } - .more-description { - @include opacity(0.7); - display: block; - overflow: hidden; - text-overflow: ellipsis; - } - tbody { - > tr { - &:hover td { - background: #f7f7f7; - } - &:first-child { - td { - border: none; - } - } - td { - width: 100%; - white-space: nowrap; - @include legacy-pie-clearfix; - text-overflow: ellipsis; - padding: 8px 8px 8px 15px; - &.td--action { - text-align: right; - padding: 8px 15px 8px 8px; - width: 80px; - vertical-align: middle; - position: relative; - &.lg { - width: 110px; - } - } - } - } - } - } .modal-image { position:relative; width:100%; diff --git a/web/sass-files/sass/partials/_post.scss b/web/sass-files/sass/partials/_post.scss index 323691d89..5d0985fc0 100644 --- a/web/sass-files/sass/partials/_post.scss +++ b/web/sass-files/sass/partials/_post.scss @@ -396,8 +396,8 @@ body.ios { .post-list__content { display: table-cell; vertical-align: bottom; - > div:last-child { - .dropdown-menu { + .dropdown-menu { + &.bottom { top: auto; bottom: 25px; } diff --git a/web/sass-files/sass/partials/_post_right.scss b/web/sass-files/sass/partials/_post_right.scss index bd3d60622..01be9e288 100644 --- a/web/sass-files/sass/partials/_post_right.scss +++ b/web/sass-files/sass/partials/_post_right.scss @@ -11,7 +11,15 @@ .post-right-comments-container { position: relative; - padding: 0.7em 0 0; + + .post { + + &:first-child { + padding-top: 15px; + } + + } + } .post { @@ -19,6 +27,11 @@ &.post--root { padding-bottom: 1.2em; border-bottom: 1px solid #ddd; + + .post__body { + background: transparent !important; + } + } .post__header { diff --git a/web/sass-files/sass/partials/_responsive.scss b/web/sass-files/sass/partials/_responsive.scss index 5d6cbee60..1a8e3f374 100644 --- a/web/sass-files/sass/partials/_responsive.scss +++ b/web/sass-files/sass/partials/_responsive.scss @@ -92,12 +92,14 @@ font-size: 1em; } - .date-separator.hovered--after:before, .new-separator.hovered--after:before { + .date-separator.hovered--before:after, .date-separator.hovered--after:before, .new-separator.hovered--after:before, .new-separator.hovered--before:after { background: none !important; } .post { + background: none !important; + .post__dropdown { line-height: 9px; text-decoration: none; @@ -114,6 +116,10 @@ } + .comment-icon__container { + visibility: visible; + } + .post-list__content & { &:hover { @@ -776,6 +782,24 @@ } } @media screen and (max-width: 480px) { + .settings-modal { + + .settings-table { + + .security-links { + margin-bottom: 10px; + display: block; + + &:last-child { + margin-bottom: 0; + } + + } + + } + + } + .tip-overlay.tip-overlay--sidebar { min-height: 350px; } diff --git a/web/sass-files/sass/partials/_settings.scss b/web/sass-files/sass/partials/_settings.scss index 1bbec566c..c79b54922 100644 --- a/web/sass-files/sass/partials/_settings.scss +++ b/web/sass-files/sass/partials/_settings.scss @@ -144,7 +144,11 @@ select { padding-right: 25px; } + select::-ms-expand { + display: none; + } &:before { + pointer-events: none; position: absolute; top: 11px; right: 50px; @@ -181,12 +185,57 @@ .theme-elements { padding-left:15px; + .element { margin-right:10px; + + &:nth-child(2n) { + margin-right: 0; + } + } } + .theme-elements__header { + margin: 10px 20px 0px 0; + border-bottom: 1px solid #ccc; + padding: 5px 0 10px; + font-size: em(13.5px); + font-weight: 600; + cursor: pointer; + + .fa-minus { + display: none; + } + + &.open { + .fa-minus { + display: inline-block; + } + .fa-plus { + display: none; + } + } + + .header__icon { + float: right; + @include opacity(0.5); + } + + } + + .theme-elements__body { + padding-top: 5px; + display: none; + @include legacy-pie-clearfix; + background: rgba(255, 255, 255, 0.05); + padding: 20px 0 0 20px; + margin: 0 20px 0 0; + @include border-radius(0 0 3px 3px); + } + .custom-label { + white-space: nowrap; font-weight: normal; font-size: 12px; width: 100%; @@ -207,6 +256,7 @@ .section-title { margin-bottom: 5px; font-weight: 600; + padding-right: 50px; } .section-edit { diff --git a/web/sass-files/sass/partials/_tutorial.scss b/web/sass-files/sass/partials/_tutorial.scss index 0a2d1e704..1f93fc6a9 100644 --- a/web/sass-files/sass/partials/_tutorial.scss +++ b/web/sass-files/sass/partials/_tutorial.scss @@ -105,7 +105,7 @@ font-size: 12px; span { - @include opacity(0.5); + @include opacity(0.9); } } diff --git a/web/static/i18n/en.json b/web/static/i18n/en.json index 2c091ca7d..1dc0bdebc 100644 --- a/web/static/i18n/en.json +++ b/web/static/i18n/en.json @@ -485,6 +485,11 @@ "audit_table.revokedAll": "Revoked all current sessions for the team", "audit_table.loginAttempt": " (Login attempt)", "audit_table.loginFailure": " (Login failure)", + "audit_table.attemptedLicenseAdd": "Attempted to add new license", + "audit_table.successfullLicenseAdd": "Successfully added new license", + "audit_table.failedExpiredLicenseAdd": "Failed to add a new license as it has either expired or not yet been started", + "audit_table.failedInvalidLicenseAdd": "Failed to add an invalid license", + "audit_table.licenseRemoved": "Successfully removed a license", "audit_table.userId": "User ID", "audit_table.ip": "IP Address", "audit_table.session": "Session ID", @@ -547,7 +552,8 @@ "claim.email_to_sso.pwdError": "Please enter your password.", "claim.email_to_sso.pwd": "Password", "claim.email_to_sso.title": "Switch Email/Password Account to {uiType}", - "claim.email_to_sso.ssoType": "Upon claiming your account, you will only be able to login with {type} SSO", + "claim.email_to_sso.ssoType": "Upon claiming your account, you will only be able to login with {type} SSO. You must already have a valid {type} account", + "claim.email_to_sso.ssoNote": "You must already have a valid {type} account", "claim.email_to_sso.enterPwd": "Enter the password for your {team} {site} account", "claim.email_to_sso.switchTo": "Switch account to {uiType}", "claim.sso_to_email.enterPwd": "Please enter a password.", @@ -683,6 +689,10 @@ "member_item.makeAdmin": "Make Admin", "member_item.removeMember": "Remove Member", "member_item.member": "Member", + "member_team_item.confirmDemoteRoleTitle": "Confirm demotion from System Admin role", + "member_team_item.confirmDemotion": "Confirm Demotion", + "member_team_item.confirmDemoteDescription": "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.", + "member_team_item.confirmDemotionCmd": "platform -assign_role -team_name=\"yourteam\" -email=\"name@yourcompany.com\" -role=\"system_admin\"", "member_team_item.member": "Member", "member_team_item.systemAdmin": "System Admin", "member_team_item.teamAdmin": "Team Admin", @@ -827,11 +837,13 @@ "rename_channel.cancel": "Cancel", "rename_channel.save": "Save", "rhs_comment.comment": "Comment", + "rhs_comment.permalink": "Permalink", "rhs_comment.edit": "Edit", "rhs_comment.del": "Delete", "rhs_comment.retry": "Retry", "rhs_header.details": "Message Details", "rhs_root.direct": "Direct Message", + "rhs_root.permalink": "Permalink", "rhs_root.edit": "Edit", "rhs_root.del": "Delete", "search_bar.search": "Search", @@ -1099,7 +1111,7 @@ "user.settings.hooks_in.channel": "Channel: ", "user.settings.hooks_in.none": "None", "user.settings.hooks_in.existing": "Existing incoming webhooks", - "user.settings.hooks_in.description": "Create webhook URLs for use in external integrations. Please see<a href=\"http://mattermost.org/webhooks\" target=\"_blank\">http://mattermost.org/webhooks</a> to learn more.", + "user.settings.hooks_in.description": "Create webhook URLs for use in external integrations. Please see <a href=\"http://docs.mattermost.com/developer/webhooks-incoming.html\" target=\"_blank\">incoming webhooks documentation</a> to learn more.", "user.settings.hooks_in.addTitle": "Add a new incoming webhook", "user.settings.hooks_in.add": "Add", "user.settings.languages.change": "Change interface language", @@ -1112,7 +1124,7 @@ "user.settings.hooks_out.regen": "Regen Token", "user.settings.hooks_out.none": "None", "user.settings.hooks_out.existing": "Existing outgoing webhooks", - "user.settings.hooks_out.addDescription": "Create webhooks to send new message events to an external integration. Please see <a href=\"http://mattermost.org/webhooks\">http://mattermost.org/webhooks</a> to learn more.", + "user.settings.hooks_out.addDescription": "Create webhooks to send new message events to an external integration. Please see <a href=\"http://docs.mattermost.com/developer/webhooks-outgoing.html\" target=\"_blank\">outgoing webhooks documentation</a> to learn more.", "user.settings.hooks_out.addTitle": "Add a new outgoing webhook", "user.settings.hooks_out.only": "Only public channels can be used", "user.settings.hooks_out.comma": "Comma separated words to trigger on", @@ -1263,4 +1275,4 @@ "intro_messages.beginning": "Beginning of {name}", "intro_messages.invite": "Invite others to this {type}", "intro_messages.setHeader": "Set a Header" -}
\ No newline at end of file +} diff --git a/web/static/i18n/es.json b/web/static/i18n/es.json index c44545e67..5723bd171 100644 --- a/web/static/i18n/es.json +++ b/web/static/i18n/es.json @@ -445,6 +445,7 @@ "audit_table.accountInactive": "Cuentas desactivadas", "audit_table.action": "Acción", "audit_table.attemptedAllowOAuthAccess": "Intento de permitir acceso a un nuevo servicio OAuth", + "audit_table.attemptedLicenseAdd": "Se ha intentado añadir nueva licencia", "audit_table.attemptedLogin": "Intento de inicio de sesión", "audit_table.attemptedOAuthToken": "Intento de obtener un token de acceso con OAuth", "audit_table.attemptedPassword": "Intento de cambio de contraseña", @@ -457,6 +458,8 @@ "audit_table.channelCreated": "Creado el canal/grupo {channelName}", "audit_table.channelDeleted": "Borrado el canal/grupo con el URL {url}", "audit_table.establishedDM": "Establecido un canal de mensajes directos con {username}", + "audit_table.failedExpiredLicenseAdd": "No se pudo agregar una nueva licencia, ya que ha caducado o aún no ha comenzado", + "audit_table.failedInvalidLicenseAdd": "No se pudo agregar una licencia inválida", "audit_table.failedLogin": "intento de inicio de sesión FALLIDO", "audit_table.failedOAuthAccess": "Falla al permitir acceso al nuevo servicio de OAuth - El URI de redirección no coincide con el previamente registrado", "audit_table.failedPassword": "Falla al cambiar la contraseña - intento de actualizar la contraseña del usuario que está autenticado a través de oauth", @@ -464,6 +467,7 @@ "audit_table.failedWebhookDelete": "Falla al borrar un webhook - condiciones inapropiadas", "audit_table.headerUpdated": "Actualizado el encabezado del canal/grupo {channelName}", "audit_table.ip": "IP: {ip}", + "audit_table.licenseRemoved": "Licencia eliminada con éxito", "audit_table.loginAttempt": " (intento de inicio de sesión)", "audit_table.loginFailure": " (inicio de sesión fallido)", "audit_table.logout": "Cerrada la sesión de tu cuenta", @@ -474,6 +478,7 @@ "audit_table.sentEmail": "Correo electrónico enviado a {email} para restablecer tu contraseña", "audit_table.session": "ID de Sesión: {id}", "audit_table.sessionRevoked": "La sesión con id {sessionId} fue revocada", + "audit_table.successfullLicenseAdd": "Licencia agregada con éxito", "audit_table.successfullLogin": "Inicio de sesión satisfactorio", "audit_table.successfullOAuthAccess": "Se entrego acceso al nuevo servicio de OAuth satisfactoriamente", "audit_table.successfullOAuthToken": "Se agregó el nuevo servicio de OAuth satisfactoriamente", @@ -576,7 +581,8 @@ "claim.email_to_sso.enterPwd": "Ingresa la contraseña para tu cuenta para {team} {site}", "claim.email_to_sso.pwd": "Contraseña", "claim.email_to_sso.pwdError": "Por favor introduce tu contraseña.", - "claim.email_to_sso.ssoType": "Al reclamar tu cuenta, sólo podrás iniciar sesión con {type} SSO", + "claim.email_to_sso.ssoNote": "Debes tener una cuenta válida con {type}", + "claim.email_to_sso.ssoType": "Al reclamar tu cuenta, sólo podrás iniciar sesión con {type}. Debes tener una cuenta válida con {type}", "claim.email_to_sso.switchTo": "Cambiar cuenta a {uiType}", "claim.email_to_sso.title": "Cambiar Cuenta de Correo/Contraseña a {uiType}", "claim.sso_to_email.confirm": "Confirmar Contraseña", @@ -747,6 +753,10 @@ "member_item.makeAdmin": "Convertir en Admin de Equipo", "member_item.member": "Miembro", "member_item.removeMember": "Elminar Miembro", + "member_team_item.confirmDemoteDescription": "Si te degradas a ti mismo de la función de Administrador de Sistema y no hay otro usuario con privilegios de Administrador de Sistema, tendrás que volver a asignar un Administrador del Sistema accediendo al servidor de Mattermost a través de un terminal y ejecutar el siguiente comando.", + "member_team_item.confirmDemoteRoleTitle": "Confirmar el decenso del rol de Administrador de Sistema", + "member_team_item.confirmDemotion": "Confirmar decenso", + "member_team_item.confirmDemotionCmd": "platform -assign_role -team_name=\"tuequipo\" -email=\"nombre@tuempresa.com\" -role=\"system_admin\"", "member_team_item.inactive": "Inactivo", "member_team_item.makeActive": "Activar", "member_team_item.makeAdmin": "Convertir a Admin de Equipo", @@ -869,11 +879,13 @@ "rhs_comment.comment": "Comentario", "rhs_comment.del": "Borrar", "rhs_comment.edit": "Editar", + "rhs_comment.permalink": "Enlace permanente", "rhs_comment.retry": "Reintentar", "rhs_header.details": "Detalles del Mensaje", "rhs_root.del": "Borrar", "rhs_root.direct": "Mensaje Directo", "rhs_root.edit": "Editar", + "rhs_root.permalink": "Enlace permanente", "search_bar.cancel": "Cancelar", "search_bar.search": "Buscar", "search_bar.usage": "<h4>Opciones de Búsqueda</h4><ul><li><span>Utiliza </span><b>\"comillas\"</b><span> para buscar frases</span></li><li><span>Utiliza </span><b>from:</b><span> para encontrar mensajes de usuarios en específico e </span><b>in:</b><span> para encontrar mensajes de canales específicos</span></li></ul>", @@ -1173,11 +1185,11 @@ "user.settings.hooks_in.add": "Agregar", "user.settings.hooks_in.addTitle": "Agregar un nuevo webhook de entrada", "user.settings.hooks_in.channel": "Canal: ", - "user.settings.hooks_in.description": "Crea webhook URLs para ser utilizados por integraciones externas. Por favor revisa <a href=\"http://mattermost.org/webhooks\">http://mattermost.org/webhooks</a> para conocer más.", + "user.settings.hooks_in.description": "Crea webhooks URLs para utilizarlos con integraciones externas. Por favor revisa <a href=\"http://docs.mattermost.com/developer/webhooks-incoming.html\" target=\"_blank\">documentación de webhook de entrada</a> para aprender más.", "user.settings.hooks_in.existing": "Webhooks de entrada existentes", "user.settings.hooks_in.none": "Ninguno", "user.settings.hooks_out.add": "Agregar", - "user.settings.hooks_out.addDescription": "Crea webhooks para enviar los nuevos mensajes a una integración externa. Por favor revisa <a href=\"http://mattermost.org/webhooks\">http://mattermost.org/webhooks</a> para conocer más.", + "user.settings.hooks_out.addDescription": "Crea webhooks para enviar los nuevos mensajes a una integración externa. Por favor revisa <a href=\"http://docs.mattermost.com/developer/webhooks-outgoing.html\" target=\"_blank\">documentación de webhook de salida</a> para conocer más.", "user.settings.hooks_out.addTitle": "Agregar un nuevo webhook de salida", "user.settings.hooks_out.callback": "Callback URLs:", "user.settings.hooks_out.callbackDesc": "Separa por una nueva linea cada URL donde quieres recibir el evento de HTTP POST", @@ -1263,4 +1275,4 @@ "view_image_popover.download": "Descargar", "view_image_popover.file": "Archivo {count} de {total}", "view_image_popover.publicLink": "Obtener Enlace Público" -}
\ No newline at end of file +} diff --git a/web/templates/head.html b/web/templates/head.html index da65e1779..94a86e3dd 100644 --- a/web/templates/head.html +++ b/web/templates/head.html @@ -79,6 +79,7 @@ $(function() { if (window.mm_preferences != null) { PreferenceStore.setPreferences(window.mm_preferences); + PreferenceStore.emitChange(); } }); |